Compare commits

..
1 Commits
Author SHA1 Message Date
wenyifan 6108000cce utls 2023-11-14 00:32:42 +08:00
724 changed files with 9179 additions and 101052 deletions

No files matched your search

-139
View File
@@ -1,139 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this directory.
## Build & Verify
```bash
cd x && go build ./... # verify compilation
cd x && go vet ./... # static analysis
cd x && go build ./... && go vet ./... # both
```
There are no tests in this module. Build + vet is the verification path.
## Architecture
`x/` is the **implementation layer** for the GOST proxy framework. Every interface defined in `core/` is implemented here. The module ties the whole system together: it provides ~25 handler implementations, ~25 listener implementations, ~20 dialer implementations, ~15 connector implementations, plus config parsing, registry management, and all cross-cutting concerns.
### Component pattern (applies to every handler, listener, dialer, connector)
Every component follows this exact pattern:
```go
// 1. init() self-registration via blank import
func init() {
registry.HandlerRegistry().Register("http", NewHandler)
}
// 2. Private struct holding options + parsed metadata
type httpHandler struct {
md metadata
options handler.Options
}
// 3. Functional options constructor
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &httpHandler{options: options}
}
// 4. Init extracts typed values from generic metadata
func (h *httpHandler) Init(md md.Metadata) error {
return h.parseMetadata(md)
}
```
Each component package also defines a private `metadata` struct and a `parseMetadata(md)` method that uses `mdutil.GetBool/GetInt/GetString/GetDuration(md, keys...)` to extract configuration. Metadata keys are lowercased and multiple fallback keys are tried (e.g., `"observePeriod"`, `"observer.period"`, `"observer.observePeriod"`).
### Config parsing and service composition
`config/parsing/service/parse.go` is the critical wiring function. `ParseService(cfg)` does:
1. Defaults listener type to `"tcp"`, handler type to `"auto"`
2. Sets up TLS config, network namespace, authentication, admission, bypass
3. Looks up the listener and handler factories from their registries, calls them with composed options
4. Creates a `service.Service` (which runs the accept loop calling `handler.Handle` per connection)
5. Wraps the listener in order: proxyproto → metrics → stats → admission → traffic limiter → connection limiter
Each config sub-package (`config/parsing/{auth,bypass,admission,chain,hop,limiter,...}/`) has a `parse.go` that converts config structs into registry lookups and constructed objects.
### Metadata system (`metadata/`)
`metadata.NewMetadata(map[string]any)` wraps a raw config map. All keys are lowercased on lookup. The `mdutil` package (`metadata/util/`) provides typed accessors used everywhere:
- `mdutil.GetBool(md, keys...)`, `GetInt`, `GetFloat`, `GetString`, `GetStrings`
- `mdutil.GetDuration(md, keys...)` — int values treated as seconds; strings parsed with `time.ParseDuration`
- `mdutil.GetStringMap(md, keys...)`, `GetStringMapString`
Metadata key constants are defined in `config/parsing/parse.go` (e.g., `MDKeyProxyProtocol`, `MDKeySoMark`, `MDKeyInterface`, `MDKeyEnableStats`).
### Internal packages (`internal/`)
**Do not import from external modules.** Go's `internal/` restriction enforces this. Key internal packages:
| Package | Purpose |
|---------|---------|
| `internal/ctx/` | Context values for session state during requests |
| `internal/io/` | Custom IO readers/writers |
| `internal/loader/` | `Loader`, `Lister`, `Mapper` interfaces for hot-reloadable components |
| `internal/matcher/` | Pattern matching utilities |
| `internal/net/` | Network utilities (resolver, dialer, proxyproto, HTTP helpers, UDP listener) |
| `internal/plugin/` | External plugin process management |
| `internal/util/sniffing/` | Protocol sniffing (TLS, HTTP, WebSocket) |
| `internal/util/stats/` | Connection stats tracking |
| `internal/util/tls/` | TLS config loading, cert management |
| `internal/util/ws/` | WebSocket utilities |
### Context propagation (`ctx/`)
`x/ctx/value.go` provides typed context key/value helpers used to pass per-request data through the handler chain:
- `ContextWithSid` / `SidFromContext` — Session ID
- `ContextWithSrcAddr` / `SrcAddrFromContext` — Source address
- `ContextWithDstAddr` / `DstAddrFromContext` — Destination address
- `ContextWithClientID` / `ClientIDFromContext` — Client ID (for hash-based load balancing)
- `ContextWithHash` / `HashFromContext` — Hash source for selector
### Hot-reloadable components
Auth, bypass, admission, hosts, ingress, limiter, recorder, router, and SD components all support periodic reload from file/redis/http sources. The pattern:
1. Options include `fileLoader`, `redisLoader`, `httpLoader` plus a `period` duration
2. A background goroutine runs `periodReload(ctx)` calling `loader.Load()` on each tick
3. The component swaps its internal data structure (e.g., `ipMatcher`, `hostMatcher`) atomically
### Listener wrapper ordering
Listeners wrap `net.Listener` in a fixed order. Getting this wrong breaks metrics/admission/limiting:
```
proxyproto → metrics → stats → admission → traffic_limiter → conn_limiter → (protocol-specific)
```
### Registry
All registries are in `x/registry/registry.go` — a single file with ~25 typed global singletons backed by `sync.Map`. Each is accessed via an exported function (e.g., `registry.HandlerRegistry()`, `registry.ListenerRegistry()`). Registration returns `ErrDup` on name collision; empty names are silently ignored.
### Selectors (`selector/`)
Load-balancing strategies for hops and chain groups:
| Strategy | Behavior |
|----------|----------|
| `RoundRobinStrategy` | Atomic counter, modulo selection |
| `RandomStrategy` | Weighted random using `RandomWeighted` |
| `FIFOStrategy` | Always first element (stickiness) |
| `HashStrategy` | CRC32 of client ID or hash context value |
Filters (`FailFilter`, `BackupFilter`) are applied before the strategy, reducing the candidate pool.
### API (`api/`)
Uses Gin framework. Registers CRUD endpoints under `/config` for all component types. Supports basic auth when an auther is configured. Swagger docs at `/docs`.
### Metrics (`metrics/`)
Prometheus-style metrics following `_total`, `_seconds` naming conventions. Constants for all metric names (e.g., `MetricServiceRequestsCounter`, `MetricNodeConnectDurationObserver`). `Enable(bool)` toggles globally. When disabled, `GetCounter/Gauge/Observer` return noop implementations.
+1 -33
View File
@@ -1,33 +1 @@
# x
The implementation layer for the GOST proxy framework. Every interface defined in [core/](../core/) is implemented here — handlers, listeners, dialers, connectors, plus config parsing, registries, and all cross-cutting concerns.
## Package layout
| Directory | Purpose |
|-----------|---------|
| `handler/` | Protocol handlers (http, socks4/5, ss, ssh, tunnel, tun, dns, redirect, etc.) |
| `listener/` | Protocol listeners (tcp, tls, ws, http2/3, quic, kcp, icmp, tun, udp, etc.) |
| `dialer/` | Outbound dialers (tcp, tls, ws, http2/3, quic, grpc, ssh, wg, kcp, icmp, etc.) |
| `connector/` | Destination connectors (http, socks4/5, ss, ssh, relay, tunnel, direct, etc.) |
| `config/` | Config struct, YAML/JSON parsing, and service construction |
| `registry/` | Typed global registries for all component types |
| `service/` | Accept loop wiring listener + handler |
| `chain/` | Multi-hop forwarding chain and route implementation |
| `router/` | Route table router (destination-based routing) |
| `hop/` | Node group with load-balanced selection |
| `selector/` | Load-balancing strategies (round-robin, random, weighted, hash) |
| `auth/`, `bypass/`, `admission/` | Authentication, bypass rules, admission control |
| `resolver/`, `hosts/` | DNS resolution and host mapping |
| `limiter/` | Traffic, connection, and rate limiters |
| `recorder/`, `observer/` | Traffic recording and observability |
| `ingress/`, `sd/`, `routing/` | Ingress control, service discovery, routing rules |
| `logger/`, `metrics/`, `api/` | Logging, Prometheus metrics, Web API |
| `metadata/`, `ctx/` | Metadata key-value system and context propagation |
| `internal/` | Shared internals — not importable externally |
## Component pattern
Every handler, listener, dialer, and connector follows the same pattern: `init()` registers a constructor into a global registry, the constructor takes functional options, and `Init(metadata.Metadata)` extracts typed configuration from the metadata key-value map.
See [CLAUDE.md](CLAUDE.md) for detailed architecture and conventions.
# The extended (may be experimental) features outside the main gost tree.
+31 -167
View File
@@ -1,20 +1,3 @@
// Package admission implements access control for incoming connections.
// Before a connection is handled, the admission controller checks whether
// the client's address is allowed to use the service.
//
// The package provides:
// - NewAdmission: a local admission controller backed by IP/CIDR matchers,
// with optional periodic reload from file, Redis, or HTTP sources.
// - AdmissionGroup: composes multiple admission controllers; all must
// admit for the connection to proceed.
// - Plugin-based admission (gRPC and HTTP) in the plugin sub-package.
// - Connection wrappers in the wrapper sub-package that enforce
// admission checks per-read (for TCP) and per-packet (for UDP).
//
// Matching modes:
// - Whitelist (whitelist=true): only addresses matching a rule are admitted.
// - Blacklist (whitelist=false): addresses matching a rule are denied;
// everything else is admitted. This is the default.
package admission
import (
@@ -30,160 +13,95 @@ import (
"github.com/go-gost/core/logger"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/matcher"
xlogger "github.com/go-gost/x/logger"
)
// options holds the configuration for a localAdmission instance.
type options struct {
// whitelist toggles between blacklist and whitelist mode.
// When false (default): matching addresses are denied.
// When true: only matching addresses are allowed.
whitelist bool
// matchers holds static patterns provided at construction time
// (e.g. from config file or command-line arguments).
matchers []string
// fileLoader loads patterns from a file or directory.
// Supports hot-reload via file watching if the loader implements Lister.
fileLoader loader.Loader
// redisLoader loads patterns from a Redis set or key.
// Supports hot-reload via periodic polling if the loader implements Lister.
whitelist bool
matchers []string
fileLoader loader.Loader
redisLoader loader.Loader
// httpLoader loads patterns from an HTTP endpoint.
// Supports hot-reload via periodic polling.
httpLoader loader.Loader
// period controls the interval between automatic reloads.
// Values less than 1 second are clamped to 1 second.
// A value <= 0 disables periodic reload (load once at startup).
period time.Duration
// logger is used for debug and warning messages.
// Falls back to a no-op logger if nil.
logger logger.Logger
httpLoader loader.Loader
period time.Duration
logger logger.Logger
}
// Option is a functional option for configuring an admission controller.
type Option func(opts *options)
// WhitelistOption sets whether the admission controller operates in
// whitelist mode. In whitelist mode, only addresses matching one or more
// rules are admitted; all others are denied.
func WhitelistOption(whitelist bool) Option {
return func(opts *options) {
opts.whitelist = whitelist
}
}
// MatchersOption sets static match patterns that are combined with
// dynamically loaded patterns on each reload. Patterns may be:
// - Bare IP addresses: "192.168.1.1", "::1"
// - CIDR networks: "10.0.0.0/8", "fd00::/8"
// - Hostnames: resolved to IP addresses via DNS
func MatchersOption(matchers []string) Option {
return func(opts *options) {
opts.matchers = matchers
}
}
// ReloadPeriodOption sets the interval for automatically reloading
// patterns from external loaders (file, Redis, HTTP). A value <= 0
// disables periodic reloading.
func ReloadPeriodOption(period time.Duration) Option {
return func(opts *options) {
opts.period = period
}
}
// FileLoaderOption sets the file-based pattern loader.
func FileLoaderOption(fileLoader loader.Loader) Option {
return func(opts *options) {
opts.fileLoader = fileLoader
}
}
// RedisLoaderOption sets the Redis-based pattern loader.
func RedisLoaderOption(redisLoader loader.Loader) Option {
return func(opts *options) {
opts.redisLoader = redisLoader
}
}
// HTTPLoaderOption sets the HTTP-based pattern loader.
func HTTPLoaderOption(httpLoader loader.Loader) Option {
return func(opts *options) {
opts.httpLoader = httpLoader
}
}
// LoggerOption sets the logger for the admission controller.
func LoggerOption(logger logger.Logger) Option {
return func(opts *options) {
opts.logger = logger
}
}
// localAdmission is the in-process admission controller. It maintains
// two matchers — one for individual IP addresses and one for CIDR
// networks — and checks incoming addresses against them.
//
// The matchers are rebuilt atomically on each reload: new patterns
// are loaded, parsed into IPs and CIDRs, and the matchers are swapped
// under a write lock.
type localAdmission struct {
ipMatcher matcher.Matcher // matches individual IP addresses
cidrMatcher matcher.Matcher // matches CIDR network ranges
mu sync.RWMutex // protects ipMatcher and cidrMatcher
ipMatcher matcher.Matcher
cidrMatcher matcher.Matcher
mu sync.RWMutex
cancelFunc context.CancelFunc
options options
logger logger.Logger
}
// NewAdmission creates a local admission controller with the given options.
// It starts a background goroutine that periodically reloads patterns
// from configured external sources (file, Redis, HTTP).
//
// By default the controller operates in blacklist mode: any address
// matching the loaded patterns is denied. Use WhitelistOption(true)
// to invert this.
// NewAdmission creates and initializes a new Admission using matcher patterns as its match rules.
// The rules will be reversed if the reverse is true.
func NewAdmission(opts ...Option) admission.Admission {
var options options
for _, opt := range opts {
opt(&options)
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.TODO())
p := &localAdmission{
ipMatcher: matcher.NopMatcher(),
cidrMatcher: matcher.NopMatcher(),
cancelFunc: cancel,
options: options,
logger: options.logger,
}
if p.logger == nil {
p.logger = xlogger.Nop()
cancelFunc: cancel,
options: options,
}
go p.periodReload(ctx)
if err := p.reload(ctx); err != nil {
options.logger.Warnf("reload: %v", err)
}
if p.options.period > 0 {
go p.periodReload(ctx)
}
return p
}
// Admit decides whether the given network address is allowed.
// It returns true if the address should be accepted, false otherwise.
//
// The address is first stripped of its port (if present), then matched
// against the IP and CIDR matchers. The whitelist/blacklist mode
// determines how the match result is interpreted:
//
// Blacklist (whitelist=false): admit if NOT matched
// Whitelist (whitelist=true): admit only if matched
//
// An empty address or a nil receiver always returns true.
func (p *localAdmission) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
func (p *localAdmission) Admit(ctx context.Context, addr string, opts ...admission.Option) bool {
if addr == "" || p == nil {
return true
}
@@ -199,22 +117,13 @@ func (p *localAdmission) Admit(ctx context.Context, network, addr string, opts .
p.options.whitelist && matched
if !b {
p.logger.Debugf("%s is denied", addr)
p.options.logger.Debugf("%s is denied", addr)
}
return b
}
// periodReload triggers an immediate reload, then — if a positive period
// is configured — reloads on each tick. It exits when ctx is cancelled.
func (p *localAdmission) periodReload(ctx context.Context) error {
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
}
period := p.options.period
if period <= 0 {
return nil
}
if period < time.Second {
period = time.Second
}
@@ -225,7 +134,8 @@ func (p *localAdmission) periodReload(ctx context.Context) error {
select {
case <-ticker.C:
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
p.options.logger.Warnf("reload: %v", err)
// return err
}
case <-ctx.Done():
return ctx.Err()
@@ -233,8 +143,6 @@ func (p *localAdmission) periodReload(ctx context.Context) error {
}
}
// reload loads patterns from all configured sources, parses them into
// IP addresses and CIDR networks, and atomically swaps the matchers.
func (p *localAdmission) reload(ctx context.Context) error {
v, err := p.load(ctx)
if err != nil {
@@ -253,9 +161,8 @@ func (p *localAdmission) reload(ctx context.Context) error {
inets = append(inets, inet)
continue
}
// Try DNS resolution as a last resort.
if ipAddr, _ := net.ResolveIPAddr("ip", pattern); ipAddr != nil {
p.logger.Debugf("resolve IP: %s -> %s", pattern, ipAddr)
p.options.logger.Debugf("resolve IP: %s -> %s", pattern, ipAddr)
ips = append(ips, ipAddr.IP)
}
}
@@ -269,15 +176,12 @@ func (p *localAdmission) reload(ctx context.Context) error {
return nil
}
// load collects patterns from all configured external loaders
// (file, Redis, HTTP). It prefers the Lister interface when available,
// falling back to loading raw content via Load and parsing line-by-line.
func (p *localAdmission) load(ctx context.Context) (patterns []string, err error) {
if p.options.fileLoader != nil {
if lister, ok := p.options.fileLoader.(loader.Lister); ok {
list, er := lister.List(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
p.options.logger.Warnf("file loader: %v", er)
}
for _, s := range list {
if line := p.parseLine(s); line != "" {
@@ -287,7 +191,7 @@ func (p *localAdmission) load(ctx context.Context) (patterns []string, err error
} else {
r, er := p.options.fileLoader.Load(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
p.options.logger.Warnf("file loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
@@ -298,13 +202,13 @@ func (p *localAdmission) load(ctx context.Context) (patterns []string, err error
if lister, ok := p.options.redisLoader.(loader.Lister); ok {
list, er := lister.List(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
p.options.logger.Warnf("redis loader: %v", er)
}
patterns = append(patterns, list...)
} else {
r, er := p.options.redisLoader.Load(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
p.options.logger.Warnf("redis loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
@@ -315,19 +219,17 @@ func (p *localAdmission) load(ctx context.Context) (patterns []string, err error
if p.options.httpLoader != nil {
r, er := p.options.httpLoader.Load(ctx)
if er != nil {
p.logger.Warnf("http loader: %v", er)
p.options.logger.Warnf("http loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
}
}
p.logger.Debugf("load items %d", len(patterns))
p.options.logger.Debugf("load items %d", len(patterns))
return
}
// parsePatterns reads a newline-delimited stream and returns a list of
// non-empty, trimmed lines after stripping comments.
func (p *localAdmission) parsePatterns(r io.Reader) (patterns []string, err error) {
if r == nil {
return
@@ -344,9 +246,6 @@ func (p *localAdmission) parsePatterns(r io.Reader) (patterns []string, err erro
return
}
// parseLine strips inline comments (everything after the first '#')
// and trims surrounding whitespace. It returns an empty string if
// the line was a comment-only or blank line.
func (p *localAdmission) parseLine(s string) string {
if n := strings.IndexByte(s, '#'); n >= 0 {
s = s[:n]
@@ -354,8 +253,6 @@ func (p *localAdmission) parseLine(s string) string {
return strings.TrimSpace(s)
}
// matched returns true if addr is matched by either the IP or CIDR
// matcher. Caller must hold at least a read lock.
func (p *localAdmission) matched(addr string) bool {
p.mu.RLock()
defer p.mu.RUnlock()
@@ -364,9 +261,6 @@ func (p *localAdmission) matched(addr string) bool {
p.cidrMatcher.Match(addr)
}
// Close stops the background reload goroutine and closes all external
// loaders. It is safe to call after the admission controller is no
// longer needed.
func (p *localAdmission) Close() error {
p.cancelFunc()
if p.options.fileLoader != nil {
@@ -375,35 +269,5 @@ func (p *localAdmission) Close() error {
if p.options.redisLoader != nil {
p.options.redisLoader.Close()
}
if p.options.httpLoader != nil {
p.options.httpLoader.Close()
}
return nil
}
// admissionGroup composes multiple admission controllers into one.
// All controllers in the group must admit for the overall result to
// be true (logical AND).
type admissionGroup struct {
admissions []admission.Admission
}
// AdmissionGroup creates a composite admission controller that requires
// every member to admit the address. If any member denies, the address
// is denied immediately (short-circuit evaluation).
//
// Nil members are skipped.
func AdmissionGroup(admissions ...admission.Admission) admission.Admission {
return &admissionGroup{
admissions: admissions,
}
}
func (p *admissionGroup) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
for _, admission := range p.admissions {
if admission != nil && !admission.Admit(ctx, network, addr, opts...) {
return false
}
}
return true
}
-727
View File
@@ -1,727 +0,0 @@
package admission
import (
"context"
"io"
"strings"
"testing"
"time"
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/internal/loader"
xlogger "github.com/go-gost/x/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Option tests ---
func TestWhitelistOption(t *testing.T) {
var opts options
WhitelistOption(true)(&opts)
assert.True(t, opts.whitelist)
WhitelistOption(false)(&opts)
assert.False(t, opts.whitelist)
}
func TestMatchersOption(t *testing.T) {
var opts options
MatchersOption([]string{"192.168.1.1", "10.0.0.0/8"})(&opts)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.0/8"}, opts.matchers)
}
func TestReloadPeriodOption(t *testing.T) {
var opts options
ReloadPeriodOption(5 * time.Second)(&opts)
assert.Equal(t, 5*time.Second, opts.period)
}
func TestFileLoaderOption(t *testing.T) {
var opts options
fl := &mockLoader{}
FileLoaderOption(fl)(&opts)
assert.Equal(t, fl, opts.fileLoader)
}
func TestRedisLoaderOption(t *testing.T) {
var opts options
rl := &mockLoader{}
RedisLoaderOption(rl)(&opts)
assert.Equal(t, rl, opts.redisLoader)
}
func TestHTTPLoaderOption(t *testing.T) {
var opts options
hl := &mockLoader{}
HTTPLoaderOption(hl)(&opts)
assert.Equal(t, hl, opts.httpLoader)
}
func TestLoggerOption(t *testing.T) {
var opts options
l := xlogger.Nop()
LoggerOption(l)(&opts)
assert.Equal(t, l, opts.logger)
}
// newSyncedAdmission creates an admission controller and blocks until the initial
// background reload completes, so matchers are ready for immediate assertions.
func newSyncedAdmission(opts ...Option) *localAdmission {
a := NewAdmission(opts...)
la := a.(*localAdmission)
// Force a synchronous reload so tests don't race with the background goroutine.
la.reload(context.Background())
return la
}
// --- NewAdmission tests ---
func TestNewAdmission_Defaults(t *testing.T) {
adm := newSyncedAdmission()
require.NotNil(t, adm)
defer adm.Close()
// Should admit by default (blacklist mode with no rules)
assert.True(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestNewAdmission_WithLogger(t *testing.T) {
adm := newSyncedAdmission(LoggerOption(xlogger.Nop()))
require.NotNil(t, adm)
defer adm.Close()
}
// --- Admit tests ---
func TestAdmit_EmptyAddr(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
assert.True(t, adm.Admit(context.Background(), "tcp", ""))
}
func TestAdmit_NilReceiver(t *testing.T) {
var p *localAdmission
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmit_Blacklist_Matched(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
// In blacklist mode, matching IP is denied
assert.False(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmit_Blacklist_NotMatched(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
// In blacklist mode, non-matching IP is admitted
assert.True(t, adm.Admit(context.Background(), "tcp", "10.0.0.1"))
}
func TestAdmit_Whitelist_Matched(t *testing.T) {
adm := newSyncedAdmission(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
)
defer adm.Close()
// In whitelist mode, matching IP is admitted
assert.True(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmit_Whitelist_NotMatched(t *testing.T) {
adm := newSyncedAdmission(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
)
defer adm.Close()
// In whitelist mode, non-matching IP is denied
assert.False(t, adm.Admit(context.Background(), "tcp", "10.0.0.1"))
}
func TestAdmit_StripsPort(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
// Address with port should have port stripped before matching
assert.False(t, adm.Admit(context.Background(), "tcp", "192.168.1.1:8080"))
}
func TestAdmit_CIDRMatch(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"10.0.0.0/8"}))
defer adm.Close()
assert.False(t, adm.Admit(context.Background(), "tcp", "10.0.0.1"))
assert.True(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmit_InvalidAddr(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
// Invalid IP should not match and be admitted in blacklist mode
assert.True(t, adm.Admit(context.Background(), "tcp", "invalid"))
}
// --- parseLine tests ---
func TestParseLine_Normal(t *testing.T) {
adm := &localAdmission{}
assert.Equal(t, "192.168.1.1", adm.parseLine("192.168.1.1"))
}
func TestParseLine_WithComment(t *testing.T) {
adm := &localAdmission{}
assert.Equal(t, "192.168.1.1", adm.parseLine("192.168.1.1 # a comment"))
}
func TestParseLine_CommentOnly(t *testing.T) {
adm := &localAdmission{}
assert.Equal(t, "", adm.parseLine("# comment only"))
}
func TestParseLine_Empty(t *testing.T) {
adm := &localAdmission{}
assert.Equal(t, "", adm.parseLine(""))
}
func TestParseLine_Whitespace(t *testing.T) {
adm := &localAdmission{}
assert.Equal(t, "192.168.1.1", adm.parseLine(" 192.168.1.1 "))
}
// --- parsePatterns tests ---
func TestParsePatterns_NilReader(t *testing.T) {
adm := &localAdmission{}
patterns, err := adm.parsePatterns(nil)
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestParsePatterns_Normal(t *testing.T) {
adm := &localAdmission{}
r := strings.NewReader("192.168.1.1\n10.0.0.1\n# comment\n\n fd00::1 \n")
patterns, err := adm.parsePatterns(r)
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1", "fd00::1"}, patterns)
}
// --- matched tests ---
func TestMatched_IP(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
assert.True(t, adm.matched("192.168.1.1"))
assert.False(t, adm.matched("10.0.0.1"))
}
func TestMatched_CIDR(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"10.0.0.0/8"}))
defer adm.Close()
assert.True(t, adm.matched("10.0.0.1"))
assert.False(t, adm.matched("192.168.1.1"))
}
func TestMatched_BothIPAndCIDR(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1", "10.0.0.0/8"}))
defer adm.Close()
assert.True(t, adm.matched("192.168.1.1"))
assert.True(t, adm.matched("10.0.0.1"))
assert.False(t, adm.matched("172.16.0.1"))
}
// --- reload tests ---
func TestReload_WithHostnameResolution(t *testing.T) {
adm := newSyncedAdmission(
MatchersOption([]string{"localhost"}),
LoggerOption(xlogger.Nop()),
)
defer adm.Close()
// After reload, localhost should be resolved to 127.0.0.1 or ::1
err := adm.reload(context.Background())
assert.NoError(t, err)
}
// --- load tests ---
func TestLoad_AllNilLoaders(t *testing.T) {
adm := &localAdmission{
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithList(t *testing.T) {
adm := &localAdmission{
options: options{
fileLoader: &mockListerLoader{list: []string{"192.168.1.1", "# comment", "10.0.0.1"}},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1"}, patterns)
}
func TestLoad_FileLoaderWithLoad(t *testing.T) {
adm := &localAdmission{
options: options{
fileLoader: &mockLoader{data: "192.168.1.1\n10.0.0.1\n"},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1"}, patterns)
}
func TestLoad_RedisLoaderWithList(t *testing.T) {
adm := &localAdmission{
options: options{
redisLoader: &mockListerLoader{list: []string{"redis-host-1", "redis-host-2"}},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"redis-host-1", "redis-host-2"}, patterns)
}
func TestLoad_RedisLoaderWithLoad(t *testing.T) {
adm := &localAdmission{
options: options{
redisLoader: &mockLoader{data: "192.168.2.1\n"},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.2.1"}, patterns)
}
func TestLoad_HTTPLoader(t *testing.T) {
adm := &localAdmission{
options: options{
httpLoader: &mockLoader{data: "10.10.10.1\n10.10.10.2\n"},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"10.10.10.1", "10.10.10.2"}, patterns)
}
func TestLoad_FileLoaderWithListError(t *testing.T) {
adm := &localAdmission{
options: options{
fileLoader: &mockListerLoader{listErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithLoadError(t *testing.T) {
adm := &localAdmission{
options: options{
fileLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_RedisLoaderWithListError(t *testing.T) {
adm := &localAdmission{
options: options{
redisLoader: &mockListerLoader{listErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_RedisLoaderWithLoadError(t *testing.T) {
adm := &localAdmission{
options: options{
redisLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_HTTPLoaderError(t *testing.T) {
adm := &localAdmission{
options: options{
httpLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithListParsing(t *testing.T) {
adm := &localAdmission{
options: options{
fileLoader: &mockListerLoader{list: []string{" # comment", "", " 192.168.1.1 "}},
},
logger: xlogger.Nop(),
}
patterns, err := adm.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1"}, patterns)
}
// --- periodReload tests ---
func TestPeriodReload_ZeroPeriod(t *testing.T) {
adm := &localAdmission{
logger: xlogger.Nop(),
options: options{period: 0},
}
err := adm.periodReload(context.Background())
assert.NoError(t, err)
}
func TestPeriodReload_NegativePeriod(t *testing.T) {
adm := &localAdmission{
logger: xlogger.Nop(),
options: options{period: -1},
}
err := adm.periodReload(context.Background())
assert.NoError(t, err)
}
func TestPeriodReload_WithContextCancel(t *testing.T) {
// 500ms < 1s triggers the clamping branch (period = time.Second),
// then sleep > 1s to let at least one ticker cycle fire before cancel.
adm := &localAdmission{
logger: xlogger.Nop(),
options: options{period: 500 * time.Millisecond},
}
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
errCh <- adm.periodReload(ctx)
}()
time.Sleep(1100 * time.Millisecond)
cancel()
select {
case err := <-errCh:
assert.Equal(t, context.Canceled, err)
case <-time.After(3 * time.Second):
t.Fatal("periodReload did not return after context cancel")
}
}
// --- Close tests ---
func TestClose_NoLoaders(t *testing.T) {
adm := newSyncedAdmission()
err := adm.Close()
assert.NoError(t, err)
}
func TestClose_WithAllLoaders(t *testing.T) {
fl := &mockLoader{}
rl := &mockLoader{}
hl := &mockLoader{}
adm := newSyncedAdmission(
FileLoaderOption(fl),
RedisLoaderOption(rl),
HTTPLoaderOption(hl),
)
err := adm.Close()
assert.NoError(t, err)
assert.True(t, fl.closed)
assert.True(t, rl.closed)
assert.True(t, hl.closed)
}
// --- DNS resolution in reload ---
func TestReload_DNSResolution(t *testing.T) {
adm := &localAdmission{
ipMatcher: nil,
cidrMatcher: nil,
options: options{
matchers: []string{"localhost"},
},
logger: xlogger.Nop(),
}
err := adm.reload(context.Background())
assert.NoError(t, err)
// localhost should resolve to 127.0.0.1 (or ::1)
assert.True(t, adm.matched("127.0.0.1") || adm.matched("::1"))
}
// --- AdmissionGroup tests ---
func TestAdmissionGroup_Empty(t *testing.T) {
g := AdmissionGroup()
assert.True(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmissionGroup_AllAdmit(t *testing.T) {
g := AdmissionGroup(
alwaysAdmit{},
alwaysAdmit{},
)
assert.True(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmissionGroup_OneDenies(t *testing.T) {
g := AdmissionGroup(
alwaysAdmit{},
alwaysDeny{},
)
assert.False(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmissionGroup_NilMember(t *testing.T) {
g := AdmissionGroup(
nil,
alwaysAdmit{},
nil,
)
assert.True(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestAdmissionGroup_ShortCircuit(t *testing.T) {
// The first deny should short-circuit, never calling the second.
callCount := 0
tracking := &trackingAdmission{admit: false, callCount: &callCount}
g := AdmissionGroup(tracking, alwaysAdmit{})
assert.False(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
assert.Equal(t, 1, callCount)
}
// --- Mock types ---
type mockLoader struct {
data string
loadErr error
closed bool
}
func (m *mockLoader) Load(ctx context.Context) (io.Reader, error) {
if m.loadErr != nil {
return nil, m.loadErr
}
return strings.NewReader(m.data), nil
}
func (m *mockLoader) Close() error {
m.closed = true
return nil
}
type mockListerLoader struct {
list []string
listErr error
}
func (m *mockListerLoader) Load(ctx context.Context) (io.Reader, error) {
return strings.NewReader(strings.Join(m.list, "\n")), nil
}
func (m *mockListerLoader) List(ctx context.Context) ([]string, error) {
if m.listErr != nil {
return nil, m.listErr
}
return m.list, nil
}
func (m *mockListerLoader) Close() error {
return nil
}
type alwaysAdmit struct{}
func (alwaysAdmit) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
return true
}
type alwaysDeny struct{}
func (alwaysDeny) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
return false
}
type trackingAdmission struct {
admit bool
callCount *int
}
func (t *trackingAdmission) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
*t.callCount++
return t.admit
}
// Ensure mock types implement the interfaces.
var _ loader.Loader = (*mockLoader)(nil)
var _ loader.Lister = (*mockListerLoader)(nil)
var _ loader.Loader = (*mockListerLoader)(nil)
var _ admission.Admission = alwaysAdmit{}
var _ admission.Admission = alwaysDeny{}
// TestAdmit_IPv6 tests that IPv6 addresses are handled correctly.
func TestAdmit_IPv6(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"fd00::1"}))
defer adm.Close()
assert.False(t, adm.Admit(context.Background(), "tcp", "fd00::1"))
assert.True(t, adm.Admit(context.Background(), "tcp", "fd00::2"))
}
// TestAdmit_IPv6WithPort tests that IPv6 addresses with port brackets are handled.
func TestAdmit_IPv6WithPort(t *testing.T) {
adm := newSyncedAdmission(MatchersOption([]string{"::1"}))
defer adm.Close()
// net.SplitHostPort handles [::1]:8080 format
assert.False(t, adm.Admit(context.Background(), "tcp", "[::1]:8080"))
}
// TestNewAdmission_ReloadErrorInBackground verifies that the initial reload
// in the background goroutine doesn't panic, even if it fails.
func TestNewAdmission_ReloadError(t *testing.T) {
fl := &mockLoader{loadErr: io.ErrUnexpectedEOF}
adm := newSyncedAdmission(
FileLoaderOption(fl),
LoggerOption(xlogger.Nop()),
)
defer adm.Close()
// Should still work, just logged a warning
assert.True(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
// TestAdmissionGroup_NilMemberFirst tests nil as first member.
func TestAdmissionGroup_NilMemberFirst(t *testing.T) {
g := AdmissionGroup(nil, alwaysDeny{})
assert.False(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
// TestAdmissionGroup_AllNil tests all nil members.
func TestAdmissionGroup_AllNil(t *testing.T) {
g := AdmissionGroup(nil, nil)
assert.True(t, g.Admit(context.Background(), "tcp", "192.168.1.1"))
}
// TestLoggerDefault tests that default logger is set when nil is passed.
func TestLoggerOption_Nil(t *testing.T) {
var opts options
LoggerOption(nil)(&opts)
assert.Nil(t, opts.logger)
}
// TestAdmit_IpMatcher test case where only IP matcher is valid.
func TestReload_OnlyIPs(t *testing.T) {
adm := &localAdmission{
options: options{
matchers: []string{"1.1.1.1", "8.8.8.8"},
},
logger: xlogger.Nop(),
}
err := adm.reload(context.Background())
assert.NoError(t, err)
assert.True(t, adm.matched("1.1.1.1"))
assert.True(t, adm.matched("8.8.8.8"))
assert.False(t, adm.matched("9.9.9.9"))
}
// TestReload_OnlyCIDRs tests reload with only CIDR patterns.
func TestReload_OnlyCIDRs(t *testing.T) {
adm := &localAdmission{
options: options{
matchers: []string{"192.168.0.0/16", "10.0.0.0/8"},
},
logger: xlogger.Nop(),
}
err := adm.reload(context.Background())
assert.NoError(t, err)
assert.True(t, adm.matched("192.168.1.1"))
assert.True(t, adm.matched("10.0.0.1"))
assert.False(t, adm.matched("172.16.0.1"))
}
// TestAdmit_LoggerDebugOutput tests that debug logging happens on deny.
func TestAdmit_DenyLogs(t *testing.T) {
adm := newSyncedAdmission(
MatchersOption([]string{"192.168.1.1"}),
LoggerOption(xlogger.Nop()),
)
defer adm.Close()
// Should not panic; debug log is written
assert.False(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
}
// TestAdmit_WhitelistDenyLogs tests logging for whitelist deny.
func TestAdmit_WhitelistDenyLogs(t *testing.T) {
adm := newSyncedAdmission(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
LoggerOption(xlogger.Nop()),
)
defer adm.Close()
assert.False(t, adm.Admit(context.Background(), "tcp", "10.0.0.1"))
}
// TestParsePatterns_WithCommentsAndEmptyLines tests edge cases.
func TestParsePatterns_EdgeCases(t *testing.T) {
adm := &localAdmission{}
r := strings.NewReader("# full comment\n\n # indented comment\n192.168.1.1 # inline")
patterns, err := adm.parsePatterns(r)
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1"}, patterns)
}
func TestAdmit_NetworkIgnoredForMatching(t *testing.T) {
// Network parameter doesn't affect matching behavior
adm := newSyncedAdmission(MatchersOption([]string{"192.168.1.1"}))
defer adm.Close()
assert.False(t, adm.Admit(context.Background(), "tcp", "192.168.1.1"))
assert.False(t, adm.Admit(context.Background(), "udp", "192.168.1.1"))
assert.False(t, adm.Admit(context.Background(), "ip4", "192.168.1.1"))
}
// Compile-time interface check
var _ logger.Logger = xlogger.Nop()
// Ensure admission.Admission is implemented
var _ admission.Admission = (*localAdmission)(nil)
+142
View File
@@ -0,0 +1,142 @@
package admission
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
"github.com/go-gost/plugin/admission/proto"
"github.com/go-gost/x/internal/plugin"
"google.golang.org/grpc"
)
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.AdmissionClient
log logger.Logger
}
// NewGRPCPlugin creates an Admission plugin based on gRPC.
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) admission.Admission {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "admission",
"admission": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewAdmissionClient(conn)
}
return p
}
func (p *grpcPlugin) Admit(ctx context.Context, addr string, opts ...admission.Option) bool {
if p.client == nil {
return false
}
r, err := p.client.Admit(ctx,
&proto.AdmissionRequest{
Addr: addr,
})
if err != nil {
p.log.Error(err)
return false
}
return r.Ok
}
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
type httpPluginRequest struct {
Addr string `json:"addr"`
}
type httpPluginResponse struct {
OK bool `json:"ok"`
}
type httpPlugin struct {
url string
client *http.Client
header http.Header
log logger.Logger
}
// NewHTTPPlugin creates an Admission plugin based on HTTP.
func NewHTTPPlugin(name string, url string, opts ...plugin.Option) admission.Admission {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
return &httpPlugin{
url: url,
client: plugin.NewHTTPClient(&options),
header: options.Header,
log: logger.Default().WithFields(map[string]any{
"kind": "admission",
"admission": name,
}),
}
}
func (p *httpPlugin) Admit(ctx context.Context, addr string, opts ...admission.Option) (ok bool) {
if p.client == nil {
return
}
rb := httpPluginRequest{
Addr: addr,
}
v, err := json.Marshal(&rb)
if err != nil {
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
if err != nil {
return
}
if p.header != nil {
req.Header = p.header.Clone()
}
req.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
res := httpPluginResponse{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return
}
return res.OK
}
-102
View File
@@ -1,102 +0,0 @@
// Package admission implements gRPC-based admission plugins.
// An admission plugin delegates the admit/deny decision to an external
// service via gRPC. This allows admission logic to be implemented in
// a separate process using any language supported by protobuf.
//
// The gRPC service definition is in plugin/admission/proto.
package admission
import (
"context"
"io"
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
"github.com/go-gost/plugin/admission/proto"
"github.com/go-gost/x/internal/plugin"
"google.golang.org/grpc"
)
// grpcPlugin is an admission controller that delegates to an external
// gRPC admission service.
//
// If the gRPC connection cannot be established at construction time,
// the client field remains nil and Admit returns true (fail-open),
// allowing all traffic through rather than blocking everything.
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.AdmissionClient
log logger.Logger
}
// NewGRPCPlugin creates an admission controller that communicates with
// an external gRPC admission service at the given address.
//
// The name is used only for log identification. The addr should be a
// gRPC target string (e.g. "127.0.0.1:9000"). Additional plugin options
// (TLS, token, retry) can be passed via opts.
//
// If the connection fails, the plugin logs the error and operates in
// fail-open mode: all admission requests return true.
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) admission.Admission {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "admission",
"admission": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewAdmissionClient(conn)
}
return p
}
// Admit sends an admission request to the external gRPC service.
// It passes the network, address, and service name from the client
// connection.
//
// If the gRPC client is nil (connection failed at startup), it returns
// true (fail-open). If the RPC returns an error, it logs the error and
// returns false.
func (p *grpcPlugin) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
if p.client == nil {
return true
}
var options admission.Options
for _, opt := range opts {
opt(&options)
}
r, err := p.client.Admit(ctx,
&proto.AdmissionRequest{
Service: options.Service,
Network: network,
Addr: addr,
})
if err != nil {
p.log.Error(err)
return false
}
return r.Ok
}
// Close closes the underlying gRPC connection if it implements io.Closer.
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
-199
View File
@@ -1,199 +0,0 @@
package admission
import (
"context"
"io"
"net"
"os"
"testing"
"github.com/go-gost/core/admission"
"github.com/go-gost/plugin/admission/proto"
"github.com/go-gost/x/internal/plugin"
xlogger "github.com/go-gost/x/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
corelogger "github.com/go-gost/core/logger"
)
func TestMain(m *testing.M) {
corelogger.SetDefault(xlogger.Nop())
os.Exit(m.Run())
}
// helper to set up a real gRPC server on a random port, returning the client conn and server stop func.
func newTestGRPCConn(t *testing.T, srv proto.AdmissionServer) (*grpc.ClientConn, func()) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterAdmissionServer(gsrv, srv)
go gsrv.Serve(lis)
conn, err := grpc.NewClient(lis.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
return conn, func() {
conn.Close()
gsrv.Stop()
}
}
func TestGRPCPlugin_FailOpen(t *testing.T) {
// Test fail-open: create directly with nil client to ensure nil path is covered.
p := &grpcPlugin{
conn: nil,
client: nil,
log: xlogger.Nop(),
}
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Admit_Success(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAdmissionServer{admit: true})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAdmissionClient(conn),
log: xlogger.Nop(),
}
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Admit_Deny(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAdmissionServer{admit: false})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAdmissionClient(conn),
log: xlogger.Nop(),
}
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Admit_WithService(t *testing.T) {
server := &testAdmissionServer{admit: true}
conn, cleanup := newTestGRPCConn(t, server)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAdmissionClient(conn),
log: xlogger.Nop(),
}
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1", admission.WithService("myservice")))
assert.Equal(t, "myservice", server.lastService)
}
func TestGRPCPlugin_Admit_NilClient(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
}
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Close_NilConn(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_NonCloserConn(t *testing.T) {
p := &grpcPlugin{
conn: &nonCloserConn{},
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_WithRealConn(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAdmissionServer{admit: true})
defer cleanup()
// conn is *grpc.ClientConn which implements io.Closer
p := &grpcPlugin{
conn: conn,
client: nil,
log: xlogger.Nop(),
}
assert.NoError(t, p.Close())
}
func TestNewGRPCPlugin_RealConn(t *testing.T) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterAdmissionServer(gsrv, &testAdmissionServer{admit: true})
go gsrv.Serve(lis)
defer gsrv.Stop()
// Call the real NewGRPCPlugin function.
p := NewGRPCPlugin("test", lis.Addr().String(), plugin.TimeoutOption(500))
require.NotNil(t, p)
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
assert.NoError(t, p.(io.Closer).Close())
}
func TestGRPCPlugin_Admit_ServerError(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &errorAdmissionServer{})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAdmissionClient(conn),
log: xlogger.Nop(),
}
// Server returns an error; Admit should return false.
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
// --- test helpers ---
type testAdmissionServer struct {
proto.UnimplementedAdmissionServer
admit bool
lastService string
}
func (s *testAdmissionServer) Admit(ctx context.Context, req *proto.AdmissionRequest) (*proto.AdmissionReply, error) {
s.lastService = req.Service
return &proto.AdmissionReply{Ok: s.admit}, nil
}
type errorAdmissionServer struct {
proto.UnimplementedAdmissionServer
}
func (s *errorAdmissionServer) Admit(ctx context.Context, req *proto.AdmissionRequest) (*proto.AdmissionReply, error) {
return nil, status.Error(codes.Internal, "internal error")
}
type nonCloserConn struct{}
func (n *nonCloserConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
return nil
}
func (n *nonCloserConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
}
var _ grpc.ClientConnInterface = (*nonCloserConn)(nil)
var _ io.Closer = (*grpcPlugin)(nil)
-129
View File
@@ -1,129 +0,0 @@
package admission
import (
"bytes"
"context"
"encoding/json"
"net/http"
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/internal/plugin"
)
// httpPluginRequest is the JSON payload sent to the HTTP admission service.
type httpPluginRequest struct {
Service string `json:"service"`
Network string `json:"network"`
Addr string `json:"addr"`
}
// httpPluginResponse is the JSON response expected from the HTTP
// admission service. OK indicates whether the address is admitted.
type httpPluginResponse struct {
OK bool `json:"ok"`
}
// httpPlugin is an admission controller that delegates to an external
// HTTP admission service via JSON POST requests.
//
// The HTTP client is created via plugin.NewHTTPClient, which configures
// timeouts, TLS, and token-based authentication from plugin options.
type httpPlugin struct {
url string
client *http.Client
header http.Header
log logger.Logger
}
// NewHTTPPlugin creates an admission controller that communicates with
// an external HTTP admission service at the given URL.
//
// The name is used only for log identification. On each Admit call,
// a JSON-encoded httpPluginRequest is POSTed to the URL. A 200 response
// with {"ok": true} admits the address; any other response denies it.
//
// If the HTTP client is nil, all admission requests return false.
func NewHTTPPlugin(name string, url string, opts ...plugin.Option) admission.Admission {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
return &httpPlugin{
url: url,
client: plugin.NewHTTPClient(&options),
header: options.Header,
log: logger.Default().WithFields(map[string]any{
"kind": "admission",
"admission": name,
}),
}
}
// Admit sends a JSON POST request to the configured HTTP endpoint.
// The request includes the service name, network, and client address.
//
// The address is admitted only if ALL of the following are true:
// - The HTTP client is non-nil
// - The request is successfully sent
// - The response has status 200 OK
// - The response body decodes as JSON with "ok": true
//
// Any error, non-200 status, or decode failure results in denial.
func (p *httpPlugin) Admit(ctx context.Context, network, addr string, opts ...admission.Option) (ok bool) {
if p.client == nil {
return
}
var options admission.Options
for _, opt := range opts {
opt(&options)
}
rb := httpPluginRequest{
Service: options.Service,
Network: network,
Addr: addr,
}
v, err := json.Marshal(&rb)
if err != nil {
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
if err != nil {
return
}
if p.header != nil {
req.Header = p.header.Clone()
}
req.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
res := httpPluginResponse{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return
}
return res.OK
}
// Close closes idle connections in the HTTP client's connection pool.
// It does not shut down the client itself.
func (p *httpPlugin) Close() error {
if p.client != nil {
if tr := plugin.HTTPClientTransport(p.client); tr != nil {
tr.CloseIdleConnections()
}
}
return nil
}
-161
View File
@@ -1,161 +0,0 @@
package admission
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-gost/core/admission"
"github.com/go-gost/x/internal/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHTTPPlugin(t *testing.T) {
p := NewHTTPPlugin("test", "http://localhost:9999")
require.NotNil(t, p)
hp, ok := p.(*httpPlugin)
require.True(t, ok)
assert.Equal(t, "http://localhost:9999", hp.url)
assert.NotNil(t, hp.client)
assert.NotNil(t, hp.log)
}
func TestHTTPPlugin_Admit_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_Deny(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": false}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_Non200(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_InvalidJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`not json`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_ConnectionRefused(t *testing.T) {
p := NewHTTPPlugin("test", "http://127.0.0.1:0", plugin.TimeoutOption(0))
assert.False(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_NilClient(t *testing.T) {
hp := &httpPlugin{
url: "http://localhost",
client: nil,
}
assert.False(t, hp.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_WithServiceAndHeader(t *testing.T) {
var receivedService string
var receivedHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedService = r.URL.Query().Get("check_service")
receivedHeader = r.Header.Get("X-Custom")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
"X-Custom": []string{"test-value"},
}))
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1", admission.WithService("myservice")))
_ = receivedService
_ = receivedHeader
}
func TestHTTPPlugin_Admit_WithNoHeader(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
// Create plugin without custom headers
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
header: nil,
}
assert.True(t, hp.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Close_NilClient(t *testing.T) {
hp := &httpPlugin{client: nil}
err := hp.Close()
assert.NoError(t, err)
}
func TestHTTPPlugin_Close_WithClient(t *testing.T) {
hp := &httpPlugin{
client: plugin.NewHTTPClient(&plugin.Options{}),
}
err := hp.Close()
assert.NoError(t, err)
}
func TestHTTPPlugin_Admit_WithAllOptions(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer mytoken", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
// Test with various plugin options
p := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
header: http.Header{"Authorization": []string{"Bearer mytoken"}},
}
assert.True(t, p.Admit(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Admit_BadURL(t *testing.T) {
// A URL that causes http.NewRequestWithContext to fail.
hp := &httpPlugin{
url: "http://invalid hostname", // space causes NewRequest to fail
client: plugin.NewHTTPClient(&plugin.Options{}),
}
assert.False(t, hp.Admit(context.Background(), "tcp", "192.168.1.1"))
}
var _ io.Closer = (*httpPlugin)(nil)
+17 -116
View File
@@ -8,8 +8,7 @@ import (
"syscall"
"github.com/go-gost/core/admission"
"github.com/go-gost/x/ctx"
xio "github.com/go-gost/x/internal/io"
"github.com/go-gost/core/metadata"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/net/udp"
)
@@ -18,22 +17,11 @@ var (
errUnsupport = errors.New("unsupported operation")
)
// serverConn wraps a TCP connection (net.Conn) with per-read admission
// checking. Unlike the listener wrapper — which rejects at accept time —
// this wrapper re-validates the remote address on each Read call.
// This is useful when the admission rules may change during the lifetime
// of a connection, or when the connection was established before
// admission wrappers were applied.
type serverConn struct {
net.Conn
admission admission.Admission
}
// WrapConn wraps a net.Conn with per-read admission control.
// If admission is nil, the original connection is returned unchanged.
//
// When admission is denied on a Read, io.EOF is returned so the caller
// sees a clean end-of-stream rather than a hard error.
func WrapConn(admission admission.Admission, c net.Conn) net.Conn {
if admission == nil {
return c
@@ -44,27 +32,15 @@ func WrapConn(admission admission.Admission, c net.Conn) net.Conn {
}
}
// UnwrapConn returns the underlying connection, allowing type assertions
// through wrapper layers.
func (c *serverConn) UnwrapConn() net.Conn {
return c.Conn
}
// Read checks the remote address against the admission controller
// before delegating to the underlying connection. If denied, it returns
// io.EOF to signal end-of-stream.
func (c *serverConn) Read(b []byte) (n int, err error) {
if c.admission != nil &&
!c.admission.Admit(context.Background(), c.RemoteAddr().Network(), c.RemoteAddr().String()) {
!c.admission.Admit(context.Background(), c.RemoteAddr().String()) {
err = io.EOF
return
}
return c.Conn.Read(b)
}
// SyscallConn exposes the underlying raw connection for socket-level
// operations (e.g. setting SO_MARK, SO_REUSEPORT). Returns
// errUnsupport if the inner connection does not support this.
func (c *serverConn) SyscallConn() (rc syscall.RawConn, err error) {
if sc, ok := c.Conn.(syscall.Conn); ok {
rc, err = sc.SyscallConn()
@@ -74,44 +50,18 @@ func (c *serverConn) SyscallConn() (rc syscall.RawConn, err error) {
return
}
// CloseRead shuts down the read side of the connection if supported.
func (c *serverConn) CloseRead() error {
if sc, ok := c.Conn.(xio.CloseRead); ok {
return sc.CloseRead()
}
return xio.ErrUnsupported
}
// CloseWrite shuts down the write side of the connection if supported.
func (c *serverConn) CloseWrite() error {
if sc, ok := c.Conn.(xio.CloseWrite); ok {
return sc.CloseWrite()
}
return xio.ErrUnsupported
}
// Context returns the context carried by the underlying connection,
// or nil if the connection does not carry context.
func (c *serverConn) Context() context.Context {
if innerCtx, ok := c.Conn.(ctx.Context); ok {
return innerCtx.Context()
func (c *serverConn) Metadata() metadata.Metadata {
if md, ok := c.Conn.(metadata.Metadatable); ok {
return md.Metadata()
}
return nil
}
// packetConn wraps a net.PacketConn with per-packet admission checking.
// Packets from denied addresses are silently dropped and the read
// loop continues to the next packet.
type packetConn struct {
net.PacketConn
admission admission.Admission
}
// WrapPacketConn wraps a net.PacketConn with per-packet admission control.
// If admission is nil, the original connection is returned unchanged.
//
// Denied packets are silently discarded; ReadFrom loops until it receives
// a packet from an allowed address or encounters an error.
func WrapPacketConn(admission admission.Admission, pc net.PacketConn) net.PacketConn {
if admission == nil {
return pc
@@ -122,8 +72,6 @@ func WrapPacketConn(admission admission.Admission, pc net.PacketConn) net.Packet
}
}
// ReadFrom reads the next packet from an admitted address. Packets from
// denied addresses are discarded and the method retries automatically.
func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
n, addr, err = c.PacketConn.ReadFrom(p)
@@ -132,7 +80,7 @@ func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
!c.admission.Admit(context.Background(), addr.String()) {
continue
}
@@ -140,49 +88,25 @@ func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
}
}
// Context returns the context carried by the underlying packet connection.
func (c *packetConn) Context() context.Context {
if innerCtx, ok := c.PacketConn.(ctx.Context); ok {
return innerCtx.Context()
func (c *packetConn) Metadata() metadata.Metadata {
if md, ok := c.PacketConn.(metadata.Metadatable); ok {
return md.Metadata()
}
return nil
}
// udpConn wraps a packet connection as a UDP-specific connection.
// It provides the same admission filtering as packetConn, plus UDP-
// specific operations (ReadFromUDP, ReadMsgUDP, WriteToUDP, WriteMsgUDP,
// SetDSCP, etc.).
//
// The wrapper delegates to optional interfaces on the underlying
// connection, returning errUnsupport if the capability is not available.
type udpConn struct {
net.PacketConn
admission admission.Admission
}
// WrapUDPConn wraps a net.PacketConn as a udp.Conn with admission control.
// This is used in UDP forwarding paths where the connection is treated
// as a connected UDP socket.
// If pc is nil, nil is returned. If admission is nil, the original connection
// is returned unchanged (no-op).
func WrapUDPConn(adm admission.Admission, pc net.PacketConn) udp.Conn {
if pc == nil {
return nil
}
if adm == nil {
if uc, ok := pc.(udp.Conn); ok {
return uc
}
}
func WrapUDPConn(admission admission.Admission, pc net.PacketConn) udp.Conn {
return &udpConn{
PacketConn: pc,
admission: adm,
admission: admission,
}
}
// RemoteAddr returns the remote address from the underlying connection
// if it implements the RemoteAddr interface. This is used for "connected"
// UDP sockets that have a fixed peer.
func (c *udpConn) RemoteAddr() net.Addr {
if nc, ok := c.PacketConn.(xnet.RemoteAddr); ok {
return nc.RemoteAddr()
@@ -190,7 +114,6 @@ func (c *udpConn) RemoteAddr() net.Addr {
return nil
}
// SetReadBuffer sets the socket receive buffer size.
func (c *udpConn) SetReadBuffer(n int) error {
if nc, ok := c.PacketConn.(xnet.SetBuffer); ok {
return nc.SetReadBuffer(n)
@@ -198,7 +121,6 @@ func (c *udpConn) SetReadBuffer(n int) error {
return errUnsupport
}
// SetWriteBuffer sets the socket send buffer size.
func (c *udpConn) SetWriteBuffer(n int) error {
if nc, ok := c.PacketConn.(xnet.SetBuffer); ok {
return nc.SetWriteBuffer(n)
@@ -206,8 +128,6 @@ func (c *udpConn) SetWriteBuffer(n int) error {
return errUnsupport
}
// Read delegates to the underlying connection's Read method if available
// (for connected UDP sockets that support stream-like reads).
func (c *udpConn) Read(b []byte) (n int, err error) {
if nc, ok := c.PacketConn.(io.Reader); ok {
n, err = nc.Read(b)
@@ -217,8 +137,6 @@ func (c *udpConn) Read(b []byte) (n int, err error) {
return
}
// ReadFrom reads the next packet from an admitted address, discarding
// packets from denied addresses.
func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
n, addr, err = c.PacketConn.ReadFrom(p)
@@ -226,15 +144,13 @@ func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
!c.admission.Admit(context.Background(), addr.String()) {
continue
}
return
}
}
// ReadFromUDP reads the next UDP packet from an admitted address.
// Packets from denied addresses are discarded transparently.
func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
if nc, ok := c.PacketConn.(udp.ReadUDP); ok {
for {
@@ -243,7 +159,7 @@ func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
!c.admission.Admit(context.Background(), addr.String()) {
continue
}
return
@@ -253,8 +169,6 @@ func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
return
}
// ReadMsgUDP reads a UDP packet with out-of-band data from an admitted
// address. Packets from denied addresses are silently skipped.
func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
if nc, ok := c.PacketConn.(udp.ReadUDP); ok {
for {
@@ -263,7 +177,7 @@ func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAd
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
!c.admission.Admit(context.Background(), addr.String()) {
continue
}
return
@@ -273,8 +187,6 @@ func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAd
return
}
// Write delegates to the underlying connection's Write method if available
// (for connected UDP sockets).
func (c *udpConn) Write(b []byte) (n int, err error) {
if nc, ok := c.PacketConn.(io.Writer); ok {
n, err = nc.Write(b)
@@ -284,15 +196,11 @@ func (c *udpConn) Write(b []byte) (n int, err error) {
return
}
// WriteTo sends a packet to the specified address via the underlying
// packet connection. No admission check is performed on writes.
func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
n, err = c.PacketConn.WriteTo(p, addr)
return
}
// WriteToUDP sends a UDP packet to the specified address if the
// underlying connection supports UDP-specific writes.
func (c *udpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) {
if nc, ok := c.PacketConn.(udp.WriteUDP); ok {
n, err = nc.WriteToUDP(b, addr)
@@ -302,7 +210,6 @@ func (c *udpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) {
return
}
// WriteMsgUDP sends a UDP packet with out-of-band data if supported.
func (c *udpConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
if nc, ok := c.PacketConn.(udp.WriteUDP); ok {
n, oobn, err = nc.WriteMsgUDP(b, oob, addr)
@@ -312,8 +219,6 @@ func (c *udpConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, er
return
}
// SyscallConn exposes the underlying raw connection for socket-level
// operations if supported.
func (c *udpConn) SyscallConn() (rc syscall.RawConn, err error) {
if nc, ok := c.PacketConn.(xnet.SyscallConn); ok {
return nc.SyscallConn()
@@ -322,9 +227,6 @@ func (c *udpConn) SyscallConn() (rc syscall.RawConn, err error) {
return
}
// SetDSCP sets the Differentiated Services Code Point on the socket
// for traffic classification/QoS. Silently succeeds if not supported
// (best-effort).
func (c *udpConn) SetDSCP(n int) error {
if nc, ok := c.PacketConn.(xnet.SetDSCP); ok {
return nc.SetDSCP(n)
@@ -332,10 +234,9 @@ func (c *udpConn) SetDSCP(n int) error {
return nil
}
// Context returns the context carried by the underlying packet connection.
func (c *udpConn) Context() context.Context {
if innerCtx, ok := c.PacketConn.(ctx.Context); ok {
return innerCtx.Context()
func (c *udpConn) Metadata() metadata.Metadata {
if md, ok := c.PacketConn.(metadata.Metadatable); ok {
return md.Metadata()
}
return nil
}
File diff suppressed because it is too large. Load diff
+2 -51
View File
@@ -1,14 +1,3 @@
// Package wrapper provides net.Listener and net.Conn wrappers that
// enforce admission control at the network level.
//
// The listener wrapper checks each accepted connection against the
// admission controller before returning it to the caller. Denied
// connections are closed immediately and the listener continues
// accepting (transparent rejection).
//
// The connection wrappers (TCP, UDP, generic packet) check admission
// on each read operation, effectively dropping traffic from denied
// addresses without closing the underlying transport.
package wrapper
import (
@@ -17,70 +6,32 @@ import (
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
xctx "github.com/go-gost/x/ctx"
)
// listener wraps a net.Listener and performs admission checks on each
// accepted connection. Connections from denied addresses are silently
// closed and the accept loop continues.
type listener struct {
service string
net.Listener
admission admission.Admission
log logger.Logger
}
// WrapListener wraps a net.Listener with admission control. If admission
// is nil, the original listener is returned unchanged (no-op).
//
// The service name is passed to the admission controller via
// admission.WithService so that the controller can distinguish
// between different services sharing the same admission rules.
func WrapListener(service string, admission admission.Admission, ln net.Listener) net.Listener {
func WrapListener(admission admission.Admission, ln net.Listener) net.Listener {
if admission == nil {
return ln
}
return &listener{
service: service,
Listener: ln,
admission: admission,
}
}
// Accept blocks until a connection is accepted from the underlying
// listener, then checks it against the admission controller. Denied
// connections are closed and the loop retries. The caller never sees
// a rejected connection.
//
// The client address used for the admission check is determined by:
// 1. The srcAddr value from the connection's context (if available),
// which is the original client address before any proxy protocol
// or NAT rewriting.
// 2. Falling back to the raw RemoteAddr of the accepted connection.
func (ln *listener) Accept() (net.Conn, error) {
for {
c, err := ln.Listener.Accept()
if err != nil {
return nil, err
}
// Extract the context from the connection if it carries one.
ctx := context.Background()
if cc, ok := c.(xctx.Context); ok {
if cv := cc.Context(); cv != nil {
ctx = cv
}
}
// Prefer the original source address from context (e.g. set by
// proxy protocol handling), falling back to the raw remote address.
clientAddr := c.RemoteAddr()
if addr := xctx.SrcAddrFromContext(ctx); addr != nil {
clientAddr = addr
}
if ln.admission != nil &&
!ln.admission.Admit(ctx, clientAddr.Network(), clientAddr.String(), admission.WithService(ln.service)) {
!ln.admission.Admit(context.Background(), c.RemoteAddr().String()) {
c.Close()
continue
}
-234
View File
@@ -1,234 +0,0 @@
package wrapper
import (
"context"
"errors"
"net"
"testing"
"github.com/go-gost/core/admission"
xctx "github.com/go-gost/x/ctx"
"github.com/stretchr/testify/assert"
)
// --- WrapListener tests ---
func TestWrapListener_NilAdmission(t *testing.T) {
ln := &fakeListener{}
result := WrapListener("myservice", nil, ln)
assert.Equal(t, ln, result) // should return the original listener
}
func TestWrapListener_WithAdmission(t *testing.T) {
ln := &fakeListener{}
result := WrapListener("myservice", alwaysAdmitAdmission{}, ln)
assert.IsType(t, &listener{}, result)
}
// --- listener.Accept tests ---
func TestListener_Accept_Admit(t *testing.T) {
c := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234}}
ln := &fakeListener{conns: []net.Conn{c}}
wrapped := WrapListener("myservice", alwaysAdmitAdmission{}, ln)
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.NotNil(t, conn)
assert.False(t, c.closed)
}
func TestListener_Accept_Deny(t *testing.T) {
c := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234}}
denied := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678}}
ln := &fakeListener{conns: []net.Conn{denied, c}}
wrapped := WrapListener("myservice", &denyFirstConnAdmission{}, ln)
// First connection is denied, second is admitted
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.Equal(t, c, conn)
assert.True(t, denied.closed)
}
func TestListener_Accept_Error(t *testing.T) {
ln := &fakeListener{acceptErr: errors.New("accept failed")}
wrapped := WrapListener("myservice", alwaysAdmitAdmission{}, ln)
_, err := wrapped.Accept()
assert.EqualError(t, err, "accept failed")
}
func TestListener_Accept_WithContextSrcAddr(t *testing.T) {
srcAddr := &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1234}
c := &fakeConn{
raddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234},
ctx: xctx.ContextWithSrcAddr(context.Background(), srcAddr),
}
ln := &fakeListener{conns: []net.Conn{c}}
// Use an admission that records the address it receives
recorder := &recordingAdmission{admit: true}
wrapped := WrapListener("myservice", recorder, ln)
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.NotNil(t, conn)
// Should have used the src addr from context (String() includes port)
assert.Equal(t, "1.2.3.4:1234", recorder.lastAddr)
}
func TestListener_Accept_WithContextNilContext(t *testing.T) {
c := &fakeConn{
raddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234},
ctx: nil, // returns nil context
}
ln := &fakeListener{conns: []net.Conn{c}}
recorder := &recordingAdmission{admit: true}
wrapped := WrapListener("myservice", recorder, ln)
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.NotNil(t, conn)
// Should fall back to RemoteAddr (String() includes port)
assert.Equal(t, "192.168.1.1:1234", recorder.lastAddr)
}
func TestListener_Accept_NoContextInterface(t *testing.T) {
// A plain net.Conn without xctx.Context support
c := &plainNetConn{raddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 9999}}
ln := &fakeListener{conns: []net.Conn{c}}
recorder := &recordingAdmission{admit: true}
wrapped := WrapListener("myservice", recorder, ln)
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.NotNil(t, conn)
// Should use RemoteAddr (String() includes port)
assert.Equal(t, "10.0.0.1:9999", recorder.lastAddr)
}
func TestListener_Accept_MultipleDeniesThenAdmit(t *testing.T) {
denied1 := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1}}
denied2 := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.2"), Port: 2}}
admitted := &fakeConn{raddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 3}}
ln := &fakeListener{conns: []net.Conn{denied1, denied2, admitted}}
// Deny the first two, admit the third
count := 0
conditional := &conditionalAdmission{
fn: func(ctx context.Context, network, addr string, opts ...admission.Option) bool {
count++
return count > 2
},
}
wrapped := WrapListener("myservice", conditional, ln)
conn, err := wrapped.Accept()
assert.NoError(t, err)
assert.Equal(t, admitted, conn)
assert.True(t, denied1.closed)
assert.True(t, denied2.closed)
assert.False(t, admitted.closed)
}
// Test that Close is forwarded to the underlying listener
func TestListener_Close(t *testing.T) {
ln := &fakeListener{}
wrapped := WrapListener("myservice", alwaysAdmitAdmission{}, ln)
err := wrapped.Close()
assert.NoError(t, err)
assert.True(t, ln.closed)
}
// Test that Addr is forwarded to the underlying listener
func TestListener_Addr(t *testing.T) {
ln := &fakeListener{}
wrapped := WrapListener("myservice", alwaysAdmitAdmission{}, ln)
addr := wrapped.Addr()
assert.NotNil(t, addr)
}
// --- fake types for listener tests ---
type fakeListener struct {
conns []net.Conn
pos int
acceptErr error
closed bool
}
func (ln *fakeListener) Accept() (net.Conn, error) {
if ln.acceptErr != nil {
return nil, ln.acceptErr
}
if ln.pos >= len(ln.conns) {
return nil, errors.New("no more connections")
}
c := ln.conns[ln.pos]
ln.pos++
return c, nil
}
func (ln *fakeListener) Close() error {
ln.closed = true
return nil
}
func (ln *fakeListener) Addr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}
}
// --- admission helpers for listener tests ---
type recordingAdmission struct {
admit bool
lastAddr string
}
func (r *recordingAdmission) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
r.lastAddr = addr
return r.admit
}
type denyFirstConnAdmission struct {
count int
}
func (d *denyFirstConnAdmission) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
d.count++
return d.count > 1 // deny first, admit rest
}
type conditionalAdmission struct {
fn func(ctx context.Context, network, addr string, opts ...admission.Option) bool
}
func (c *conditionalAdmission) Admit(ctx context.Context, network, addr string, opts ...admission.Option) bool {
return c.fn(ctx, network, addr, opts...)
}
// Compile-time checks
var (
_ net.Listener = (*fakeListener)(nil)
_ net.Conn = (*fakeConn)(nil)
_ xctx.Context = (*fakeConn)(nil)
_ net.Conn = (*plainNetConn)(nil)
)
// Test that plainNetConn does NOT implement xctx.Context
func TestPlainNetConn_NoContext(t *testing.T) {
c := &plainNetConn{raddr: &net.TCPAddr{}}
_, ok := any(c).(xctx.Context)
assert.False(t, ok)
}
// Test that fakeConn DOES implement xctx.Context
func TestFakeConn_HasContext(t *testing.T) {
c := &fakeConn{raddr: &net.TCPAddr{}}
_, ok := any(c).(xctx.Context)
assert.True(t, ok)
}
+2 -153
View File
@@ -1,14 +1,7 @@
// swagger generate spec -o swagger.yaml
package api
import (
"embed"
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/auth"
)
var (
@@ -17,150 +10,6 @@ var (
)
type Response struct {
Code int `json:"code,omitempty"`
Msg string `json:"msg,omitempty"`
Data interface{} `json:"data,omitempty"`
}
type Options struct {
AccessLog bool
PathPrefix string
Auther auth.Authenticator
}
func Register(r *gin.Engine, opts *Options) {
if opts == nil {
opts = &Options{}
}
r.Use(
cors.New((cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"*"},
AllowPrivateNetwork: true,
})),
gin.Recovery(),
)
if opts.AccessLog {
r.Use(mwLogger())
}
router := r.Group("")
if opts.PathPrefix != "" {
router = router.Group(opts.PathPrefix)
}
router.StaticFS("/docs", http.FS(swaggerDoc))
config := router.Group("/config")
config.Use(mwBasicAuth(opts.Auther))
config.GET("", getConfig)
config.POST("", saveConfig)
config.POST("/reload", reloadConfig)
config.GET("/services", getServiceList)
config.GET("/services/:service", getService)
config.POST("/services", createService)
config.PUT("/services/:service", updateService)
config.DELETE("/services/:service", deleteService)
config.GET("/chains", getChainList)
config.GET("/chains/:chain", getChain)
config.POST("/chains", createChain)
config.PUT("/chains/:chain", updateChain)
config.DELETE("/chains/:chain", deleteChain)
config.GET("/hops", getHopList)
config.GET("/hops/:hop", getHop)
config.POST("/hops", createHop)
config.PUT("/hops/:hop", updateHop)
config.DELETE("/hops/:hop", deleteHop)
config.GET("/authers", getAutherList)
config.GET("/authers/:auther", getAuther)
config.POST("/authers", createAuther)
config.PUT("/authers/:auther", updateAuther)
config.DELETE("/authers/:auther", deleteAuther)
config.GET("/admissions", getAdmissionList)
config.GET("/admissions/:admission", getAdmission)
config.POST("/admissions", createAdmission)
config.PUT("/admissions/:admission", updateAdmission)
config.DELETE("/admissions/:admission", deleteAdmission)
config.GET("/bypasses", getBypassList)
config.GET("/bypasses/:bypass", getBypass)
config.POST("/bypasses", createBypass)
config.PUT("/bypasses/:bypass", updateBypass)
config.DELETE("/bypasses/:bypass", deleteBypass)
config.GET("/resolvers", getResolverList)
config.GET("/resolvers/:resolver", getResolver)
config.POST("/resolvers", createResolver)
config.PUT("/resolvers/:resolver", updateResolver)
config.DELETE("/resolvers/:resolver", deleteResolver)
config.GET("/hosts", getHostsList)
config.GET("/hosts/:hosts", getHosts)
config.POST("/hosts", createHosts)
config.PUT("/hosts/:hosts", updateHosts)
config.DELETE("/hosts/:hosts", deleteHosts)
config.GET("/ingresses", getIngressList)
config.GET("/ingresses/:ingress", getIngress)
config.POST("/ingresses", createIngress)
config.PUT("/ingresses/:ingress", updateIngress)
config.DELETE("/ingresses/:ingress", deleteIngress)
config.GET("/routers", getRouterList)
config.GET("/routers/:router", getRouter)
config.POST("/routers", createRouter)
config.PUT("/routers/:router", updateRouter)
config.DELETE("/routers/:router", deleteRouter)
config.GET("/observers", getObserverList)
config.GET("/observers/:observer", getObserver)
config.POST("/observers", createObserver)
config.PUT("/observers/:observer", updateObserver)
config.DELETE("/observers/:observer", deleteObserver)
config.GET("/recorders", getRecorderList)
config.GET("/recorders/:recorder", getRecorder)
config.POST("/recorders", createRecorder)
config.PUT("/recorders/:recorder", updateRecorder)
config.DELETE("/recorders/:recorder", deleteRecorder)
config.GET("/sds", getSDList)
config.GET("/sds/:sd", getSD)
config.POST("/sds", createSD)
config.PUT("/sds/:sd", updateSD)
config.DELETE("/sds/:sd", deleteSD)
config.GET("/limiters", getLimiterList)
config.GET("/limiters/:limiter", getLimiter)
config.POST("/limiters", createLimiter)
config.PUT("/limiters/:limiter", updateLimiter)
config.DELETE("/limiters/:limiter", deleteLimiter)
config.GET("/quotas", getQuotaList)
config.GET("/quotas/:quota", getQuota)
config.POST("/quotas", createQuota)
config.PUT("/quotas/:quota", updateQuota)
config.DELETE("/quotas/:quota", deleteQuota)
config.POST("/quotas/:quota/reset", resetQuota)
config.GET("/climiters", getConnLimiterList)
config.GET("/climiters/:limiter", getConnLimiter)
config.POST("/climiters", createConnLimiter)
config.PUT("/climiters/:limiter", updateConnLimiter)
config.DELETE("/climiters/:limiter", deleteConnLimiter)
config.GET("/rlimiters", getRateLimiterList)
config.GET("/rlimiters/:limiter", getRateLimiter)
config.POST("/rlimiters", createRateLimiter)
config.PUT("/rlimiters/:limiter", updateRateLimiter)
config.DELETE("/rlimiters/:limiter", deleteRateLimiter)
Code int `json:"code,omitempty"`
Msg string `json:"msg,omitempty"`
}
+3 -78
View File
@@ -7,68 +7,9 @@ import (
"os"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/observer/stats"
"github.com/go-gost/x/config"
"github.com/go-gost/x/registry"
"github.com/go-gost/x/service"
)
type serviceStatus interface {
Status() *service.Status
}
func fillServiceStatus(svc *config.ServiceConfig) {
s := registry.ServiceRegistry().Get(svc.Name)
ss, ok := s.(serviceStatus)
if ok && ss != nil {
status := ss.Status()
svc.Status = &config.ServiceStatus{
CreateTime: status.CreateTime().Unix(),
State: string(status.State()),
}
if st := status.Stats(); st != nil {
svc.Status.Stats = &config.ServiceStats{
TotalConns: st.Get(stats.KindTotalConns),
CurrentConns: st.Get(stats.KindCurrentConns),
TotalErrs: st.Get(stats.KindTotalErrs),
InputBytes: st.Get(stats.KindInputBytes),
OutputBytes: st.Get(stats.KindOutputBytes),
}
}
var stopped bool
for _, qname := range svc.Quotas {
lim := registry.QuotaLimiterRegistry().Get(qname)
if lim == nil {
continue
}
qs := lim.Snapshot()
if qs.Blocked {
stopped = true
}
svc.Status.Quotas = append(svc.Status.Quotas, config.ServiceQuota{
Name: qname,
Used: qs.Used,
Limit: qs.Limit,
StartsAt: qs.StartsAtUnix,
ExpiresAt: qs.ExpiresAtUnix,
Active: qs.Active,
Expired: qs.Expired,
Blocked: qs.Blocked,
Direction: qs.Direction,
})
}
svc.Status.StoppedByLimit = stopped
for _, ev := range status.Events() {
if !ev.Time.IsZero() {
svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{
Time: ev.Time.Unix(),
Msg: ev.Message,
})
}
}
}
}
// swagger:parameters getConfigRequest
type getConfigRequest struct {
// output format, one of yaml|json, default is json.
@@ -96,15 +37,6 @@ func getConfig(ctx *gin.Context) {
var req getConfigRequest
ctx.ShouldBindQuery(&req)
config.OnUpdate(func(c *config.Config) error {
for _, svc := range c.Services {
if svc == nil {
continue
}
fillServiceStatus(svc)
}
return nil
})
var resp getConfigResponse
resp.Config = config.Global()
@@ -130,9 +62,6 @@ type saveConfigRequest struct {
// output format, one of yaml|json, default is yaml.
// in: query
Format string `form:"format" json:"format"`
// file path, default is gost.yaml|gost.json in current working directory.
// in: query
Path string `form:"path" json:"path"`
}
// successful operation.
@@ -163,15 +92,11 @@ func saveConfig(ctx *gin.Context) {
req.Format = "yaml"
}
if req.Path != "" {
file = req.Path
}
f, err := os.Create(file)
if err != nil {
writeError(ctx, &Error{
statusCode: http.StatusInternalServerError,
Code: ErrCodeSaveConfigFailed,
Code: 40005,
Msg: fmt.Sprintf("create file: %s", err.Error()),
})
return
@@ -181,8 +106,8 @@ func saveConfig(ctx *gin.Context) {
if err := config.Global().Write(f, req.Format); err != nil {
writeError(ctx, &Error{
statusCode: http.StatusInternalServerError,
Code: ErrCodeSaveConfigFailed,
Msg: fmt.Sprintf("save config: %s", err.Error()),
Code: 40006,
Msg: fmt.Sprintf("write: %s", err.Error()),
})
return
}
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getAdmissionListRequest
type getAdmissionListRequest struct {
}
// successful operation.
// swagger:response getAdmissionListResponse
type getAdmissionListResponse struct {
// in: body
Data admissionList
}
type admissionList struct {
Count int `json:"count"`
List []*config.AdmissionConfig `json:"list"`
}
func getAdmissionList(ctx *gin.Context) {
// swagger:route GET /config/admissions Admission getAdmissionListRequest
//
// Get admission list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getAdmissionListResponse
var req getAdmissionListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Admissions
var resp getAdmissionListResponse
resp.Data = admissionList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getAdmissionRequest
type getAdmissionRequest struct {
// in: path
// required: true
Admission string `uri:"admission" json:"admission"`
}
// successful operation.
// swagger:response getAdmissionResponse
type getAdmissionResponse struct {
// in: body
Data *config.AdmissionConfig
}
func getAdmission(ctx *gin.Context) {
// swagger:route GET /config/admissions/{admission} Admission getAdmissionRequest
//
// Get admission.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getAdmissionResponse
var req getAdmissionRequest
ctx.ShouldBindUri(&req)
var resp getAdmissionResponse
for _, admission := range config.Global().Admissions {
if admission == nil {
continue
}
if admission.Name == req.Admission {
resp.Data = admission
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createAdmissionRequest
type createAdmissionRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createAdmission(ctx *gin.Context) {
var req createAdmissionRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "admission name is required"))
return
}
req.Data.Name = name
if registry.AdmissionRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("admission %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseAdmission(&req.Data)
if err := registry.AdmissionRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("admission %s already exists", name)))
if err := registry.AdmissionRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateAdmission(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Admission)
if !registry.AdmissionRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("admission %s not found", name)))
if !registry.AdmissionRegistry().IsRegistered(req.Admission) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Admission
v := parser.ParseAdmission(&req.Data)
registry.AdmissionRegistry().Unregister(name)
registry.AdmissionRegistry().Unregister(req.Admission)
if err := registry.AdmissionRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("admission %s already exists", name)))
if err := registry.AdmissionRegistry().Register(req.Admission, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Admissions {
if c.Admissions[i].Name == name {
if c.Admissions[i].Name == req.Admission {
c.Admissions[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteAdmission(ctx *gin.Context) {
var req deleteAdmissionRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Admission)
if !registry.AdmissionRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("admission %s not found", name)))
if !registry.AdmissionRegistry().IsRegistered(req.Admission) {
writeError(ctx, ErrNotFound)
return
}
registry.AdmissionRegistry().Unregister(name)
registry.AdmissionRegistry().Unregister(req.Admission)
config.OnUpdate(func(c *config.Config) error {
admissiones := c.Admissions
c.Admissions = nil
for _, s := range admissiones {
if s.Name == name {
if s.Name == req.Admission {
continue
}
c.Admissions = append(c.Admissions, s)
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getAutherListRequest
type getAutherListRequest struct {
}
// successful operation.
// swagger:response getAutherListResponse
type getAutherListResponse struct {
// in: body
Data autherList
}
type autherList struct {
Count int `json:"count"`
List []*config.AutherConfig `json:"list"`
}
func getAutherList(ctx *gin.Context) {
// swagger:route GET /config/authers Auther getAutherListRequest
//
// Get auther list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getAutherListResponse
var req getAutherListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Authers
var resp getAutherListResponse
resp.Data = autherList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getAutherRequest
type getAutherRequest struct {
// in: path
// required: true
Auther string `uri:"auther" json:"auther"`
}
// successful operation.
// swagger:response getAutherResponse
type getAutherResponse struct {
// in: body
Data *config.AutherConfig
}
func getAuther(ctx *gin.Context) {
// swagger:route GET /config/authers/{auther} Auther getAutherRequest
//
// Get auther.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getAutherResponse
var req getAutherRequest
ctx.ShouldBindUri(&req)
var resp getAutherResponse
for _, auther := range config.Global().Authers {
if auther == nil {
continue
}
if auther.Name == req.Auther {
resp.Data = auther
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createAutherRequest
type createAutherRequest struct {
// in: body
@@ -124,21 +35,14 @@ func createAuther(ctx *gin.Context) {
var req createAutherRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "auther name is required"))
return
}
req.Data.Name = name
if registry.AutherRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("auther %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseAuther(&req.Data)
if err := registry.AutherRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("auther %s already exists", name)))
if err := registry.AutherRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -182,26 +86,24 @@ func updateAuther(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Auther)
if !registry.AutherRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("auther %s not found", name)))
if !registry.AutherRegistry().IsRegistered(req.Auther) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Auther
v := parser.ParseAuther(&req.Data)
registry.AutherRegistry().Unregister(name)
registry.AutherRegistry().Unregister(req.Auther)
if err := registry.AutherRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("auther %s already exists", name)))
if err := registry.AutherRegistry().Register(req.Auther, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Authers {
if c.Authers[i].Name == name {
if c.Authers[i].Name == req.Auther {
c.Authers[i] = &req.Data
break
}
@@ -241,19 +143,17 @@ func deleteAuther(ctx *gin.Context) {
var req deleteAutherRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Auther)
if !registry.AutherRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("auther %s not found", name)))
if !registry.AutherRegistry().IsRegistered(req.Auther) {
writeError(ctx, ErrNotFound)
return
}
registry.AutherRegistry().Unregister(name)
registry.AutherRegistry().Unregister(req.Auther)
config.OnUpdate(func(c *config.Config) error {
authers := c.Authers
c.Authers = nil
for _, s := range authers {
if s.Name == name {
if s.Name == req.Auther {
continue
}
c.Authers = append(c.Authers, s)
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getBypassListRequest
type getBypassListRequest struct {
}
// successful operation.
// swagger:response getBypassListResponse
type getBypassListResponse struct {
// in: body
Data bypassList
}
type bypassList struct {
Count int `json:"count"`
List []*config.BypassConfig `json:"list"`
}
func getBypassList(ctx *gin.Context) {
// swagger:route GET /config/bypasses Bypass getBypassListRequest
//
// Get bypass list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getBypassListResponse
var req getBypassListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Bypasses
var resp getBypassListResponse
resp.Data = bypassList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getBypassRequest
type getBypassRequest struct {
// in: path
// required: true
Bypass string `uri:"bypass" json:"bypass"`
}
// successful operation.
// swagger:response getBypassResponse
type getBypassResponse struct {
// in: body
Data *config.BypassConfig
}
func getBypass(ctx *gin.Context) {
// swagger:route GET /config/bypasses/{bypass} Bypass getBypassRequest
//
// Get bypass.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getBypassResponse
var req getBypassRequest
ctx.ShouldBindUri(&req)
var resp getBypassResponse
for _, bypass := range config.Global().Bypasses {
if bypass == nil {
continue
}
if bypass.Name == req.Bypass {
resp.Data = bypass
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createBypassRequest
type createBypassRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createBypass(ctx *gin.Context) {
var req createBypassRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "bypass name is required"))
return
}
req.Data.Name = name
if registry.BypassRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("bypass %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseBypass(&req.Data)
if err := registry.BypassRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("bypass %s already exists", name)))
if err := registry.BypassRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateBypass(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Bypass)
if !registry.BypassRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("bypass %s not found", name)))
if !registry.BypassRegistry().IsRegistered(req.Bypass) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Bypass
v := parser.ParseBypass(&req.Data)
registry.BypassRegistry().Unregister(name)
registry.BypassRegistry().Unregister(req.Bypass)
if err := registry.BypassRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("bypass %s already exists", name)))
if err := registry.BypassRegistry().Register(req.Bypass, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Bypasses {
if c.Bypasses[i].Name == name {
if c.Bypasses[i].Name == req.Bypass {
c.Bypasses[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteBypass(ctx *gin.Context) {
var req deleteBypassRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Bypass)
if !registry.BypassRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("bypass %s not found", name)))
if !registry.BypassRegistry().IsRegistered(req.Bypass) {
writeError(ctx, ErrNotFound)
return
}
registry.BypassRegistry().Unregister(name)
registry.BypassRegistry().Unregister(req.Bypass)
config.OnUpdate(func(c *config.Config) error {
bypasses := c.Bypasses
c.Bypasses = nil
for _, s := range bypasses {
if s.Name == name {
if s.Name == req.Bypass {
continue
}
c.Bypasses = append(c.Bypasses, s)
+19 -120
View File
@@ -1,104 +1,14 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/chain"
"github.com/go-gost/x/registry"
)
// swagger:parameters getChainListRequest
type getChainListRequest struct {
}
// successful operation.
// swagger:response getChainListResponse
type getChainListResponse struct {
// in: body
Data chainList
}
type chainList struct {
Count int `json:"count"`
List []*config.ChainConfig `json:"list"`
}
func getChainList(ctx *gin.Context) {
// swagger:route GET /config/chains Chain getChainListRequest
//
// Get chain list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getChainListResponse
var req getChainListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Chains
var resp getChainListResponse
resp.Data = chainList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getChainRequest
type getChainRequest struct {
// in: path
// required: true
Chain string `uri:"chain" json:"chain"`
}
// successful operation.
// swagger:response getChainResponse
type getChainResponse struct {
// in: body
Data *config.ChainConfig
}
func getChain(ctx *gin.Context) {
// swagger:route GET /config/chains/{chain} Chain getChainRequest
//
// Get chain.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getChainResponse
var req getChainRequest
ctx.ShouldBindUri(&req)
var resp getChainResponse
for _, chain := range config.Global().Chains {
if chain == nil {
continue
}
if chain.Name == req.Chain {
resp.Data = chain
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createChainRequest
type createChainRequest struct {
// in: body
@@ -125,26 +35,19 @@ func createChain(ctx *gin.Context) {
var req createChainRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "chain name is required"))
return
}
req.Data.Name = name
if registry.ChainRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("chain %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v, err := parser.ParseChain(&req.Data, logger.Default())
v, err := parser.ParseChain(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create chain %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
if err := registry.ChainRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("chain %s already exists", name)))
if err := registry.ChainRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -189,31 +92,29 @@ func updateChain(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Chain)
if !registry.ChainRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("chain %s not found", name)))
if !registry.ChainRegistry().IsRegistered(req.Chain) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Chain
v, err := parser.ParseChain(&req.Data, logger.Default())
v, err := parser.ParseChain(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create chain %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
registry.ChainRegistry().Unregister(name)
registry.ChainRegistry().Unregister(req.Chain)
if err := registry.ChainRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("chain %s already exists", name)))
if err := registry.ChainRegistry().Register(req.Chain, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Chains {
if c.Chains[i].Name == name {
if c.Chains[i].Name == req.Chain {
c.Chains[i] = &req.Data
break
}
@@ -253,19 +154,17 @@ func deleteChain(ctx *gin.Context) {
var req deleteChainRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Chain)
if !registry.ChainRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("chain %s not found", name)))
if !registry.ChainRegistry().IsRegistered(req.Chain) {
writeError(ctx, ErrNotFound)
return
}
registry.ChainRegistry().Unregister(name)
registry.ChainRegistry().Unregister(req.Chain)
config.OnUpdate(func(c *config.Config) error {
chains := c.Chains
c.Chains = nil
for _, s := range chains {
if s.Name == name {
if s.Name == req.Chain {
continue
}
c.Chains = append(c.Chains, s)
+16 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getConnLimiterListRequest
type getConnLimiterListRequest struct {
}
// successful operation.
// swagger:response getConnLimiterListResponse
type getConnLimiterListResponse struct {
// in: body
Data connLimiterList
}
type connLimiterList struct {
Count int `json:"count"`
List []*config.LimiterConfig `json:"list"`
}
func getConnLimiterList(ctx *gin.Context) {
// swagger:route GET /config/climiters Limiter getConnLimiterListRequest
//
// Get conn limiter list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getConnLimiterListResponse
var req getConnLimiterListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().CLimiters
var resp getConnLimiterListResponse
resp.Data = connLimiterList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getConnLimiterRequest
type getConnLimiterRequest struct {
// in: path
// required: true
Limiter string `uri:"limiter" json:"limiter"`
}
// successful operation.
// swagger:response getConnLimiterResponse
type getConnLimiterResponse struct {
// in: body
Data *config.LimiterConfig
}
func getConnLimiter(ctx *gin.Context) {
// swagger:route GET /config/climiters/{limiter} Limiter getConnLimiterRequest
//
// Get conn limiter.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getConnLimiterResponse
var req getConnLimiterRequest
ctx.ShouldBindUri(&req)
var resp getConnLimiterResponse
for _, limiter := range config.Global().CLimiters{
if limiter == nil {
continue
}
if limiter.Name == req.Limiter {
resp.Data = limiter
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createConnLimiterRequest
type createConnLimiterRequest struct {
// in: body
@@ -124,21 +35,15 @@ func createConnLimiter(ctx *gin.Context) {
var req createConnLimiterRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "limiter name is required"))
return
}
req.Data.Name = name
if registry.ConnLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseConnLimiter(&req.Data)
if err := registry.ConnLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create limmiter %s failed: %s", name, err.Error())))
if err := registry.ConnLimiterRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -182,27 +87,25 @@ func updateConnLimiter(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Limiter)
if !registry.ConnLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.ConnLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Limiter
v := parser.ParseConnLimiter(&req.Data)
registry.ConnLimiterRegistry().Unregister(name)
registry.ConnLimiterRegistry().Unregister(req.Limiter)
if err := registry.ConnLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if err := registry.ConnLimiterRegistry().Register(req.Limiter, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.CLimiters {
if c.CLimiters[i].Name == name {
if c.CLimiters[i].Name == req.Limiter {
c.CLimiters[i] = &req.Data
break
}
@@ -242,19 +145,17 @@ func deleteConnLimiter(ctx *gin.Context) {
var req deleteConnLimiterRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Limiter)
if !registry.ConnLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.ConnLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
registry.ConnLimiterRegistry().Unregister(name)
registry.ConnLimiterRegistry().Unregister(req.Limiter)
config.OnUpdate(func(c *config.Config) error {
limiteres := c.CLimiters
c.CLimiters = nil
for _, s := range limiteres {
if s.Name == name {
if s.Name == req.Limiter {
continue
}
c.CLimiters = append(c.CLimiters, s)
+19 -119
View File
@@ -1,104 +1,14 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/hop"
"github.com/go-gost/x/registry"
)
// swagger:parameters getHopListRequest
type getHopListRequest struct {
}
// successful operation.
// swagger:response getHopListResponse
type getHopListResponse struct {
// in: body
Data hopList
}
type hopList struct {
Count int `json:"count"`
List []*config.HopConfig `json:"list"`
}
func getHopList(ctx *gin.Context) {
// swagger:route GET /config/hops Hop getHopListRequest
//
// Get hop list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getHopListResponse
var req getHopListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Hops
var resp getHopListResponse
resp.Data = hopList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getHopRequest
type getHopRequest struct {
// in: path
// required: true
Hop string `uri:"hop" json:"hop"`
}
// successful operation.
// swagger:response getHopResponse
type getHopResponse struct {
// in: body
Data *config.HopConfig
}
func getHop(ctx *gin.Context) {
// swagger:route GET /config/hops/{hop} Hop getHopRequest
//
// Get hop.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getHopResponse
var req getHopRequest
ctx.ShouldBindUri(&req)
var resp getHopResponse
for _, hop := range config.Global().Hops {
if hop == nil {
continue
}
if hop.Name == req.Hop {
resp.Data = hop
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createHopRequest
type createHopRequest struct {
// in: body
@@ -125,26 +35,19 @@ func createHop(ctx *gin.Context) {
var req createHopRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "hop name is required"))
return
}
req.Data.Name = name
if registry.HopRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hop %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v, err := parser.ParseHop(&req.Data, logger.Default())
v, err := parser.ParseHop(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create hop %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
if err := registry.HopRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hop %s already exists", name)))
if err := registry.HopRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -189,30 +92,29 @@ func updateHop(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Hop)
if !registry.HopRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("hop %s not found", name)))
if !registry.HopRegistry().IsRegistered(req.Hop) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Hop
v, err := parser.ParseHop(&req.Data, logger.Default())
v, err := parser.ParseHop(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create hop %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
registry.HopRegistry().Unregister(name)
registry.HopRegistry().Unregister(req.Hop)
if err := registry.HopRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hop %s already exists", name)))
if err := registry.HopRegistry().Register(req.Hop, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Hops {
if c.Hops[i].Name == name {
if c.Hops[i].Name == req.Hop {
c.Hops[i] = &req.Data
break
}
@@ -252,19 +154,17 @@ func deleteHop(ctx *gin.Context) {
var req deleteHopRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Hop)
if !registry.HopRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("hop %s not found", name)))
if !registry.HopRegistry().IsRegistered(req.Hop) {
writeError(ctx, ErrNotFound)
return
}
registry.HopRegistry().Unregister(name)
registry.HopRegistry().Unregister(req.Hop)
config.OnUpdate(func(c *config.Config) error {
hops := c.Hops
c.Hops = nil
for _, s := range hops {
if s.Name == name {
if s.Name == req.Hop {
continue
}
c.Hops = append(c.Hops, s)
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getHostsListRequest
type getHostsListRequest struct {
}
// successful operation.
// swagger:response getHostsListResponse
type getHostsListResponse struct {
// in: body
Data hostsList
}
type hostsList struct {
Count int `json:"count"`
List []*config.HostsConfig `json:"list"`
}
func getHostsList(ctx *gin.Context) {
// swagger:route GET /config/hosts Hosts getHostsListRequest
//
// Get hosts list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getHostsListResponse
var req getHostsListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Hosts
var resp getHostsListResponse
resp.Data = hostsList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getHostsRequest
type getHostsRequest struct {
// in: path
// required: true
Hosts string `uri:"hosts" json:"hosts"`
}
// successful operation.
// swagger:response getHostsResponse
type getHostsResponse struct {
// in: body
Data *config.HostsConfig
}
func getHosts(ctx *gin.Context) {
// swagger:route GET /config/hosts/{hosts} Hosts getHostsRequest
//
// Get hosts.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getHostsResponse
var req getHostsRequest
ctx.ShouldBindUri(&req)
var resp getHostsResponse
for _, hosts := range config.Global().Hosts {
if hosts == nil {
continue
}
if hosts.Name == req.Hosts {
resp.Data = hosts
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createHostsRequest
type createHostsRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createHosts(ctx *gin.Context) {
var req createHostsRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "hosts name is required"))
return
}
req.Data.Name = name
if registry.HostsRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hosts %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseHostMapper(&req.Data)
if err := registry.HostsRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hosts %s already exists", name)))
if err := registry.HostsRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateHosts(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Hosts)
if !registry.HostsRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("hosts %s not found", name)))
if !registry.HostsRegistry().IsRegistered(req.Hosts) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Hosts
v := parser.ParseHostMapper(&req.Data)
registry.HostsRegistry().Unregister(name)
registry.HostsRegistry().Unregister(req.Hosts)
if err := registry.HostsRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("hosts %s already exists", name)))
if err := registry.HostsRegistry().Register(req.Hosts, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Hosts {
if c.Hosts[i].Name == name {
if c.Hosts[i].Name == req.Hosts {
c.Hosts[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteHosts(ctx *gin.Context) {
var req deleteHostsRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Hosts)
if !registry.HostsRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("hosts %s not found", name)))
if !registry.HostsRegistry().IsRegistered(req.Hosts) {
writeError(ctx, ErrNotFound)
return
}
registry.HostsRegistry().Unregister(name)
registry.HostsRegistry().Unregister(req.Hosts)
config.OnUpdate(func(c *config.Config) error {
hosts := c.Hosts
c.Hosts = nil
for _, s := range hosts {
if s.Name == name {
if s.Name == req.Hosts {
continue
}
c.Hosts = append(c.Hosts, s)
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getIngressListRequest
type getIngressListRequest struct {
}
// successful operation.
// swagger:response getIngressListResponse
type getIngressListResponse struct {
// in: body
Data ingressList
}
type ingressList struct {
Count int `json:"count"`
List []*config.IngressConfig `json:"list"`
}
func getIngressList(ctx *gin.Context) {
// swagger:route GET /config/ingresses Ingress getIngressListRequest
//
// Get ingress list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getIngressListResponse
var req getIngressListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Ingresses
var resp getIngressListResponse
resp.Data = ingressList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getIngressRequest
type getIngressRequest struct {
// in: path
// required: true
Ingress string `uri:"ingress" json:"ingress"`
}
// successful operation.
// swagger:response getIngressResponse
type getIngressResponse struct {
// in: body
Data *config.IngressConfig
}
func getIngress(ctx *gin.Context) {
// swagger:route GET /config/ingresses/{ingress} Ingress getIngressRequest
//
// Get ingress.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getIngressResponse
var req getIngressRequest
ctx.ShouldBindUri(&req)
var resp getIngressResponse
for _, ingress := range config.Global().Ingresses {
if ingress == nil {
continue
}
if ingress.Name == req.Ingress {
resp.Data = ingress
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createIngressRequest
type createIngressRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createIngress(ctx *gin.Context) {
var req createIngressRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "ingress name is required"))
return
}
req.Data.Name = name
if registry.IngressRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("ingress %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseIngress(&req.Data)
if err := registry.IngressRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("ingress %s already exists", name)))
if err := registry.IngressRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateIngress(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Ingress)
if !registry.IngressRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("ingress %s not found", name)))
if !registry.IngressRegistry().IsRegistered(req.Ingress) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Ingress
v := parser.ParseIngress(&req.Data)
registry.IngressRegistry().Unregister(name)
registry.IngressRegistry().Unregister(req.Ingress)
if err := registry.IngressRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("ingress %s already exists", name)))
if err := registry.IngressRegistry().Register(req.Ingress, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Ingresses {
if c.Ingresses[i].Name == name {
if c.Ingresses[i].Name == req.Ingress {
c.Ingresses[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteIngress(ctx *gin.Context) {
var req deleteIngressRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Ingress)
if !registry.IngressRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("ingress %s not found", name)))
if !registry.IngressRegistry().IsRegistered(req.Ingress) {
writeError(ctx, ErrNotFound)
return
}
registry.IngressRegistry().Unregister(name)
registry.IngressRegistry().Unregister(req.Ingress)
config.OnUpdate(func(c *config.Config) error {
ingresses := c.Ingresses
c.Ingresses = nil
for _, s := range ingresses {
if s.Name == name {
if s.Name == req.Ingress {
continue
}
c.Ingresses = append(c.Ingresses, s)
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getLimiterListRequest
type getLimiterListRequest struct {
}
// successful operation.
// swagger:response getLimiterListResponse
type getLimiterListResponse struct {
// in: body
Data limiterList
}
type limiterList struct {
Count int `json:"count"`
List []*config.LimiterConfig `json:"list"`
}
func getLimiterList(ctx *gin.Context) {
// swagger:route GET /config/limiters Limiter getLimiterListRequest
//
// Get limiter list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getLimiterListResponse
var req getLimiterListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Limiters
var resp getLimiterListResponse
resp.Data = limiterList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getLimiterRequest
type getLimiterRequest struct {
// in: path
// required: true
Limiter string `uri:"limiter" json:"limiter"`
}
// successful operation.
// swagger:response getLimiterResponse
type getLimiterResponse struct {
// in: body
Data *config.LimiterConfig
}
func getLimiter(ctx *gin.Context) {
// swagger:route GET /config/limiters/{limiter} Limiter getLimiterRequest
//
// Get limiter.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getLimiterResponse
var req getLimiterRequest
ctx.ShouldBindUri(&req)
var resp getLimiterResponse
for _, limiter := range config.Global().Limiters {
if limiter == nil {
continue
}
if limiter.Name == req.Limiter {
resp.Data = limiter
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createLimiterRequest
type createLimiterRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createLimiter(ctx *gin.Context) {
var req createLimiterRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "limiter name is required"))
return
}
req.Data.Name = name
if registry.TrafficLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseTrafficLimiter(&req.Data)
if err := registry.TrafficLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if err := registry.TrafficLimiterRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateLimiter(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Limiter)
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.TrafficLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Limiter
v := parser.ParseTrafficLimiter(&req.Data)
registry.TrafficLimiterRegistry().Unregister(name)
registry.TrafficLimiterRegistry().Unregister(req.Limiter)
if err := registry.TrafficLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if err := registry.TrafficLimiterRegistry().Register(req.Limiter, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Limiters {
if c.Limiters[i].Name == name {
if c.Limiters[i].Name == req.Limiter {
c.Limiters[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteLimiter(ctx *gin.Context) {
var req deleteLimiterRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Limiter)
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.TrafficLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
registry.TrafficLimiterRegistry().Unregister(name)
registry.TrafficLimiterRegistry().Unregister(req.Limiter)
config.OnUpdate(func(c *config.Config) error {
limiteres := c.Limiters
c.Limiters = nil
for _, s := range limiteres {
if s.Name == name {
if s.Name == req.Limiter {
continue
}
c.Limiters = append(c.Limiters, s)
-269
View File
@@ -1,269 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/observer"
"github.com/go-gost/x/registry"
)
// swagger:parameters getObserverListRequest
type getObserverListRequest struct {
}
// successful operation.
// swagger:response getObserverListResponse
type getObserverListResponse struct {
// in: body
Data observerList
}
type observerList struct {
Count int `json:"count"`
List []*config.ObserverConfig `json:"list"`
}
func getObserverList(ctx *gin.Context) {
// swagger:route GET /config/observers Observer getObserverListRequest
//
// Get observer list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getObserverListResponse
var req getObserverListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Observers
var resp getObserverListResponse
resp.Data = observerList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getObserverRequest
type getObserverRequest struct {
// in: path
// required: true
Observer string `uri:"observer" json:"observer"`
}
// successful operation.
// swagger:response getObserverResponse
type getObserverResponse struct {
// in: body
Data *config.ObserverConfig
}
func getObserver(ctx *gin.Context) {
// swagger:route GET /config/observers/{observer} Observer getObserverRequest
//
// Get observer.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getObserverResponse
var req getObserverRequest
ctx.ShouldBindUri(&req)
var resp getObserverResponse
for _, observer := range config.Global().Observers {
if observer == nil {
continue
}
if observer.Name == req.Observer {
resp.Data = observer
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createObserverRequest
type createObserverRequest struct {
// in: body
Data config.ObserverConfig `json:"data"`
}
// successful operation.
// swagger:response createObserverResponse
type createObserverResponse struct {
Data Response
}
func createObserver(ctx *gin.Context) {
// swagger:route POST /config/observers Observer createObserverRequest
//
// Create a new observer, the name of the observer must be unique in observer list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: createObserverResponse
var req createObserverRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "observer name is required"))
return
}
req.Data.Name = name
if registry.ObserverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("observer %s already exists", name)))
return
}
v := parser.ParseObserver(&req.Data)
if err := registry.ObserverRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("observer %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
c.Observers = append(c.Observers, &req.Data)
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters updateObserverRequest
type updateObserverRequest struct {
// in: path
// required: true
Observer string `uri:"observer" json:"observer"`
// in: body
Data config.ObserverConfig `json:"data"`
}
// successful operation.
// swagger:response updateObserverResponse
type updateObserverResponse struct {
Data Response
}
func updateObserver(ctx *gin.Context) {
// swagger:route PUT /config/observers/{observer} Observer updateObserverRequest
//
// Update observer by name, the observer must already exist.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: updateObserverResponse
var req updateObserverRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Observer)
if !registry.ObserverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("observer %s not found", name)))
return
}
req.Data.Name = name
v := parser.ParseObserver(&req.Data)
registry.ObserverRegistry().Unregister(name)
if err := registry.ObserverRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("observer %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Observers {
if c.Observers[i].Name == name {
c.Observers[i] = &req.Data
break
}
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters deleteObserverRequest
type deleteObserverRequest struct {
// in: path
// required: true
Observer string `uri:"observer" json:"observer"`
}
// successful operation.
// swagger:response deleteObserverResponse
type deleteObserverResponse struct {
Data Response
}
func deleteObserver(ctx *gin.Context) {
// swagger:route DELETE /config/observers/{observer} Observer deleteObserverRequest
//
// Delete observer by name.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: deleteObserverResponse
var req deleteObserverRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Observer)
if !registry.ObserverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("observer %s not found", name)))
return
}
registry.ObserverRegistry().Unregister(name)
config.OnUpdate(func(c *config.Config) error {
observers := c.Observers
c.Observers = nil
for _, s := range observers {
if s.Name == name {
continue
}
c.Observers = append(c.Observers, s)
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
-350
View File
@@ -1,350 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/quota"
"github.com/go-gost/x/limiter/quota"
"github.com/go-gost/x/registry"
)
func fillQuotaStatus(q *config.QuotaConfig) {
lim := registry.QuotaLimiterRegistry().Get(q.Name)
if lim == nil {
return
}
s := lim.Snapshot()
q.Status = &config.QuotaStatus{
Used: s.Used,
Limit: s.Limit,
StartsAt: s.StartsAtUnix,
ExpiresAt: s.ExpiresAtUnix,
Active: s.Active,
Expired: s.Expired,
Blocked: s.Blocked,
Direction: s.Direction,
}
}
// swagger:parameters getQuotaListRequest
type getQuotaListRequest struct {
}
// successful operation.
// swagger:response getQuotaListResponse
type getQuotaListResponse struct {
// in: body
Data quotaList
}
type quotaList struct {
Count int `json:"count"`
List []*config.QuotaConfig `json:"list"`
}
func getQuotaList(ctx *gin.Context) {
// swagger:route GET /config/quotas Quota getQuotaListRequest
//
// Get quota list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getQuotaListResponse
var req getQuotaListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Quotas
for _, q := range list {
if q == nil {
continue
}
fillQuotaStatus(q)
}
ctx.JSON(http.StatusOK, Response{
Data: quotaList{
Count: len(list),
List: list,
},
})
}
// swagger:parameters getQuotaRequest
type getQuotaRequest struct {
// in: path
// required: true
Quota string `uri:"quota" json:"quota"`
}
// successful operation.
// swagger:response getQuotaResponse
type getQuotaResponse struct {
// in: body
Data *config.QuotaConfig
}
func getQuota(ctx *gin.Context) {
// swagger:route GET /config/quotas/{quota} Quota getQuotaRequest
//
// Get quota.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getQuotaResponse
var req getQuotaRequest
ctx.ShouldBindUri(&req)
var resp getQuotaResponse
for _, q := range config.Global().Quotas {
if q == nil {
continue
}
if q.Name == req.Quota {
fillQuotaStatus(q)
resp.Data = q
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createQuotaRequest
type createQuotaRequest struct {
// in: body
Data config.QuotaConfig `json:"data"`
}
// successful operation.
// swagger:response createQuotaResponse
type createQuotaResponse struct {
Data Response
}
func createQuota(ctx *gin.Context) {
// swagger:route POST /config/quotas Quota createQuotaRequest
//
// Create a new quota, the name of the quota must be unique in quota list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: createQuotaResponse
var req createQuotaRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "quota name is required"))
return
}
req.Data.Name = name
req.Data.Status = nil
if registry.QuotaLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("quota %s already exists", name)))
return
}
v := parser.ParseQuotaLimiter(&req.Data)
if err := registry.QuotaLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("quota %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
c.Quotas = append(c.Quotas, &req.Data)
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters updateQuotaRequest
type updateQuotaRequest struct {
// in: path
// required: true
Quota string `uri:"quota" json:"quota"`
// in: body
Data config.QuotaConfig `json:"data"`
}
// successful operation.
// swagger:response updateQuotaResponse
type updateQuotaResponse struct {
Data Response
}
func updateQuota(ctx *gin.Context) {
// swagger:route PUT /config/quotas/{quota} Quota updateQuotaRequest
//
// Update quota by name, the quota must already exist. The cumulative counter
// is preserved across the update as long as the window is unchanged.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: updateQuotaResponse
var req updateQuotaRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Quota)
if !registry.QuotaLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("quota %s not found", name)))
return
}
req.Data.Name = name
req.Data.Status = nil
v := parser.ParseQuotaLimiter(&req.Data)
registry.QuotaLimiterRegistry().Unregister(name)
if err := registry.QuotaLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("quota %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Quotas {
if c.Quotas[i].Name == name {
c.Quotas[i] = &req.Data
break
}
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters deleteQuotaRequest
type deleteQuotaRequest struct {
// in: path
// required: true
Quota string `uri:"quota" json:"quota"`
}
// successful operation.
// swagger:response deleteQuotaResponse
type deleteQuotaResponse struct {
Data Response
}
func deleteQuota(ctx *gin.Context) {
// swagger:route DELETE /config/quotas/{quota} Quota deleteQuotaRequest
//
// Delete quota by name. Services referencing it stop being limited (fail-open).
//
// Security:
// basicAuth: []
//
// Responses:
// 200: deleteQuotaResponse
var req deleteQuotaRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Quota)
if !registry.QuotaLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("quota %s not found", name)))
return
}
registry.QuotaLimiterRegistry().Unregister(name)
config.OnUpdate(func(c *config.Config) error {
quotas := c.Quotas
c.Quotas = nil
for _, q := range quotas {
if q.Name == name {
continue
}
c.Quotas = append(c.Quotas, q)
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters resetQuotaRequest
type resetQuotaRequest struct {
// in: path
// required: true
Quota string `uri:"quota" json:"quota"`
// in: body
Data resetQuotaData `json:"data"`
}
type resetQuotaData struct {
Used *uint64 `json:"used,omitempty"`
}
// successful operation.
// swagger:response resetQuotaResponse
type resetQuotaResponse struct {
Data Response
}
func resetQuota(ctx *gin.Context) {
// swagger:route POST /config/quotas/{quota}/reset Quota resetQuotaRequest
//
// Overwrite the cumulative counter of a quota (defaults to 0). The new value
// is applied immediately and persisted; if the quota was blocking, services
// resume once the value is below the limit.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: resetQuotaResponse
var req resetQuotaRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Quota)
lim := registry.QuotaLimiterRegistry().Get(name)
if lim == nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("quota %s not found", name)))
return
}
used := uint64(0)
if req.Data.Used != nil {
used = *req.Data.Used
}
lim.Update(quota.Update{Used: &used})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
+15 -115
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getRateLimiterListRequest
type getRateLimiterListRequest struct {
}
// successful operation.
// swagger:response getRateLimiterListResponse
type getRateLimiterListResponse struct {
// in: body
Data rateLimiterList
}
type rateLimiterList struct {
Count int `json:"count"`
List []*config.LimiterConfig `json:"list"`
}
func getRateLimiterList(ctx *gin.Context) {
// swagger:route GET /config/rlimiters Limiter getRateLimiterListRequest
//
// Get rate limiter list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRateLimiterListResponse
var req getRateLimiterListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().CLimiters
var resp getRateLimiterListResponse
resp.Data = rateLimiterList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getRateLimiterRequest
type getRateLimiterRequest struct {
// in: path
// required: true
Limiter string `uri:"limiter" json:"limiter"`
}
// successful operation.
// swagger:response getRateLimiterResponse
type getRateLimiterResponse struct {
// in: body
Data *config.LimiterConfig
}
func getRateLimiter(ctx *gin.Context) {
// swagger:route GET /config/rlimiters/{limiter} Limiter getRateLimiterRequest
//
// Get conn limiter.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRateLimiterResponse
var req getRateLimiterRequest
ctx.ShouldBindUri(&req)
var resp getRateLimiterResponse
for _, limiter := range config.Global().CLimiters{
if limiter == nil {
continue
}
if limiter.Name == req.Limiter {
resp.Data = limiter
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createRateLimiterRequest
type createRateLimiterRequest struct {
// in: body
@@ -124,22 +35,15 @@ func createRateLimiter(ctx *gin.Context) {
var req createRateLimiterRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "limiter name is required"))
return
}
req.Data.Name = name
if registry.RateLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v := parser.ParseRateLimiter(&req.Data)
if err := registry.RateLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if err := registry.RateLimiterRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -183,27 +87,25 @@ func updateRateLimiter(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Limiter)
if !registry.RateLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.RateLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Limiter
v := parser.ParseRateLimiter(&req.Data)
registry.RateLimiterRegistry().Unregister(name)
registry.RateLimiterRegistry().Unregister(req.Limiter)
if err := registry.RateLimiterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("limiter %s already exists", name)))
if err := registry.RateLimiterRegistry().Register(req.Limiter, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.RLimiters {
if c.RLimiters[i].Name == name {
if c.RLimiters[i].Name == req.Limiter {
c.RLimiters[i] = &req.Data
break
}
@@ -243,19 +145,17 @@ func deleteRateLimiter(ctx *gin.Context) {
var req deleteRateLimiterRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Limiter)
if !registry.RateLimiterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("limiter %s not found", name)))
if !registry.RateLimiterRegistry().IsRegistered(req.Limiter) {
writeError(ctx, ErrNotFound)
return
}
registry.RateLimiterRegistry().Unregister(name)
registry.RateLimiterRegistry().Unregister(req.Limiter)
config.OnUpdate(func(c *config.Config) error {
limiteres := c.RLimiters
c.RLimiters = nil
for _, s := range limiteres {
if s.Name == name {
if s.Name == req.Limiter {
continue
}
c.RLimiters = append(c.RLimiters, s)
-268
View File
@@ -1,268 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/recorder"
"github.com/go-gost/x/registry"
)
// swagger:parameters getRecorderListRequest
type getRecorderListRequest struct {
}
// successful operation.
// swagger:response getRecorderListResponse
type getRecorderListResponse struct {
// in: body
Data recorderList
}
type recorderList struct {
Count int `json:"count"`
List []*config.RecorderConfig `json:"list"`
}
func getRecorderList(ctx *gin.Context) {
// swagger:route GET /config/recorders Recorder getRecorderListRequest
//
// Get recorder list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRecorderListResponse
var req getRecorderListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Recorders
var resp getRecorderListResponse
resp.Data = recorderList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getRecorderRequest
type getRecorderRequest struct {
// in: path
// required: true
Recorder string `uri:"recorder" json:"recorder"`
}
// successful operation.
// swagger:response getRecorderResponse
type getRecorderResponse struct {
// in: body
Data *config.RecorderConfig
}
func getRecorder(ctx *gin.Context) {
// swagger:route GET /config/recorders/{recorder} Recorder getRecorderRequest
//
// Get recorder.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRecorderResponse
var req getRecorderRequest
ctx.ShouldBindUri(&req)
var resp getRecorderResponse
for _, recorder := range config.Global().Recorders {
if recorder == nil {
continue
}
if recorder.Name == req.Recorder {
resp.Data = recorder
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createRecorderRequest
type createRecorderRequest struct {
// in: body
Data config.RecorderConfig `json:"data"`
}
// successful operation.
// swagger:response createRecorderResponse
type createRecorderResponse struct {
Data Response
}
func createRecorder(ctx *gin.Context) {
// swagger:route POST /config/recorders Recorder createRecorderRequest
//
// Create a new recorder, the name of the recorder must be unique in recorder list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: createRecorderResponse
var req createRecorderRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "recorder name is required"))
return
}
req.Data.Name = name
if registry.RecorderRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("recorder %s already exists", name)))
return
}
v := parser.ParseRecorder(&req.Data)
if err := registry.RecorderRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("recorder %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
c.Recorders = append(c.Recorders, &req.Data)
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters updateRecorderRequest
type updateRecorderRequest struct {
// in: path
// required: true
Recorder string `uri:"recorder" json:"recorder"`
// in: body
Data config.RecorderConfig `json:"data"`
}
// successful operation.
// swagger:response updateRecorderResponse
type updateRecorderResponse struct {
Data Response
}
func updateRecorder(ctx *gin.Context) {
// swagger:route PUT /config/recorders/{recorder} Recorder updateRecorderRequest
//
// Update recorder by name, the recorder must already exist.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: updateRecorderResponse
var req updateRecorderRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Recorder)
if !registry.RecorderRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("recorder %s not found", name)))
return
}
req.Data.Name = name
v := parser.ParseRecorder(&req.Data)
registry.RecorderRegistry().Unregister(name)
if err := registry.RecorderRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("recorder %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Recorders {
if c.Recorders[i].Name == name {
c.Recorders[i] = &req.Data
break
}
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters deleteRecorderRequest
type deleteRecorderRequest struct {
// in: path
// required: true
Recorder string `uri:"recorder" json:"recorder"`
}
// successful operation.
// swagger:response deleteRecorderResponse
type deleteRecorderResponse struct {
Data Response
}
func deleteRecorder(ctx *gin.Context) {
// swagger:route DELETE /config/recorders/{recorder} Recorder deleteRecorderRequest
//
// Delete recorder by name.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: deleteRecorderResponse
var req deleteRecorderRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Recorder)
if !registry.RecorderRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("recorder %s not found", name)))
return
}
registry.RecorderRegistry().Unregister(name)
config.OnUpdate(func(c *config.Config) error {
recorders := c.Recorders
c.Recorders = nil
for _, s := range recorders {
if s.Name == name {
continue
}
c.Recorders = append(c.Recorders, s)
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
-56
View File
@@ -1,56 +0,0 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/loader"
"github.com/go-gost/x/config/parsing/parser"
"github.com/go-gost/x/registry"
)
// swagger:parameters reloadConfigRequest
type reloadConfigRequest struct{}
// successful operation.
// swagger:response reloadConfigResponse
type reloadConfigResponse struct {
Data Response
}
func reloadConfig(ctx *gin.Context) {
// swagger:route POST /config/reload Reload reloadConfigRequest
//
// Hot reload config.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: reloadConfigResponse
cfg, err := parser.Parse()
if err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, err.Error()))
return
}
config.Set(cfg)
if err := loader.Load(cfg); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, err.Error()))
return
}
for _, svc := range registry.ServiceRegistry().GetAll() {
svc := svc
go func() {
svc.Serve()
}()
}
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
+17 -117
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,93 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getResolverListRequest
type getResolverListRequest struct {
}
// successful operation.
// swagger:response getResolverListResponse
type getResolverListResponse struct {
// in: body
Data resolverList
}
type resolverList struct {
Count int `json:"count"`
List []*config.ResolverConfig `json:"list"`
}
func getResolverList(ctx *gin.Context) {
// swagger:route GET /config/resolvers Resolver getResolverListRequest
//
// Get resolver list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getResolverListResponse
var req getResolverListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Resolvers
var resp getResolverListResponse
resp.Data = resolverList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getResolverRequest
type getResolverRequest struct {
// in: path
// required: true
Resolver string `uri:"resolver" json:"resolver"`
}
// successful operation.
// swagger:response getResolverResponse
type getResolverResponse struct {
// in: body
Data *config.ResolverConfig
}
func getResolver(ctx *gin.Context) {
// swagger:route GET /config/resolvers/{resolver} Resolver getResolverRequest
//
// Get resolver.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getResolverResponse
var req getResolverRequest
ctx.ShouldBindUri(&req)
var resp getResolverResponse
for _, resolver := range config.Global().Resolvers {
if resolver == nil {
continue
}
if resolver.Name == req.Resolver {
resp.Data = resolver
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createResolverRequest
type createResolverRequest struct {
// in: body
@@ -124,26 +35,19 @@ func createResolver(ctx *gin.Context) {
var req createResolverRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "resolver name is required"))
return
}
req.Data.Name = name
if registry.ResolverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("resolver %s already exists", name)))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
v, err := parser.ParseResolver(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create resolver %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
if err := registry.ResolverRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("resolver %s already exists", name)))
if err := registry.ResolverRegistry().Register(req.Data.Name, v); err != nil {
writeError(ctx, ErrDup)
return
}
@@ -187,31 +91,29 @@ func updateResolver(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Resolver)
if !registry.ResolverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("resolver %s not found", name)))
if !registry.ResolverRegistry().IsRegistered(req.Resolver) {
writeError(ctx, ErrNotFound)
return
}
req.Data.Name = name
req.Data.Name = req.Resolver
v, err := parser.ParseResolver(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create resolver %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
registry.ResolverRegistry().Unregister(name)
registry.ResolverRegistry().Unregister(req.Resolver)
if err := registry.ResolverRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("resolver %s already exists", name)))
if err := registry.ResolverRegistry().Register(req.Resolver, v); err != nil {
writeError(ctx, ErrDup)
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Resolvers {
if c.Resolvers[i].Name == name {
if c.Resolvers[i].Name == req.Resolver {
c.Resolvers[i] = &req.Data
break
}
@@ -251,19 +153,17 @@ func deleteResolver(ctx *gin.Context) {
var req deleteResolverRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Resolver)
if !registry.ResolverRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("resolver %s not found", name)))
if !registry.ResolverRegistry().IsRegistered(req.Resolver) {
writeError(ctx, ErrNotFound)
return
}
registry.ResolverRegistry().Unregister(name)
registry.ResolverRegistry().Unregister(req.Resolver)
config.OnUpdate(func(c *config.Config) error {
resolvers := c.Resolvers
c.Resolvers = nil
for _, s := range resolvers {
if s.Name == name {
if s.Name == req.Resolver {
continue
}
c.Resolvers = append(c.Resolvers, s)
-269
View File
@@ -1,269 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/router"
"github.com/go-gost/x/registry"
)
// swagger:parameters getRouterListRequest
type getRouterListRequest struct {
}
// successful operation.
// swagger:response getRouterListResponse
type getRouterListResponse struct {
// in: body
Data routerList
}
type routerList struct {
Count int `json:"count"`
List []*config.RouterConfig `json:"list"`
}
func getRouterList(ctx *gin.Context) {
// swagger:route GET /config/routers Router getRouterListRequest
//
// Get router list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRouterListResponse
var req getRouterListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Routers
var resp getRouterListResponse
resp.Data = routerList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getRouterRequest
type getRouterRequest struct {
// in: path
// required: true
Router string `uri:"router" json:"router"`
}
// successful operation.
// swagger:response getRouterResponse
type getRouterResponse struct {
// in: body
Data *config.RouterConfig
}
func getRouter(ctx *gin.Context) {
// swagger:route GET /config/routers/{router} Router getRouterRequest
//
// Get router.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getRouterResponse
var req getRouterRequest
ctx.ShouldBindUri(&req)
var resp getRouterResponse
for _, router := range config.Global().Routers {
if router == nil {
continue
}
if router.Name == req.Router {
resp.Data = router
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createRouterRequest
type createRouterRequest struct {
// in: body
Data config.RouterConfig `json:"data"`
}
// successful operation.
// swagger:response createRouterResponse
type createRouterResponse struct {
Data Response
}
func createRouter(ctx *gin.Context) {
// swagger:route POST /config/routers Router createRouterRequest
//
// Create a new router, the name of the router must be unique in router list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: createRouterResponse
var req createRouterRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "router name is required"))
return
}
req.Data.Name = name
if registry.RouterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("router %s already exists", name)))
return
}
v := parser.ParseRouter(&req.Data)
if err := registry.RouterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("router %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
c.Routers = append(c.Routers, &req.Data)
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters updateRouterRequest
type updateRouterRequest struct {
// in: path
// required: true
Router string `uri:"router" json:"router"`
// in: body
Data config.RouterConfig `json:"data"`
}
// successful operation.
// swagger:response updateRouterResponse
type updateRouterResponse struct {
Data Response
}
func updateRouter(ctx *gin.Context) {
// swagger:route PUT /config/routers/{router} Router updateRouterRequest
//
// Update router by name, the router must already exist.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: updateRouterResponse
var req updateRouterRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Router)
if !registry.RouterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("router %s not found", name)))
return
}
req.Data.Name = name
v := parser.ParseRouter(&req.Data)
registry.RouterRegistry().Unregister(name)
if err := registry.RouterRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("router %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.Routers {
if c.Routers[i].Name == name {
c.Routers[i] = &req.Data
break
}
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters deleteRouterRequest
type deleteRouterRequest struct {
// in: path
// required: true
Router string `uri:"router" json:"router"`
}
// successful operation.
// swagger:response deleteRouterResponse
type deleteRouterResponse struct {
Data Response
}
func deleteRouter(ctx *gin.Context) {
// swagger:route DELETE /config/routers/{router} Router deleteRouterRequest
//
// Delete router by name.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: deleteRouterResponse
var req deleteRouterRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Router)
if !registry.RouterRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("router %s not found", name)))
return
}
registry.RouterRegistry().Unregister(name)
config.OnUpdate(func(c *config.Config) error {
routeres := c.Routers
c.Routers = nil
for _, s := range routeres {
if s.Name == name {
continue
}
c.Routers = append(c.Routers, s)
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
-269
View File
@@ -1,269 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
parser "github.com/go-gost/x/config/parsing/sd"
"github.com/go-gost/x/registry"
)
// swagger:parameters getSDListRequest
type getSDListRequest struct {
}
// successful operation.
// swagger:response getSDListResponse
type getSDListResponse struct {
// in: body
Data sdList
}
type sdList struct {
Count int `json:"count"`
List []*config.SDConfig `json:"list"`
}
func getSDList(ctx *gin.Context) {
// swagger:route GET /config/sds SD getSDListRequest
//
// Get sd list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getSDListResponse
var req getSDListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().SDs
var resp getSDListResponse
resp.Data = sdList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getSDRequest
type getSDRequest struct {
// in: path
// required: true
SD string `uri:"sd" json:"sd"`
}
// successful operation.
// swagger:response getSDResponse
type getSDResponse struct {
// in: body
Data *config.SDConfig
}
func getSD(ctx *gin.Context) {
// swagger:route GET /config/sds/{sd} SD getSDRequest
//
// Get sd.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getSDResponse
var req getSDRequest
ctx.ShouldBindUri(&req)
var resp getSDResponse
for _, sd := range config.Global().SDs {
if sd == nil {
continue
}
if sd.Name == req.SD {
resp.Data = sd
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createSDRequest
type createSDRequest struct {
// in: body
Data config.SDConfig `json:"data"`
}
// successful operation.
// swagger:response createSDResponse
type createSDResponse struct {
Data Response
}
func createSD(ctx *gin.Context) {
// swagger:route POST /config/sds SD createSDRequest
//
// Create a new SD, the name of the SD must be unique in SD list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: createSDResponse
var req createSDRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "sd name is required"))
return
}
req.Data.Name = name
if registry.SDRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("sd %s already exists", name)))
return
}
v := parser.ParseSD(&req.Data)
if err := registry.SDRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("sd %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
c.SDs = append(c.SDs, &req.Data)
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters updateSDRequest
type updateSDRequest struct {
// in: path
// required: true
SD string `uri:"sd" json:"sd"`
// in: body
Data config.SDConfig `json:"data"`
}
// successful operation.
// swagger:response updateSDResponse
type updateSDResponse struct {
Data Response
}
func updateSD(ctx *gin.Context) {
// swagger:route PUT /config/sds/{sd} SD updateSDRequest
//
// Update SD by name, the SD must already exist.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: updateSDResponse
var req updateSDRequest
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.SD)
if !registry.SDRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("sd %s not found", name)))
return
}
req.Data.Name = name
v := parser.ParseSD(&req.Data)
registry.SDRegistry().Unregister(name)
if err := registry.SDRegistry().Register(name, v); err != nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("sd %s already exists", name)))
return
}
config.OnUpdate(func(c *config.Config) error {
for i := range c.SDs {
if c.SDs[i].Name == name {
c.SDs[i] = &req.Data
break
}
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
// swagger:parameters deleteSDRequest
type deleteSDRequest struct {
// in: path
// required: true
SD string `uri:"sd" json:"sd"`
}
// successful operation.
// swagger:response deleteSDResponse
type deleteSDResponse struct {
Data Response
}
func deleteSD(ctx *gin.Context) {
// swagger:route DELETE /config/sds/{sd} SD deleteSDRequest
//
// Delete SD by name.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: deleteSDResponse
var req deleteSDRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.SD)
if !registry.SDRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("sd %s not found", name)))
return
}
registry.SDRegistry().Unregister(name)
config.OnUpdate(func(c *config.Config) error {
sds := c.SDs
c.SDs = nil
for _, s := range sds {
if s.Name == name {
continue
}
c.SDs = append(c.SDs, s)
}
return nil
})
ctx.JSON(http.StatusOK, Response{
Msg: "OK",
})
}
+19 -122
View File
@@ -1,9 +1,7 @@
package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-gost/x/config"
@@ -11,101 +9,6 @@ import (
"github.com/go-gost/x/registry"
)
// swagger:parameters getServiceListRequest
type getServiceListRequest struct {
}
// successful operation.
// swagger:response getServiceListResponse
type getServiceListResponse struct {
// in: body
Data serviceList
}
type serviceList struct {
Count int `json:"count"`
List []*config.ServiceConfig `json:"list"`
}
func getServiceList(ctx *gin.Context) {
// swagger:route GET /config/services Service getServiceListRequest
//
// Get service list.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getServiceListResponse
var req getServiceListRequest
ctx.ShouldBindQuery(&req)
list := config.Global().Services
for _, svc := range list {
if svc == nil {
continue
}
fillServiceStatus(svc)
}
var resp getServiceListResponse
resp.Data = serviceList{
Count: len(list),
List: list,
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters getServiceRequest
type getServiceRequest struct {
// in: path
// required: true
Service string `uri:"service" json:"service"`
}
// successful operation.
// swagger:response getServiceResponse
type getServiceResponse struct {
// in: body
Data *config.ServiceConfig
}
func getService(ctx *gin.Context) {
// swagger:route GET /config/services/{service} Service getServiceRequest
//
// Get service.
//
// Security:
// basicAuth: []
//
// Responses:
// 200: getServiceResponse
var req getServiceRequest
ctx.ShouldBindUri(&req)
var resp getServiceResponse
for _, service := range config.Global().Services {
if service == nil {
continue
}
if service.Name == req.Service {
fillServiceStatus(service)
resp.Data = service
}
}
ctx.JSON(http.StatusOK, Response{
Data: resp.Data,
})
}
// swagger:parameters createServiceRequest
type createServiceRequest struct {
// in: body
@@ -132,27 +35,25 @@ func createService(ctx *gin.Context) {
var req createServiceRequest
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Data.Name)
if name == "" {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, "service name is required"))
if req.Data.Name == "" {
writeError(ctx, ErrInvalid)
return
}
req.Data.Name = name
if registry.ServiceRegistry().IsRegistered(name) {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("service %s already exists", name)))
if registry.ServiceRegistry().IsRegistered(req.Data.Name) {
writeError(ctx, ErrDup)
return
}
svc, err := parser.ParseService(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create service %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
if err := registry.ServiceRegistry().Register(name, svc); err != nil {
if err := registry.ServiceRegistry().Register(req.Data.Name, svc); err != nil {
svc.Close()
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("service %s already exists", name)))
writeError(ctx, ErrDup)
return
}
@@ -198,28 +99,26 @@ func updateService(ctx *gin.Context) {
ctx.ShouldBindUri(&req)
ctx.ShouldBindJSON(&req.Data)
name := strings.TrimSpace(req.Service)
old := registry.ServiceRegistry().Get(name)
old := registry.ServiceRegistry().Get(req.Service)
if old == nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("service %s not found", name)))
writeError(ctx, ErrNotFound)
return
}
old.Close()
req.Data.Name = name
req.Data.Name = req.Service
svc, err := parser.ParseService(&req.Data)
if err != nil {
writeError(ctx, NewError(http.StatusInternalServerError, ErrCodeFailed, fmt.Sprintf("create service %s failed: %s", name, err.Error())))
writeError(ctx, ErrCreate)
return
}
registry.ServiceRegistry().Unregister(name)
registry.ServiceRegistry().Unregister(req.Service)
if err := registry.ServiceRegistry().Register(name, svc); err != nil {
if err := registry.ServiceRegistry().Register(req.Service, svc); err != nil {
svc.Close()
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeDup, fmt.Sprintf("service %s already exists", name)))
writeError(ctx, ErrDup)
return
}
@@ -227,7 +126,7 @@ func updateService(ctx *gin.Context) {
config.OnUpdate(func(c *config.Config) error {
for i := range c.Services {
if c.Services[i].Name == name {
if c.Services[i].Name == req.Service {
c.Services[i] = &req.Data
break
}
@@ -267,22 +166,20 @@ func deleteService(ctx *gin.Context) {
var req deleteServiceRequest
ctx.ShouldBindUri(&req)
name := strings.TrimSpace(req.Service)
svc := registry.ServiceRegistry().Get(name)
svc := registry.ServiceRegistry().Get(req.Service)
if svc == nil {
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeNotFound, fmt.Sprintf("service %s not found", name)))
writeError(ctx, ErrNotFound)
return
}
registry.ServiceRegistry().Unregister(name)
registry.ServiceRegistry().Unregister(req.Service)
svc.Close()
config.OnUpdate(func(c *config.Config) error {
services := c.Services
c.Services = nil
for _, s := range services {
if s.Name == name {
if s.Name == req.Service {
continue
}
c.Services = append(c.Services, s)
-18
View File
@@ -15,16 +15,6 @@ var (
ErrSave = &Error{statusCode: http.StatusInternalServerError, Code: 40005, Msg: "save config failed"}
)
type ErrCode int
const (
ErrCodeInvalid = 40001
ErrCodeDup = 40002
ErrCodeFailed = 40003
ErrCodeNotFound = 40004
ErrCodeSaveConfigFailed = 40005
)
// Error is an api error.
type Error struct {
statusCode int
@@ -32,14 +22,6 @@ type Error struct {
Msg string `json:"msg"`
}
func NewError(status, code int, msg string) error {
return &Error{
statusCode: status,
Code: code,
Msg: msg,
}
}
func (e *Error) Error() string {
b, _ := json.Marshal(e)
return string(b)
+2 -7
View File
@@ -35,13 +35,8 @@ func mwBasicAuth(auther auth.Authenticator) gin.HandlerFunc {
return
}
u, p, _ := c.Request.BasicAuth()
if _, ok := auther.Authenticate(c, u, p, auth.WithService("@api")); !ok {
c.Writer.Header().Set("WWW-Authenticate", "Basic")
c.JSON(http.StatusUnauthorized, Response{
Code: http.StatusUnauthorized,
Msg: "Unauthorized",
})
c.Abort()
if _, ok := auther.Authenticate(c, u, p); !ok {
c.AbortWithStatus(http.StatusUnauthorized)
}
}
}
+152
View File
@@ -0,0 +1,152 @@
package api
import (
"net"
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/service"
)
type options struct {
accessLog bool
pathPrefix string
auther auth.Authenticator
}
type Option func(*options)
func PathPrefixOption(pathPrefix string) Option {
return func(o *options) {
o.pathPrefix = pathPrefix
}
}
func AccessLogOption(enable bool) Option {
return func(o *options) {
o.accessLog = enable
}
}
func AutherOption(auther auth.Authenticator) Option {
return func(o *options) {
o.auther = auther
}
}
type server struct {
s *http.Server
ln net.Listener
}
func NewService(addr string, opts ...Option) (service.Service, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
var options options
for _, opt := range opts {
opt(&options)
}
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(
cors.New((cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"*"},
})),
gin.Recovery(),
)
if options.accessLog {
r.Use(mwLogger())
}
router := r.Group("")
if options.pathPrefix != "" {
router = router.Group(options.pathPrefix)
}
router.StaticFS("/docs", http.FS(swaggerDoc))
config := router.Group("/config")
config.Use(mwBasicAuth(options.auther))
registerConfig(config)
return &server{
s: &http.Server{
Handler: r,
},
ln: ln,
}, nil
}
func (s *server) Serve() error {
return s.s.Serve(s.ln)
}
func (s *server) Addr() net.Addr {
return s.ln.Addr()
}
func (s *server) Close() error {
return s.s.Close()
}
func registerConfig(config *gin.RouterGroup) {
config.GET("", getConfig)
config.POST("", saveConfig)
config.POST("/services", createService)
config.PUT("/services/:service", updateService)
config.DELETE("/services/:service", deleteService)
config.POST("/chains", createChain)
config.PUT("/chains/:chain", updateChain)
config.DELETE("/chains/:chain", deleteChain)
config.POST("/hops", createHop)
config.PUT("/hops/:hop", updateHop)
config.DELETE("/hops/:hop", deleteHop)
config.POST("/authers", createAuther)
config.PUT("/authers/:auther", updateAuther)
config.DELETE("/authers/:auther", deleteAuther)
config.POST("/admissions", createAdmission)
config.PUT("/admissions/:admission", updateAdmission)
config.DELETE("/admissions/:admission", deleteAdmission)
config.POST("/bypasses", createBypass)
config.PUT("/bypasses/:bypass", updateBypass)
config.DELETE("/bypasses/:bypass", deleteBypass)
config.POST("/resolvers", createResolver)
config.PUT("/resolvers/:resolver", updateResolver)
config.DELETE("/resolvers/:resolver", deleteResolver)
config.POST("/hosts", createHosts)
config.PUT("/hosts/:hosts", updateHosts)
config.DELETE("/hosts/:hosts", deleteHosts)
config.POST("/ingresses", createIngress)
config.PUT("/ingresses/:ingress", updateIngress)
config.DELETE("/ingresses/:ingress", deleteIngress)
config.POST("/limiters", createLimiter)
config.PUT("/limiters/:limiter", updateLimiter)
config.DELETE("/limiters/:limiter", deleteLimiter)
config.POST("/climiters", createConnLimiter)
config.PUT("/climiters/:limiter", updateConnLimiter)
config.DELETE("/climiters/:limiter", deleteConnLimiter)
config.POST("/rlimiters", createRateLimiter)
config.PUT("/rlimiters/:limiter", updateRateLimiter)
config.DELETE("/rlimiters/:limiter", deleteRateLimiter)
}
-96
View File
@@ -1,96 +0,0 @@
package service
import (
"net"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/service"
"github.com/go-gost/x/api"
)
type options struct {
accessLog bool
pathPrefix string
auther auth.Authenticator
}
type Option func(*options)
func PathPrefixOption(pathPrefix string) Option {
return func(o *options) {
o.pathPrefix = pathPrefix
}
}
func AccessLogOption(enable bool) Option {
return func(o *options) {
o.accessLog = enable
}
}
func AutherOption(auther auth.Authenticator) Option {
return func(o *options) {
o.auther = auther
}
}
type server struct {
s *http.Server
ln net.Listener
cclose chan struct{}
}
func NewService(network, addr string, opts ...Option) (service.Service, error) {
if network == "" {
network = "tcp"
}
ln, err := net.Listen(network, addr)
if err != nil {
return nil, err
}
var options options
for _, opt := range opts {
opt(&options)
}
gin.SetMode(gin.ReleaseMode)
r := gin.New()
api.Register(r, &api.Options{
AccessLog: options.accessLog,
PathPrefix: options.pathPrefix,
Auther: options.auther,
})
return &server{
s: &http.Server{
Handler: r,
},
ln: ln,
cclose: make(chan struct{}),
}, nil
}
func (s *server) Serve() error {
return s.s.Serve(s.ln)
}
func (s *server) Addr() net.Addr {
return s.ln.Addr()
}
func (s *server) Close() error {
return s.s.Close()
}
func (s *server) IsClosed() bool {
select {
case <-s.cclose:
return true
default:
return false
}
}
+19 -1534
View File
File diff suppressed because it is too large. Load diff
+41 -87
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"context"
"io"
"maps"
"strings"
"sync"
"time"
@@ -68,7 +67,6 @@ type authenticator struct {
mu sync.RWMutex
cancelFunc context.CancelFunc
options options
logger logger.Logger
}
// NewAuthenticator creates an Authenticator that authenticates client by pre-defined user mapping.
@@ -77,19 +75,23 @@ func NewAuthenticator(opts ...Option) auth.Authenticator {
for _, opt := range opts {
opt(&options)
}
if options.logger == nil {
options.logger = xlogger.Nop()
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.TODO())
p := &authenticator{
kvs: make(map[string]string),
cancelFunc: cancel,
options: options,
logger: options.logger,
}
if p.logger == nil {
p.logger = xlogger.Nop()
}
go p.periodReload(ctx)
if err := p.reload(ctx); err != nil {
options.logger.Warnf("reload: %v", err)
}
if p.options.period > 0 {
go p.periodReload(ctx)
}
return p
}
@@ -108,18 +110,11 @@ func (p *authenticator) Authenticate(ctx context.Context, user, password string,
}
v, ok := p.kvs[user]
return user, ok && (v == "" || password == v)
return "", ok && (v == "" || password == v)
}
func (p *authenticator) periodReload(ctx context.Context) error {
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
}
period := p.options.period
if period <= 0 {
return nil
}
if period < time.Second {
period = time.Second
}
@@ -130,7 +125,7 @@ func (p *authenticator) periodReload(ctx context.Context) error {
select {
case <-ticker.C:
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
p.options.logger.Warnf("reload: %v", err)
// return err
}
case <-ctx.Done():
@@ -139,92 +134,80 @@ func (p *authenticator) periodReload(ctx context.Context) error {
}
}
func (p *authenticator) reload(ctx context.Context) error {
func (p *authenticator) reload(ctx context.Context) (err error) {
kvs := make(map[string]string)
maps.Copy(kvs, p.options.auths)
for k, v := range p.options.auths {
kvs[k] = v
}
m, err := p.load(ctx)
if err != nil {
return err
for k, v := range m {
kvs[k] = v
}
maps.Copy(kvs, m)
p.logger.Debugf("load items %d", len(m))
p.mu.Lock()
defer p.mu.Unlock()
p.kvs = kvs
return nil
return
}
func (p *authenticator) load(ctx context.Context) (map[string]string, error) {
m := make(map[string]string)
var loadErr error
func (p *authenticator) load(ctx context.Context) (m map[string]string, err error) {
m = make(map[string]string)
if p.options.fileLoader != nil {
if mapper, ok := p.options.fileLoader.(loader.Mapper); ok {
auths, er := mapper.Map(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
loadErr = er
p.options.logger.Warnf("file loader: %v", er)
}
m = auths
} else {
r, er := p.options.fileLoader.Load(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
loadErr = er
p.options.logger.Warnf("file loader: %v", er)
}
if auths, err := p.parseAuths(r); err == nil {
if auths, _ := p.parseAuths(r); auths != nil {
m = auths
} else {
p.logger.Warnf("file loader parse: %v", err)
}
}
}
if p.options.redisLoader != nil {
if mapper, ok := p.options.redisLoader.(loader.Mapper); ok {
if mapper, ok := p.options.fileLoader.(loader.Mapper); ok {
auths, er := mapper.Map(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
if loadErr == nil {
loadErr = er
}
p.options.logger.Warnf("file loader: %v", er)
}
for k, v := range auths {
m[k] = v
}
maps.Copy(m, auths)
} else {
r, er := p.options.redisLoader.Load(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
if loadErr == nil {
loadErr = er
}
p.options.logger.Warnf("redis loader: %v", er)
}
if auths, err := p.parseAuths(r); err == nil {
maps.Copy(m, auths)
} else {
p.logger.Warnf("redis loader parse: %v", err)
if auths, _ := p.parseAuths(r); auths != nil {
for k, v := range auths {
m[k] = v
}
}
}
}
if p.options.httpLoader != nil {
r, er := p.options.httpLoader.Load(ctx)
if er != nil {
p.logger.Warnf("http loader: %v", er)
if loadErr == nil {
loadErr = er
}
p.options.logger.Warnf("http loader: %v", er)
}
if auths, err := p.parseAuths(r); err == nil {
maps.Copy(m, auths)
} else {
p.logger.Warnf("http loader parse: %v", err)
if auths, _ := p.parseAuths(r); auths != nil {
for k, v := range auths {
m[k] = v
}
}
}
return m, loadErr
p.options.logger.Debugf("load items %d", len(m))
return
}
func (p *authenticator) parseAuths(r io.Reader) (auths map[string]string, err error) {
@@ -266,34 +249,5 @@ func (p *authenticator) Close() error {
if p.options.redisLoader != nil {
p.options.redisLoader.Close()
}
if p.options.httpLoader != nil {
p.options.httpLoader.Close()
}
return nil
}
type authenticatorGroup struct {
authers []auth.Authenticator
}
func AuthenticatorGroup(authers ...auth.Authenticator) auth.Authenticator {
return &authenticatorGroup{
authers: authers,
}
}
func (p *authenticatorGroup) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
if len(p.authers) == 0 {
return "", false
}
for _, auther := range p.authers {
if auther == nil {
continue
}
if id, ok := auther.Authenticate(ctx, user, password, opts...); ok {
return id, ok
}
}
return "", false
}
-1100
View File
File diff suppressed because it is too large. Load diff
+63 -15
View File
@@ -9,12 +9,72 @@ import (
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/plugin/auth/proto"
"github.com/go-gost/x/internal/plugin"
auth_util "github.com/go-gost/x/internal/util/auth"
"google.golang.org/grpc"
)
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.AuthenticatorClient
log logger.Logger
}
// NewGRPCPlugin creates an Authenticator plugin based on gRPC.
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) auth.Authenticator {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "auther",
"auther": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewAuthenticatorClient(conn)
}
return p
}
// Authenticate checks the validity of the provided user-password pair.
func (p *grpcPlugin) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
if p.client == nil {
return "", false
}
r, err := p.client.Authenticate(ctx,
&proto.AuthenticateRequest{
Username: user,
Password: password,
Client: string(auth_util.ClientAddrFromContext(ctx)),
})
if err != nil {
p.log.Error(err)
return "", false
}
return r.Id, r.Ok
}
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
type httpPluginRequest struct {
Service string `json:"service"`
Username string `json:"username"`
Password string `json:"password"`
Client string `json:"client"`
@@ -55,21 +115,10 @@ func (p *httpPlugin) Authenticate(ctx context.Context, user, password string, op
return
}
var options auth.Options
for _, opt := range opts {
opt(&options)
}
var clientAddr string
if v := xctx.SrcAddrFromContext(ctx); v != nil {
clientAddr = v.String()
}
rb := httpPluginRequest{
Service: options.Service,
Username: user,
Password: password,
Client: clientAddr,
Client: string(auth_util.ClientAddrFromContext(ctx)),
}
v, err := json.Marshal(&rb)
if err != nil {
@@ -92,7 +141,6 @@ func (p *httpPlugin) Authenticate(ctx context.Context, user, password string, op
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
return
}
-82
View File
@@ -1,82 +0,0 @@
package auth
import (
"context"
"io"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger"
"github.com/go-gost/plugin/auth/proto"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/plugin"
"google.golang.org/grpc"
)
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.AuthenticatorClient
log logger.Logger
}
// NewGRPCPlugin creates an Authenticator plugin based on gRPC.
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) auth.Authenticator {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "auther",
"auther": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewAuthenticatorClient(conn)
}
return p
}
// Authenticate checks the validity of the provided user-password pair.
func (p *grpcPlugin) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
if p.client == nil {
return "", false
}
var options auth.Options
for _, opt := range opts {
opt(&options)
}
var clientAddr string
if v := xctx.SrcAddrFromContext(ctx); v != nil {
clientAddr = v.String()
}
r, err := p.client.Authenticate(ctx,
&proto.AuthenticateRequest{
Service: options.Service,
Username: user,
Password: password,
Client: clientAddr,
})
if err != nil {
p.log.Error(err)
return "", false
}
return r.Id, r.Ok
}
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
-252
View File
@@ -1,252 +0,0 @@
package auth
import (
"context"
"io"
"net"
"os"
"testing"
"github.com/go-gost/core/auth"
corelogger "github.com/go-gost/core/logger"
"github.com/go-gost/plugin/auth/proto"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/plugin"
xlogger "github.com/go-gost/x/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
)
func TestMain(m *testing.M) {
corelogger.SetDefault(xlogger.Nop())
os.Exit(m.Run())
}
func newTestGRPCConn(t *testing.T, srv proto.AuthenticatorServer) (*grpc.ClientConn, func()) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterAuthenticatorServer(gsrv, srv)
go gsrv.Serve(lis)
conn, err := grpc.NewClient(lis.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
return conn, func() {
conn.Close()
gsrv.Stop()
}
}
func TestGRPCPlugin_NilClient(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Authenticate_Success(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAuthServer{ok: true, id: "user1"})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "user1", id)
}
func TestGRPCPlugin_Authenticate_Fail(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAuthServer{ok: false, id: ""})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestGRPCPlugin_Authenticate_WithService(t *testing.T) {
server := &testAuthServer{ok: true, id: "srv"}
conn, cleanup := newTestGRPCConn(t, server)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p", auth.WithService("myservice"))
assert.True(t, ok)
assert.Equal(t, "srv", id)
assert.Equal(t, "myservice", server.lastService)
}
func TestGRPCPlugin_Authenticate_WithClientAddr(t *testing.T) {
server := &testAuthServer{ok: true, id: "addr"}
conn, cleanup := newTestGRPCConn(t, server)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
ctx := context.Background()
ctx = xctx.ContextWithSrcAddr(ctx, &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234})
id, ok := p.Authenticate(ctx, "u", "p")
assert.True(t, ok)
assert.Equal(t, "addr", id)
assert.Equal(t, "10.0.0.1:1234", server.lastClient)
}
func TestGRPCPlugin_Authenticate_WithoutClientAddr(t *testing.T) {
server := &testAuthServer{ok: true, id: "noclient"}
conn, cleanup := newTestGRPCConn(t, server)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "noclient", id)
assert.Empty(t, server.lastClient)
}
func TestGRPCPlugin_Authenticate_ServerError(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &errorAuthServer{})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewAuthenticatorClient(conn),
log: xlogger.Nop(),
}
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestNewGRPCPlugin_RealConn(t *testing.T) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterAuthenticatorServer(gsrv, &testAuthServer{ok: true, id: "real"})
go gsrv.Serve(lis)
defer gsrv.Stop()
p := NewGRPCPlugin("test", lis.Addr().String(), plugin.TimeoutOption(500))
require.NotNil(t, p)
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "real", id)
assert.NoError(t, p.(io.Closer).Close())
}
func TestNewGRPCPlugin_InvalidAddr(t *testing.T) {
// Null byte causes grpc.NewClient target parsing to fail synchronously.
// This covers the log.Error(err) path.
// NOTE: depends on grpc-go's net/url control-character validation.
// If a future grpc version changes target parsing, this test may silently
// stop covering the error branch.
p := NewGRPCPlugin("test", "\x00")
require.NotNil(t, p)
// conn is nil, so client is nil, so Authenticate returns false
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestGRPCPlugin_Close_NilConn(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_NonCloserConn(t *testing.T) {
p := &grpcPlugin{
conn: &nonCloserConn{},
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_WithRealConn(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testAuthServer{ok: true})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: nil,
log: xlogger.Nop(),
}
assert.NoError(t, p.Close())
}
// --- test helpers ---
type testAuthServer struct {
proto.UnimplementedAuthenticatorServer
ok bool
id string
lastService string
lastClient string
}
func (s *testAuthServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateReply, error) {
s.lastService = req.Service
s.lastClient = req.Client
return &proto.AuthenticateReply{Ok: s.ok, Id: s.id}, nil
}
type errorAuthServer struct {
proto.UnimplementedAuthenticatorServer
}
func (s *errorAuthServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateReply, error) {
return nil, status.Error(codes.Internal, "internal error")
}
type nonCloserConn struct{}
func (n *nonCloserConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
return nil
}
func (n *nonCloserConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
}
var _ grpc.ClientConnInterface = (*nonCloserConn)(nil)
var _ io.Closer = (*grpcPlugin)(nil)
-206
View File
@@ -1,206 +0,0 @@
package auth
import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-gost/core/auth"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHTTPPlugin(t *testing.T) {
p := NewHTTPPlugin("test", "http://localhost:9999")
require.NotNil(t, p)
hp, ok := p.(*httpPlugin)
require.True(t, ok)
assert.Equal(t, "http://localhost:9999", hp.url)
assert.NotNil(t, hp.client)
assert.NotNil(t, hp.log)
}
func TestHTTPPlugin_Authenticate_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "user1"}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "user1", id)
}
func TestHTTPPlugin_Authenticate_Fail(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": false, "id": ""}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_Non200(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_InvalidJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`not json`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_ConnectionRefused(t *testing.T) {
p := NewHTTPPlugin("test", "http://127.0.0.1:0", plugin.TimeoutOption(0))
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_NilClient(t *testing.T) {
hp := &httpPlugin{
url: "http://localhost",
client: nil,
}
id, ok := hp.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_WithService(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "svc"}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
id, ok := p.Authenticate(context.Background(), "u", "p", auth.WithService("myservice"))
assert.True(t, ok)
assert.Equal(t, "svc", id)
}
func TestHTTPPlugin_Authenticate_WithHeader(t *testing.T) {
var receivedHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeader = r.Header.Get("X-Custom")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "hdr"}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
"X-Custom": []string{"test-value"},
}))
id, ok := p.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "hdr", id)
assert.Equal(t, "test-value", receivedHeader)
}
func TestHTTPPlugin_Authenticate_NoHeader(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "nohdr"}`))
}))
defer srv.Close()
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
header: nil,
}
id, ok := hp.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "nohdr", id)
}
func TestHTTPPlugin_Authenticate_WithClientAddr(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "addr"}`))
}))
defer srv.Close()
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
}
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678})
id, ok := hp.Authenticate(ctx, "u", "p")
assert.True(t, ok)
assert.Equal(t, "addr", id)
}
func TestHTTPPlugin_Authenticate_WithoutClientAddr(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "noaddr"}`))
}))
defer srv.Close()
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
}
id, ok := hp.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "noaddr", id)
}
func TestHTTPPlugin_Authenticate_BadURL(t *testing.T) {
hp := &httpPlugin{
url: "http://invalid hostname",
client: plugin.NewHTTPClient(&plugin.Options{}),
}
id, ok := hp.Authenticate(context.Background(), "u", "p")
assert.False(t, ok)
assert.Empty(t, id)
}
func TestHTTPPlugin_Authenticate_WithAllOptions(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer mytoken", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true, "id": "all"}`))
}))
defer srv.Close()
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
header: http.Header{"Authorization": []string{"Bearer mytoken"}},
}
id, ok := hp.Authenticate(context.Background(), "u", "p")
assert.True(t, ok)
assert.Equal(t, "all", id)
}
-61
View File
@@ -1,61 +0,0 @@
package auth
import (
"context"
"net"
"strings"
"github.com/go-gost/core/auth"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/matcher"
)
// whitelistedAuthenticator wraps an Authenticator to skip authentication
// for client IPs matching the configured whitelist patterns.
type whitelistedAuthenticator struct {
auther auth.Authenticator
ipMatcher matcher.Matcher
cidrMatcher matcher.Matcher
}
// WhitelistedAuthenticator returns an Authenticator that skips the underlying
// auther when the client's IP address matches one of the given patterns.
// Patterns may be IP addresses (e.g. "192.168.1.1") or CIDR ranges
// (e.g. "10.0.0.0/8"). Invalid patterns are silently ignored.
func WhitelistedAuthenticator(auther auth.Authenticator, patterns []string) auth.Authenticator {
var ips []net.IP
var inets []*net.IPNet
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
if ip := net.ParseIP(pattern); ip != nil {
ips = append(ips, ip)
continue
}
if _, inet, err := net.ParseCIDR(pattern); err == nil {
inets = append(inets, inet)
}
}
return &whitelistedAuthenticator{
auther: auther,
ipMatcher: matcher.IPMatcher(ips),
cidrMatcher: matcher.CIDRMatcher(inets),
}
}
func (w *whitelistedAuthenticator) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
host, _, err := net.SplitHostPort(srcAddr.String())
if err == nil && host != "" {
if w.ipMatcher.Match(host) || w.cidrMatcher.Match(host) {
return "", true
}
}
}
if w.auther != nil {
return w.auther.Authenticate(ctx, user, password, opts...)
}
return "", false
}
+95 -375
View File
@@ -1,25 +1,8 @@
// Package bypass implements address-based routing that decides whether a
// target address should skip the proxy chain and connect directly.
//
// The package provides:
// - NewBypass: a local bypass that matches addresses against CIDR, IP range,
// wildcard, and exact-address patterns, with optional periodic reload from
// file, Redis, or HTTP sources.
// - BypassGroup: composes multiple bypass rules; whitelist rules use AND
// logic, blacklist rules use OR logic (evaluated only if whitelist fails).
// - Plugin-based bypass (gRPC and HTTP) in the plugin sub-package.
//
// Matching modes:
// - Blacklist (whitelist=false): matching addresses bypass the proxy.
// This is the default.
// - Whitelist (whitelist=true): only matching addresses bypass the proxy;
// all other addresses go through the proxy chain.
package bypass
import (
"bufio"
"context"
"errors"
"io"
"net"
"strings"
@@ -28,247 +11,112 @@ import (
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
ctxvalue "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/matcher"
xnet "github.com/go-gost/x/internal/net"
xlogger "github.com/go-gost/x/logger"
"github.com/gobwas/glob"
)
// ErrBypass is returned by wrapped connections when the bypass rule
// determines that the connection should skip the proxy chain.
var ErrBypass = errors.New("bypass")
// options holds the configuration for a localBypass instance.
type options struct {
// whitelist toggles between blacklist and whitelist mode.
// When false (default): matching addresses bypass the proxy.
// When true: only matching addresses bypass the proxy.
whitelist bool
// network restricts this bypass to a specific network protocol.
// When set, the incoming network must match before address matchers
// are evaluated. Recognized values include "tcp" and "udp".
// An empty value disables the network check.
network string
// matchers holds static patterns provided at construction time
// (e.g. from config file or command-line arguments).
matchers []string
// fileLoader loads patterns from a file or directory.
fileLoader loader.Loader
// redisLoader loads patterns from a Redis set or key.
whitelist bool
matchers []string
fileLoader loader.Loader
redisLoader loader.Loader
// httpLoader loads patterns from an HTTP endpoint.
httpLoader loader.Loader
// period controls the interval between automatic reloads.
// Values less than 1 second are clamped to 1 second.
// A value <= 0 disables periodic reload (load once at startup).
period time.Duration
// logger is used for debug and warning messages.
// Falls back to a no-op logger if nil.
logger logger.Logger
httpLoader loader.Loader
period time.Duration
logger logger.Logger
}
// Option is a functional option for configuring a Bypass instance.
type Option func(opts *options)
// WhitelistOption sets whether the bypass operates in whitelist mode.
// In whitelist mode, only matching addresses bypass the proxy;
// all others go through the proxy chain.
func WhitelistOption(whitelist bool) Option {
return func(opts *options) {
opts.whitelist = whitelist
}
}
// NetworkOption restricts this bypass to a specific network protocol.
// When set, Contains returns false when the incoming network does not
// match, regardless of address matchers. Accepts values like "tcp" or "udp".
func NetworkOption(network string) Option {
return func(opts *options) {
opts.network = network
}
}
// MatchersOption sets the static bypass patterns (CIDR, IP range, wildcard, or address).
func MatchersOption(matchers []string) Option {
return func(opts *options) {
opts.matchers = matchers
}
}
// ReloadPeriodOption sets the interval between automatic reloads of bypass
// patterns from external loaders (file, Redis, HTTP).
func ReloadPeriodOption(period time.Duration) Option {
return func(opts *options) {
opts.period = period
}
}
// FileLoaderOption sets the file loader for reading bypass patterns from a
// file or directory.
func FileLoaderOption(fileLoader loader.Loader) Option {
return func(opts *options) {
opts.fileLoader = fileLoader
}
}
// RedisLoaderOption sets the Redis loader for reading bypass patterns from a
// Redis set or key.
func RedisLoaderOption(redisLoader loader.Loader) Option {
return func(opts *options) {
opts.redisLoader = redisLoader
}
}
// HTTPLoaderOption sets the HTTP loader for reading bypass patterns from an
// HTTP endpoint.
func HTTPLoaderOption(httpLoader loader.Loader) Option {
return func(opts *options) {
opts.httpLoader = httpLoader
}
}
// LoggerOption sets the logger for debug and warning messages.
func LoggerOption(logger logger.Logger) Option {
return func(opts *options) {
opts.logger = logger
}
}
// bypassDecision represents the outcome of evaluating a bypass rule.
type bypassDecision int
const (
// decisionBypass means the address should connect directly, skipping the proxy chain.
decisionBypass bypassDecision = iota
// decisionProxy means the address should go through the proxy chain.
decisionProxy
)
func (d bypassDecision) String() string {
switch d {
case decisionBypass:
return "bypass"
case decisionProxy:
return "proxy"
default:
return "unknown"
}
}
// patternSet holds classified pattern matchers for a single bypass rule.
// All four matcher types are built together by classifyPatterns and swapped
// atomically under the write lock.
type patternSet struct {
cidr matcher.Matcher
addr matcher.Matcher
wildcard matcher.Matcher
ipRange matcher.Matcher
}
// matchAny reports whether addr matches any pattern in the set,
// trying IP range, address, CIDR, and wildcard matchers in order.
// Returns false if ps is nil.
func (ps *patternSet) matchAny(addr string) bool {
if ps == nil {
return false
}
if ps.ipRange.Match(addr) {
return true
}
if ps.addr.Match(addr) {
return true
}
host, _, _ := net.SplitHostPort(addr)
if host == "" {
host = addr
}
if ip := net.ParseIP(host); ip != nil && ps.cidr.Match(host) {
return true
}
return ps.wildcard.Match(addr)
}
// localBypass is a Bypass that matches addresses against local pattern
// matchers. Patterns are classified into CIDR, wildcard, IP range, and
// exact address matchers. Patterns can be loaded from static config,
// file, Redis, or HTTP sources with optional periodic reload.
type localBypass struct {
patterns *patternSet
options options
logger logger.Logger
mu sync.RWMutex
cancelFunc context.CancelFunc
cidrMatcher matcher.Matcher
addrMatcher matcher.Matcher
wildcardMatcher matcher.Matcher
cancelFunc context.CancelFunc
options options
mu sync.RWMutex
}
// NewBypass creates and initializes a local Bypass instance.
//
// In blacklist mode (the default), addresses matching any pattern bypass the
// proxy. In whitelist mode (set via WhitelistOption(true)), only matching
// addresses bypass the proxy and all others go through the chain.
//
// If a reload period is configured, patterns from external loaders are
// refreshed automatically in the background.
// NewBypass creates and initializes a new Bypass.
// The rules will be reversed if the reverse option is true.
func NewBypass(opts ...Option) bypass.Bypass {
var options options
for _, opt := range opts {
opt(&options)
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.TODO())
p := &localBypass{
bp := &localBypass{
cancelFunc: cancel,
options: options,
logger: options.logger,
}
if p.logger == nil {
p.logger = xlogger.Nop()
}
if p.hasLoaders() {
go p.periodReload(ctx)
} else {
_ = p.reload(ctx)
if err := bp.reload(ctx); err != nil {
options.logger.Warnf("reload: %v", err)
}
if bp.options.period > 0 {
go bp.periodReload(ctx)
}
return p
return bp
}
// periodReload loads patterns immediately, then periodically reloads them
// from external sources at the configured interval. Returns when ctx is
// cancelled.
func (p *localBypass) periodReload(ctx context.Context) error {
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
}
period := p.options.period
if period <= 0 {
return nil
}
func (bp *localBypass) periodReload(ctx context.Context) error {
period := bp.options.period
if period < time.Second {
period = time.Second
}
ticker := time.NewTicker(period)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := p.reload(ctx); err != nil {
p.logger.Warnf("reload: %v", err)
if err := bp.reload(ctx); err != nil {
bp.options.logger.Warnf("reload: %v", err)
// return err
}
case <-ctx.Done():
return ctx.Err()
@@ -276,134 +124,99 @@ func (p *localBypass) periodReload(ctx context.Context) error {
}
}
// reload loads patterns from all configured sources, classifies them into
// CIDR, wildcard, IP range, and address matchers, then atomically swaps the
// pattern set under the write lock.
func (p *localBypass) reload(ctx context.Context) error {
v, err := p.load(ctx)
func (bp *localBypass) reload(ctx context.Context) error {
v, err := bp.load(ctx)
if err != nil {
return err
}
patterns := append(p.options.matchers, v...)
p.logger.Debugf("load items %d", len(patterns))
patterns := append(bp.options.matchers, v...)
p.mu.Lock()
if len(patterns) > 0 {
p.patterns = classifyPatterns(patterns, p.logger)
}
p.mu.Unlock()
return nil
}
// hasLoaders reports whether any external data source is configured,
// which determines whether a background reload goroutine is needed.
func (p *localBypass) hasLoaders() bool {
return p.options.fileLoader != nil || p.options.redisLoader != nil || p.options.httpLoader != nil || p.options.period > 0
}
// classifyPatterns sorts raw pattern strings into typed matchers.
// Invalid wildcard patterns are logged and fall through to address matching.
func classifyPatterns(patterns []string, log logger.Logger) *patternSet {
var addrs []string
var inets []*net.IPNet
var wildcards []string
var ipRanges []xnet.IPRange
for _, pattern := range patterns {
if _, inet, err := net.ParseCIDR(pattern); err == nil {
inets = append(inets, inet)
continue
}
if strings.ContainsAny(pattern, "*?") {
if _, err := glob.Compile(pattern); err == nil {
wildcards = append(wildcards, pattern)
continue
}
log.Warnf("invalid wildcard pattern %q, treating as plain address", pattern)
}
r := xnet.IPRange{}
if err := r.Parse(pattern); err == nil {
ipRanges = append(ipRanges, r)
wildcards = append(wildcards, pattern)
continue
}
addrs = append(addrs, pattern)
}
return &patternSet{
cidr: matcher.CIDRMatcher(inets),
addr: matcher.AddrMatcher(addrs),
wildcard: matcher.WildcardMatcher(wildcards),
ipRange: matcher.IPRangeMatcher(ipRanges),
}
bp.mu.Lock()
defer bp.mu.Unlock()
bp.cidrMatcher = matcher.CIDRMatcher(inets)
bp.addrMatcher = matcher.AddrMatcher(addrs)
bp.wildcardMatcher = matcher.WildcardMatcher(wildcards)
return nil
}
// load reads patterns from file, Redis and HTTP loaders if configured.
func (p *localBypass) load(ctx context.Context) (patterns []string, err error) {
if p.options.fileLoader != nil {
if lister, ok := p.options.fileLoader.(loader.Lister); ok {
func (bp *localBypass) load(ctx context.Context) (patterns []string, err error) {
if bp.options.fileLoader != nil {
if lister, ok := bp.options.fileLoader.(loader.Lister); ok {
list, er := lister.List(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
bp.options.logger.Warnf("file loader: %v", er)
}
for _, s := range list {
if line := p.parseLine(s); line != "" {
if line := bp.parseLine(s); line != "" {
patterns = append(patterns, line)
}
}
} else {
r, er := p.options.fileLoader.Load(ctx)
r, er := bp.options.fileLoader.Load(ctx)
if er != nil {
p.logger.Warnf("file loader: %v", er)
bp.options.logger.Warnf("file loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
if v, _ := bp.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
}
}
}
if p.options.redisLoader != nil {
if lister, ok := p.options.redisLoader.(loader.Lister); ok {
if bp.options.redisLoader != nil {
if lister, ok := bp.options.redisLoader.(loader.Lister); ok {
list, er := lister.List(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
bp.options.logger.Warnf("redis loader: %v", er)
}
patterns = append(patterns, list...)
} else {
r, er := p.options.redisLoader.Load(ctx)
r, er := bp.options.redisLoader.Load(ctx)
if er != nil {
p.logger.Warnf("redis loader: %v", er)
bp.options.logger.Warnf("redis loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
if v, _ := bp.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
}
}
}
if p.options.httpLoader != nil {
r, er := p.options.httpLoader.Load(ctx)
if bp.options.httpLoader != nil {
r, er := bp.options.httpLoader.Load(ctx)
if er != nil {
p.logger.Warnf("http loader: %v", er)
bp.options.logger.Warnf("http loader: %v", er)
}
if v, _ := p.parsePatterns(r); v != nil {
if v, _ := bp.parsePatterns(r); v != nil {
patterns = append(patterns, v...)
}
}
bp.options.logger.Debugf("load items %d", len(patterns))
return
}
// parsePatterns reads lines from r, strips comments and whitespace, and
// returns non-empty lines as patterns.
func (p *localBypass) parsePatterns(r io.Reader) (patterns []string, err error) {
func (bp *localBypass) parsePatterns(r io.Reader) (patterns []string, err error) {
if r == nil {
return
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if line := p.parseLine(scanner.Text()); line != "" {
if line := bp.parseLine(scanner.Text()); line != "" {
patterns = append(patterns, line)
}
}
@@ -412,148 +225,55 @@ func (p *localBypass) parsePatterns(r io.Reader) (patterns []string, err error)
return
}
func (p *localBypass) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
if addr == "" || p == nil {
func (bp *localBypass) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
if addr == "" || bp == nil {
return false
}
decision := p.decide(network, addr)
matched := bp.matched(addr)
log := p.logger.WithFields(map[string]any{
"sid": ctxvalue.SidFromContext(ctx),
})
log.Debugf("%s: %s, whitelist: %t", decision, addr, p.options.whitelist)
return decision == decisionBypass
b := !bp.options.whitelist && matched ||
bp.options.whitelist && !matched
if b {
bp.options.logger.Debugf("bypass: %s", addr)
}
return b
}
// decide returns the bypass decision for the given address, applying the
// whitelist or blacklist mode to the pattern match result.
func (p *localBypass) decide(network string, addr string) bypassDecision {
p.mu.RLock()
defer p.mu.RUnlock()
if p.options.network != "" && p.options.network != network {
return decisionProxy
}
if p.patterns == nil {
if p.options.network != "" {
if p.options.whitelist {
return decisionProxy
}
return decisionBypass
}
return decisionProxy
}
matched := p.patterns.matchAny(addr)
if p.options.whitelist {
// Whitelist mode: the pattern set specifies addresses that MUST use the proxy.
// Matched addresses go through the proxy; unmatched addresses bypass.
if matched {
return decisionProxy
}
return decisionBypass
}
// Blacklist mode: the pattern set specifies addresses that should bypass the proxy.
// Matched addresses bypass; unmatched addresses go through the proxy.
if matched {
return decisionBypass
}
return decisionProxy
}
func (p *localBypass) IsWhitelist() bool {
return p.options.whitelist
}
// parseLine removes comments (starting with '#') and trims whitespace.
func (p *localBypass) parseLine(s string) string {
func (bp *localBypass) parseLine(s string) string {
if n := strings.IndexByte(s, '#'); n >= 0 {
s = s[:n]
}
return strings.TrimSpace(s)
}
func (p *localBypass) Close() error {
p.cancelFunc()
if p.options.fileLoader != nil {
p.options.fileLoader.Close()
func (bp *localBypass) matched(addr string) bool {
bp.mu.RLock()
defer bp.mu.RUnlock()
if bp.addrMatcher.Match(addr) {
return true
}
if p.options.redisLoader != nil {
p.options.redisLoader.Close()
host, _, _ := net.SplitHostPort(addr)
if host == "" {
host = addr
}
if p.options.httpLoader != nil {
p.options.httpLoader.Close()
if ip := net.ParseIP(host); ip != nil {
return bp.cidrMatcher.Match(host)
}
return bp.wildcardMatcher.Match(addr)
}
func (bp *localBypass) Close() error {
bp.cancelFunc()
if bp.options.fileLoader != nil {
bp.options.fileLoader.Close()
}
if bp.options.redisLoader != nil {
bp.options.redisLoader.Close()
}
return nil
}
// bypassGroup composes multiple Bypass instances into a single rule.
// Whitelist rules use AND logic (all must match); blacklist rules use
// OR logic (any match triggers bypass) and are only evaluated when
// whitelist rules fail.
type bypassGroup struct {
bypasses []bypass.Bypass
}
// BypassGroup creates a composite Bypass from multiple rules.
// Whitelist rules use AND logic (all must match); blacklist rules use
// OR logic (any match triggers bypass) and are only evaluated when
// whitelist rules fail.
func BypassGroup(bypasses ...bypass.Bypass) bypass.Bypass {
return &bypassGroup{
bypasses: bypasses,
}
}
// Contains evaluates all bypass rules in the group with the following logic:
// - Whitelist rules: ALL must match (AND logic). If any whitelist rule
// returns false, the combined whitelist result is false.
// - Blacklist rules: only evaluated if the whitelist result is false.
// ANY matching blacklist rule triggers a bypass (OR logic).
func (p *bypassGroup) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return p.evaluate(ctx, network, addr, opts...) == decisionBypass
}
// evaluate returns the bypass decision for the group by applying two-phase
// evaluation: whitelist rules use AND logic (all must agree), then blacklist
// rules use OR logic (any match wins), evaluated only if whitelist fails.
func (p *bypassGroup) evaluate(ctx context.Context, network, addr string, opts ...bypass.Option) bypassDecision {
// Phase 1: Whitelist AND — all must agree to bypass.
hasWhitelist := false
allWhitelistBypass := true
for _, bp := range p.bypasses {
if !bp.IsWhitelist() {
continue
}
hasWhitelist = true
if !bp.Contains(ctx, network, addr, opts...) {
allWhitelistBypass = false
break // short-circuit: one whitelist failure is enough
}
}
if hasWhitelist && allWhitelistBypass {
return decisionBypass
}
// Phase 2: Blacklist OR — any match triggers bypass.
for _, bp := range p.bypasses {
if bp.IsWhitelist() {
continue
}
if bp.Contains(ctx, network, addr, opts...) {
return decisionBypass
}
}
return decisionProxy
}
// IsWhitelist always returns false for a group, since the group may contain
// a mix of whitelist and blacklist rules.
func (p *bypassGroup) IsWhitelist() bool {
return false
}
File diff suppressed because it is too large. Load diff
+77 -41
View File
@@ -4,17 +4,82 @@ import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
ctxvalue "github.com/go-gost/x/ctx"
"github.com/go-gost/plugin/bypass/proto"
"github.com/go-gost/x/internal/plugin"
auth_util "github.com/go-gost/x/internal/util/auth"
"google.golang.org/grpc"
)
// httpPluginRequest is the JSON payload sent to a remote HTTP bypass service.
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.BypassClient
log logger.Logger
}
// NewGRPCPlugin creates a Bypass plugin based on gRPC.
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) bypass.Bypass {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "bypass",
"bypass": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewBypassClient(conn)
}
return p
}
func (p *grpcPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
if p.client == nil {
return true
}
var options bypass.Options
for _, opt := range opts {
opt(&options)
}
r, err := p.client.Bypass(ctx,
&proto.BypassRequest{
Network: network,
Addr: addr,
Client: string(auth_util.IDFromContext(ctx)),
Host: options.Host,
Path: options.Path,
})
if err != nil {
p.log.Error(err)
return true
}
return r.Ok
}
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
type httpPluginRequest struct {
Service string `json:"service"`
Network string `json:"network"`
Addr string `json:"addr"`
Client string `json:"client"`
@@ -22,14 +87,10 @@ type httpPluginRequest struct {
Path string `json:"path"`
}
// httpPluginResponse is the JSON response from a remote HTTP bypass service.
type httpPluginResponse struct {
OK bool `json:"ok"`
}
// httpPlugin delegates bypass decisions to a remote HTTP service.
// All error paths return true (fail-open) so that a down plugin
// does not block traffic.
type httpPlugin struct {
url string
client *http.Client
@@ -37,9 +98,7 @@ type httpPlugin struct {
log logger.Logger
}
// NewHTTPPlugin creates a Bypass that delegates decisions to an HTTP
// bypass service at url. The service should accept POST requests with
// a JSON body and return {"ok": true/false}.
// NewHTTPPlugin creates an Bypass plugin based on HTTP.
func NewHTTPPlugin(name string, url string, opts ...plugin.Option) bypass.Bypass {
var options plugin.Options
for _, opt := range opts {
@@ -57,14 +116,9 @@ func NewHTTPPlugin(name string, url string, opts ...plugin.Option) bypass.Bypass
}
}
func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) (ok bool) {
if p.client == nil {
return true
}
log := p.log
if log == nil {
log = logger.Default()
return
}
var options bypass.Options
@@ -73,23 +127,20 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
}
rb := httpPluginRequest{
Service: options.Service,
Network: network,
Addr: addr,
Client: string(ctxvalue.ClientIDFromContext(ctx)),
Client: string(auth_util.IDFromContext(ctx)),
Host: options.Host,
Path: options.Path,
}
v, err := json.Marshal(&rb)
if err != nil {
log.Error(err)
return true
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
if err != nil {
log.Error(err)
return true
return
}
if p.header != nil {
@@ -98,32 +149,17 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
req.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(req)
if err != nil {
log.Error(err)
return true
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return true
return
}
res := httpPluginResponse{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
log.Error(err)
return true
return
}
return res.OK
}
func (p *httpPlugin) Close() error {
if p.client != nil {
if tr := plugin.HTTPClientTransport(p.client); tr != nil {
tr.CloseIdleConnections()
}
}
return nil
}
func (p *httpPlugin) IsWhitelist() bool {
return false
}
-90
View File
@@ -1,90 +0,0 @@
// Package bypass implements plugin-based Bypass using gRPC and HTTP
// transports. Plugins delegate bypass decisions to an external process
// or service.
package bypass
import (
"context"
"io"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
"github.com/go-gost/plugin/bypass/proto"
ctxvalue "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/plugin"
"google.golang.org/grpc"
)
// grpcPlugin delegates bypass decisions to a remote gRPC service.
// If the connection fails or the client is nil, all addresses bypass
// the proxy (fail-open).
type grpcPlugin struct {
conn grpc.ClientConnInterface
client proto.BypassClient
log logger.Logger
}
// NewGRPCPlugin creates a Bypass that delegates decisions to a gRPC
// bypass service at addr. On connection failure, the plugin logs the
// error and returns a fail-open instance (all addresses bypass).
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) bypass.Bypass {
var options plugin.Options
for _, opt := range opts {
opt(&options)
}
log := logger.Default().WithFields(map[string]any{
"kind": "bypass",
"bypass": name,
})
conn, err := plugin.NewGRPCConn(addr, &options)
if err != nil {
log.Error(err)
}
p := &grpcPlugin{
conn: conn,
log: log,
}
if conn != nil {
p.client = proto.NewBypassClient(conn)
}
return p
}
func (p *grpcPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
if p.client == nil {
return true
}
var options bypass.Options
for _, opt := range opts {
opt(&options)
}
r, err := p.client.Bypass(ctx,
&proto.BypassRequest{
Service: options.Service,
Network: network,
Addr: addr,
Client: string(ctxvalue.ClientIDFromContext(ctx)),
Host: options.Host,
Path: options.Path,
})
if err != nil {
p.log.Error(err)
return true
}
return r.Ok
}
func (p *grpcPlugin) Close() error {
if closer, ok := p.conn.(io.Closer); ok {
return closer.Close()
}
return nil
}
func (p *grpcPlugin) IsWhitelist() bool {
return false
}
-222
View File
@@ -1,222 +0,0 @@
package bypass
import (
"context"
"io"
"net"
"os"
"testing"
"github.com/go-gost/core/bypass"
corelogger "github.com/go-gost/core/logger"
"github.com/go-gost/plugin/bypass/proto"
"github.com/go-gost/x/internal/plugin"
xlogger "github.com/go-gost/x/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
)
func TestMain(m *testing.M) {
corelogger.SetDefault(xlogger.Nop())
os.Exit(m.Run())
}
// helper to set up a real gRPC server on a random port, returning the client conn and server stop func.
func newTestGRPCConn(t *testing.T, srv proto.BypassServer) (*grpc.ClientConn, func()) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterBypassServer(gsrv, srv)
go gsrv.Serve(lis)
conn, err := grpc.NewClient(lis.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
return conn, func() {
conn.Close()
gsrv.Stop()
}
}
func TestGRPCPlugin_FailOpen_NilClient(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
log: xlogger.Nop(),
}
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Contains_Success(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: true})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewBypassClient(conn),
log: xlogger.Nop(),
}
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Contains_Deny(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: false})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewBypassClient(conn),
log: xlogger.Nop(),
}
assert.False(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Contains_WithService(t *testing.T) {
srv := &testBypassServer{ok: true}
conn, cleanup := newTestGRPCConn(t, srv)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewBypassClient(conn),
log: xlogger.Nop(),
}
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1", bypass.WithService("myservice")))
assert.Equal(t, "myservice", srv.lastService)
}
func TestGRPCPlugin_Contains_WithHostAndPath(t *testing.T) {
srv := &testBypassServer{ok: true}
conn, cleanup := newTestGRPCConn(t, srv)
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewBypassClient(conn),
log: xlogger.Nop(),
}
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1",
bypass.WithHostOption("example.com"),
bypass.WithPathOption("/api/v1"),
))
assert.Equal(t, "example.com", srv.lastHost)
assert.Equal(t, "/api/v1", srv.lastPath)
}
func TestGRPCPlugin_Contains_NilClient(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
}
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Contains_ServerError(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &errorBypassServer{})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: proto.NewBypassClient(conn),
log: xlogger.Nop(),
}
// Server returns an error; Contains should return true (fail-open)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestGRPCPlugin_Close_NilConn(t *testing.T) {
p := &grpcPlugin{
conn: nil,
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_NonCloserConn(t *testing.T) {
p := &grpcPlugin{
conn: &nonCloserConn{},
client: nil,
}
assert.NoError(t, p.Close())
}
func TestGRPCPlugin_Close_WithRealConn(t *testing.T) {
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: true})
defer cleanup()
p := &grpcPlugin{
conn: conn,
client: nil,
log: xlogger.Nop(),
}
assert.NoError(t, p.Close())
}
func TestNewGRPCPlugin_RealConn(t *testing.T) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
gsrv := grpc.NewServer()
proto.RegisterBypassServer(gsrv, &testBypassServer{ok: true})
go gsrv.Serve(lis)
defer gsrv.Stop()
p := NewGRPCPlugin("test", lis.Addr().String(), plugin.TimeoutOption(500))
require.NotNil(t, p)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
assert.NoError(t, p.(io.Closer).Close())
}
func TestGRPCPlugin_IsWhitelist(t *testing.T) {
p := &grpcPlugin{}
assert.False(t, p.IsWhitelist())
}
// --- test helpers ---
type testBypassServer struct {
proto.UnimplementedBypassServer
ok bool
lastService string
lastHost string
lastPath string
}
func (s *testBypassServer) Bypass(ctx context.Context, req *proto.BypassRequest) (*proto.BypassReply, error) {
s.lastService = req.Service
s.lastHost = req.Host
s.lastPath = req.Path
return &proto.BypassReply{Ok: s.ok}, nil
}
type errorBypassServer struct {
proto.UnimplementedBypassServer
}
func (s *errorBypassServer) Bypass(ctx context.Context, req *proto.BypassRequest) (*proto.BypassReply, error) {
return nil, status.Error(codes.Internal, "internal error")
}
type nonCloserConn struct{}
func (n *nonCloserConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
return nil
}
func (n *nonCloserConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
}
var _ grpc.ClientConnInterface = (*nonCloserConn)(nil)
var _ io.Closer = (*grpcPlugin)(nil)
-174
View File
@@ -1,174 +0,0 @@
package bypass
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-gost/core/bypass"
"github.com/go-gost/x/internal/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHTTPPlugin(t *testing.T) {
p := NewHTTPPlugin("test", "http://localhost:9999")
require.NotNil(t, p)
hp, ok := p.(*httpPlugin)
require.True(t, ok)
assert.Equal(t, "http://localhost:9999", hp.url)
assert.NotNil(t, hp.client)
assert.NotNil(t, hp.log)
}
func TestHTTPPlugin_Contains_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_Deny(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": false}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.False(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_Non200(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
// Non-200 returns true (fail-open)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_InvalidJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`not json`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
// Invalid JSON returns true (fail-open)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_ConnectionRefused(t *testing.T) {
p := NewHTTPPlugin("test", "http://127.0.0.1:0", plugin.TimeoutOption(0))
// Connection refused returns true (fail-open)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_NilClient(t *testing.T) {
hp := &httpPlugin{
url: "http://localhost",
client: nil,
}
// Nil client returns true (fail-open)
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_WithService(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1", bypass.WithService("myservice")))
}
func TestHTTPPlugin_Contains_WithHostAndPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL)
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1",
bypass.WithHostOption("example.com"),
bypass.WithPathOption("/api/v1"),
))
}
func TestHTTPPlugin_Contains_WithCustomHeader(t *testing.T) {
var receivedHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeader = r.Header.Get("X-Custom")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
"X-Custom": []string{"test-value"},
}))
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
assert.Equal(t, "test-value", receivedHeader)
}
func TestHTTPPlugin_Contains_NoHeader(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
hp := &httpPlugin{
url: srv.URL,
client: plugin.NewHTTPClient(&plugin.Options{}),
header: nil,
}
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Contains_BadURL(t *testing.T) {
hp := &httpPlugin{
url: "http://invalid hostname", // space causes NewRequest to fail
client: plugin.NewHTTPClient(&plugin.Options{}),
}
// Bad URL returns true (fail-open)
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestHTTPPlugin_Close_NilClient(t *testing.T) {
hp := &httpPlugin{client: nil}
err := hp.Close()
assert.NoError(t, err)
}
func TestHTTPPlugin_Close_WithClient(t *testing.T) {
hp := &httpPlugin{
client: plugin.NewHTTPClient(&plugin.Options{}),
}
err := hp.Close()
assert.NoError(t, err)
}
func TestHTTPPlugin_IsWhitelist(t *testing.T) {
hp := &httpPlugin{}
assert.False(t, hp.IsWhitelist())
}
var _ io.Closer = (*httpPlugin)(nil)
+2 -39
View File
@@ -1,31 +1,3 @@
// Package chain implements the core routing infrastructure for GOST.
//
// It provides three key abstractions:
//
// - Router: top-level entry point that resolves addresses, selects routes
// via a Chainer, retries on failure, and records telemetry.
//
// - Chain: a named sequence of proxy hops (nodes). Each hop selects a node
// from its group, and the resulting Route carries traffic through every
// selected node in order.
//
// - Transport: bundles a dialer and connector for a single chain node. It
// handles Dial, Handshake, Connect, and Bind — the four steps needed to
// move traffic through a proxy hop.
//
// # Route traversal
//
// For a chain of N nodes, the first node is reached via Dial → Handshake,
// and each subsequent node via Connect → Handshake through the previous
// connection. On failure, connections are cleaned up and nodes are marked
// so selectors can deprioritize them.
//
// # Multiplexing
//
// When a node's transport supports multiplexing, Chain splits the route at
// that point: nodes before the multiplex-capable node form a sub-route that is
// copied into the transport, establishing a reusable tunnel for subsequent
// connections.
package chain
import (
@@ -73,7 +45,6 @@ type Chain struct {
logger logger.Logger
}
// NewChain creates a new Chain with the given name and options.
func NewChain(name string, opts ...ChainOption) *Chain {
var options ChainOptions
for _, opt := range opts {
@@ -90,14 +61,11 @@ func NewChain(name string, opts ...ChainOption) *Chain {
}
}
// AddHop appends a hop to the chain. Hops are traversed in order during
// route construction.
func (c *Chain) AddHop(hop hop.Hop) {
c.hops = append(c.hops, hop)
}
// Metadata returns the chain's metadata.
// Implements metadata.Metadatable interface.
// Metadata implements metadata.Metadatable interface.
func (c *Chain) Metadata() metadata.Metadata {
return c.metadata
}
@@ -111,9 +79,6 @@ func (c *Chain) Name() string {
return c.name
}
// Route builds a route by selecting one node from each hop. If a node
// supports multiplexing, the route is split — nodes before it form a
// sub-route that is copied into the transport for reuse.
func (c *Chain) Route(ctx context.Context, network, address string, opts ...chain.RouteOption) chain.Route {
if c == nil || len(c.hops) == 0 {
return nil
@@ -139,7 +104,7 @@ func (c *Chain) Route(ctx context.Context, network, address string, opts ...chai
tr.Options().Route = rt
node = node.Copy()
node.Options().Transport = tr
rt = NewRoute(ChainRouteOption(c))
rt = NewRoute()
}
rt.addNode(node)
@@ -152,8 +117,6 @@ type chainGroup struct {
selector selector.Selector[chain.Chainer]
}
// NewChainGroup creates a chain group that selects one Chainer from the
// given list using the configured selector (round-robin by default).
func NewChainGroup(chains ...chain.Chainer) *chainGroup {
return &chainGroup{chains: chains}
}
-1769
View File
File diff suppressed because it is too large. Load diff
+18 -120
View File
@@ -2,8 +2,6 @@ package chain
import (
"context"
"errors"
"fmt"
"net"
"time"
@@ -12,117 +10,27 @@ import (
"github.com/go-gost/core/logger"
"github.com/go-gost/core/metrics"
"github.com/go-gost/core/selector"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/net/dialer"
"github.com/go-gost/x/internal/net/udp"
xmetrics "github.com/go-gost/x/metrics"
)
var (
// ErrEmptyRoute is returned when the configured chain produces a route
// with no nodes.
ErrEmptyRoute = errors.New("empty route")
)
var (
// DefaultRoute is a fallback route that dials the target directly
// without going through any proxy nodes.
DefaultRoute chain.Route = &defaultRoute{}
)
// defaultRoute dials the target directly without any proxy nodes.
type defaultRoute struct{}
func (*defaultRoute) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
var options chain.DialOptions
for _, opt := range opts {
opt(&options)
}
netd := dialer.Dialer{
Interface: options.Interface,
Netns: options.Netns,
Log: options.Logger,
}
if options.SockOpts != nil {
netd.Mark = options.SockOpts.Mark
}
return netd.Dial(ctx, network, address)
}
func (*defaultRoute) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
var options chain.BindOptions
for _, opt := range opts {
opt(&options)
}
switch network {
case "tcp", "tcp4", "tcp6":
addr, err := net.ResolveTCPAddr(network, address)
if err != nil {
return nil, err
}
return net.ListenTCP(network, addr)
case "udp", "udp4", "udp6":
addr, err := net.ResolveUDPAddr(network, address)
if err != nil {
return nil, err
}
conn, err := net.ListenUDP(network, addr)
if err != nil {
return nil, err
}
logger := logger.Default().WithFields(map[string]any{
"network": network,
"address": address,
})
ln := udp.NewListener(conn, &udp.ListenConfig{
Backlog: options.Backlog,
ReadQueueSize: options.UDPDataQueueSize,
ReadBufferSize: options.UDPDataBufferSize,
TTL: options.UDPConnTTL,
Keepalive: true,
Logger: logger,
})
return ln, err
case "unix":
addr, err := net.ResolveUnixAddr(network, address)
if err != nil {
return nil, err
}
return net.ListenUnix(network, addr)
default:
err := fmt.Errorf("network %s unsupported", network)
return nil, err
}
}
func (r *defaultRoute) Nodes() []*chain.Node {
return nil
}
type RouteOptions struct {
Chain chain.Chainer
}
type RouteOption func(*RouteOptions)
// ChainRouteOption sets the parent chain on a route, enabling error
// tracking and metrics for the chain.
func ChainRouteOption(c chain.Chainer) RouteOption {
return func(o *RouteOptions) {
o.Chain = c
}
}
type chainRoute struct {
type route struct {
nodes []*chain.Node
options RouteOptions
}
// NewRoute creates a new route with the given options.
func NewRoute(opts ...RouteOption) *chainRoute {
func NewRoute(opts ...RouteOption) *route {
var options RouteOptions
for _, opt := range opts {
if opt != nil {
@@ -130,18 +38,18 @@ func NewRoute(opts ...RouteOption) *chainRoute {
}
}
return &chainRoute{
return &route{
options: options,
}
}
func (r *chainRoute) addNode(nodes ...*chain.Node) {
func (r *route) addNode(nodes ...*chain.Node) {
r.nodes = append(r.nodes, nodes...)
}
func (r *chainRoute) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
func (r *route) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
if len(r.Nodes()) == 0 {
return DefaultRoute.Dial(ctx, network, address, opts...)
return chain.DefaultRoute.Dial(ctx, network, address, opts...)
}
var options chain.DialOptions
@@ -165,9 +73,9 @@ func (r *chainRoute) Dial(ctx context.Context, network, address string, opts ...
return cc, nil
}
func (r *chainRoute) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
func (r *route) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
if len(r.Nodes()) == 0 {
return DefaultRoute.Bind(ctx, network, address, opts...)
return chain.DefaultRoute.Bind(ctx, network, address, opts...)
}
var options chain.BindOptions
@@ -198,7 +106,7 @@ func (r *chainRoute) Bind(ctx context.Context, network, address string, opts ...
return ln, nil
}
func (r *chainRoute) connect(ctx context.Context, logger logger.Logger) (conn net.Conn, err error) {
func (r *route) connect(ctx context.Context, logger logger.Logger) (conn net.Conn, err error) {
network := "ip"
node := r.nodes[0]
@@ -221,16 +129,15 @@ func (r *chainRoute) connect(ctx context.Context, logger logger.Logger) (conn ne
metrics.Labels{"chain": name, "node": node.Name}); v != nil {
v.Inc()
}
return
}
if marker != nil {
marker.Reset()
} else {
if marker != nil {
marker.Reset()
}
}
}
}()
addr, err := xnet.Resolve(ctx, network, node.Addr, node.Options().Resolver, node.Options().HostMapper, logger)
addr, err := chain.Resolve(ctx, network, node.Addr, node.Options().Resolver, node.Options().HostMapper, logger)
marker := node.Marker()
if err != nil {
if marker != nil {
@@ -274,7 +181,7 @@ func (r *chainRoute) connect(ctx context.Context, logger logger.Logger) (conn ne
preNode := node
for _, node := range r.nodes[1:] {
marker := node.Marker()
addr, err = xnet.Resolve(ctx, network, node.Addr, node.Options().Resolver, node.Options().HostMapper, logger)
addr, err = chain.Resolve(ctx, network, node.Addr, node.Options().Resolver, node.Options().HostMapper, logger)
if err != nil {
cn.Close()
if marker != nil {
@@ -284,29 +191,20 @@ func (r *chainRoute) connect(ctx context.Context, logger logger.Logger) (conn ne
}
cc, err = preNode.Options().Transport.Connect(ctx, cn, "tcp", addr)
if err != nil {
if cc != nil {
cc.Close()
}
cn.Close()
if marker != nil {
marker.Mark()
}
return
}
var cc2 net.Conn
cc2, err = node.Options().Transport.Handshake(ctx, cc)
cc, err = node.Options().Transport.Handshake(ctx, cc)
if err != nil {
if cc2 != nil {
cc2.Close()
}
cc.Close()
cn.Close()
if marker != nil {
marker.Mark()
}
return
}
cc = cc2
if marker != nil {
marker.Reset()
}
@@ -319,14 +217,14 @@ func (r *chainRoute) connect(ctx context.Context, logger logger.Logger) (conn ne
return
}
func (r *chainRoute) getNode(index int) *chain.Node {
func (r *route) getNode(index int) *chain.Node {
if r == nil || len(r.Nodes()) == 0 || index < 0 || index >= len(r.Nodes()) {
return nil
}
return r.nodes[index]
}
func (r *chainRoute) Nodes() []*chain.Node {
func (r *route) Nodes() []*chain.Node {
if r != nil {
return r.nodes
}
-274
View File
@@ -1,274 +0,0 @@
package chain
import (
"bytes"
"context"
"fmt"
"net"
"strings"
"time"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/recorder"
xctx "github.com/go-gost/x/ctx"
ictx "github.com/go-gost/x/internal/ctx"
xnet "github.com/go-gost/x/internal/net"
)
// Router is the top-level routing entry point. It resolves addresses,
// selects routes through the configured chain, retries on failure, and
// records telemetry (address events, errors, metrics).
type Router struct {
options chain.RouterOptions
}
// NewRouter creates a Router with the given options. Defaults: 15s timeout,
// a logger with kind "router" if none is provided.
func NewRouter(opts ...chain.RouterOption) *Router {
r := &Router{}
for _, opt := range opts {
if opt != nil {
opt(&r.options)
}
}
if r.options.Timeout == 0 {
r.options.Timeout = 15 * time.Second
}
if r.options.Logger == nil {
r.options.Logger = logger.Default().WithFields(map[string]any{"kind": "router"})
}
return r
}
func (r *Router) Options() *chain.RouterOptions {
if r == nil {
return nil
}
return &r.options
}
// Dial establishes a connection to the target address through the configured
// chain. It resolves the address, records the dial event, retries up to
// Retries+1 times, and returns the resulting connection.
// For UDP networks the returned connection is wrapped as a PacketConn if the
// underlying transport does not already implement net.PacketConn.
func (r *Router) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (conn net.Conn, err error) {
host := address
if h, _, _ := net.SplitHostPort(address); h != "" {
host = h
}
r.record(ctx, recorder.RecorderServiceRouterDialAddress, []byte(host))
log := r.options.Logger.WithFields(map[string]any{
"sid": xctx.SidFromContext(ctx),
})
conn, err = r.dial(ctx, network, address, log, opts...)
if err != nil {
r.record(ctx, recorder.RecorderServiceRouterDialAddressError, []byte(host))
return
}
if network == "udp" || network == "udp4" || network == "udp6" {
if _, ok := conn.(net.PacketConn); !ok {
return &packetConn{conn}, nil
}
}
return
}
func (r *Router) record(ctx context.Context, name string, data []byte) error {
if len(data) == 0 {
return nil
}
for _, rec := range r.options.Recorders {
if rec.Record == name {
return rec.Recorder.Record(ctx, data)
}
}
return nil
}
func (r *Router) dial(ctx context.Context, network, address string, log logger.Logger, callerOpts ...chain.DialOption) (conn net.Conn, err error) {
count := r.options.Retries + 1
if count <= 0 {
count = 1
}
log.Debugf("dial %s/%s", address, network)
for i := 0; i < count; i++ {
func() {
ctx := ctx
if r.options.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.options.Timeout)
defer cancel()
}
buf := ictx.BufferFromContext(ctx)
if buf != nil {
buf.Reset()
}
var ipAddr string
ipAddr, err = xnet.Resolve(ctx, "ip", address, r.options.Resolver, r.options.HostMapper, log)
if err != nil {
log.Error(err)
return
}
if buf != nil {
buf.Reset()
}
var route chain.Route
if r.options.Chain != nil {
route = r.options.Chain.Route(ctx, network, ipAddr, chain.WithHostRouteOption(address))
}
if buf == nil {
buf = &bytes.Buffer{}
}
for _, node := range routePath(route) {
fmt.Fprintf(buf, "%s@%s > ", node.Name, node.Addr)
}
fmt.Fprintf(buf, "%s", ipAddr)
log.Debugf("route(retry=%d) %s", i, buf.String())
if route == nil {
route = DefaultRoute
}
ifceName := r.options.IfceName
if strings.Contains(ifceName, "auto") {
// "auto" triggers per-connection interface detection: the
// ingress IP (conn.LocalAddr()) is used as the bind address,
// ensuring egress traffic leaves via the same interface that
// received the connection — the "source-in source-out"
// pattern for multi-homed hosts.
if dstAddr := xctx.DstAddrFromContext(ctx); dstAddr != nil {
if host, _, _ := net.SplitHostPort(dstAddr.String()); host != "" &&
host != "0.0.0.0" && host != "::" {
// Replace only exact "auto" tokens in the
// comma-separated interface list.
parts := strings.Split(ifceName, ",")
for i, p := range parts {
if p == "auto" {
parts[i] = host
}
}
ifceName = strings.Join(parts, ",")
}
}
if strings.Contains(ifceName, "auto") {
log.Debugf("auto interface: no suitable ingress address, keeping literal %q", ifceName)
}
}
conn, err = route.Dial(ctx, network, ipAddr,
append([]chain.DialOption{
chain.InterfaceDialOption(ifceName),
chain.NetnsDialOption(r.options.Netns),
chain.SockOptsDialOption(r.options.SockOpts),
chain.LoggerDialOption(log),
}, callerOpts...)...,
)
if err == nil {
return
}
log.Errorf("route(retry=%d) %s", i, err)
}()
if conn != nil || err == nil {
break
}
}
return
}
// Bind creates a listener bound to the given address through the configured
// chain. It retries up to Retries+1 times on failure.
func (r *Router) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (ln net.Listener, err error) {
count := r.options.Retries + 1
if count <= 0 {
count = 1
}
log := r.options.Logger.WithFields(map[string]any{
"sid": xctx.SidFromContext(ctx),
})
log.Debugf("bind on %s/%s", address, network)
for i := 0; i < count; i++ {
func() {
ctx := ctx
if r.options.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.options.Timeout)
defer cancel()
}
var route chain.Route
if r.options.Chain != nil {
route = r.options.Chain.Route(ctx, network, address)
if route == nil || len(route.Nodes()) == 0 {
err = ErrEmptyRoute
return
}
}
if log.IsLevelEnabled(logger.DebugLevel) {
buf := bytes.Buffer{}
for _, node := range routePath(route) {
fmt.Fprintf(&buf, "%s@%s > ", node.Name, node.Addr)
}
fmt.Fprintf(&buf, "%s", address)
log.Debugf("route(retry=%d) %s", i, buf.String())
}
if route == nil {
route = DefaultRoute
}
ln, err = route.Bind(ctx, network, address, opts...)
if err == nil {
return
}
log.Errorf("route(retry=%d) %s", i, err)
}()
if ln != nil || err == nil {
break
}
}
return
}
func routePath(route chain.Route) (path []*chain.Node) {
if route == nil {
return
}
for _, node := range route.Nodes() {
if tr := node.Options().Transport; tr != nil {
path = append(path, routePath(tr.Options().Route)...)
}
path = append(path, node)
}
return
}
type packetConn struct {
net.Conn
}
func (c *packetConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
n, err = c.Read(b)
addr = c.Conn.RemoteAddr()
return
}
func (c *packetConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
return c.Write(b)
}
-113
View File
@@ -1,113 +0,0 @@
package chain
import (
"context"
"net"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/connector"
"github.com/go-gost/core/dialer"
net_dialer "github.com/go-gost/x/internal/net/dialer"
)
// Transport bundles a dialer and connector for a single chain node.
// It implements chain.Transporter and handles the four operations in
// the proxy chain lifecycle: Dial, Handshake, Connect, and Bind.
type Transport struct {
dialer dialer.Dialer
connector connector.Connector
options chain.TransportOptions
}
// NewTransport creates a Transport with the given dialer and connector.
func NewTransport(d dialer.Dialer, c connector.Connector, opts ...chain.TransportOption) *Transport {
tr := &Transport{
dialer: d,
connector: c,
}
for _, opt := range opts {
if opt != nil {
opt(&tr.options)
}
}
return tr
}
func (tr *Transport) Dial(ctx context.Context, addr string) (net.Conn, error) {
netd := &net_dialer.Dialer{
Interface: tr.options.IfceName,
Netns: tr.options.Netns,
}
if tr.options.SockOpts != nil {
netd.Mark = tr.options.SockOpts.Mark
}
if tr.options.Route != nil && len(tr.options.Route.Nodes()) > 0 {
netd.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
return tr.options.Route.Dial(ctx, network, addr)
}
}
opts := []dialer.DialOption{
dialer.HostDialOption(tr.options.Addr),
dialer.NetDialerDialOption(netd),
}
return tr.dialer.Dial(ctx, addr, opts...)
}
func (tr *Transport) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) {
var err error
if hs, ok := tr.dialer.(dialer.Handshaker); ok {
conn, err = hs.Handshake(ctx, conn,
dialer.AddrHandshakeOption(tr.options.Addr))
if err != nil {
return nil, err
}
}
if hs, ok := tr.connector.(connector.Handshaker); ok {
return hs.Handshake(ctx, conn)
}
return conn, nil
}
func (tr *Transport) Connect(ctx context.Context, conn net.Conn, network, address string) (net.Conn, error) {
netd := &net_dialer.Dialer{
Interface: tr.options.IfceName,
Netns: tr.options.Netns,
}
if tr.options.SockOpts != nil {
netd.Mark = tr.options.SockOpts.Mark
}
return tr.connector.Connect(ctx, conn, network, address,
connector.DialerConnectOption(netd),
)
}
func (tr *Transport) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
if binder, ok := tr.connector.(connector.Binder); ok {
return binder.Bind(ctx, conn, network, address, opts...)
}
return nil, connector.ErrBindUnsupported
}
func (tr *Transport) Multiplex() bool {
if mux, ok := tr.dialer.(dialer.Multiplexer); ok {
return mux.Multiplex()
}
return false
}
func (tr *Transport) Options() *chain.TransportOptions {
if tr != nil {
return &tr.options
}
return nil
}
// Copy returns a shallow copy of the transport. Used by Chain when
// splitting a route at a multiplex-capable node so the sub-route can
// be embedded in the transport without sharing state across calls.
func (tr *Transport) Copy() chain.Transporter {
tr2 := &Transport{}
*tr2 = *tr
return tr2
}
-959
View File
@@ -1,959 +0,0 @@
// Package cmd converts CLI command-line arguments (-L services, -F chain nodes)
// into structured config.Config objects. It supports URL-format service and node
// specifications, metadata query parameters, and auto-detection of handler,
// listener, dialer, and connector types via the registry.
package cmd
import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/go-gost/x/config"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/limiter/conn"
"github.com/go-gost/x/limiter/traffic"
mdx "github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
"github.com/go-gost/x/registry"
)
var (
// ErrInvalidCmd is returned when the command-line argument is empty.
ErrInvalidCmd = errors.New("invalid cmd")
// ErrInvalidNode is returned when a node specification cannot be parsed.
ErrInvalidNode = errors.New("invalid node")
)
// BuildConfigFromCmd converts CLI service (-L) and node (-F) arguments into
// a Config. Each node becomes a hop in a single chain; each service produces
// a ServiceConfig with auto-detected handler and listener types. Query
// parameters in the URL are parsed as metadata and routed to the appropriate
// config sub-struct (handler, listener, hop, connector, dialer, service).
func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error) {
namePrefix := ""
cfg := &config.Config{}
var chain *config.ChainConfig
if len(nodeList) > 0 {
chain = &config.ChainConfig{
Name: fmt.Sprintf("%schain-0", namePrefix),
}
cfg.Chains = append(cfg.Chains, chain)
}
for i, node := range nodeList {
url, err := Norm(node)
if err != nil {
return nil, err
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
hopConfig := &config.HopConfig{
Name: fmt.Sprintf("%shop-%d", namePrefix, i),
Selector: parseSelector(m),
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
hopConfig.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(m, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
Only: mdutil.GetString(md, "only"),
},
)
}
hopConfig.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(m, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
hopConfig.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(m, "hosts")
}
if v := mdutil.GetString(md, "interface"); v != "" {
m["hop.interface"] = v
delete(m, "interface")
}
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
m["hop.so_mark"] = v
delete(m, "so_mark")
}
if v := mdutil.GetInt(md, "proxyProtocol"); v > 0 {
m["hop.proxyProtocol"] = v
delete(m, "proxyProtocol")
}
hopMd := map[string]any{}
for k, v := range m {
if strings.HasPrefix(k, "node.") ||
strings.HasPrefix(k, "connector.") ||
strings.HasPrefix(k, "dialer.") {
continue
}
if s, found := strings.CutPrefix(k, "hop."); found {
hopMd[s] = v
delete(m, k)
continue
}
hopMd[k] = v
}
hopConfig.Metadata = hopMd
nodeConfig, err := buildNodeConfig(url, m)
if err != nil {
return nil, err
}
nodeConfig.Name = fmt.Sprintf("%snode-0", namePrefix)
var nodes []*config.NodeConfig
for _, host := range strings.Split(nodeConfig.Addr, ",") {
if host == "" {
continue
}
nodeCfg := &config.NodeConfig{}
*nodeCfg = *nodeConfig
nodeCfg.Connector = copyConnectorConfig(nodeConfig.Connector)
nodeCfg.Dialer = copyDialerConfig(nodeConfig.Dialer)
nodeCfg.Metadata = copyMap(nodeConfig.Metadata)
nodeCfg.Name = fmt.Sprintf("%snode-%d", namePrefix, len(nodes))
nodeCfg.Addr = host
nodes = append(nodes, nodeCfg)
}
hopConfig.Nodes = nodes
chain.Hops = append(chain.Hops, hopConfig)
}
var services []*config.ServiceConfig
for _, svc := range serviceList {
svc = strings.TrimSpace(svc)
if svc == "" {
continue
}
if svc[0] == ':' || !strings.Contains(svc, "://") {
svc = "auto://" + svc
}
host, svc := cutHost(svc)
url, err := Norm(svc)
if err != nil {
return nil, err
}
url.Host = host
svcs, err := buildServiceConfig(url)
if err != nil {
return nil, err
}
services = append(services, svcs...)
}
for i, service := range services {
service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i)
if chain != nil {
if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" || service.Listener.Type == "runix" {
service.Listener.Chain = chain.Name
} else {
service.Handler.Chain = chain.Name
}
}
cfg.Services = append(cfg.Services, service)
mh := service.Handler.Metadata
md := mdx.NewMetadata(mh)
if v := mdutil.GetInt(md, "retries"); v > 0 {
service.Handler.Retries = v
delete(mh, "retries")
}
if v := mdutil.GetString(md, "admission"); v != "" {
admCfg := &config.AdmissionConfig{
Name: fmt.Sprintf("%sadmission-%d", namePrefix, len(cfg.Admissions)),
}
if v[0] == '~' {
admCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
admCfg.Matchers = append(admCfg.Matchers, s)
}
service.Admission = admCfg.Name
cfg.Admissions = append(cfg.Admissions, admCfg)
delete(mh, "admission")
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
service.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(mh, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
Only: mdutil.GetString(md, "only"),
},
)
}
service.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(mh, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
service.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(mh, "hosts")
}
in := mdutil.GetString(md, "limiter.in")
out := mdutil.GetString(md, "limiter.out")
cin := mdutil.GetString(md, "limiter.conn.in")
cout := mdutil.GetString(md, "limiter.conn.out")
if in != "" || cin != "" || out != "" || cout != "" {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%slimiter-%d", namePrefix, len(cfg.Limiters)),
}
if in != "" || out != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.ServiceLimitKey, in, out))
}
if cin != "" || cout != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.ConnLimitKey, cin, cout))
}
service.Limiter = limiter.Name
cfg.Limiters = append(cfg.Limiters, limiter)
delete(mh, "limiter.in")
delete(mh, "limiter.out")
delete(mh, "limiter.conn.in")
delete(mh, "limiter.conn.out")
}
if climit := mdutil.GetInt(md, "climiter"); climit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%sclimiter-%d", namePrefix, len(cfg.CLimiters)),
Limits: []string{fmt.Sprintf("%s %d", conn.GlobalLimitKey, climit)},
}
service.CLimiter = limiter.Name
cfg.CLimiters = append(cfg.CLimiters, limiter)
delete(mh, "climiter")
}
if rlimit := mdutil.GetFloat(md, "rlimiter"); rlimit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%srlimiter-%d", namePrefix, len(cfg.RLimiters)),
Limits: []string{fmt.Sprintf("%s %s", conn.GlobalLimitKey, strconv.FormatFloat(rlimit, 'f', -1, 64))},
}
service.RLimiter = limiter.Name
cfg.RLimiters = append(cfg.RLimiters, limiter)
delete(mh, "rlimiter")
}
}
return cfg, nil
}
// cutHost extracts the host portion from a URL string, stripping any auth
// info (user:pass@). It returns the host and the remainder of the URL with
// the host portion removed.
func cutHost(s string) (host, remain string) {
if s == "" {
return
}
n := strings.IndexByte(s, ':')
start := n + 3
end := strings.IndexAny(s[start:], "/?")
if end < 0 {
end = len(s)
} else {
end += start
}
// auth info
if n = strings.LastIndexByte(s[start:end], '@'); n >= 0 {
start += (n + 1)
}
host = s[start:end]
remain = s[:start] + s[end:]
return
}
// buildServiceConfig converts a parsed service URL into one or more
// ServiceConfig entries. Scheme components (handler+listener) are resolved
// against the registry; path-based protocols use the URL path as the listen
// address; a non-empty path for other protocols indicates forwarding mode.
func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
namePrefix := ""
if v := os.Getenv("_GOST_ID"); v != "" {
namePrefix = fmt.Sprintf("go-%s@", v)
}
var handler, listener string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
handler = schemes[0]
listener = schemes[0]
}
if len(schemes) == 2 {
handler = schemes[0]
listener = schemes[1]
}
// For path-based protocols (Unix socket listeners and serial), use path as address
isPathBasedProtocol := listener == "unix" || listener == "runix" || listener == "serial"
var addrs []string
if isPathBasedProtocol {
path := url.EscapedPath()
if url.Host != "" {
addrs = append(addrs, url.Host+strings.TrimPrefix(path, "/"))
} else if path != "" {
addrs = append(addrs, path)
} else {
addrs = append(addrs, url.Host)
}
} else {
addrs = xnet.AddrPortRange(url.Host).Addrs()
if len(addrs) == 0 {
addrs = append(addrs, url.Host)
}
}
var services []*config.ServiceConfig
for _, addr := range addrs {
services = append(services, &config.ServiceConfig{
Addr: addr,
})
}
if h := registry.HandlerRegistry().Get(handler); h == nil {
handler = "auto"
}
if ln := registry.ListenerRegistry().Get(listener); ln == nil {
listener = "tcp"
if handler == "ssu" {
listener = "udp"
}
}
var nodes []*config.ForwardNodeConfig
// forward mode
// For path-based protocols, path is address not forward target
if remotes := strings.Trim(url.EscapedPath(), "/"); remotes != "" && !isPathBasedProtocol {
i := 0
for _, addr := range strings.Split(remotes, ",") {
addrs := xnet.AddrPortRange(addr).Addrs()
if len(addrs) == 0 {
addrs = append(addrs, addr)
}
for _, adr := range addrs {
nodes = append(nodes, &config.ForwardNodeConfig{
Name: fmt.Sprintf("%starget-%d", namePrefix, i),
Addr: adr,
})
i++
}
}
if handler != "relay" {
if listener == "tcp" || listener == "udp" ||
listener == "rtcp" || listener == "rudp" ||
listener == "tun" || listener == "tap" ||
listener == "dns" || listener == "unix" || listener == "runix" ||
listener == "serial" {
handler = listener
} else {
handler = "forward"
}
}
}
// stdio listener uses url.Host as the forwarding target.
if listener == "stdio" && len(nodes) == 0 && url.Host != "" {
nodes = append(nodes, &config.ForwardNodeConfig{
Name: fmt.Sprintf("%starget-0", namePrefix),
Addr: url.Host,
})
if handler == "auto" {
handler = "tcp"
}
}
if len(nodes) > 0 {
if len(services) == 1 {
services[0].Forwarder = &config.ForwarderConfig{
Nodes: nodes,
}
} else {
for i, svc := range services {
if len(nodes) == 1 {
svc.Forwarder = &config.ForwarderConfig{
Nodes: nodes,
}
} else {
if i < len(nodes) {
svc.Forwarder = &config.ForwarderConfig{
Nodes: []*config.ForwardNodeConfig{nodes[i]},
}
}
}
}
}
}
var auth *config.AuthConfig
if url.User != nil {
if strings.HasPrefix(url.Scheme, "ss") {
auth = decodeSIP002Auth(url)
}
if auth == nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
if sa := mdutil.GetString(md, "auth"); sa != "" && auth == nil {
au, err := parseAuthFromCmd(sa)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "tls.certFile", "certFile", "cert"),
KeyFile: mdutil.GetString(md, "tls.keyFile", "keyFile", "key"),
CAFile: mdutil.GetString(md, "tls.caFile", "caFile", "ca"),
}
delete(m, "tls.certFile")
delete(m, "certFile")
delete(m, "cert")
delete(m, "tls.keyFile")
delete(m, "keyFile")
delete(m, "key")
delete(m, "tls.caFile")
delete(m, "caFile")
delete(m, "ca")
if tlsConfig.CertFile == "" {
tlsConfig = nil
}
if v := mdutil.GetString(md, "dns"); v != "" {
m["dns"] = strings.Split(v, ",")
}
if v := mdutil.GetString(md, "interface"); v != "" {
m["service.interface"] = v
delete(m, "interface")
}
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
m["service.so_mark"] = v
delete(m, "so_mark")
}
if v := mdutil.GetInt(md, "proxyProtocol"); v > 0 {
m["service.proxyProtocol"] = v
delete(m, "proxyProtocol")
}
if v := mdutil.GetString(md, "netns"); v != "" {
m["service.netns"] = v
delete(m, "netns")
}
if v := mdutil.GetString(md, "netns.out"); v != "" {
m["service.netns.out"] = v
delete(m, "netns.out")
}
selector := parseSelector(m)
serviceMd := map[string]any{}
handleMd := map[string]any{}
listenerMd := map[string]any{}
for k, v := range m {
if s, found := strings.CutPrefix(k, "service."); found {
serviceMd[s] = v
continue
}
if s, found := strings.CutPrefix(k, "handler."); found {
handleMd[s] = v
continue
}
if s, found := strings.CutPrefix(k, "listener."); found {
listenerMd[s] = v
continue
}
handleMd[k] = v
listenerMd[k] = v
serviceMd[k] = v
}
handlerCfg := &config.HandlerConfig{
Type: handler,
Auth: auth,
Metadata: handleMd,
}
listenerCfg := &config.ListenerConfig{
Type: listener,
TLS: tlsConfig,
Metadata: listenerMd,
}
if listenerCfg.Type == "ssh" || listenerCfg.Type == "sshd" {
handlerCfg.Auth = nil
listenerCfg.Auth = auth
}
for _, svc := range services {
if svc.Forwarder != nil {
svc.Forwarder.Selector = selector
}
svc.Handler = copyHandlerConfig(handlerCfg)
svc.Listener = copyListenerConfig(listenerCfg)
svc.Metadata = copyMap(serviceMd)
}
return services, nil
}
// buildNodeConfig converts a parsed node URL into a NodeConfig. Scheme
// components (connector+dialer) are resolved against the registry; path-based
// dialers use the URL path as the node address.
func buildNodeConfig(url *url.URL, m map[string]any) (*config.NodeConfig, error) {
var connector, dialer string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
connector = schemes[0]
dialer = schemes[0]
}
if len(schemes) == 2 {
connector = schemes[0]
dialer = schemes[1]
}
md := mdx.NewMetadata(m)
if c := registry.ConnectorRegistry().Get(connector); c == nil {
connector = "http"
}
if d := registry.DialerRegistry().Get(dialer); d == nil {
dialer = "tcp"
if connector == "ssu" {
dialer = "udp"
}
}
var auth *config.AuthConfig
if url.User != nil {
if strings.HasPrefix(url.Scheme, "ss") {
auth = decodeSIP002Auth(url)
}
if auth == nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
}
if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil {
au, err := parseAuthFromCmd(sauth)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "tls.certFile", "certFile", "cert"),
KeyFile: mdutil.GetString(md, "tls.keyFile", "keyFile", "key"),
CAFile: mdutil.GetString(md, "tls.caFile", "caFile", "ca"),
Secure: mdutil.GetBool(md, "tls.secure", "secure"),
ServerName: mdutil.GetString(md, "tls.servername", "servername"),
}
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = url.Hostname()
}
delete(m, "tls.certFile")
delete(m, "certFile")
delete(m, "cert")
delete(m, "tls.keyFile")
delete(m, "keyFile")
delete(m, "key")
delete(m, "tls.caFile")
delete(m, "caFile")
delete(m, "ca")
delete(m, "tls.secure")
delete(m, "secure")
delete(m, "tls.servername")
delete(m, "serverName")
if !tlsConfig.Secure && tlsConfig.CertFile == "" && tlsConfig.CAFile == "" && tlsConfig.ServerName == "" {
tlsConfig = nil
}
nodeMd := map[string]any{}
connectorMd := map[string]any{}
dialerMd := map[string]any{}
for k, v := range m {
if s, found := strings.CutPrefix(k, "node."); found {
nodeMd[s] = v
continue
}
if s, found := strings.CutPrefix(k, "connector."); found {
connectorMd[s] = v
continue
}
if s, found := strings.CutPrefix(k, "dialer."); found {
dialerMd[s] = v
continue
}
connectorMd[k] = v
dialerMd[k] = v
nodeMd[k] = v
}
// For path-based protocols (Unix socket dialers and serial), use path as address
var nodeAddr string
isPathBasedProtocol := dialer == "unix" || dialer == "serial"
if isPathBasedProtocol {
path := url.EscapedPath()
if url.Host != "" {
// Two slashes: host + path = relative path
nodeAddr = url.Host + strings.TrimPrefix(path, "/")
} else if path != "" {
// Three slashes: path is absolute (keep leading slash)
nodeAddr = path
} else {
nodeAddr = url.Host
}
} else {
nodeAddr = url.Host
}
node := &config.NodeConfig{
Addr: nodeAddr,
Metadata: nodeMd,
}
node.Connector = &config.ConnectorConfig{
Type: connector,
Auth: auth,
Metadata: connectorMd,
}
node.Dialer = &config.DialerConfig{
Type: dialer,
TLS: tlsConfig,
Metadata: dialerMd,
}
if node.Dialer.Type == "ssh" || node.Dialer.Type == "sshd" {
node.Connector.Auth = nil
node.Dialer.Auth = auth
}
return node, nil
}
// Norm normalizes a raw command-line string into a parsed URL. Missing schemes
// default to "auto" and "https" is rewritten to "http+tls". An empty string
// returns ErrInvalidCmd.
func Norm(s string) (*url.URL, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, ErrInvalidCmd
}
if s[0] == ':' || !strings.Contains(s, "://") {
s = "auto://" + s
}
url, err := url.Parse(s)
if err != nil {
return nil, err
}
if url.Scheme == "https" {
url.Scheme = "http+tls"
}
return url, nil
}
// decodeSIP002Auth attempts to decode a Shadowsocks SIP002-style URL where
// the userinfo is a single base64-encoded "method:password" string with no
// separate password field. Returns nil if the URL does not use this format.
func decodeSIP002Auth(u *url.URL) *config.AuthConfig {
if u.User == nil {
return nil
}
_, hasPassword := u.User.Password()
if hasPassword {
return nil
}
username := u.User.Username()
if username == "" {
return nil
}
decoded, err := base64.RawURLEncoding.DecodeString(username)
if err != nil {
decoded, err = base64.StdEncoding.DecodeString(username)
if err != nil {
return nil
}
}
cs := string(decoded)
n := strings.IndexByte(cs, ':')
if n < 0 {
return nil
}
return &config.AuthConfig{
Username: cs[:n],
Password: cs[n+1:],
}
}
// parseAuthFromCmd decodes a base64-encoded "username:password" string from
// a query parameter into an AuthConfig. The password portion is optional.
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
v, err := base64.RawURLEncoding.DecodeString(sa)
if err != nil {
v, err = base64.StdEncoding.DecodeString(sa)
if err != nil {
return nil, err
}
}
cs := string(v)
n := strings.IndexByte(cs, ':')
if n < 0 {
return &config.AuthConfig{
Username: cs,
}, nil
}
return &config.AuthConfig{
Username: cs[:n],
Password: cs[n+1:],
}, nil
}
// parseSelector extracts selector configuration (strategy, maxFails,
// failTimeout) from metadata and removes the consumed keys. Returns nil
// if no selector parameters are present.
func parseSelector(m map[string]any) *config.SelectorConfig {
md := mdx.NewMetadata(m)
strategy := mdutil.GetString(md, "strategy")
maxFails := mdutil.GetInt(md, "maxFails", "max_fails")
failTimeout := mdutil.GetDuration(md, "failTimeout", "fail_timeout")
if strategy == "" && maxFails <= 0 && failTimeout <= 0 {
return nil
}
if strategy == "" {
strategy = "round"
}
if maxFails <= 0 {
maxFails = 1
}
if failTimeout <= 0 {
failTimeout = 30 * time.Second
}
delete(m, "strategy")
delete(m, "maxFails")
delete(m, "max_fails")
delete(m, "failTimeout")
delete(m, "fail_timeout")
return &config.SelectorConfig{
Strategy: strategy,
MaxFails: maxFails,
FailTimeout: failTimeout,
}
}
func copyMap(m map[string]any) map[string]any {
if m == nil {
return nil
}
c := make(map[string]any, len(m))
for k, v := range m {
c[k] = v
}
return c
}
func copyAuth(cfg *config.AuthConfig) *config.AuthConfig {
if cfg == nil {
return nil
}
c := *cfg
return &c
}
func copyTLS(cfg *config.TLSConfig) *config.TLSConfig {
if cfg == nil {
return nil
}
c := *cfg
return &c
}
func copyConnectorConfig(cfg *config.ConnectorConfig) *config.ConnectorConfig {
if cfg == nil {
return nil
}
c := *cfg
c.Auth = copyAuth(cfg.Auth)
c.TLS = copyTLS(cfg.TLS)
c.Metadata = copyMap(cfg.Metadata)
return &c
}
func copyDialerConfig(cfg *config.DialerConfig) *config.DialerConfig {
if cfg == nil {
return nil
}
c := *cfg
c.Auth = copyAuth(cfg.Auth)
c.TLS = copyTLS(cfg.TLS)
c.Metadata = copyMap(cfg.Metadata)
return &c
}
func copyHandlerConfig(cfg *config.HandlerConfig) *config.HandlerConfig {
if cfg == nil {
return nil
}
c := *cfg
c.Auth = copyAuth(cfg.Auth)
c.TLS = copyTLS(cfg.TLS)
c.Metadata = copyMap(cfg.Metadata)
return &c
}
func copyListenerConfig(cfg *config.ListenerConfig) *config.ListenerConfig {
if cfg == nil {
return nil
}
c := *cfg
c.Auth = copyAuth(cfg.Auth)
c.TLS = copyTLS(cfg.TLS)
c.Metadata = copyMap(cfg.Metadata)
return &c
}
File diff suppressed because it is too large. Load diff
+50 -253
View File
@@ -3,8 +3,6 @@ package config
import (
"encoding/json"
"io"
"path/filepath"
"strings"
"sync"
"time"
@@ -81,11 +79,6 @@ type LogRotationConfig struct {
Compress bool `yaml:"compress,omitempty" json:"compress,omitempty"`
}
type LoggerConfig struct {
Name string `json:"name"`
Log *LogConfig `yaml:",omitempty" json:"log,omitempty"`
}
type ProfilingConfig struct {
Addr string `json:"addr"`
}
@@ -117,16 +110,12 @@ type TLSConfig struct {
Validity time.Duration `yaml:",omitempty" json:"validity,omitempty"`
CommonName string `yaml:"commonName,omitempty" json:"commonName,omitempty"`
Organization string `yaml:",omitempty" json:"organization,omitempty"`
RejectUnknownSNI bool `yaml:"rejectUnknownSNI,omitempty" json:"rejectUnknownSNI,omitempty"`
ServerNames []string `yaml:"serverNames,omitempty" json:"serverNames,omitempty"`
}
type TLSOptions struct {
MinVersion string `yaml:"minVersion,omitempty" json:"minVersion,omitempty"`
MaxVersion string `yaml:"maxVersion,omitempty" json:"maxVersion,omitempty"`
CipherSuites []string `yaml:"cipherSuites,omitempty" json:"cipherSuites,omitempty"`
ALPN []string `yaml:"alpn,omitempty" json:"alpn,omitempty"`
}
type PluginConfig struct {
@@ -148,9 +137,8 @@ type AutherConfig struct {
}
type AuthConfig struct {
Username string `yaml:",omitempty" json:"username,omitempty"`
Username string `json:"username"`
Password string `yaml:",omitempty" json:"password,omitempty"`
File string `yaml:",omitempty" json:"file,omitempty"`
}
type SelectorConfig struct {
@@ -161,7 +149,7 @@ type SelectorConfig struct {
type AdmissionConfig struct {
Name string `json:"name"`
// Deprecated: use whitelist instead
// DEPRECATED by whitelist since beta.4
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
@@ -174,10 +162,9 @@ type AdmissionConfig struct {
type BypassConfig struct {
Name string `json:"name"`
// Deprecated: use whitelist instead
// DEPRECATED by whitelist since beta.4
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
@@ -193,7 +180,6 @@ type FileLoader struct {
type RedisLoader struct {
Addr string `json:"addr"`
DB int `yaml:",omitempty" json:"db,omitempty"`
Username string `yaml:",omitempty" json:"username,omitempty"`
Password string `yaml:",omitempty" json:"password,omitempty"`
Key string `yaml:",omitempty" json:"key,omitempty"`
Type string `yaml:",omitempty" json:"type,omitempty"`
@@ -258,23 +244,6 @@ type SDConfig struct {
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
}
type RouterRouteConfig struct {
// Deprecated: use dst instead
Net string `yaml:",omitempty" json:"net,omitempty"`
Dst string `yaml:",omitempty" json:"dst,omitempty"`
Gateway string `json:"gateway"`
}
type RouterConfig struct {
Name string `json:"name"`
Routes []*RouterRouteConfig `yaml:",omitempty" json:"routes,omitempty"`
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
Redis *RedisLoader `yaml:",omitempty" json:"redis,omitempty"`
HTTP *HTTPLoader `yaml:"http,omitempty" json:"http,omitempty"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
}
type RecorderConfig struct {
Name string `json:"name"`
File *FileRecorder `yaml:",omitempty" json:"file,omitempty"`
@@ -285,9 +254,8 @@ type RecorderConfig struct {
}
type FileRecorder struct {
Path string `json:"path"`
Sep string `yaml:",omitempty" json:"sep,omitempty"`
Rotation *LogRotationConfig `yaml:",omitempty" json:"rotation,omitempty"`
Path string `json:"path"`
Sep string `yaml:",omitempty" json:"sep,omitempty"`
}
type TCPRecorder struct {
@@ -296,29 +264,22 @@ type TCPRecorder struct {
}
type HTTPRecorder struct {
URL string `yaml:"url" json:"url"`
Timeout time.Duration `yaml:",omitempty" json:"timeout,omitempty"`
Header map[string]string `yaml:",omitempty" json:"header,omitempty"`
URL string `json:"url" yaml:"url"`
Timeout time.Duration `json:"timeout"`
}
type RedisRecorder struct {
Addr string `json:"addr"`
DB int `yaml:",omitempty" json:"db,omitempty"`
Username string `yaml:",omitempty" json:"username,omitempty"`
Password string `yaml:",omitempty" json:"password,omitempty"`
Key string `yaml:",omitempty" json:"key,omitempty"`
Type string `yaml:",omitempty" json:"type,omitempty"`
}
type RecorderObject struct {
Name string `json:"name"`
Record string `json:"record"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
}
type RewriterConfig struct {
Name string `json:"name"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
Name string `json:"name"`
Record string `json:"record"`
Metadata map[string]any
}
type LimiterConfig struct {
@@ -328,12 +289,6 @@ type LimiterConfig struct {
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
Redis *RedisLoader `yaml:",omitempty" json:"redis,omitempty"`
HTTP *HTTPLoader `yaml:"http,omitempty" json:"http,omitempty"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
}
type ObserverConfig struct {
Name string `json:"name"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
}
type ListenerConfig struct {
@@ -356,98 +311,33 @@ type HandlerConfig struct {
Authers []string `yaml:",omitempty" json:"authers,omitempty"`
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"`
Limiter string `yaml:",omitempty" json:"limiter,omitempty"`
Observer string `yaml:",omitempty" json:"observer,omitempty"`
Ingress string `yaml:",omitempty" json:"ingress,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
}
type ForwarderConfig struct {
// Deprecated: use hop instead
Name string `yaml:",omitempty" json:"name,omitempty"`
// the referenced hop name
Hop string `yaml:",omitempty" json:"hop,omitempty"`
Name string `yaml:",omitempty" json:"name,omitempty"`
Selector *SelectorConfig `yaml:",omitempty" json:"selector,omitempty"`
Nodes []*ForwardNodeConfig `json:"nodes"`
}
type ForwardNodeConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
// Deprecated: use matcher instead
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
// Deprecated: use matcher instead
Host string `yaml:",omitempty" json:"host,omitempty"`
// Deprecated: use matcher instead
Path string `yaml:",omitempty" json:"path,omitempty"`
// Deprecated: use matcher instead
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
Matcher *NodeMatcherConfig `yaml:",omitempty" json:"matcher,omitempty"`
// Deprecated: use http.auth instead
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
Name string `yaml:",omitempty" json:"name,omitempty"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
Host string `yaml:",omitempty" json:"host,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
Path string `yaml:",omitempty" json:"path,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
}
type HTTPURLRewriteConfig struct {
Match string
Replacement string
}
type HTTPBodyRewriteConfig struct {
// filter by MIME types
Type string
Match string
Replacement string
// name of the rewriter plugin (via registry)
Rewriter string
}
type NodeFilterConfig struct {
Host string `yaml:",omitempty" json:"host,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
Path string `yaml:",omitempty" json:"path,omitempty"`
}
// NodeMatcherConfig defines a routing-rule matcher for a hop node.
//
// Priority controls election among multiple matching nodes:
// - 0 (default): auto-computed from the rule string length — longer rules
// (more specific) get higher priority.
// - negative: the node participates in matching but priority short-circuit
// is disabled; the selector (round-robin, random, hash, etc.) always applies.
// - positive: explicit priority; when a single node has strictly higher
// priority than all others, it wins directly, bypassing the selector.
type NodeMatcherConfig struct {
Rule string `yaml:",omitempty" json:"rule,omitempty"`
Priority int `yaml:",omitempty" json:"priority,omitempty"`
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
}
type HTTPNodeConfig struct {
// rewrite host header
Host string `yaml:",omitempty" json:"host,omitempty"`
// Deprecated: use requestHeader instead
Host string `yaml:",omitempty" json:"host,omitempty"`
Header map[string]string `yaml:",omitempty" json:"header,omitempty"`
// additional request header
RequestHeader map[string]string `yaml:"requestHeader,omitempty" json:"requestHeader,omitempty"`
// additional response header
ResponseHeader map[string]string `yaml:"responseHeader,omitempty" json:"responseHeader,omitempty"`
// Deprecated: use rewriteURL instead
Rewrite []HTTPURLRewriteConfig `yaml:",omitempty" json:"rewrite,omitempty"`
// rewrite URL
RewriteURL []HTTPURLRewriteConfig `yaml:"rewriteURL,omitempty" json:"rewriteURL,omitempty"`
// Deprecated: use rewriteResponseBody instead
RewriteBody []HTTPBodyRewriteConfig `yaml:"rewriteBody,omitempty" json:"rewriteBody,omitempty"`
// rewrite request body
RewriteRequestBody []HTTPBodyRewriteConfig `yaml:"rewriteRequestBody,omitempty" json:"rewriteRequestBody,omitempty"`
// rewrite response body
RewriteResponseBody []HTTPBodyRewriteConfig `yaml:"rewriteResponseBody,omitempty" json:"rewriteResponseBody,omitempty"`
// HTTP basic auth
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
}
type TLSNodeConfig struct {
@@ -477,9 +367,9 @@ type SockOptsConfig struct {
type ServiceConfig struct {
Name string `json:"name"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
// Deprecated: use metadata.interface instead
// DEPRECATED by metadata.interface since beta.5
Interface string `yaml:",omitempty" json:"interface,omitempty"`
// Deprecated: use metadata.so_mark instead
// DEPRECATED by metadata.so_mark since beta.5
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Admission string `yaml:",omitempty" json:"admission,omitempty"`
Admissions []string `yaml:",omitempty" json:"admissions,omitempty"`
@@ -488,93 +378,13 @@ type ServiceConfig struct {
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Limiter string `yaml:",omitempty" json:"limiter,omitempty"`
Quotas []string `yaml:",omitempty" json:"quotas,omitempty"`
CLimiter string `yaml:"climiter,omitempty" json:"climiter,omitempty"`
RLimiter string `yaml:"rlimiter,omitempty" json:"rlimiter,omitempty"`
Logger string `yaml:",omitempty" json:"logger,omitempty"`
Loggers []string `yaml:",omitempty" json:"loggers,omitempty"`
Observer string `yaml:",omitempty" json:"observer,omitempty"`
Rewriter string `yaml:",omitempty" json:"rewriter,omitempty"`
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"`
Forwarder *ForwarderConfig `yaml:",omitempty" json:"forwarder,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
// service status, read-only
Status *ServiceStatus `yaml:",omitempty" json:"status,omitempty"`
}
type ServiceStatus struct {
CreateTime int64 `yaml:"createTime" json:"createTime"`
State string `yaml:"state" json:"state"`
Events []ServiceEvent `yaml:",omitempty" json:"events,omitempty"`
Stats *ServiceStats `yaml:",omitempty" json:"stats,omitempty"`
StoppedByLimit bool `yaml:"stoppedByLimit,omitempty" json:"stoppedByLimit,omitempty"`
Quotas []ServiceQuota `yaml:",omitempty" json:"quotas,omitempty"`
}
type ServiceEvent struct {
Time int64 `yaml:"time" json:"time"`
Msg string `yaml:"msg" json:"msg"`
}
type ServiceStats struct {
TotalConns uint64 `yaml:"totalConns" json:"totalConns"`
CurrentConns uint64 `yaml:"currentConns" json:"currentConns"`
TotalErrs uint64 `yaml:"totalErrs" json:"totalErrs"`
InputBytes uint64 `yaml:"inputBytes" json:"inputBytes"`
OutputBytes uint64 `yaml:"outputBytes" json:"outputBytes"`
}
// QuotaConfig is a named cumulative traffic-volume limiter; the same name
// referenced by several services (ServiceConfig.Quotas) shares one counter.
type QuotaConfig struct {
Name string `json:"name"`
Limit string `yaml:",omitempty" json:"limit,omitempty"`
StartsAt string `yaml:"startsAt,omitempty" json:"startsAt,omitempty"`
ExpiresAt string `yaml:"expiresAt,omitempty" json:"expiresAt,omitempty"`
Direction string `yaml:",omitempty" json:"direction,omitempty"`
Flush string `yaml:",omitempty" json:"flush,omitempty"`
Store *QuotaStoreConfig `yaml:",omitempty" json:"store,omitempty"`
// read-only
Status *QuotaStatus `yaml:",omitempty" json:"status,omitempty"`
}
type QuotaStoreConfig struct {
Type string `json:"type"` // "file" (default) | "redis" (stub, not implemented)
File string `yaml:",omitempty" json:"file,omitempty"`
Redis *QuotaRedisConfig `yaml:",omitempty" json:"redis,omitempty"`
}
type QuotaRedisConfig struct {
Addr string `json:"addr"`
DB int `yaml:",omitempty" json:"db,omitempty"`
Username string `yaml:",omitempty" json:"username,omitempty"`
Password string `yaml:",omitempty" json:"password,omitempty"`
Key string `yaml:",omitempty" json:"key,omitempty"`
}
type QuotaStatus struct {
Used uint64 `yaml:"used" json:"used"`
Limit uint64 `yaml:"limit" json:"limit"`
StartsAt int64 `yaml:"startsAt,omitempty" json:"startsAt,omitempty"`
ExpiresAt int64 `yaml:"expiresAt,omitempty" json:"expiresAt,omitempty"`
Active bool `yaml:"active" json:"active"`
Expired bool `yaml:"expired,omitempty" json:"expired,omitempty"`
Blocked bool `yaml:"blocked" json:"blocked"`
Direction string `yaml:"direction,omitempty" json:"direction,omitempty"`
}
type ServiceQuota struct {
Name string `yaml:"name" json:"name"`
Used uint64 `yaml:"used" json:"used"`
Limit uint64 `yaml:"limit" json:"limit"`
StartsAt int64 `yaml:"startsAt,omitempty" json:"startsAt,omitempty"`
ExpiresAt int64 `yaml:"expiresAt,omitempty" json:"expiresAt,omitempty"`
Active bool `yaml:"active" json:"active"`
Expired bool `yaml:"expired,omitempty" json:"expired,omitempty"`
Blocked bool `yaml:"blocked" json:"blocked"`
Direction string `yaml:"direction,omitempty" json:"direction,omitempty"`
}
type ChainConfig struct {
@@ -589,47 +399,41 @@ type ChainGroupConfig struct {
}
type HopConfig struct {
Name string `json:"name"`
// Deprecated: use metadata.interface instead
Interface string `yaml:",omitempty" json:"interface,omitempty"`
// Deprecated: use metadata.so_mark instead
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Selector *SelectorConfig `yaml:",omitempty" json:"selector,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Nodes []*NodeConfig `yaml:",omitempty" json:"nodes,omitempty"`
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
Redis *RedisLoader `yaml:",omitempty" json:"redis,omitempty"`
HTTP *HTTPLoader `yaml:"http,omitempty" json:"http,omitempty"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
Name string `json:"name"`
Interface string `yaml:",omitempty" json:"interface,omitempty"`
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Selector *SelectorConfig `yaml:",omitempty" json:"selector,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Nodes []*NodeConfig `yaml:",omitempty" json:"nodes,omitempty"`
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
Redis *RedisLoader `yaml:",omitempty" json:"redis,omitempty"`
HTTP *HTTPLoader `yaml:"http,omitempty" json:"http,omitempty"`
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
}
type NodeConfig struct {
Name string `json:"name"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
Host string `yaml:",omitempty" json:"host,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
Path string `yaml:",omitempty" json:"path,omitempty"`
Interface string `yaml:",omitempty" json:"interface,omitempty"`
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Connector *ConnectorConfig `yaml:",omitempty" json:"connector,omitempty"`
Dialer *DialerConfig `yaml:",omitempty" json:"dialer,omitempty"`
// Deprecated: use metadata.interface instead
Interface string `yaml:",omitempty" json:"interface,omitempty"`
// Deprecated: use metadata.netns instead
Netns string `yaml:",omitempty" json:"netns,omitempty"`
// Deprecated: use metadata.so_mark instead
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
// Deprecated: use matcher instead
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
Matcher *NodeMatcherConfig `yaml:",omitempty" json:"matcher,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
}
type Config struct {
@@ -642,16 +446,11 @@ type Config struct {
Resolvers []*ResolverConfig `yaml:",omitempty" json:"resolvers,omitempty"`
Hosts []*HostsConfig `yaml:",omitempty" json:"hosts,omitempty"`
Ingresses []*IngressConfig `yaml:",omitempty" json:"ingresses,omitempty"`
Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"`
SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"`
Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"`
Rewriters []*RewriterConfig `yaml:",omitempty" json:"rewriters,omitempty"`
Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"`
Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"`
CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"`
RLimiters []*LimiterConfig `yaml:"rlimiters,omitempty" json:"rlimiters,omitempty"`
Observers []*ObserverConfig `yaml:",omitempty" json:"observers,omitempty"`
Loggers []*LoggerConfig `yaml:",omitempty" json:"loggers,omitempty"`
TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"`
Log *LogConfig `yaml:",omitempty" json:"log,omitempty"`
Profiling *ProfilingConfig `yaml:",omitempty" json:"profiling,omitempty"`
@@ -667,8 +466,7 @@ func (c *Config) Load() error {
return v.Unmarshal(c)
}
func (c *Config) Read(r io.Reader, configType string) error {
v.SetConfigType(configType)
func (c *Config) Read(r io.Reader) error {
if err := v.ReadConfig(r); err != nil {
return err
}
@@ -678,7 +476,6 @@ func (c *Config) Read(r io.Reader, configType string) error {
func (c *Config) ReadFile(file string) error {
v.SetConfigFile(file)
v.SetConfigType(strings.TrimPrefix(filepath.Ext(file), ".")) // force format from extension
if err := v.ReadInConfig(); err != nil {
return err
}
-364
View File
@@ -1,364 +0,0 @@
// Package loader converts a parsed [config.Config] into running components
// by parsing each config section and registering the results into the
// global registries. Components are registered in dependency order so that
// when a component reads its dependencies from registries during parsing,
// those dependencies are already available.
package loader
import (
"github.com/go-gost/core/admission"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/hosts"
"github.com/go-gost/core/ingress"
"github.com/go-gost/core/limiter/conn"
"github.com/go-gost/core/limiter/rate"
"github.com/go-gost/core/limiter/traffic"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/observer"
"github.com/go-gost/core/recorder"
reg "github.com/go-gost/core/registry"
"github.com/go-gost/core/resolver"
"github.com/go-gost/core/router"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/core/sd"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
admission_parser "github.com/go-gost/x/config/parsing/admission"
auth_parser "github.com/go-gost/x/config/parsing/auth"
bypass_parser "github.com/go-gost/x/config/parsing/bypass"
chain_parser "github.com/go-gost/x/config/parsing/chain"
hop_parser "github.com/go-gost/x/config/parsing/hop"
hosts_parser "github.com/go-gost/x/config/parsing/hosts"
ingress_parser "github.com/go-gost/x/config/parsing/ingress"
limiter_parser "github.com/go-gost/x/config/parsing/limiter"
logger_parser "github.com/go-gost/x/config/parsing/logger"
observer_parser "github.com/go-gost/x/config/parsing/observer"
quota_parser "github.com/go-gost/x/config/parsing/quota"
recorder_parser "github.com/go-gost/x/config/parsing/recorder"
resolver_parser "github.com/go-gost/x/config/parsing/resolver"
rewriter_parser "github.com/go-gost/x/config/parsing/rewriter"
router_parser "github.com/go-gost/x/config/parsing/router"
sd_parser "github.com/go-gost/x/config/parsing/sd"
service_parser "github.com/go-gost/x/config/parsing/service"
quota "github.com/go-gost/x/limiter/quota"
"github.com/go-gost/x/registry"
)
// defaultLoader is a singleton loader used by the top-level Load function.
var (
defaultLoader *loader = &loader{}
)
// Load parses all config sections from cfg and registers the resulting
// components into the global registries, then sets up the default logger
// and TLS config.
func Load(cfg *config.Config) error {
return defaultLoader.Load(cfg)
}
// loader holds no state; its Load method is the entry point for converting
// a config into running components.
type loader struct{}
// Load builds the default TLS config, registers all named components from
// cfg into the global registries, and sets the default logger and TLS config.
func (l *loader) Load(cfg *config.Config) error {
if cfg == nil {
return nil
}
logCfg := cfg.Log
if logCfg == nil {
logCfg = &config.LogConfig{}
}
logger.SetDefault(logger_parser.ParseLogger(&config.LoggerConfig{Log: logCfg}))
tlsCfg, err := parsing.BuildDefaultTLSConfig(cfg.TLS)
if err != nil {
return err
}
parsing.SetDefaultTLSConfig(tlsCfg)
if err := register(cfg); err != nil {
return err
}
return nil
}
// named is a generic name+value pair used to buffer parsed components
// before registering them.
type named[T any] struct {
name string
v T
}
// registerGroup replaces all entries in r with the given entries. Old entries
// are unregistered first (see unregisterAll); if any new entry fails to
// register, the group is left partially updated (matching the historical
// reload behavior for intra-group failures).
func registerGroup[T any](entries []named[T], r reg.Registry[T]) error {
unregisterAll(r)
for _, e := range entries {
if err := r.Register(e.name, e.v); err != nil {
return err
}
}
return nil
}
// unregisterAll removes every entry from r. registry.Unregister closes a value
// before deleting it when it implements io.Closer, so for services this frees
// the bound port (a service's Close closes its listener).
func unregisterAll[T any](r reg.Registry[T]) {
for name := range r.GetAll() {
r.Unregister(name)
}
}
// register parses config sections and registers them into the global
// registries. Groups are processed in dependency order: leaf components
// first, then hops, chains, and finally services. This ensures that
// when a component reads its dependencies from registries during
// parsing, those dependencies are already registered.
func register(cfg *config.Config) error {
if cfg == nil {
return nil
}
// --- leaf components (no inter-group registry dependencies) ---
{
var entries []named[logger.Logger]
for _, c := range cfg.Loggers {
entries = append(entries, named[logger.Logger]{c.Name, logger_parser.ParseLogger(c)})
}
if err := registerGroup(entries, registry.LoggerRegistry()); err != nil {
return err
}
}
{
var entries []named[auth.Authenticator]
for _, c := range cfg.Authers {
entries = append(entries, named[auth.Authenticator]{c.Name, auth_parser.ParseAuther(c)})
}
if err := registerGroup(entries, registry.AutherRegistry()); err != nil {
return err
}
}
{
var entries []named[admission.Admission]
for _, c := range cfg.Admissions {
entries = append(entries, named[admission.Admission]{c.Name, admission_parser.ParseAdmission(c)})
}
if err := registerGroup(entries, registry.AdmissionRegistry()); err != nil {
return err
}
}
{
var entries []named[bypass.Bypass]
for _, c := range cfg.Bypasses {
entries = append(entries, named[bypass.Bypass]{c.Name, bypass_parser.ParseBypass(c)})
}
if err := registerGroup(entries, registry.BypassRegistry()); err != nil {
return err
}
}
{
var entries []named[resolver.Resolver]
for _, c := range cfg.Resolvers {
r, err := resolver_parser.ParseResolver(c)
if err != nil {
return err
}
entries = append(entries, named[resolver.Resolver]{c.Name, r})
}
if err := registerGroup(entries, registry.ResolverRegistry()); err != nil {
return err
}
}
{
var entries []named[hosts.HostMapper]
for _, c := range cfg.Hosts {
entries = append(entries, named[hosts.HostMapper]{c.Name, hosts_parser.ParseHostMapper(c)})
}
if err := registerGroup(entries, registry.HostsRegistry()); err != nil {
return err
}
}
{
var entries []named[ingress.Ingress]
for _, c := range cfg.Ingresses {
entries = append(entries, named[ingress.Ingress]{c.Name, ingress_parser.ParseIngress(c)})
}
if err := registerGroup(entries, registry.IngressRegistry()); err != nil {
return err
}
}
{
var entries []named[router.Router]
for _, c := range cfg.Routers {
entries = append(entries, named[router.Router]{c.Name, router_parser.ParseRouter(c)})
}
if err := registerGroup(entries, registry.RouterRegistry()); err != nil {
return err
}
}
{
var entries []named[sd.SD]
for _, c := range cfg.SDs {
entries = append(entries, named[sd.SD]{c.Name, sd_parser.ParseSD(c)})
}
if err := registerGroup(entries, registry.SDRegistry()); err != nil {
return err
}
}
{
var entries []named[observer.Observer]
for _, c := range cfg.Observers {
entries = append(entries, named[observer.Observer]{c.Name, observer_parser.ParseObserver(c)})
}
if err := registerGroup(entries, registry.ObserverRegistry()); err != nil {
return err
}
}
{
var entries []named[recorder.Recorder]
for _, c := range cfg.Recorders {
entries = append(entries, named[recorder.Recorder]{c.Name, recorder_parser.ParseRecorder(c)})
}
if err := registerGroup(entries, registry.RecorderRegistry()); err != nil {
return err
}
}
{
var entries []named[rewriter.Rewriter]
for _, c := range cfg.Rewriters {
entries = append(entries, named[rewriter.Rewriter]{c.Name, rewriter_parser.ParseRewriter(c)})
}
if err := registerGroup(entries, registry.RewriterRegistry()); err != nil {
return err
}
}
{
var entries []named[traffic.TrafficLimiter]
for _, c := range cfg.Limiters {
entries = append(entries, named[traffic.TrafficLimiter]{c.Name, limiter_parser.ParseTrafficLimiter(c)})
}
if err := registerGroup(entries, registry.TrafficLimiterRegistry()); err != nil {
return err
}
}
{
var entries []named[*quota.Limiter]
for _, c := range cfg.Quotas {
entries = append(entries, named[*quota.Limiter]{c.Name, quota_parser.ParseQuotaLimiter(c)})
}
if err := registerGroup(entries, registry.QuotaLimiterRegistry()); err != nil {
return err
}
}
{
var entries []named[conn.ConnLimiter]
for _, c := range cfg.CLimiters {
entries = append(entries, named[conn.ConnLimiter]{c.Name, limiter_parser.ParseConnLimiter(c)})
}
if err := registerGroup(entries, registry.ConnLimiterRegistry()); err != nil {
return err
}
}
{
var entries []named[rate.RateLimiter]
for _, c := range cfg.RLimiters {
entries = append(entries, named[rate.RateLimiter]{c.Name, limiter_parser.ParseRateLimiter(c)})
}
if err := registerGroup(entries, registry.RateLimiterRegistry()); err != nil {
return err
}
}
// --- hops (references bypasses, resolvers, hosts from registries) ---
{
var entries []named[hop.Hop]
for _, c := range cfg.Hops {
h, err := hop_parser.ParseHop(c, logger.Default())
if err != nil {
return err
}
entries = append(entries, named[hop.Hop]{c.Name, h})
}
if err := registerGroup(entries, registry.HopRegistry()); err != nil {
return err
}
}
// --- chains (references hops from registries) ---
{
var entries []named[chain.Chainer]
for _, c := range cfg.Chains {
ch, err := chain_parser.ParseChain(c, logger.Default())
if err != nil {
return err
}
entries = append(entries, named[chain.Chainer]{c.Name, ch})
}
if err := registerGroup(entries, registry.ChainRegistry()); err != nil {
return err
}
}
// --- services (references chains, resolvers, hosts, recorders,
// limiters, observers, hops from registries) ---
//
// Services are the only component group whose construction binds a port:
// service_parser.ParseService calls listener.Init, which binds. The generic
// registerGroup "parse-all-then-swap" sequence used by the other groups
// would therefore bind every new listener while the old services are still
// listening, causing EADDRINUSE on SIGHUP reload (issue #754, regressed by
// 82e7e50). Instead, unregister all old services first — unregisterAll
// closes each one (a service implements io.Closer), freeing its port —
// then parse, bind, and register the new ones.
//
// Trade-off: a parse error in the loop below leaves the registry partially
// updated (the old services are already closed and only the services parsed
// before the failure are registered). The construct/bind split would be
// needed for atomic reload, but this restores the pre-82e7e50 behavior.
{
unregisterAll(registry.ServiceRegistry())
for _, c := range cfg.Services {
svc, err := service_parser.ParseService(c)
if err != nil {
return err
}
if svc != nil {
if err := registry.ServiceRegistry().Register(c.Name, svc); err != nil {
return err
}
}
}
}
return nil
}
-935
View File
@@ -1,935 +0,0 @@
package loader
import (
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"testing"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/listener"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/metadata"
reg "github.com/go-gost/core/registry"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
"github.com/go-gost/x/registry"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
// --- mock registry for registerGroup tests ---
type mockRegistry[T any] struct {
m map[string]T
}
func newMockRegistry[T any]() *mockRegistry[T] {
return &mockRegistry[T]{m: make(map[string]T)}
}
func (r *mockRegistry[T]) Register(name string, v T) error {
if name == "" {
return nil
}
if _, ok := r.m[name]; ok {
return errors.New("duplicate")
}
r.m[name] = v
return nil
}
func (r *mockRegistry[T]) Unregister(name string) {
delete(r.m, name)
}
func (r *mockRegistry[T]) IsRegistered(name string) bool {
_, ok := r.m[name]
return ok
}
func (r *mockRegistry[T]) Get(name string) T {
return r.m[name]
}
func (r *mockRegistry[T]) GetAll() map[string]T {
result := make(map[string]T)
for k, v := range r.m {
result[k] = v
}
return result
}
// Ensure mockRegistry satisfies reg.Registry[T].
var _ reg.Registry[int] = (*mockRegistry[int])(nil)
// --- registerGroup tests ---
func TestRegisterGroup_EmptyEntries_EmptyRegistry(t *testing.T) {
r := newMockRegistry[int]()
err := registerGroup([]named[int]{}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(r.GetAll()) != 0 {
t.Fatalf("expected empty registry, got %d entries", len(r.GetAll()))
}
}
func TestRegisterGroup_EmptyEntries_ClearsExisting(t *testing.T) {
r := newMockRegistry[int]()
r.Register("a", 1)
r.Register("b", 2)
err := registerGroup([]named[int]{}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(r.GetAll()) != 0 {
t.Fatalf("expected empty registry, got %d entries", len(r.GetAll()))
}
}
func TestRegisterGroup_SingleEntry(t *testing.T) {
r := newMockRegistry[int]()
err := registerGroup([]named[int]{{"x", 42}}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered("x") {
t.Fatal("expected 'x' to be registered")
}
if v := r.Get("x"); v != 42 {
t.Fatalf("expected 42, got %v", v)
}
}
func TestRegisterGroup_MultipleEntries(t *testing.T) {
r := newMockRegistry[int]()
err := registerGroup([]named[int]{
{"x", 1},
{"y", 2},
{"z", 3},
}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
all := r.GetAll()
if len(all) != 3 {
t.Fatalf("expected 3 entries, got %d", len(all))
}
for _, name := range []string{"x", "y", "z"} {
if !r.IsRegistered(name) {
t.Fatalf("expected '%s' to be registered", name)
}
}
}
func TestRegisterGroup_ReplacesExisting(t *testing.T) {
r := newMockRegistry[int]()
r.Register("old", 1)
r.Register("keep", 2) // this one should be removed
err := registerGroup([]named[int]{
{"old", 99},
{"new", 100},
}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if v := r.Get("old"); v != 99 {
t.Fatalf("expected 99, got %v", v)
}
if !r.IsRegistered("new") {
t.Fatal("expected 'new' to be registered")
}
if r.IsRegistered("keep") {
t.Fatal("expected 'keep' to be unregistered")
}
}
func TestRegisterGroup_DuplicateNameInEntries(t *testing.T) {
r := newMockRegistry[int]()
err := registerGroup([]named[int]{
{"dup", 1},
{"dup", 2},
}, r)
if err == nil {
t.Fatal("expected error for duplicate name")
}
// First entry should be registered, second failed.
if v := r.Get("dup"); v != 1 {
t.Fatalf("expected first entry (1), got %v", v)
}
}
func TestRegisterGroup_ZeroValue(t *testing.T) {
r := newMockRegistry[int]()
// int zero value (0) is still registered
err := registerGroup([]named[int]{{"zero", 0}}, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered("zero") {
t.Fatal("expected 'zero' to be registered")
}
}
// --- register tests ---
func TestRegister_NilConfig(t *testing.T) {
if err := register(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRegister_EmptyConfig(t *testing.T) {
if err := register(&config.Config{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRegister_Loggers(t *testing.T) {
reg := registry.LoggerRegistry()
name := "test-logger"
t.Cleanup(func() { reg.Unregister(name) })
cfg := &config.Config{
Loggers: []*config.LoggerConfig{
{Name: name, Log: &config.LogConfig{Level: "info"}},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reg.IsRegistered(name) {
t.Fatal("expected logger to be registered")
}
}
func TestRegister_Authers(t *testing.T) {
r := registry.AutherRegistry()
name := "test-auther"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Authers: []*config.AutherConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected auther to be registered")
}
}
func TestRegister_Admissions(t *testing.T) {
r := registry.AdmissionRegistry()
name := "test-admission"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Admissions: []*config.AdmissionConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected admission to be registered")
}
}
func TestRegister_Bypasses(t *testing.T) {
r := registry.BypassRegistry()
name := "test-bypass"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Bypasses: []*config.BypassConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected bypass to be registered")
}
}
func TestRegister_Resolvers(t *testing.T) {
r := registry.ResolverRegistry()
name := "test-resolver"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Resolvers: []*config.ResolverConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected resolver to be registered")
}
}
func TestRegister_Hosts(t *testing.T) {
r := registry.HostsRegistry()
name := "test-hosts"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Hosts: []*config.HostsConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected host mapper to be registered")
}
}
func TestRegister_Ingresses(t *testing.T) {
r := registry.IngressRegistry()
name := "test-ingress"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Ingresses: []*config.IngressConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected ingress to be registered")
}
}
func TestRegister_Routers(t *testing.T) {
r := registry.RouterRegistry()
name := "test-router"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Routers: []*config.RouterConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected router to be registered")
}
}
func TestRegister_SDs(t *testing.T) {
r := registry.SDRegistry()
name := "test-sd"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
SDs: []*config.SDConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected SD to be registered")
}
}
func TestRegister_Observers(t *testing.T) {
r := registry.ObserverRegistry()
name := "test-observer"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Observers: []*config.ObserverConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected observer to be registered")
}
}
func TestRegister_Recorders(t *testing.T) {
r := registry.RecorderRegistry()
name := "test-recorder"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Recorders: []*config.RecorderConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected recorder to be registered")
}
}
func TestRegister_TrafficLimiters(t *testing.T) {
r := registry.TrafficLimiterRegistry()
name := "test-traffic-limiter"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Limiters: []*config.LimiterConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected traffic limiter to be registered")
}
}
func TestRegister_ConnLimiters(t *testing.T) {
r := registry.ConnLimiterRegistry()
name := "test-conn-limiter"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
CLimiters: []*config.LimiterConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected conn limiter to be registered")
}
}
func TestRegister_RateLimiters(t *testing.T) {
r := registry.RateLimiterRegistry()
name := "test-rate-limiter"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
RLimiters: []*config.LimiterConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected rate limiter to be registered")
}
}
func TestRegister_Hops(t *testing.T) {
r := registry.HopRegistry()
name := "test-hop"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Hops: []*config.HopConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected hop to be registered")
}
}
func TestRegister_Chains(t *testing.T) {
r := registry.ChainRegistry()
name := "test-chain"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Chains: []*config.ChainConfig{
{Name: name},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected chain to be registered")
}
}
// stubListener satisfies listener.Listener for testing.
type stubListener struct{}
func (l *stubListener) Init(md metadata.Metadata) error { return nil }
func (l *stubListener) Accept() (net.Conn, error) { return nil, net.ErrClosed }
func (l *stubListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
func (l *stubListener) Close() error { return nil }
// stubHandler satisfies handler.Handler for testing.
type stubHandler struct{}
func (h *stubHandler) Init(md metadata.Metadata) error { return nil }
func (h *stubHandler) Handle(_ context.Context, _ net.Conn, _ ...handler.HandleOption) error { return nil }
func TestRegister_Services(t *testing.T) {
// ParseService needs "tcp" listener and "auto" handler in registries.
// These are registered via init() in their respective packages but those
// packages are not imported here, so the factories won't be found.
// Register stub factories so the test can exercise the service section.
//
// This save/restore pattern assumes no other test or transitive import
// has already registered "tcp" or "auto" factory functions. If those
// packages are ever imported by this test file, origListener/origHandler
// will be non-nil and the restore path will re-register the real
// factories, which is the desired behavior.
origListener := registry.ListenerRegistry().Get("tcp")
origHandler := registry.HandlerRegistry().Get("auto")
registry.ListenerRegistry().Register("tcp", func(opts ...listener.Option) listener.Listener {
return &stubListener{}
})
registry.HandlerRegistry().Register("auto", func(opts ...handler.Option) handler.Handler {
return &stubHandler{}
})
t.Cleanup(func() {
registry.ListenerRegistry().Unregister("tcp")
registry.HandlerRegistry().Unregister("auto")
if origListener != nil {
registry.ListenerRegistry().Register("tcp", origListener)
}
if origHandler != nil {
registry.HandlerRegistry().Register("auto", origHandler)
}
})
r := registry.ServiceRegistry()
name := "test-service"
t.Cleanup(func() { r.Unregister(name) })
cfg := &config.Config{
Services: []*config.ServiceConfig{
{Name: name, Addr: ":0"},
},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !r.IsRegistered(name) {
t.Fatal("expected service to be registered")
}
}
func TestRegister_MultipleSections(t *testing.T) {
// Verify that multiple sections are all registered and in dependency order.
bypassReg := registry.BypassRegistry()
hopReg := registry.HopRegistry()
chainReg := registry.ChainRegistry()
bypassName := "multi-bypass"
hopName := "multi-hop"
chainName := "multi-chain"
t.Cleanup(func() {
bypassReg.Unregister(bypassName)
hopReg.Unregister(hopName)
chainReg.Unregister(chainName)
})
cfg := &config.Config{
Bypasses: []*config.BypassConfig{{Name: bypassName}},
Hops: []*config.HopConfig{{Name: hopName}},
Chains: []*config.ChainConfig{{Name: chainName}},
}
if err := register(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !bypassReg.IsRegistered(bypassName) {
t.Fatal("expected bypass to be registered")
}
if !hopReg.IsRegistered(hopName) {
t.Fatal("expected hop to be registered")
}
if !chainReg.IsRegistered(chainName) {
t.Fatal("expected chain to be registered")
}
}
func TestRegister_ComponentOrder(t *testing.T) {
// register() processes sections in dependency order: leaf components
// (loggers, authers, resolvers, etc.) first, then hops, then chains,
// then services. Hops look up loggers/resolvers from registries;
// chains look up hops; services look up chains.
//
// This test directly calls registerGroup in the canonical dependency
// sequence and verifies each layer can see the previous layer's
// registrations.
// Use a sequence-tracking registry to verify registration order.
type seqEntry struct {
name string
seq int
}
var seq int
leafReg := newMockRegistry[seqEntry]()
hopReg := newMockRegistry[seqEntry]()
chainReg := newMockRegistry[seqEntry]()
svcReg := newMockRegistry[seqEntry]()
seq++
registerGroup([]named[seqEntry]{{"leaf", seqEntry{"logger", seq}}}, leafReg)
// Hops should see leaf registrations.
if !leafReg.IsRegistered("leaf") {
t.Fatal("expected leaf to be registered before hops")
}
seq++
registerGroup([]named[seqEntry]{{"hop", seqEntry{"hop", seq}}}, hopReg)
// Chains should see hop registrations.
if !hopReg.IsRegistered("hop") {
t.Fatal("expected hop to be registered before chains")
}
seq++
registerGroup([]named[seqEntry]{{"chain", seqEntry{"chain", seq}}}, chainReg)
// Services should see chain registrations.
if !chainReg.IsRegistered("chain") {
t.Fatal("expected chain to be registered before services")
}
seq++
registerGroup([]named[seqEntry]{{"svc", seqEntry{"svc", seq}}}, svcReg)
// All should be registered.
for _, r := range []struct {
name string
reg *mockRegistry[seqEntry]
}{
{"leaf", leafReg},
{"hop", hopReg},
{"chain", chainReg},
{"svc", svcReg},
} {
if len(r.reg.GetAll()) != 1 {
t.Fatalf("expected 1 entry in %s registry", r.name)
}
}
// Verify registration sequence: leaf < hop < chain < svc.
if leafReg.Get("leaf").seq >= hopReg.Get("hop").seq {
t.Fatal("leaf must be registered before hop")
}
if hopReg.Get("hop").seq >= chainReg.Get("chain").seq {
t.Fatal("hop must be registered before chain")
}
if chainReg.Get("chain").seq >= svcReg.Get("svc").seq {
t.Fatal("chain must be registered before service")
}
}
// --- Load tests ---
func TestLoad_NilConfig(t *testing.T) {
if err := Load(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoad_EmptyConfig(t *testing.T) {
cfg := &config.Config{}
if err := Load(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoad_WithTLS(t *testing.T) {
// BuildDefaultTLSConfig with nil TLS falls back to default cert/key files
// which don't exist, but it then generates a self-signed cert, so it
// should still succeed.
cfg := &config.Config{
TLS: &config.TLSConfig{
Validity: 3600,
},
}
if err := Load(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoad_WithLog(t *testing.T) {
cfg := &config.Config{
Log: &config.LogConfig{
Level: "error",
},
}
if err := Load(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoad_SetsDefaultLogger(t *testing.T) {
cfg := &config.Config{
Log: &config.LogConfig{
Level: "warn",
Format: "json",
},
}
if err := Load(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// After Load, logger.Default() should be non-nil.
if logger.Default() == nil {
t.Fatal("expected default logger to be set")
}
}
// --- defaultLoader singleton tests ---
func TestDefaultLoader_IsSet(t *testing.T) {
if defaultLoader == nil {
t.Fatal("defaultLoader should not be nil")
}
if err := defaultLoader.Load(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// --- registerGroup edge cases ---
type countingRegistry[T any] struct {
m map[string]T
close int
}
func newCountingRegistry[T any]() *countingRegistry[T] {
return &countingRegistry[T]{m: make(map[string]T)}
}
func (r *countingRegistry[T]) Register(name string, v T) error { r.m[name] = v; return nil }
func (r *countingRegistry[T]) Unregister(name string) { delete(r.m, name); r.close++ }
func (r *countingRegistry[T]) IsRegistered(name string) bool { _, ok := r.m[name]; return ok }
func (r *countingRegistry[T]) Get(name string) T { return r.m[name] }
func (r *countingRegistry[T]) GetAll() map[string]T { return r.m }
func TestRegisterGroup_UnregistersAllOldEntries(t *testing.T) {
r := newCountingRegistry[int]()
r.Register("a", 1)
r.Register("b", 2)
r.Register("c", 3)
_ = registerGroup([]named[int]{{"x", 10}}, r)
if r.close != 3 {
t.Fatalf("expected 3 unregister calls, got %d", r.close)
}
if !r.IsRegistered("x") {
t.Fatal("expected 'x' to be registered")
}
if r.IsRegistered("a") || r.IsRegistered("b") || r.IsRegistered("c") {
t.Fatal("old entries should be unregistered")
}
}
// --- register error paths ---
func TestRegister_ServiceBadTLS(t *testing.T) {
// A non-empty CertFile that doesn't exist causes
// tls_util.LoadServerConfig to fail before reaching the
// listener registry lookup.
//
// ParseService logs an error via the default logger when the
// TLS cert can't be loaded. We suppress that log here since
// the error is expected.
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
discardLogger := xlogger.NewLogger(xlogger.OutputOption(io.Discard))
t.Cleanup(func() { logger.SetDefault(discardLogger) })
cfg := &config.Config{
Services: []*config.ServiceConfig{
{
Name: "bad-tls",
Addr: ":0",
Listener: &config.ListenerConfig{
TLS: &config.TLSConfig{
CertFile: "/nonexistent/cert.pem",
},
},
},
},
}
if err := register(cfg); err == nil {
t.Fatal("expected error for bad TLS config, got nil")
}
}
func TestRegister_ServiceUnknownListener(t *testing.T) {
// An unregistered listener type triggers the
// "unknown listener" error in ParseService.
cfg := &config.Config{
Services: []*config.ServiceConfig{
{
Name: "unknown-ln",
Addr: ":0",
Listener: &config.ListenerConfig{
Type: "nonexistent",
},
},
},
}
if err := register(cfg); err == nil {
t.Fatal("expected error for unknown listener type, got nil")
}
}
// bindingStubListener is a stubListener whose Init actually binds a TCP port
// via net.Listen, reproducing the EADDRINUSE condition the close-old-before-
// bind ordering in register() must prevent. Used only by reload-collision tests.
type bindingStubListener struct {
addr string
ln net.Listener
}
func (l *bindingStubListener) Init(md metadata.Metadata) error {
ln, err := net.Listen("tcp", l.addr)
if err != nil {
return err
}
l.ln = ln
return nil
}
func (l *bindingStubListener) Accept() (net.Conn, error) {
if l.ln == nil {
return nil, net.ErrClosed
}
return l.ln.Accept()
}
func (l *bindingStubListener) Addr() net.Addr {
if l.ln != nil {
return l.ln.Addr()
}
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
}
func (l *bindingStubListener) Close() error {
if l.ln == nil {
return nil
}
return l.ln.Close()
}
// freeTCPPort returns the number of a TCP port that is free at call time by
// opening and immediately closing a listener on 127.0.0.1:0. There is a small
// race window before the caller rebinds, which is acceptable for tests.
func freeTCPPort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen for free port: %v", err)
}
addr := l.Addr().(*net.TCPAddr)
l.Close()
return addr.Port
}
// TestRegister_ServiceReloadNoCollision verifies that calling register() twice
// with a service on a fixed port does NOT return EADDRINUSE on the second call
// (issue #754). Before the close-old-before-bind ordering, the second
// register() bound the new listener while the old one was still listening.
func TestRegister_ServiceReloadNoCollision(t *testing.T) {
port := freeTCPPort(t)
addr := fmt.Sprintf("127.0.0.1:%d", port)
// Register a binding listener factory under a test-specific name so we
// don't interfere with the "tcp" save/restore in TestRegister_Services.
const factoryName = "tcp-binding-test"
origListener := registry.ListenerRegistry().Get(factoryName)
registry.ListenerRegistry().Register(factoryName, func(opts ...listener.Option) listener.Listener {
options := listener.Options{}
for _, opt := range opts {
opt(&options)
}
return &bindingStubListener{addr: options.Addr}
})
// Reuse the inert "auto" handler stub.
origHandler := registry.HandlerRegistry().Get("auto")
registry.HandlerRegistry().Register("auto", func(opts ...handler.Option) handler.Handler {
return &stubHandler{}
})
t.Cleanup(func() {
registry.ListenerRegistry().Unregister(factoryName)
registry.HandlerRegistry().Unregister("auto")
if origListener != nil {
registry.ListenerRegistry().Register(factoryName, origListener)
}
if origHandler != nil {
registry.HandlerRegistry().Register("auto", origHandler)
}
})
r := registry.ServiceRegistry()
const svcName = "reload-svc"
t.Cleanup(func() { r.Unregister(svcName) })
buildCfg := func() *config.Config {
return &config.Config{
Services: []*config.ServiceConfig{
{
Name: svcName,
Addr: addr,
Listener: &config.ListenerConfig{Type: factoryName},
Handler: &config.HandlerConfig{Type: "auto"},
},
},
}
}
// First load: registers the service and binds the port.
if err := register(buildCfg()); err != nil {
t.Fatalf("first register: unexpected error: %v", err)
}
if !r.IsRegistered(svcName) {
t.Fatal("expected service registered after first load")
}
// Second load simulates a SIGHUP reload. With the old ordering this bound
// the new listener while the old one was still open and returned
// "address already in use"; the fix closes old services first.
if err := register(buildCfg()); err != nil {
if strings.Contains(err.Error(), "address already in use") {
t.Fatalf("second register (reload): port collision not avoided: %v", err)
}
t.Fatalf("second register (reload): unexpected error: %v", err)
}
if !r.IsRegistered(svcName) {
t.Fatal("expected service registered after reload")
}
}
+2 -12
View File
@@ -7,17 +7,12 @@ import (
"github.com/go-gost/core/admission"
"github.com/go-gost/core/logger"
xadmission "github.com/go-gost/x/admission"
admission_plugin "github.com/go-gost/x/admission/plugin"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
"github.com/go-gost/x/registry"
)
// ParseAdmission converts an AdmissionConfig into an admission.Admission. It
// resolves plugin backends (HTTP or gRPC) when cfg.Plugin is set, or
// constructs an in-process admission controller with optional file, Redis, and
// HTTP hot-reload support.
func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
if cfg == nil {
return nil
@@ -33,14 +28,13 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return admission_plugin.NewHTTPPlugin(
return xadmission.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return admission_plugin.NewGRPCPlugin(
return xadmission.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -64,7 +58,6 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
opts = append(opts, xadmission.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -79,9 +72,6 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
return xadmission.NewAdmission(opts...)
}
// List resolves one or more admission controller names from the registry. It
// returns only the controllers that were found, skipping any that are not
// registered.
func List(name string, names ...string) []admission.Admission {
var admissions []admission.Admission
if adm := registry.AdmissionRegistry().Get(name); adm != nil {
-144
View File
@@ -1,144 +0,0 @@
package admission
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseAdmission_Nil(t *testing.T) {
adm := ParseAdmission(nil)
if adm != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseAdmission_PluginHTTP(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "http-adm",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_PluginGRPC(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "grpc-adm",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "admission.local",
},
},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_PluginDefaultType(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "default-adm",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_WithMatchers(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "matcher-adm",
Matchers: []string{"192.168.0.0/16", "10.0.0.1"},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_Whitelist(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "whitelist-adm",
Whitelist: true,
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_Reverse(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "reverse-adm",
Reverse: true,
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_FileLoader(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "file-adm",
File: &config.FileLoader{Path: "/tmp/admission.txt"},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_HTTPLoader(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "http-adm",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/list"},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestParseAdmission_RedisLoader(t *testing.T) {
adm := ParseAdmission(&config.AdmissionConfig{
Name: "redis-adm",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "admission-set",
},
})
if adm == nil {
t.Fatal("expected non-nil admission")
}
}
func TestList_ReturnsEntries(t *testing.T) {
// The registry returns a lazy wrapper for any non-empty name.
got := List("test-admission", "test-admission-2")
if len(got) != 2 {
t.Fatalf("expected 2 entries, got %d", len(got))
}
}
func TestList_EmptyNameReturnsNothing(t *testing.T) {
// Empty names are filtered by the registry Get.
got := List("")
if len(got) != 0 {
t.Fatal("expected empty list for empty name")
}
}
+3 -68
View File
@@ -1,26 +1,18 @@
package auth
import (
"bufio"
"crypto/tls"
"io"
"net/url"
"os"
"strings"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger"
xauth "github.com/go-gost/x/auth"
auth_plugin "github.com/go-gost/x/auth/plugin"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
"github.com/go-gost/x/registry"
)
// ParseAuther converts an AutherConfig into an auth.Authenticator. It handles
// plugin backends (HTTP or gRPC), inline auth credentials, and optional file,
// Redis, and HTTP hot-reload sources.
func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
if cfg == nil {
return nil
@@ -36,14 +28,13 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
}
switch cfg.Plugin.Type {
case "http":
return auth_plugin.NewHTTPPlugin(
return xauth.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return auth_plugin.NewGRPCPlugin(
return xauth.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -75,7 +66,6 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
opts = append(opts, xauth.RedisLoaderOption(loader.RedisHashLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -89,9 +79,6 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
return xauth.NewAuthenticator(opts...)
}
// ParseAutherFromAuth creates a simple authenticator from a single
// username/password pair in an AuthConfig. Returns nil if the config is nil or
// has no username.
func ParseAutherFromAuth(au *config.AuthConfig) auth.Authenticator {
if au == nil || au.Username == "" {
return nil
@@ -108,24 +95,8 @@ func ParseAutherFromAuth(au *config.AuthConfig) auth.Authenticator {
)
}
// Info extracts authentication credentials from an AuthConfig as a
// url.Userinfo. It supports reading the first line from a password file when
// cfg.File is set, or falls back to inline username/password fields.
func Info(cfg *config.AuthConfig) *url.Userinfo {
if cfg == nil {
return nil
}
if cfg.File != "" {
if f, _ := os.Open(cfg.File); f != nil {
defer f.Close()
if infos, _ := parseInfo(f, 1); len(infos) > 0 {
return infos[0]
}
}
}
if cfg.Username == "" {
if cfg == nil || cfg.Username == "" {
return nil
}
@@ -135,42 +106,6 @@ func Info(cfg *config.AuthConfig) *url.Userinfo {
return url.UserPassword(cfg.Username, cfg.Password)
}
func parseInfo(r io.Reader, max int) (infos []*url.Userinfo, err error) {
if r == nil {
return
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
// line := strings.Replace(scanner.Text(), "\t", " ", -1)
line := strings.TrimSpace(scanner.Text())
if n := strings.IndexByte(line, '#'); n == 0 {
continue
}
sp := strings.SplitN(line, " ", 2)
if len(sp) == 1 {
if k := strings.TrimSpace(sp[0]); k != "" {
infos = append(infos, url.User(k))
}
}
if len(sp) == 2 {
if k := strings.TrimSpace(sp[0]); k != "" {
infos = append(infos, url.UserPassword(k, strings.TrimSpace(sp[1])))
}
}
if max > 0 && len(infos) >= max {
break
}
}
err = scanner.Err()
return
}
// List resolves one or more authenticator names from the registry. It returns
// only the authenticators that were found, skipping any that are not
// registered.
func List(name string, names ...string) []auth.Authenticator {
var authers []auth.Authenticator
if auther := registry.AutherRegistry().Get(name); auther != nil {
-242
View File
@@ -1,242 +0,0 @@
package auth
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestInfo_Nil(t *testing.T) {
if u := Info(nil); u != nil {
t.Fatal("expected nil for nil config")
}
}
func TestInfo_EmptyUsername(t *testing.T) {
u := Info(&config.AuthConfig{Username: ""})
if u != nil {
t.Fatal("expected nil for empty username")
}
}
func TestInfo_UsernameOnly(t *testing.T) {
u := Info(&config.AuthConfig{Username: "user"})
if u == nil {
t.Fatal("expected non-nil userinfo")
}
if u.Username() != "user" {
t.Fatalf("username = %q, want %q", u.Username(), "user")
}
if _, hasPassword := u.Password(); hasPassword {
t.Fatal("expected no password")
}
}
func TestInfo_UsernameAndPassword(t *testing.T) {
u := Info(&config.AuthConfig{Username: "user", Password: "pass"})
if u == nil {
t.Fatal("expected non-nil userinfo")
}
if u.Username() != "user" {
t.Fatalf("username = %q, want %q", u.Username(), "user")
}
if p, ok := u.Password(); !ok || p != "pass" {
t.Fatalf("password = %q, want %q", p, "pass")
}
}
func TestInfo_WithFile(t *testing.T) {
// File that doesn't exist - should fall back to username
u := Info(&config.AuthConfig{
Username: "fallback",
Password: "pass",
File: "/nonexistent/auth_file",
})
if u == nil {
t.Fatal("expected non-nil userinfo (fallback)")
}
if u.Username() != "fallback" {
t.Fatalf("username = %q, want %q", u.Username(), "fallback")
}
}
func TestParseAutherFromAuth_Nil(t *testing.T) {
a := ParseAutherFromAuth(nil)
if a != nil {
t.Fatal("expected nil for nil auth config")
}
}
func TestParseAutherFromAuth_EmptyUsername(t *testing.T) {
a := ParseAutherFromAuth(&config.AuthConfig{Username: ""})
if a != nil {
t.Fatal("expected nil for empty username")
}
}
func TestParseAutherFromAuth_Valid(t *testing.T) {
a := ParseAutherFromAuth(&config.AuthConfig{
Username: "user",
Password: "pass",
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestParseAuther_Nil(t *testing.T) {
a := ParseAuther(nil)
if a != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseAuther_WithUsers(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "test-auth",
Auths: []*config.AuthConfig{
{Username: "u1", Password: "p1"},
{Username: "u2", Password: "p2"},
{Username: "", Password: "nope"}, // skipped
},
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestParseAuther_WithEmptyAuths(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "empty-auth",
Auths: []*config.AuthConfig{},
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestParseAuther_PluginHTTP(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "http-plugin",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if a == nil {
t.Fatal("expected non-nil plugin authenticator")
}
}
func TestParseAuther_PluginGRPC(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "grpc-plugin",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "auth.local",
},
},
})
if a == nil {
t.Fatal("expected non-nil plugin authenticator")
}
}
func TestParseAuther_PluginDefaultType(t *testing.T) {
// Default plugin type should be treated as gRPC
a := ParseAuther(&config.AutherConfig{
Name: "default-plugin",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if a == nil {
t.Fatal("expected non-nil plugin authenticator")
}
}
func TestInfo_ParseFile(t *testing.T) {
// parseInfo with empty reader should return nil
u := Info(&config.AuthConfig{
File: "/nonexistent/file",
})
if u != nil {
t.Fatal("expected nil for nonexistent file (should not crash)")
}
}
func TestList_ReturnsEntries(t *testing.T) {
// The registry returns a lazy wrapper for any non-empty name.
got := List("test-auther", "test-auther-2")
if len(got) != 2 {
t.Fatalf("expected 2 entries, got %d", len(got))
}
}
func TestList_EmptyNameReturnsNothing(t *testing.T) {
// Empty names are filtered by the registry Get.
got := List("")
if len(got) != 0 {
t.Fatal("expected empty list for empty name")
}
}
func TestParseAuther_HTTPLoader(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "http-loader-auth",
HTTP: &config.HTTPLoader{
URL: "http://localhost:8080/auth",
},
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestParseAuther_FileLoader(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "file-loader-auth",
File: &config.FileLoader{
Path: "/tmp/auth.txt",
},
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestParseAuther_RedisLoader(t *testing.T) {
a := ParseAuther(&config.AutherConfig{
Name: "redis-loader-auth",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
},
})
if a == nil {
t.Fatal("expected non-nil authenticator")
}
}
func TestInfo_Integration(t *testing.T) {
// Verify Info returns *url.Userinfo whose String() matches the standard library.
u := Info(&config.AuthConfig{Username: "alice", Password: "secret"})
if u == nil {
t.Fatal("expected non-nil")
}
want := "alice:secret"
if u.String() != want {
t.Fatalf("String() = %q, want %q", u.String(), want)
}
}
+2 -11
View File
@@ -7,16 +7,12 @@ import (
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
xbypass "github.com/go-gost/x/bypass"
bypass_plugin "github.com/go-gost/x/bypass/plugin"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
"github.com/go-gost/x/registry"
)
// ParseBypass converts a BypassConfig into a bypass.Bypass. It resolves plugin
// backends (HTTP or gRPC) when cfg.Plugin is set, or constructs an in-process
// bypass with optional file, Redis, and HTTP hot-reload support.
func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
if cfg == nil {
return nil
@@ -32,14 +28,13 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return bypass_plugin.NewHTTPPlugin(
return xbypass.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return bypass_plugin.NewGRPCPlugin(
return xbypass.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -50,7 +45,6 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
opts := []xbypass.Option{
xbypass.MatchersOption(cfg.Matchers),
xbypass.WhitelistOption(cfg.Reverse || cfg.Whitelist),
xbypass.NetworkOption(cfg.Network),
xbypass.ReloadPeriodOption(cfg.Reload),
xbypass.LoggerOption(logger.Default().WithFields(map[string]any{
"kind": "bypass",
@@ -64,7 +58,6 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
opts = append(opts, xbypass.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -79,8 +72,6 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
return xbypass.NewBypass(opts...)
}
// List resolves one or more bypass names from the registry. It returns only
// the bypasses that were found, skipping any that are not registered.
func List(name string, names ...string) []bypass.Bypass {
var bypasses []bypass.Bypass
if bp := registry.BypassRegistry().Get(name); bp != nil {
-144
View File
@@ -1,144 +0,0 @@
package bypass
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseBypass_Nil(t *testing.T) {
bp := ParseBypass(nil)
if bp != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseBypass_PluginHTTP(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "http-bp",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_PluginGRPC(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "grpc-bp",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "bypass.local",
},
},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_PluginDefaultType(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "default-bp",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_WithMatchers(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "matcher-bp",
Matchers: []string{"127.0.0.1", "10.0.0.0/8", "*.example.com"},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_Whitelist(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "whitelist-bp",
Whitelist: true,
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_Reverse(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "reverse-bp",
Reverse: true,
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_FileLoader(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "file-bp",
File: &config.FileLoader{Path: "/tmp/bypass.txt"},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_HTTPLoader(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "http-bp",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/list"},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestParseBypass_RedisLoader(t *testing.T) {
bp := ParseBypass(&config.BypassConfig{
Name: "redis-bp",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "bypass-set",
},
})
if bp == nil {
t.Fatal("expected non-nil bypass")
}
}
func TestList_ReturnsEntries(t *testing.T) {
// The registry returns a lazy wrapper for any non-empty name.
got := List("test-bypass", "test-bypass-2")
if len(got) != 2 {
t.Fatalf("expected 2 entries, got %d", len(got))
}
}
func TestList_EmptyNameReturnsNothing(t *testing.T) {
// Empty names are filtered by the registry Get.
got := List("")
if len(got) != 0 {
t.Fatal("expected empty list for empty name")
}
}
+3 -10
View File
@@ -12,15 +12,12 @@ import (
"github.com/go-gost/x/registry"
)
// ParseChain converts a ChainConfig into a chain.Chainer. Each hop reference
// is either looked up from the registry (by name) or parsed inline via
// ParseHop. Returns nil with no error when cfg is nil.
func ParseChain(cfg *config.ChainConfig, log logger.Logger) (chain.Chainer, error) {
func ParseChain(cfg *config.ChainConfig) (chain.Chainer, error) {
if cfg == nil {
return nil, nil
}
chainLogger := log.WithFields(map[string]any{
chainLogger := logger.Default().WithFields(map[string]any{
"kind": "chain",
"chain": cfg.Name,
})
@@ -36,15 +33,11 @@ func ParseChain(cfg *config.ChainConfig, log logger.Logger) (chain.Chainer, erro
)
for _, ch := range cfg.Hops {
if ch == nil {
continue
}
var hop hop.Hop
var err error
if ch.Nodes != nil || ch.Plugin != nil {
if hop, err = hop_parser.ParseHop(ch, log); err != nil {
if hop, err = hop_parser.ParseHop(ch); err != nil {
return nil, err
}
} else {
-121
View File
@@ -1,121 +0,0 @@
package chain
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func testLogger() logger.Logger {
return xlogger.NewLogger(xlogger.OutputOption(io.Discard))
}
func TestParseChain_Nil(t *testing.T) {
c, err := ParseChain(nil, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseChain_EmptyHops(t *testing.T) {
c, err := ParseChain(&config.ChainConfig{
Name: "empty-hops",
Hops: []*config.HopConfig{},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain")
}
}
func TestParseChain_NilHops(t *testing.T) {
c, err := ParseChain(&config.ChainConfig{
Name: "nil-hops",
Hops: nil,
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain")
}
}
func TestParseChain_NilHopEntry(t *testing.T) {
c, err := ParseChain(&config.ChainConfig{
Name: "nil-entry",
Hops: []*config.HopConfig{nil},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain when hop entry is nil")
}
}
func TestParseChain_WithMetadata(t *testing.T) {
c, err := ParseChain(&config.ChainConfig{
Name: "meta-chain",
Metadata: map[string]any{
"key": "value",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain")
}
}
func TestParseChain_NamedHop(t *testing.T) {
// Named hop (Nodes==nil, Plugin==nil) uses registry lookup
c, err := ParseChain(&config.ChainConfig{
Name: "named-hop",
Hops: []*config.HopConfig{
{Name: "nonexistent-hop"},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain even with unregistered hop")
}
}
func TestParseChain_InlineHopWithPlugin(t *testing.T) {
// Inline hop (Nodes!=nil or Plugin!=nil) calls hop_parser.ParseHop
c, err := ParseChain(&config.ChainConfig{
Name: "inline-hop",
Hops: []*config.HopConfig{
{
Name: "plugin-hop",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected non-nil chain")
}
}
+18 -86
View File
@@ -4,28 +4,20 @@ import (
"crypto/tls"
"strings"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/logger"
xbypass "github.com/go-gost/x/bypass"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
bypass_parser "github.com/go-gost/x/config/parsing/bypass"
node_parser "github.com/go-gost/x/config/parsing/node"
selector_parser "github.com/go-gost/x/config/parsing/selector"
xhop "github.com/go-gost/x/hop"
hop_plugin "github.com/go-gost/x/hop/plugin"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
"github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
)
// ParseHop converts a HopConfig into a hop.Hop. It resolves plugin backends
// (HTTP or gRPC), parses inline nodes with metadata inheritance from the parent
// hop, wires up a node selector, and configures optional file, Redis, and HTTP
// hot-reload sources. Returns nil with no error when cfg is nil.
func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
func ParseHop(cfg *config.HopConfig) (hop.Hop, error) {
if cfg == nil {
return nil, nil
}
@@ -39,15 +31,14 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
}
}
switch strings.ToLower(cfg.Plugin.Type) {
case plugin.HTTP:
return hop_plugin.NewHTTPPlugin(
case "http":
return xhop.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
), nil
default:
return hop_plugin.NewGRPCPlugin(
return xhop.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -55,96 +46,38 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
}
}
var ppv int
var soMark int
ifce := cfg.Interface
var netns string
if cfg.SockOpts != nil {
soMark = cfg.SockOpts.Mark
}
if cfg.Metadata != nil {
md := metadata.NewMetadata(cfg.Metadata)
if v := mdutil.GetString(md, parsing.MDKeyInterface); v != "" {
ifce = v
}
if v := mdutil.GetInt(md, parsing.MDKeySoMark); v > 0 {
soMark = v
}
ppv = mdutil.GetInt(md, parsing.MDKeyProxyProtocol)
netns = mdutil.GetString(md, parsing.MDKeyNetns)
}
var nodes []*chain.Node
for _, v := range cfg.Nodes {
if v == nil {
continue
}
// Build a merged metadata map for inheritance without mutating
// the original node config.
merged := make(map[string]any)
for k, val := range v.Metadata {
merged[k] = val
}
md := metadata.NewMetadata(merged)
if v.Resolver == "" {
v.Resolver = cfg.Resolver
}
if v.Hosts == "" {
v.Hosts = cfg.Hosts
}
if !md.IsExists(parsing.MDKeyInterface) {
if ifce != "" {
merged[parsing.MDKeyInterface] = ifce
}
if v.Interface != "" {
merged[parsing.MDKeyInterface] = v.Interface
}
if v.Interface == "" {
v.Interface = cfg.Interface
}
if !md.IsExists(parsing.MDKeySoMark) {
if soMark != 0 {
merged[parsing.MDKeySoMark] = soMark
}
if v.SockOpts != nil && v.SockOpts.Mark != 0 {
merged[parsing.MDKeySoMark] = v.SockOpts.Mark
}
}
if !md.IsExists(parsing.MDKeyProxyProtocol) && ppv > 0 {
merged[parsing.MDKeyProxyProtocol] = ppv
}
if !md.IsExists(parsing.MDKeyNetns) {
if netns != "" {
merged[parsing.MDKeyNetns] = netns
}
if v.Netns != "" {
merged[parsing.MDKeyNetns] = v.Netns
}
if v.SockOpts == nil {
v.SockOpts = cfg.SockOpts
}
if v.Connector == nil {
v.Connector = &config.ConnectorConfig{}
}
if strings.TrimSpace(v.Connector.Type) == "" {
v.Connector.Type = "http"
v.Connector = &config.ConnectorConfig{
Type: "http",
}
}
if v.Dialer == nil {
v.Dialer = &config.DialerConfig{}
}
if strings.TrimSpace(v.Dialer.Type) == "" {
v.Dialer.Type = "tcp"
v.Dialer = &config.DialerConfig{
Type: "tcp",
}
}
// Temporarily swap merged metadata so ParseNode sees inherited values.
origMeta := v.Metadata
v.Metadata = merged
node, err := node_parser.ParseNode(cfg.Name, v, log)
v.Metadata = origMeta
node, err := node_parser.ParseNode(cfg.Name, v)
if err != nil {
return nil, err
}
@@ -162,9 +95,9 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
xhop.NameOption(cfg.Name),
xhop.NodeOption(nodes...),
xhop.SelectorOption(sel),
xhop.BypassOption(xbypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
xhop.BypassOption(bypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
xhop.ReloadPeriodOption(cfg.Reload),
xhop.LoggerOption(log.WithFields(map[string]any{
xhop.LoggerOption(logger.Default().WithFields(map[string]any{
"kind": "hop",
"hop": cfg.Name,
})),
@@ -177,7 +110,6 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
opts = append(opts, xhop.RedisLoaderOption(loader.RedisStringLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
-429
View File
@@ -1,429 +0,0 @@
package hop
import (
"io"
"testing"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
xlogger "github.com/go-gost/x/logger"
mdutil "github.com/go-gost/x/metadata/util"
// Register connector and dialer implementations needed for node parsing.
_ "github.com/go-gost/x/connector/http"
_ "github.com/go-gost/x/dialer/tcp"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func testLogger() logger.Logger {
return xlogger.NewLogger(xlogger.OutputOption(io.Discard))
}
func TestParseHop_Nil(t *testing.T) {
h, err := ParseHop(nil, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseHop_PluginHTTP(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "http-hop",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_PluginGRPC(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "grpc-hop",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "hop.local",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_PluginDefaultType(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "default-hop",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_PluginHTTPWithTLS(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "http-tls-hop",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9003",
TLS: &config.TLSConfig{
Secure: false,
ServerName: "hop.example.com",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_PluginGRPCWithTLSInsecure(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "grpc-insecure-hop",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9004",
TLS: &config.TLSConfig{
Secure: false, // InsecureSkipVerify = true
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_NoNodesNoPlugin(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "empty-hop",
Nodes: nil,
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop even with no nodes")
}
}
func TestParseHop_WithBypass(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "bypass-hop",
Bypass: "bypass1",
Bypasses: []string{"bypass2", "bypass3"},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_WithSelector(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "selector-hop",
Selector: &config.SelectorConfig{
Strategy: "round",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_WithNodeNilEntries(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "nil-nodes-hop",
Nodes: []*config.NodeConfig{
nil,
nil,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop when nodes contain nil entries")
}
}
func TestParseHop_SockOptsMark(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "sockopts-hop",
SockOpts: &config.SockOptsConfig{
Mark: 1234,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_MetadataSoMark(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "md-somark-hop",
Metadata: map[string]any{
parsing.MDKeySoMark: 5678,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_MetadataInterface(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "md-iface-hop",
Metadata: map[string]any{
parsing.MDKeyInterface: "eth0",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_MetadataProxyProtocol(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "md-ppv-hop",
Metadata: map[string]any{
parsing.MDKeyProxyProtocol: 2,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_MetadataNetns(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "md-netns-hop",
Metadata: map[string]any{
parsing.MDKeyNetns: "mynetns",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_InterfaceDeprecated(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "iface-deprecated-hop",
Interface: "eth1",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_FileLoader(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "file-hop",
File: &config.FileLoader{Path: "/tmp/hop.txt"},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_HTTPLoader(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "http-hop",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/hop"},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_RedisLoader(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "redis-hop",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "hop-key",
Username: "user",
Password: "pass",
DB: 1,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
}
func TestParseHop_NodeInheritsInterface(t *testing.T) {
// When hop-level interface is set and node-level is not, node inherits.
h, err := ParseHop(&config.HopConfig{
Name: "inherit-iface-hop",
Interface: "eth0",
Nodes: []*config.NodeConfig{
{
Name: "node1",
Addr: "example.com:8080",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
nodes := h.(hop.NodeList).Nodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
if got := mdutil.GetString(nodes[0].Metadata(), parsing.MDKeyInterface); got != "eth0" {
t.Fatalf("inherited interface = %q, want %q", got, "eth0")
}
}
func TestParseHop_NodeInheritsSoMark(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "inherit-somark-hop",
SockOpts: &config.SockOptsConfig{
Mark: 9999,
},
Nodes: []*config.NodeConfig{
{
Name: "node1",
Addr: "example.com:8080",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
nodes := h.(hop.NodeList).Nodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
if got := mdutil.GetInt(nodes[0].Metadata(), parsing.MDKeySoMark); got != 9999 {
t.Fatalf("inherited so_mark = %d, want %d", got, 9999)
}
}
func TestParseHop_NodeInheritsNetns(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "inherit-netns-hop",
Metadata: map[string]any{
parsing.MDKeyNetns: "mynetns",
},
Nodes: []*config.NodeConfig{
{
Name: "node1",
Addr: "example.com:8080",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
nodes := h.(hop.NodeList).Nodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
if got := mdutil.GetString(nodes[0].Metadata(), parsing.MDKeyNetns); got != "mynetns" {
t.Fatalf("inherited netns = %q, want %q", got, "mynetns")
}
}
func TestParseHop_NodeResolverHostsInheritance(t *testing.T) {
h, err := ParseHop(&config.HopConfig{
Name: "inherit-resolver-hop",
Resolver: "my-resolver",
Hosts: "my-hosts",
Nodes: []*config.NodeConfig{
{
Name: "node1",
Addr: "example.com:8080",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h == nil {
t.Fatal("expected non-nil hop")
}
// Verify the node was created (no panic from nil resolver/hosts).
// The resolver/hosts names are inherited, but registry lookup returns
// nil since these names are not registered in this test.
nodes := h.(hop.NodeList).Nodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
}
+2 -9
View File
@@ -9,14 +9,10 @@ import (
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xhosts "github.com/go-gost/x/hosts"
hosts_plugin "github.com/go-gost/x/hosts/plugin"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
)
// ParseHostMapper converts a HostsConfig into a hosts.HostMapper. It resolves
// plugin backends (HTTP or gRPC), parses inline IP→hostname mappings, and
// configures optional file, Redis, and HTTP hot-reload support.
func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
if cfg == nil {
return nil
@@ -32,14 +28,13 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return hosts_plugin.NewHTTPPlugin(
return xhosts.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return hosts_plugin.NewGRPCPlugin(
return xhosts.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -79,7 +74,6 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
opts = append(opts, xhosts.RedisLoaderOption(loader.RedisListLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -87,7 +81,6 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
opts = append(opts, xhosts.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
-154
View File
@@ -1,154 +0,0 @@
package hosts
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseHostMapper_Nil(t *testing.T) {
hm := ParseHostMapper(nil)
if hm != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseHostMapper_PluginHTTP(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "http-hosts",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_PluginGRPC(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "grpc-hosts",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "hosts.local",
},
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_PluginDefaultType(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "default-hosts",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_WithMappings(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "mapping-hosts",
Mappings: []*config.HostMappingConfig{
{IP: "127.0.0.1", Hostname: "localhost"},
{IP: "::1", Hostname: "ipv6-localhost"},
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_EmptyMapping(t *testing.T) {
// Mappings with empty IP or Hostname should be skipped
hm := ParseHostMapper(&config.HostsConfig{
Name: "partial-hosts",
Mappings: []*config.HostMappingConfig{
{IP: "", Hostname: "nope"}, // skipped
{IP: "127.0.0.1", Hostname: ""}, // skipped
{IP: "127.0.0.1", Hostname: "valid"}, // kept
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper even with partial mappings")
}
}
func TestParseHostMapper_InvalidIP(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "invalid-ip",
Mappings: []*config.HostMappingConfig{
{IP: "not-an-ip", Hostname: "example.com"}, // skipped
{IP: "127.0.0.1", Hostname: "valid"}, // kept
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper even with invalid IP")
}
}
func TestParseHostMapper_FileLoader(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "file-hosts",
File: &config.FileLoader{Path: "/etc/hosts"},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_HTTPLoader(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "http-hosts",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/hosts"},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_RedisLoader(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "redis-hosts",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "hosts-key",
Type: "set",
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
func TestParseHostMapper_RedisListLoader(t *testing.T) {
hm := ParseHostMapper(&config.HostsConfig{
Name: "redis-list-hosts",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "hosts-list",
Type: "list",
},
})
if hm == nil {
t.Fatal("expected non-nil host mapper")
}
}
+4 -11
View File
@@ -8,14 +8,10 @@ import (
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xingress "github.com/go-gost/x/ingress"
ingress_plugin "github.com/go-gost/x/ingress/plugin"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
)
// ParseIngress converts an IngressConfig into an ingress.Ingress. It resolves
// plugin backends (HTTP or gRPC), parses inline hostname→endpoint rules, and
// configures optional file, Redis, and HTTP hot-reload support.
func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
if cfg == nil {
return nil
@@ -31,14 +27,13 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return ingress_plugin.NewHTTPPlugin(
return xingress.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return ingress_plugin.NewGRPCPlugin(
return xingress.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -46,13 +41,13 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
}
}
var rules []*ingress.Rule
var rules []xingress.Rule
for _, rule := range cfg.Rules {
if rule.Hostname == "" || rule.Endpoint == "" {
continue
}
rules = append(rules, &ingress.Rule{
rules = append(rules, xingress.Rule{
Hostname: rule.Hostname,
Endpoint: rule.Endpoint,
})
@@ -74,7 +69,6 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
opts = append(opts, xingress.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -82,7 +76,6 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
opts = append(opts, xingress.RedisLoaderOption(loader.RedisHashLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
-141
View File
@@ -1,141 +0,0 @@
package ingress
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseIngress_Nil(t *testing.T) {
ing := ParseIngress(nil)
if ing != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseIngress_PluginHTTP(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "http-ing",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_PluginGRPC(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "grpc-ing",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "ingress.local",
},
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_PluginDefaultType(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "default-ing",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_WithRules(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "rule-ing",
Rules: []*config.IngressRuleConfig{
{Hostname: "example.com", Endpoint: "10.0.0.1:8080"},
{Hostname: "test.com", Endpoint: "10.0.0.2:9090"},
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_EmptyRules(t *testing.T) {
// Rules with empty hostname or endpoint should be skipped
ing := ParseIngress(&config.IngressConfig{
Name: "partial-ing",
Rules: []*config.IngressRuleConfig{
{Hostname: "", Endpoint: "nope:8080"}, // skipped
{Hostname: "nope.com", Endpoint: ""}, // skipped
{Hostname: "valid.com", Endpoint: "10.0.0.1:8080"}, // kept
},
})
if ing == nil {
t.Fatal("expected non-nil ingress even with partial rules")
}
}
func TestParseIngress_FileLoader(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "file-ing",
File: &config.FileLoader{Path: "/tmp/ingress.txt"},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_HTTPLoader(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "http-ing",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/ingress"},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_RedisLoader(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "redis-ing",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "ingress-key",
Type: "hash",
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
func TestParseIngress_RedisSetLoader(t *testing.T) {
ing := ParseIngress(&config.IngressConfig{
Name: "redis-set-ing",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "ingress-set",
Type: "set",
},
})
if ing == nil {
t.Fatal("expected non-nil ingress")
}
}
-43
View File
@@ -1,55 +1,22 @@
package limiter
import (
"crypto/tls"
"strings"
"github.com/go-gost/core/limiter/conn"
"github.com/go-gost/core/limiter/rate"
"github.com/go-gost/core/limiter/traffic"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
xconn "github.com/go-gost/x/limiter/conn"
xrate "github.com/go-gost/x/limiter/rate"
xtraffic "github.com/go-gost/x/limiter/traffic"
traffic_plugin "github.com/go-gost/x/limiter/traffic/plugin"
)
// ParseTrafficLimiter converts a LimiterConfig into a traffic.TrafficLimiter.
// It resolves plugin backends (HTTP or gRPC), inline rate limits, and optional
// file, Redis, and HTTP hot-reload support.
func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter) {
if cfg == nil {
return nil
}
if cfg.Plugin != nil {
var tlsCfg *tls.Config
if cfg.Plugin.TLS != nil {
tlsCfg = &tls.Config{
ServerName: cfg.Plugin.TLS.ServerName,
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
}
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return traffic_plugin.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return traffic_plugin.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
)
}
}
var opts []xtraffic.Option
if cfg.File != nil && cfg.File.Path != "" {
@@ -61,7 +28,6 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter)
opts = append(opts, xtraffic.RedisLoaderOption(loader.RedisListLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -69,7 +35,6 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter)
opts = append(opts, xtraffic.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -93,8 +58,6 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter)
return xtraffic.NewTrafficLimiter(opts...)
}
// ParseConnLimiter converts a LimiterConfig into a conn.ConnLimiter with
// inline limits and optional file, Redis, and HTTP hot-reload support.
func ParseConnLimiter(cfg *config.LimiterConfig) (lim conn.ConnLimiter) {
if cfg == nil {
return nil
@@ -111,7 +74,6 @@ func ParseConnLimiter(cfg *config.LimiterConfig) (lim conn.ConnLimiter) {
opts = append(opts, xconn.RedisLoaderOption(loader.RedisListLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -119,7 +81,6 @@ func ParseConnLimiter(cfg *config.LimiterConfig) (lim conn.ConnLimiter) {
opts = append(opts, xconn.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -143,8 +104,6 @@ func ParseConnLimiter(cfg *config.LimiterConfig) (lim conn.ConnLimiter) {
return xconn.NewConnLimiter(opts...)
}
// ParseRateLimiter converts a LimiterConfig into a rate.RateLimiter with
// inline limits and optional file, Redis, and HTTP hot-reload support.
func ParseRateLimiter(cfg *config.LimiterConfig) (lim rate.RateLimiter) {
if cfg == nil {
return nil
@@ -161,7 +120,6 @@ func ParseRateLimiter(cfg *config.LimiterConfig) (lim rate.RateLimiter) {
opts = append(opts, xrate.RedisLoaderOption(loader.RedisListLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
@@ -169,7 +127,6 @@ func ParseRateLimiter(cfg *config.LimiterConfig) (lim rate.RateLimiter) {
opts = append(opts, xrate.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
-234
View File
@@ -1,234 +0,0 @@
package limiter
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseTrafficLimiter_Nil(t *testing.T) {
lim := ParseTrafficLimiter(nil)
if lim != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseTrafficLimiter_WithLimits(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "traffic-limiter",
Limits: []string{"100KB", "200KB"},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_PluginHTTP(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "http-lim",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_PluginGRPC(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "grpc-lim",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "limiter.local",
},
},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_PluginDefaultType(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "default-lim",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_FileLoader(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "file-lim",
File: &config.FileLoader{Path: "/tmp/limiter.txt"},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_HTTPLoader(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "http-lim",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/limits"},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_RedisLoader(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "redis-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "limiter-key",
},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
func TestParseTrafficLimiter_RedisListLoader(t *testing.T) {
lim := ParseTrafficLimiter(&config.LimiterConfig{
Name: "redis-list-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "limiter-list",
Type: "list",
},
})
if lim == nil {
t.Fatal("expected non-nil traffic limiter")
}
}
// --- ConnLimiter ---
func TestParseConnLimiter_Nil(t *testing.T) {
lim := ParseConnLimiter(nil)
if lim != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseConnLimiter_WithLimits(t *testing.T) {
lim := ParseConnLimiter(&config.LimiterConfig{
Name: "conn-limiter",
Limits: []string{"10", "50"},
})
if lim == nil {
t.Fatal("expected non-nil conn limiter")
}
}
func TestParseConnLimiter_RedisLoader(t *testing.T) {
lim := ParseConnLimiter(&config.LimiterConfig{
Name: "redis-conn-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "conn-key",
},
})
if lim == nil {
t.Fatal("expected non-nil conn limiter")
}
}
func TestParseConnLimiter_RedisListLoader(t *testing.T) {
lim := ParseConnLimiter(&config.LimiterConfig{
Name: "redis-list-conn-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "conn-list",
Type: "list",
},
})
if lim == nil {
t.Fatal("expected non-nil conn limiter")
}
}
func TestParseConnLimiter_FileLoader(t *testing.T) {
lim := ParseConnLimiter(&config.LimiterConfig{
Name: "file-conn-lim",
File: &config.FileLoader{Path: "/tmp/conn_limiter.txt"},
})
if lim == nil {
t.Fatal("expected non-nil conn limiter")
}
}
// --- RateLimiter ---
func TestParseRateLimiter_Nil(t *testing.T) {
lim := ParseRateLimiter(nil)
if lim != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseRateLimiter_WithLimits(t *testing.T) {
lim := ParseRateLimiter(&config.LimiterConfig{
Name: "rate-limiter",
Limits: []string{"100", "200"},
})
if lim == nil {
t.Fatal("expected non-nil rate limiter")
}
}
func TestParseRateLimiter_RedisLoader(t *testing.T) {
lim := ParseRateLimiter(&config.LimiterConfig{
Name: "redis-rate-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "rate-key",
},
})
if lim == nil {
t.Fatal("expected non-nil rate limiter")
}
}
func TestParseRateLimiter_RedisListLoader(t *testing.T) {
lim := ParseRateLimiter(&config.LimiterConfig{
Name: "redis-list-rate-lim",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "rate-list",
Type: "list",
},
})
if lim == nil {
t.Fatal("expected non-nil rate limiter")
}
}
func TestParseRateLimiter_FileLoader(t *testing.T) {
lim := ParseRateLimiter(&config.LimiterConfig{
Name: "file-rate-lim",
File: &config.FileLoader{Path: "/tmp/rate_limiter.txt"},
})
if lim == nil {
t.Fatal("expected non-nil rate limiter")
}
}
-75
View File
@@ -1,75 +0,0 @@
package logger
import (
"io"
"os"
"path/filepath"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
"github.com/go-gost/x/registry"
"gopkg.in/natefinch/lumberjack.v2"
)
// ParseLogger converts a LoggerConfig into a logger.Logger. It configures
// output destination (stderr, stdout, file, or discard), log level, format,
// and optional log rotation via lumberjack.
func ParseLogger(cfg *config.LoggerConfig) logger.Logger {
if cfg == nil || cfg.Log == nil {
return nil
}
opts := []xlogger.Option{
xlogger.NameOption(cfg.Name),
xlogger.FormatOption(logger.LogFormat(cfg.Log.Format)),
xlogger.LevelOption(logger.LogLevel(cfg.Log.Level)),
}
var out io.Writer = os.Stderr
switch cfg.Log.Output {
case "none", "null":
out = io.Discard
case "stdout":
out = os.Stdout
case "stderr", "":
out = os.Stderr
default:
if cfg.Log.Rotation != nil {
out = &lumberjack.Logger{
Filename: cfg.Log.Output,
MaxSize: cfg.Log.Rotation.MaxSize,
MaxAge: cfg.Log.Rotation.MaxAge,
MaxBackups: cfg.Log.Rotation.MaxBackups,
LocalTime: cfg.Log.Rotation.LocalTime,
Compress: cfg.Log.Rotation.Compress,
}
} else {
os.MkdirAll(filepath.Dir(cfg.Log.Output), 0755)
f, err := os.OpenFile(cfg.Log.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Default().Warn(err)
} else {
out = f
}
}
}
opts = append(opts, xlogger.OutputOption(out))
return xlogger.NewLogger(opts...)
}
// List resolves one or more logger names from the registry. It returns only
// the loggers that were found, skipping any that are not registered.
func List(name string, names ...string) []logger.Logger {
var loggers []logger.Logger
if adm := registry.LoggerRegistry().Get(name); adm != nil {
loggers = append(loggers, adm)
}
for _, s := range names {
if lg := registry.LoggerRegistry().Get(s); lg != nil {
loggers = append(loggers, lg)
}
}
return loggers
}
-142
View File
@@ -1,142 +0,0 @@
package logger
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseLogger_Nil(t *testing.T) {
lg := ParseLogger(nil)
if lg != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseLogger_NilLog(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "test",
Log: nil,
})
if lg != nil {
t.Fatal("expected nil when Log is nil")
}
}
func TestParseLogger_Levels(t *testing.T) {
for _, level := range []string{"trace", "debug", "info", "warn", "error", "fatal"} {
t.Run(level, func(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "test-" + level,
Log: &config.LogConfig{
Level: level,
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
})
}
}
func TestParseLogger_OutputNone(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "null-logger",
Log: &config.LogConfig{
Output: "none",
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestParseLogger_OutputStdout(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "stdout-logger",
Log: &config.LogConfig{
Output: "stdout",
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestParseLogger_OutputStderr(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "stderr-logger",
Log: &config.LogConfig{
Output: "stderr",
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestParseLogger_OutputDefault(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "default-logger",
Log: &config.LogConfig{
Output: "",
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestParseLogger_WithFormat(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "format-logger",
Log: &config.LogConfig{
Format: "json",
Level: "info",
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestParseLogger_FileOutput(t *testing.T) {
lg := ParseLogger(&config.LoggerConfig{
Name: "file-logger",
Log: &config.LogConfig{
Output: "/tmp/gost-test.log",
Rotation: &config.LogRotationConfig{
MaxSize: 10,
MaxAge: 7,
MaxBackups: 5,
LocalTime: true,
Compress: true,
},
},
})
if lg == nil {
t.Fatal("expected non-nil logger")
}
}
func TestList_EmptyLogger(t *testing.T) {
got := List("nonexistent")
if len(got) != 0 {
t.Fatal("expected empty list for unregistered name")
}
}
func TestList_WithNames(t *testing.T) {
got := List("nonexistent", "also_nonexistent")
if len(got) != 0 {
t.Fatal("expected empty list for unregistered names")
}
}
+80 -150
View File
@@ -3,87 +3,54 @@ package node
import (
"fmt"
"net"
"regexp"
"strings"
"time"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/connector"
"github.com/go-gost/core/dialer"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/core/metadata/util"
xauth "github.com/go-gost/x/auth"
xbypass "github.com/go-gost/x/bypass"
xchain "github.com/go-gost/x/chain"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
auth_parser "github.com/go-gost/x/config/parsing/auth"
bypass_parser "github.com/go-gost/x/config/parsing/bypass"
tls_util "github.com/go-gost/x/internal/util/tls"
mdx "github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
"github.com/go-gost/x/registry"
"github.com/go-gost/x/routing"
)
// ParseNode converts a NodeConfig into a *chain.Node. It resolves the
// connector and dialer from their registries, applies TLS settings, extracts
// metadata-driven options (so_mark, interface, netns, proxy protocol), sets up
// bypass rules, node filters, HTTP settings, and TLS node settings. The hop
// parameter is used only for logging context.
func parseBodyRewrites(vs []config.HTTPBodyRewriteConfig, log logger.Logger) []chain.HTTPBodyRewriteSettings {
var out []chain.HTTPBodyRewriteSettings
for _, v := range vs {
pattern, _ := regexp.Compile(v.Match)
rw := chain.HTTPBodyRewriteSettings{
Type: v.Type,
Pattern: pattern,
Replacement: []byte(v.Replacement),
}
if v.Rewriter != "" {
if !registry.RewriterRegistry().IsRegistered(v.Rewriter) {
log.Warnf("rewriter %q not found in registry for rewrite rule", v.Rewriter)
}
rw.Rewriter = registry.RewriterRegistry().Get(v.Rewriter)
}
if pattern != nil || rw.Rewriter != nil {
out = append(out, rw)
}
}
return out
}
func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.Node, error) {
func ParseNode(hop string, cfg *config.NodeConfig) (*chain.Node, error) {
if cfg == nil {
return nil, nil
}
connCfg := cfg.Connector
if connCfg == nil {
connCfg = &config.ConnectorConfig{}
}
if connCfg.Type == "" {
connCfg.Type = "http"
if cfg.Connector == nil {
cfg.Connector = &config.ConnectorConfig{
Type: "http",
}
}
dialCfg := cfg.Dialer
if dialCfg == nil {
dialCfg = &config.DialerConfig{}
}
if dialCfg.Type == "" {
dialCfg.Type = "tcp"
if cfg.Dialer == nil {
cfg.Dialer = &config.DialerConfig{
Type: "tcp",
}
}
nodeLogger := log.WithFields(map[string]any{
nodeLogger := logger.Default().WithFields(map[string]any{
"hop": hop,
"kind": "node",
"node": cfg.Name,
"connector": connCfg.Type,
"dialer": dialCfg.Type,
"connector": cfg.Connector.Type,
"dialer": cfg.Dialer.Type,
})
serverName, _, _ := net.SplitHostPort(cfg.Addr)
tlsCfg := connCfg.TLS
tlsCfg := cfg.Connector.TLS
if tlsCfg == nil {
tlsCfg = &config.TLSConfig{}
}
@@ -96,26 +63,34 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
return nil, err
}
var nm metadata.Metadata
if cfg.Metadata != nil {
nm = mdx.NewMetadata(cfg.Metadata)
}
connectorLogger := nodeLogger.WithFields(map[string]any{
"kind": "connector",
})
var cr connector.Connector
if rf := registry.ConnectorRegistry().Get(connCfg.Type); rf != nil {
if rf := registry.ConnectorRegistry().Get(cfg.Connector.Type); rf != nil {
cr = rf(
connector.AuthOption(auth_parser.Info(connCfg.Auth)),
connector.AuthOption(auth_parser.Info(cfg.Connector.Auth)),
connector.TLSConfigOption(tlsConfig),
connector.LoggerOption(connectorLogger),
)
} else {
return nil, fmt.Errorf("unregistered connector: %s", connCfg.Type)
return nil, fmt.Errorf("unregistered connector: %s", cfg.Connector.Type)
}
if err := cr.Init(mdx.NewMetadata(connCfg.Metadata)); err != nil {
if cfg.Connector.Metadata == nil {
cfg.Connector.Metadata = make(map[string]any)
}
if err := cr.Init(mdx.NewMetadata(cfg.Connector.Metadata)); err != nil {
connectorLogger.Error("init: ", err)
return nil, err
}
tlsCfg = dialCfg.TLS
tlsCfg = cfg.Dialer.TLS
if tlsCfg == nil {
tlsCfg = &config.TLSConfig{}
}
@@ -128,133 +103,76 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
return nil, err
}
md := mdx.NewMetadata(cfg.Metadata)
var ppv int
if nm != nil {
ppv = mdutil.GetInt(nm, parsing.MDKeyProxyProtocol)
}
dialerLogger := nodeLogger.WithFields(map[string]any{
"kind": "dialer",
})
var d dialer.Dialer
if rf := registry.DialerRegistry().Get(dialCfg.Type); rf != nil {
if rf := registry.DialerRegistry().Get(cfg.Dialer.Type); rf != nil {
d = rf(
dialer.AuthOption(auth_parser.Info(dialCfg.Auth)),
dialer.AuthOption(auth_parser.Info(cfg.Dialer.Auth)),
dialer.TLSConfigOption(tlsConfig),
dialer.LoggerOption(dialerLogger),
dialer.ProxyProtocolOption(mdutil.GetInt(md, parsing.MDKeyProxyProtocol)),
dialer.ProxyProtocolOption(ppv),
)
} else {
return nil, fmt.Errorf("unregistered dialer: %s", dialCfg.Type)
return nil, fmt.Errorf("unregistered dialer: %s", cfg.Dialer.Type)
}
if err := d.Init(mdx.NewMetadata(dialCfg.Metadata)); err != nil {
if cfg.Dialer.Metadata == nil {
cfg.Dialer.Metadata = make(map[string]any)
}
if err := d.Init(mdx.NewMetadata(cfg.Dialer.Metadata)); err != nil {
dialerLogger.Error("init: ", err)
return nil, err
}
var sockOpts *chain.SockOpts
if v := mdutil.GetInt(md, parsing.MDKeySoMark); v != 0 {
if cfg.SockOpts != nil {
sockOpts = &chain.SockOpts{
Mark: v,
Mark: cfg.SockOpts.Mark,
}
}
tr := xchain.NewTransport(d, cr,
tr := chain.NewTransport(d, cr,
chain.AddrTransportOption(cfg.Addr),
chain.InterfaceTransportOption(mdutil.GetString(md, parsing.MDKeyInterface)),
chain.NetnsTransportOption(mdutil.GetString(md, parsing.MDKeyNetns)),
chain.InterfaceTransportOption(cfg.Interface),
chain.SockOptsTransportOption(sockOpts),
chain.TimeoutTransportOption(10*time.Second),
)
// convert *.example.com to .example.com
// convert *example.com to example.com
host := cfg.Host
if strings.HasPrefix(host, "*") {
host = host[1:]
if !strings.HasPrefix(host, ".") {
host = "." + host
}
}
opts := []chain.NodeOption{
chain.TransportNodeOption(tr),
chain.BypassNodeOption(xbypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
chain.ResolverNodeOption(registry.ResolverRegistry().Get(cfg.Resolver)),
chain.BypassNodeOption(bypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
chain.ResoloverNodeOption(registry.ResolverRegistry().Get(cfg.Resolver)),
chain.HostMapperNodeOption(registry.HostsRegistry().Get(cfg.Hosts)),
chain.MetadataNodeOption(md),
chain.MetadataNodeOption(nm),
chain.HostNodeOption(host),
chain.ProtocolNodeOption(cfg.Protocol),
chain.PathNodeOption(cfg.Path),
chain.NetworkNodeOption(cfg.Network),
}
if filter := cfg.Filter; filter != nil {
// convert *.example.com to .example.com
// convert *example.com to example.com
host := filter.Host
if strings.HasPrefix(host, "*") {
host = host[1:]
if !strings.HasPrefix(host, ".") {
host = "." + host
}
}
settings := &chain.NodeFilterSettings{
Protocol: filter.Protocol,
Host: host,
Path: filter.Path,
}
opts = append(opts, chain.NodeFilterOption(settings))
}
if cfg.Matcher != nil {
priority := cfg.Matcher.Priority
if rule := strings.TrimSpace(cfg.Matcher.Rule); rule != "" {
if matcher, err := routing.NewMatcher(rule); err == nil {
log.Debugf("new matcher for node %s with rule %s", cfg.Name, cfg.Matcher.Rule)
// Priority 0 means "use default": automatically set to the
// rule length so longer (more specific) rules outrank shorter
// ones. Use a negative priority to opt out of this behavior
// and always go through the selector.
if priority == 0 {
priority = len(cfg.Matcher.Rule)
}
opts = append(opts, chain.MatcherNodeOption(matcher))
} else {
log.Error(err)
priority = -1
}
}
opts = append(opts, chain.PriorityNodeOption(priority))
}
if cfg.HTTP != nil {
settings := &chain.HTTPNodeSettings{
Host: cfg.HTTP.Host,
RequestHeader: cfg.HTTP.RequestHeader,
ResponseHeader: cfg.HTTP.ResponseHeader,
}
if settings.RequestHeader == nil {
settings.RequestHeader = cfg.HTTP.Header
}
if auth := cfg.HTTP.Auth; auth != nil && auth.Username != "" {
settings.Auther = xauth.NewAuthenticator(
xauth.AuthsOption(map[string]string{auth.Username: auth.Password}),
xauth.LoggerOption(log.WithFields(map[string]any{
"kind": "node",
"node": cfg.Name,
"addr": cfg.Addr,
})),
)
}
rewriteURL := cfg.HTTP.RewriteURL
if rewriteURL == nil {
rewriteURL = cfg.HTTP.Rewrite
}
for _, v := range rewriteURL {
if pattern, _ := regexp.Compile(v.Match); pattern != nil {
settings.RewriteURL = append(settings.RewriteURL, chain.HTTPURLRewriteSetting{
Pattern: pattern,
Replacement: v.Replacement,
})
}
}
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteBody, log)...)
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteResponseBody, log)...)
settings.RewriteRequestBody = append(settings.RewriteRequestBody, parseBodyRewrites(cfg.HTTP.RewriteRequestBody, log)...)
opts = append(opts, chain.HTTPNodeOption(settings))
opts = append(opts, chain.HTTPNodeOption(&chain.HTTPNodeSettings{
Host: cfg.HTTP.Host,
Header: cfg.HTTP.Header,
}))
}
if cfg.TLS != nil {
tlsCfg := &chain.TLSNodeSettings{
ServerName: cfg.TLS.ServerName,
@@ -264,9 +182,21 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
tlsCfg.Options.MinVersion = o.MinVersion
tlsCfg.Options.MaxVersion = o.MaxVersion
tlsCfg.Options.CipherSuites = o.CipherSuites
tlsCfg.Options.ALPN = o.ALPN
}
opts = append(opts, chain.TLSNodeOption(tlsCfg))
}
if cfg.Auth != nil {
opts = append(opts, chain.AutherNodeOption(
xauth.NewAuthenticator(
xauth.AuthsOption(map[string]string{cfg.Auth.Username: cfg.Auth.Password}),
xauth.LoggerOption(logger.Default().WithFields(map[string]any{
"kind": "node",
"node": cfg.Name,
"addr": cfg.Addr,
"host": cfg.Host,
"protocol": cfg.Protocol,
})),
)))
}
return chain.NewNode(cfg.Name, cfg.Addr, opts...), nil
}
-461
View File
@@ -1,461 +0,0 @@
package node
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
xlogger "github.com/go-gost/x/logger"
// Register connector and dialer implementations needed for node parsing.
_ "github.com/go-gost/x/connector/http"
_ "github.com/go-gost/x/dialer/tcp"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func testLogger() logger.Logger {
return xlogger.NewLogger(xlogger.OutputOption(io.Discard))
}
func TestParseNode_Nil(t *testing.T) {
n, err := ParseNode("test-hop", nil, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseNode_Defaults(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "node1",
Addr: "example.com:8080",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_DefaultsEmptyName(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "",
Addr: "example.com:8080",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_DefaultsEmptyAddr(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "no-addr",
Addr: "",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_ExplicitConnector(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "explicit-conn",
Addr: "example.com:8080",
Connector: &config.ConnectorConfig{
Type: "http",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_UnregisteredConnector(t *testing.T) {
_, err := ParseNode("test-hop", &config.NodeConfig{
Name: "bad-conn",
Addr: "example.com:8080",
Connector: &config.ConnectorConfig{
Type: "nonexistent-connector-type",
},
}, testLogger())
if err == nil {
t.Fatal("expected error for unregistered connector type")
}
}
func TestParseNode_UnregisteredDialer(t *testing.T) {
_, err := ParseNode("test-hop", &config.NodeConfig{
Name: "bad-dial",
Addr: "example.com:8080",
Dialer: &config.DialerConfig{
Type: "nonexistent-dialer-type",
},
}, testLogger())
if err == nil {
t.Fatal("expected error for unregistered dialer type")
}
}
func TestParseNode_ExplicitDialer(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "explicit-dial",
Addr: "example.com:8080",
Dialer: &config.DialerConfig{
Type: "tcp",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithBypass(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "bypass-node",
Addr: "example.com:8080",
Bypass: "bp1",
Bypasses: []string{"bp2", "bp3"},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithResolver(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "resolver-node",
Addr: "example.com:8080",
Resolver: "my-resolver",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithHosts(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "hosts-node",
Addr: "example.com:8080",
Hosts: "my-hosts",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithNetwork(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "network-node",
Addr: "example.com:8080",
Network: "tcp",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithMetadata(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "meta-node",
Addr: "example.com:8080",
Metadata: map[string]any{
parsing.MDKeySoMark: 1234,
parsing.MDKeyInterface: "eth0",
parsing.MDKeyNetns: "myns",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithSockOpts(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "sockopts-node",
Addr: "example.com:8080",
SockOpts: &config.SockOptsConfig{
Mark: 9999,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithFilter(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "filter-node",
Addr: "example.com:8080",
Filter: &config.NodeFilterConfig{
Protocol: "http",
Host: "*.example.com",
Path: "/api",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithFilterStarBare(t *testing.T) {
// "*example.com" should become ".example.com"
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "star-bare-node",
Addr: "example.com:8080",
Filter: &config.NodeFilterConfig{
Host: "*example.com",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithMatcher(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "matcher-node",
Addr: "example.com:8080",
Matcher: &config.NodeMatcherConfig{
Rule: "dstport==80",
Priority: 10,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithMatcherAutoPriority(t *testing.T) {
// Priority auto-set to len(rule) when zero
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "auto-prio-node",
Addr: "example.com:8080",
Matcher: &config.NodeMatcherConfig{
Rule: "dstport==443",
Priority: 0,
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithHTTPNode(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "http-node",
Addr: "example.com:8080",
HTTP: &config.HTTPNodeConfig{
Host: "custom.example.com",
RequestHeader: map[string]string{"X-Custom": "value"},
ResponseHeader: map[string]string{"X-Response": "value"},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithHTTPAuth(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "http-auth-node",
Addr: "example.com:8080",
HTTP: &config.HTTPNodeConfig{
Auth: &config.AuthConfig{
Username: "user",
Password: "pass",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithHTTPDeprecatedHeader(t *testing.T) {
// Deprecated Header field should populate RequestHeader
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "http-deprecated-node",
Addr: "example.com:8080",
HTTP: &config.HTTPNodeConfig{
Header: map[string]string{"X-Old": "value"},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_WithTLSNode(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "tls-node",
Addr: "example.com:8080",
TLS: &config.TLSNodeConfig{
ServerName: "secure.example.com",
Secure: true,
Options: &config.TLSOptions{
MinVersion: "1.2",
MaxVersion: "1.3",
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
ALPN: []string{"h2", "http/1.1"},
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_TLSServerNameFromAddr(t *testing.T) {
// When connector/dialer TLS ServerName is empty, it defaults from addr hostname.
// The server name is set on the connector's TLS config (internal), so the best
// verification is that ParseNode does not error and returns a node.
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "tls-servername-node",
Addr: "tls.example.com:443",
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_ConnectorAuth(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "conn-auth-node",
Addr: "example.com:8080",
Connector: &config.ConnectorConfig{
Type: "http",
Auth: &config.AuthConfig{
Username: "connuser",
Password: "connpass",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_DialerAuth(t *testing.T) {
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "dial-auth-node",
Addr: "example.com:8080",
Dialer: &config.DialerConfig{
Type: "tcp",
Auth: &config.AuthConfig{
Username: "dialuser",
Password: "dialpass",
},
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
func TestParseNode_InvalidMatcherRule(t *testing.T) {
// An invalid matcher rule sets priority to -1
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "invalid-matcher-node",
Addr: "example.com:8080",
Matcher: &config.NodeMatcherConfig{
Rule: "",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node even with empty matcher rule")
}
}
func TestParseNode_DeprecatedFilterFields(t *testing.T) {
// Deprecated filter fields: Protocol/Host/Path via Filter
n, err := ParseNode("test-hop", &config.NodeConfig{
Name: "deprecated-node",
Addr: "example.com:8080",
Filter: &config.NodeFilterConfig{
Protocol: "socks5",
Host: "*.example.com",
Path: "/proxy",
},
}, testLogger())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n == nil {
t.Fatal("expected non-nil node")
}
}
-43
View File
@@ -1,43 +0,0 @@
package observer
import (
"crypto/tls"
"strings"
"github.com/go-gost/core/observer"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/plugin"
observer_plugin "github.com/go-gost/x/observer/plugin"
)
// ParseObserver converts an ObserverConfig into an observer.Observer. It only
// supports plugin backends (HTTP or gRPC); returns nil when cfg or cfg.Plugin
// is nil.
func ParseObserver(cfg *config.ObserverConfig) observer.Observer {
if cfg == nil || cfg.Plugin == nil {
return nil
}
var tlsCfg *tls.Config
if cfg.Plugin.TLS != nil {
tlsCfg = &tls.Config{
ServerName: cfg.Plugin.TLS.ServerName,
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
}
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return observer_plugin.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return observer_plugin.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
)
}
}
-75
View File
@@ -1,75 +0,0 @@
package observer
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseObserver_Nil(t *testing.T) {
obs := ParseObserver(nil)
if obs != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseObserver_NilPlugin(t *testing.T) {
obs := ParseObserver(&config.ObserverConfig{
Name: "no-plugin",
Plugin: nil,
})
if obs != nil {
t.Fatal("expected nil when plugin is nil")
}
}
func TestParseObserver_PluginHTTP(t *testing.T) {
obs := ParseObserver(&config.ObserverConfig{
Name: "http-obs",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if obs == nil {
t.Fatal("expected non-nil observer")
}
}
func TestParseObserver_PluginGRPC(t *testing.T) {
obs := ParseObserver(&config.ObserverConfig{
Name: "grpc-obs",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "observer.local",
},
},
})
if obs == nil {
t.Fatal("expected non-nil observer")
}
}
func TestParseObserver_PluginDefaultType(t *testing.T) {
obs := ParseObserver(&config.ObserverConfig{
Name: "default-obs",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if obs == nil {
t.Fatal("expected non-nil observer")
}
}
+10 -92
View File
@@ -1,99 +1,17 @@
// Package parsing converts user-facing configuration structs into registered
// runtime components. Each sub-package (service, hop, chain, auth, …) owns the
// parsing logic for its corresponding core interface.
//
// Metadata keys defined below bridge the gap between named config fields and
// generic key-value metadata. When a value is not represented by an explicit
// field on a config struct it flows through the metadata system instead and
// these keys are used to look it up at component Init time.
package parsing
const (
// MDKeyProxyProtocol sets the HAProxy proxy-protocol version on a listener.
MDKeyProxyProtocol = "proxyProtocol"
MDKeyInterface = "interface"
MDKeySoMark = "so_mark"
MDKeyHash = "hash"
MDKeyPreUp = "preUp"
MDKeyPreDown = "preDown"
MDKeyPostUp = "postUp"
MDKeyPostDown = "postDown"
MDKeyIgnoreChain = "ignoreChain"
// MDKeyInterface binds the outbound connection to a specific network interface.
MDKeyInterface = "interface"
// MDKeySoMark sets the SO_MARK socket option on outbound connections.
MDKeySoMark = "so_mark"
// MDKeyHash selects the hash source used by hash-based selectors.
MDKeyHash = "hash"
// MDKeyPreUp is a shell command executed when a service starts listening.
MDKeyPreUp = "preUp"
// MDKeyPreDown is a shell command executed immediately before a service
// stops listening.
MDKeyPreDown = "preDown"
// MDKeyPostUp is a shell command executed after a service has successfully
// started listening.
MDKeyPostUp = "postUp"
// MDKeyPostDown is a shell command executed after a service has stopped
// listening.
MDKeyPostDown = "postDown"
// MDKeyIgnoreChain disables chain routing for a service. When set the
// listener/handler will not prepend a chain, which is useful for services
// that handle routing themselves (e.g. DNS, TUN).
MDKeyIgnoreChain = "ignoreChain"
// MDKeyEnableStats enables per-service connection statistics collection.
MDKeyEnableStats = "enableStats"
// MDKeyRecorderDirection toggles whether traffic direction (client→server or
// server→client) is recorded alongside the payload.
MDKeyRecorderDirection = "direction"
// MDKeyRecorderTimestampFormat sets the timestamp layout used in recorder
// output (Go time.Format layout).
MDKeyRecorderDirection = "direction"
MDKeyRecorderTimestampFormat = "timeStampFormat"
// MDKeyRecorderHexdump enables hexadecimal dump output in recorders.
MDKeyRecorderHexdump = "hexdump"
// MDKeyRecorderHTTPBody enables capturing the HTTP request/response body in
// HTTP-based recorders.
MDKeyRecorderHTTPBody = "http.body"
// MDKeyRecorderHTTPMaxBodySize limits the maximum body size (in bytes) that
// an HTTP recorder will capture.
MDKeyRecorderHTTPMaxBodySize = "http.maxBodySize"
// MDKeyLimiterRefreshInterval sets how often a limiter reloads its
// configuration from an external source.
MDKeyLimiterRefreshInterval = "limiter.refreshInterval"
// MDKeyLimiterCleanupInterval sets how often a limiter purges expired
// entries from its internal state.
MDKeyLimiterCleanupInterval = "limiter.cleanupInterval"
// MDKeyLimiterScope controls whether a cached traffic limiter's scope is
// per-service ("service") or per-client ("client").
MDKeyLimiterScope = "limiter.scope"
// MDKeyObserverResetTraffic requests a traffic counter reset each time the
// observer reports.
MDKeyObserverResetTraffic = "observer.resetTraffic"
// MDKeyObserverPeriod sets the interval between observer reports.
MDKeyObserverPeriod = "observer.period"
// MDKeyNetns is the network namespace name or path used for the listener
// side (inbound).
MDKeyNetns = "netns"
// MDKeyNetnsOut is the network namespace name or path used for the handler
// (outbound) side.
MDKeyNetnsOut = "netns.out"
// MDKeyDialTimeout sets the dial timeout for outbound connections.
MDKeyDialTimeout = "dialTimeout"
// MDKeyLabels holds static key/value labels attached to a service's
// records and logs.
MDKeyLabels = "labels"
MDKeyRecorderHexdump = "hexdump"
)
-480
View File
@@ -1,480 +0,0 @@
package parser
import (
"bytes"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/go-gost/x/config"
)
func TestMergeConfig_NilBoth(t *testing.T) {
got := mergeConfig(nil, nil)
if got != nil {
t.Fatal("expected nil for two nil inputs")
}
}
func TestMergeConfig_NilFirst(t *testing.T) {
cfg2 := &config.Config{
Services: []*config.ServiceConfig{{Name: "svc"}},
}
got := mergeConfig(nil, cfg2)
if len(got.Services) != 1 || got.Services[0].Name != "svc" {
t.Fatal("expected cfg2 to be returned when cfg1 is nil")
}
}
func TestMergeConfig_NilSecond(t *testing.T) {
cfg1 := &config.Config{
Services: []*config.ServiceConfig{{Name: "svc"}},
}
got := mergeConfig(cfg1, nil)
if len(got.Services) != 1 || got.Services[0].Name != "svc" {
t.Fatal("expected cfg1 to be returned when cfg2 is nil")
}
}
func TestMergeConfig_AppendsAllSlices(t *testing.T) {
cfg1 := &config.Config{
Services: []*config.ServiceConfig{{Name: "s1"}},
Chains: []*config.ChainConfig{{Name: "c1"}},
Hops: []*config.HopConfig{{Name: "h1"}},
Authers: []*config.AutherConfig{{Name: "a1"}},
Admissions: []*config.AdmissionConfig{{Name: "ad1"}},
Bypasses: []*config.BypassConfig{{Name: "b1"}},
Resolvers: []*config.ResolverConfig{{Name: "r1"}},
Hosts: []*config.HostsConfig{{Name: "hosts1"}},
Ingresses: []*config.IngressConfig{{Name: "i1"}},
SDs: []*config.SDConfig{{Name: "sd1"}},
Recorders: []*config.RecorderConfig{{Name: "rec1"}},
Limiters: []*config.LimiterConfig{{Name: "l1"}},
Quotas: []*config.QuotaConfig{{Name: "q1"}},
CLimiters: []*config.LimiterConfig{{Name: "cl1"}},
RLimiters: []*config.LimiterConfig{{Name: "rl1"}},
Loggers: []*config.LoggerConfig{{Name: "log1"}},
Routers: []*config.RouterConfig{{Name: "rt1"}},
Observers: []*config.ObserverConfig{{Name: "obs1"}},
}
cfg2 := &config.Config{
Services: []*config.ServiceConfig{{Name: "s2"}},
Chains: []*config.ChainConfig{{Name: "c2"}},
Hops: []*config.HopConfig{{Name: "h2"}},
Authers: []*config.AutherConfig{{Name: "a2"}},
Admissions: []*config.AdmissionConfig{{Name: "ad2"}},
Bypasses: []*config.BypassConfig{{Name: "b2"}},
Resolvers: []*config.ResolverConfig{{Name: "r2"}},
Hosts: []*config.HostsConfig{{Name: "hosts2"}},
Ingresses: []*config.IngressConfig{{Name: "i2"}},
SDs: []*config.SDConfig{{Name: "sd2"}},
Recorders: []*config.RecorderConfig{{Name: "rec2"}},
Limiters: []*config.LimiterConfig{{Name: "l2"}},
Quotas: []*config.QuotaConfig{{Name: "q2"}},
CLimiters: []*config.LimiterConfig{{Name: "cl2"}},
RLimiters: []*config.LimiterConfig{{Name: "rl2"}},
Loggers: []*config.LoggerConfig{{Name: "log2"}},
Routers: []*config.RouterConfig{{Name: "rt2"}},
Observers: []*config.ObserverConfig{{Name: "obs2"}},
}
got := mergeConfig(cfg1, cfg2)
if len(got.Services) != 2 || got.Services[0].Name != "s1" || got.Services[1].Name != "s2" {
t.Fatal("services not properly appended")
}
if len(got.Chains) != 2 {
t.Fatal("chains not properly appended")
}
if len(got.Hops) != 2 {
t.Fatal("hops not properly appended")
}
if len(got.Authers) != 2 {
t.Fatal("authers not properly appended")
}
if len(got.Admissions) != 2 {
t.Fatal("admissions not properly appended")
}
if len(got.Bypasses) != 2 {
t.Fatal("bypasses not properly appended")
}
if len(got.Resolvers) != 2 {
t.Fatal("resolvers not properly appended")
}
if len(got.Hosts) != 2 {
t.Fatal("hosts not properly appended")
}
if len(got.Ingresses) != 2 {
t.Fatal("ingresses not properly appended")
}
if len(got.SDs) != 2 {
t.Fatal("SDs not properly appended")
}
if len(got.Recorders) != 2 {
t.Fatal("recorders not properly appended")
}
if len(got.Limiters) != 2 {
t.Fatal("limiters not properly appended")
}
if len(got.Quotas) != 2 {
t.Fatal("quotas not properly appended")
}
if len(got.CLimiters) != 2 {
t.Fatal("climiters not properly appended")
}
if len(got.RLimiters) != 2 {
t.Fatal("rlimiters not properly appended")
}
if len(got.Loggers) != 2 {
t.Fatal("loggers not properly appended")
}
if len(got.Routers) != 2 {
t.Fatal("routers not properly appended")
}
if len(got.Observers) != 2 {
t.Fatal("observers not properly appended")
}
}
func TestMergeConfig_ScalarOverrides(t *testing.T) {
cfg1 := &config.Config{
TLS: &config.TLSConfig{ServerName: "old"},
Log: &config.LogConfig{Level: "info"},
API: &config.APIConfig{Addr: ":8080"},
Metrics: &config.MetricsConfig{Addr: ":9090"},
Profiling: &config.ProfilingConfig{Addr: ":6060"},
}
cfg2 := &config.Config{
TLS: &config.TLSConfig{ServerName: "new"},
Log: &config.LogConfig{Level: "debug"},
API: &config.APIConfig{Addr: ":8081"},
Metrics: &config.MetricsConfig{Addr: ":9091"},
Profiling: &config.ProfilingConfig{Addr: ":6061"},
}
got := mergeConfig(cfg1, cfg2)
if got.TLS.ServerName != "new" {
t.Fatalf("TLS not overridden: got %q, want %q", got.TLS.ServerName, "new")
}
if got.Log.Level != "debug" {
t.Fatalf("Log not overridden: got %q, want %q", got.Log.Level, "debug")
}
if got.API.Addr != ":8081" {
t.Fatalf("API not overridden: got %q, want %q", got.API.Addr, ":8081")
}
if got.Metrics.Addr != ":9091" {
t.Fatalf("Metrics not overridden: got %q, want %q", got.Metrics.Addr, ":9091")
}
if got.Profiling.Addr != ":6061" {
t.Fatalf("Profiling not overridden: got %q, want %q", got.Profiling.Addr, ":6061")
}
}
func TestMergeConfig_ScalarKeepsCfg1WhenCfg2Nil(t *testing.T) {
cfg1 := &config.Config{
TLS: &config.TLSConfig{ServerName: "keep"},
Log: &config.LogConfig{Level: "warn"},
API: &config.APIConfig{Addr: ":8080"},
Metrics: &config.MetricsConfig{Addr: ":9090"},
Profiling: &config.ProfilingConfig{Addr: ":6060"},
}
cfg2 := &config.Config{} // all scalars nil
got := mergeConfig(cfg1, cfg2)
if got.TLS.ServerName != "keep" {
t.Fatal("TLS should be kept from cfg1")
}
if got.Log.Level != "warn" {
t.Fatal("Log should be kept from cfg1")
}
if got.API.Addr != ":8080" {
t.Fatal("API should be kept from cfg1")
}
if got.Metrics.Addr != ":9090" {
t.Fatal("Metrics should be kept from cfg1")
}
if got.Profiling.Addr != ":6060" {
t.Fatal("Profiling should be kept from cfg1")
}
}
func TestInit(t *testing.T) {
Init(Args{
CfgFiles: []string{"test.yml"},
Services: []string{"s1"},
Nodes: []string{"n1"},
Debug: true,
Trace: false,
ApiAddr: ":8080",
MetricsAddr: ":9090",
})
if len(defaultParser.args.CfgFiles) != 1 || defaultParser.args.CfgFiles[0] != "test.yml" {
t.Fatalf("CfgFiles not set: got %v", defaultParser.args.CfgFiles)
}
if len(defaultParser.args.Services) != 1 || defaultParser.args.Services[0] != "s1" {
t.Fatal("Services not set")
}
if len(defaultParser.args.Nodes) != 1 || defaultParser.args.Nodes[0] != "n1" {
t.Fatal("Nodes not set")
}
if !defaultParser.args.Debug {
t.Fatal("Debug not set")
}
if defaultParser.args.Trace {
t.Fatal("Trace should be false")
}
if defaultParser.args.ApiAddr != ":8080" {
t.Fatal("ApiAddr not set")
}
if defaultParser.args.MetricsAddr != ":9090" {
t.Fatal("MetricsAddr not set")
}
}
func TestParse_MultiFile(t *testing.T) {
// Create two temporary config files.
cfg1 := &config.Config{
Services: []*config.ServiceConfig{{Name: "s1", Addr: ":8080"}},
}
cfg2 := &config.Config{
Services: []*config.ServiceConfig{{Name: "s2", Addr: ":8081"}},
Log: &config.LogConfig{Level: "debug"},
Quotas: []*config.QuotaConfig{{Name: "q1"}},
}
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "cfg1.yml")
file2 := filepath.Join(tmpDir, "cfg2.yml")
for _, entry := range []struct {
path string
cfg *config.Config
}{
{file1, cfg1},
{file2, cfg2},
} {
f, err := os.Create(entry.path)
if err != nil {
t.Fatal(err)
}
if err := entry.cfg.Write(f, "yaml"); err != nil {
f.Close()
t.Fatal(err)
}
f.Close()
}
Init(Args{
CfgFiles: []string{file1, file2},
})
got, err := Parse()
if err != nil {
t.Fatalf("Parse(): %v", err)
}
if len(got.Services) != 2 {
t.Fatalf("expected 2 services, got %d", len(got.Services))
}
if got.Log == nil || got.Log.Level != "debug" {
t.Fatal("Log.Level not set from cfg2")
}
if len(got.Quotas) != 1 || got.Quotas[0].Name != "q1" {
t.Fatal("Quotas not merged from cfg2")
}
}
func TestIsHTTPURL(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"http://example.com/config.yaml", true},
{"https://example.com/config.json", true},
{"ftp://example.com/config.yaml", false},
{"config.yaml", false},
{"/path/to/config.yml", false},
{"http://[::1]:8080/cfg", true},
}
for _, tt := range tests {
if got := isHTTPURL(tt.input); got != tt.expected {
t.Errorf("isHTTPURL(%q) = %v, want %v", tt.input, got, tt.expected)
}
}
}
func TestDetectFormat(t *testing.T) {
tests := []struct {
contentType string
url string
want string
}{
{"", "http://example.com/config.json", "json"},
{"", "http://example.com/config.yml", "yaml"},
{"", "http://example.com/config.yaml", "yaml"},
{"", "http://example.com/config", "yaml"}, // default
{"application/json", "http://example.com/config", "json"},
{"application/yaml", "http://example.com/config", "yaml"},
{"application/x-yaml", "http://example.com/config", "yaml"},
{"text/yaml", "http://example.com/config", "yaml"},
{"text/x-yaml", "http://example.com/config", "yaml"},
{"text/plain; charset=utf-8", "http://example.com/config.json", "json"}, // content-type ignored, extension wins
{"text/plain", "http://example.com/config", "yaml"}, // unknown -> default
}
for _, tt := range tests {
if got := detectFormat(tt.contentType, tt.url); got != tt.want {
t.Errorf("detectFormat(%q, %q) = %q, want %q", tt.contentType, tt.url, got, tt.want)
}
}
}
func TestSanitizeURL(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"http://user:pass@example.com/path", "http://example.com/path"},
{"https://example.com/path", "https://example.com/path"},
{"not a url", "not%20a%20url"},
{"http://user@example.com/path", "http://example.com/path"}, // user with no password
}
for _, tt := range tests {
if got := sanitizeURL(tt.input); got != tt.expected {
t.Errorf("sanitizeURL(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
func TestReadConfigFromURL_YAML(t *testing.T) {
// Write a YAML config and serve it via httptest.
cfg := &config.Config{
Services: []*config.ServiceConfig{{Name: "svc", Addr: ":8080"}},
Log: &config.LogConfig{Level: "debug"},
}
var buf bytes.Buffer
if err := cfg.Write(&buf, "yaml"); err != nil {
t.Fatal(err)
}
body := buf.Bytes()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/yaml")
w.Write(body)
}))
defer ts.Close()
got := &config.Config{}
if err := readConfigFromURL(ts.URL, got); err != nil {
t.Fatalf("readConfigFromURL: %v", err)
}
if len(got.Services) != 1 || got.Services[0].Name != "svc" {
t.Fatalf("unexpected services: %+v", got.Services)
}
if got.Log == nil || got.Log.Level != "debug" {
t.Fatalf("unexpected log: %+v", got.Log)
}
}
func TestReadConfigFromURL_JSON(t *testing.T) {
// Remote serving JSON with explicit Content-Type.
cfg := &config.Config{
Services: []*config.ServiceConfig{{Name: "json-svc", Addr: ":9090"}},
}
var buf bytes.Buffer
if err := cfg.Write(&buf, "json"); err != nil {
t.Fatal(err)
}
body := buf.Bytes()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}))
defer ts.Close()
got := &config.Config{}
if err := readConfigFromURL(ts.URL, got); err != nil {
t.Fatalf("readConfigFromURL: %v", err)
}
if got.Services[0].Name != "json-svc" {
t.Fatalf("unexpected service name: %s", got.Services[0].Name)
}
}
func TestReadConfigFromURL_Non200(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
err := readConfigFromURL(ts.URL, &config.Config{})
if err == nil {
t.Fatal("expected error for 404 response")
}
}
func TestReadConfigFromURL_OverSize(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/yaml")
w.Write(make([]byte, maxConfigSize+1))
}))
defer ts.Close()
err := readConfigFromURL(ts.URL, &config.Config{})
if err == nil {
t.Fatal("expected error for oversized response")
}
}
func TestParse_FileAndURL(t *testing.T) {
// Create a local file.
fileCfg := &config.Config{
Services: []*config.ServiceConfig{{Name: "file-svc", Addr: ":8080"}},
}
tmpDir := t.TempDir()
f, err := os.Create(filepath.Join(tmpDir, "local.yml"))
if err != nil {
t.Fatal(err)
}
if err := fileCfg.Write(f, "yaml"); err != nil {
f.Close()
t.Fatal(err)
}
f.Close()
// Serve a second config over HTTP.
urlCfg := &config.Config{
Services: []*config.ServiceConfig{{Name: "url-svc", Addr: ":8081"}},
Log: &config.LogConfig{Level: "debug"},
}
var buf bytes.Buffer
if err := urlCfg.Write(&buf, "yaml"); err != nil {
t.Fatal(err)
}
body := buf.Bytes()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/yaml")
w.Write(body)
}))
defer ts.Close()
Init(Args{
CfgFiles: []string{filepath.Join(tmpDir, "local.yml"), ts.URL},
})
got, err := Parse()
if err != nil {
t.Fatalf("Parse(): %v", err)
}
if len(got.Services) != 2 {
t.Fatalf("expected 2 services, got %d", len(got.Services))
}
if got.Log == nil || got.Log.Level != "debug" {
t.Fatal("Log.Level not set from URL config")
}
}
-361
View File
@@ -1,361 +0,0 @@
package parser
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/cmd"
xmd "github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
)
var (
defaultParser = &parser{}
)
// Init stores the parsed CLI arguments so that subsequent calls to Parse can
// merge them with config-file and environment-variable sources.
func Init(args Args) {
defaultParser = &parser{
args: args,
}
}
// Parse reads configuration from the sources specified during Init (config
// file, CLI services/nodes, and environment variables) and returns a merged
// Config ready for loading.
func Parse() (*config.Config, error) {
return defaultParser.Parse()
}
// Args holds the raw CLI flags and positional arguments that Init will merge
// into the final configuration.
type Args struct {
// CfgFiles is the list of YAML or JSON config sources: file paths,
// HTTP(S) URLs, "-" for stdin, or an inline JSON string.
// Multiple sources are merged left-to-right.
CfgFiles []string
// Services is the list of service definitions from -L flags.
Services []string
// Nodes is the list of chain-node definitions from -F flags.
Nodes []string
// Debug toggles debug-level logging.
Debug bool
// Trace toggles trace-level logging (supersedes Debug).
Trace bool
// ApiAddr is the address for the HTTP API server.
ApiAddr string
// MetricsAddr is the address for the Prometheus metrics endpoint.
MetricsAddr string
}
type parser struct {
args Args
}
func (p *parser) Parse() (*config.Config, error) {
cfg := &config.Config{}
for _, cfgFile := range p.args.CfgFiles {
cfgFile = strings.TrimSpace(cfgFile)
if cfgFile == "" {
continue
}
fcfg, err := readConfig(cfgFile)
if err != nil {
return nil, err
}
cfg = mergeConfig(cfg, fcfg)
}
cmdCfg, err := cmd.BuildConfigFromCmd(p.args.Services, p.args.Nodes)
if err != nil {
return nil, err
}
cfg = mergeConfig(cfg, cmdCfg)
if len(cfg.Services) == 0 && p.args.ApiAddr == "" && cfg.API == nil &&
len(p.args.CfgFiles) == 0 {
if err := cfg.Load(); err != nil {
return nil, err
}
}
if v := os.Getenv("GOST_LOGGER_LEVEL"); v != "" {
if cfg.Log == nil {
cfg.Log = &config.LogConfig{}
}
cfg.Log.Level = v
}
if v := os.Getenv("GOST_API"); v != "" {
if cfg.API == nil {
cfg.API = &config.APIConfig{}
}
cfg.API.Addr = v
}
if v := os.Getenv("GOST_METRICS"); v != "" {
if cfg.Metrics == nil {
cfg.Metrics = &config.MetricsConfig{}
}
cfg.Metrics.Addr = v
}
if v := os.Getenv("GOST_PROFILING"); v != "" {
if cfg.Profiling == nil {
cfg.Profiling = &config.ProfilingConfig{}
}
cfg.Profiling.Addr = v
}
if p.args.Debug || p.args.Trace {
if cfg.Log == nil {
cfg.Log = &config.LogConfig{}
}
cfg.Log.Level = string(logger.DebugLevel)
if p.args.Trace {
cfg.Log.Level = string(logger.TraceLevel)
}
}
if p.args.ApiAddr != "" {
cfg.API = &config.APIConfig{
Addr: p.args.ApiAddr,
}
if url, _ := cmd.Norm(p.args.ApiAddr); url != nil {
cfg.API.Addr = url.Host
if url.User != nil {
username := url.User.Username()
password, _ := url.User.Password()
cfg.API.Auth = &config.AuthConfig{
Username: username,
Password: password,
}
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := xmd.NewMetadata(m)
cfg.API.PathPrefix = mdutil.GetString(md, "pathPrefix")
cfg.API.AccessLog = mdutil.GetBool(md, "accesslog")
}
}
if p.args.MetricsAddr != "" {
cfg.Metrics = &config.MetricsConfig{
Addr: p.args.MetricsAddr,
}
if url, _ := cmd.Norm(p.args.MetricsAddr); url != nil {
cfg.Metrics.Addr = url.Host
if url.User != nil {
username := url.User.Username()
password, _ := url.User.Password()
cfg.Metrics.Auth = &config.AuthConfig{
Username: username,
Password: password,
}
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := xmd.NewMetadata(m)
cfg.Metrics.Path = mdutil.GetString(md, "path")
}
}
return cfg, nil
}
// readConfig reads a single configuration source, which may be "-" (stdin),
// an HTTP(S) URL, an inline JSON string starting with "{", or a file path.
func readConfig(cfgFile string) (*config.Config, error) {
cfg := &config.Config{}
if cfgFile == "-" { // stdin
br := bufio.NewReader(os.Stdin)
b, err := br.Peek(1)
if err != nil {
return nil, err
}
if b[0] == '{' {
if err := cfg.Read(br, "json"); err != nil {
return nil, err
}
} else {
if err := cfg.Read(br, "yaml"); err != nil {
return nil, err
}
}
} else if strings.HasPrefix(cfgFile, "{") && strings.HasSuffix(cfgFile, "}") { // inline
if err := json.Unmarshal([]byte(cfgFile), cfg); err != nil {
return nil, err
}
} else if isHTTPURL(cfgFile) { // URL
if err := readConfigFromURL(cfgFile, cfg); err != nil {
return nil, err
}
} else {
if err := cfg.ReadFile(cfgFile); err != nil { // file
return nil, err
}
}
return cfg, nil
}
// isHTTPURL reports whether s is an HTTP or HTTPS URL.
func isHTTPURL(s string) bool {
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != "" &&
(u.Scheme == "http" || u.Scheme == "https")
}
// maxConfigSize is the maximum response body size for remote config files.
const maxConfigSize = 10 << 20 // 10 MiB
// sharedHTTPClient is reused across config URL fetches to benefit from
// connection pooling.
var sharedHTTPClient = &http.Client{
Timeout: 30 * time.Second,
}
// readConfigFromURL fetches a configuration from an HTTP(S) URL and reads it
// into cfg. The format (yaml or json) is detected from the Content-Type header
// or the URL path extension.
func readConfigFromURL(urlStr string, cfg *config.Config) error {
resp, err := sharedHTTPClient.Get(urlStr)
if err != nil {
return fmt.Errorf("fetch config from %s: %w", sanitizeURL(urlStr), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body) // drain body for connection reuse; ignore error since we are already failing
return fmt.Errorf("fetch config from %s: %s", sanitizeURL(urlStr), resp.Status)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, maxConfigSize+1))
if err != nil {
return fmt.Errorf("read config from %s: %w", sanitizeURL(urlStr), err)
}
if len(data) > maxConfigSize {
return fmt.Errorf("config from %s exceeds maximum size %d", sanitizeURL(urlStr), maxConfigSize)
}
format := detectFormat(resp.Header.Get("Content-Type"), urlStr)
if err := cfg.Read(bytes.NewReader(data), format); err != nil {
return fmt.Errorf("parse config from %s: %w", sanitizeURL(urlStr), err)
}
return nil
}
// sanitizeURL returns the URL with any embedded userinfo stripped for safe
// use in error messages.
func sanitizeURL(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return rawURL
}
u.User = nil
return u.String()
}
// detectFormat determines the config format from the HTTP Content-Type header
// or the URL path extension. Returns "yaml" or "json".
func detectFormat(contentType, urlStr string) string {
if mediatype, _, err := mime.ParseMediaType(contentType); err == nil {
switch mediatype {
case "application/json":
return "json"
case "application/yaml", "application/x-yaml",
"text/yaml", "text/x-yaml":
return "yaml"
}
}
if u, err := url.Parse(urlStr); err == nil {
switch strings.ToLower(strings.TrimPrefix(path.Ext(u.Path), ".")) {
case "json":
return "json"
case "yaml", "yml":
return "yaml"
}
}
return "yaml"
}
func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
if cfg1 == nil {
return cfg2
}
if cfg2 == nil {
return cfg1
}
cfg := &config.Config{
Services: append(cfg1.Services, cfg2.Services...),
Chains: append(cfg1.Chains, cfg2.Chains...),
Hops: append(cfg1.Hops, cfg2.Hops...),
Authers: append(cfg1.Authers, cfg2.Authers...),
Admissions: append(cfg1.Admissions, cfg2.Admissions...),
Bypasses: append(cfg1.Bypasses, cfg2.Bypasses...),
Resolvers: append(cfg1.Resolvers, cfg2.Resolvers...),
Hosts: append(cfg1.Hosts, cfg2.Hosts...),
Ingresses: append(cfg1.Ingresses, cfg2.Ingresses...),
SDs: append(cfg1.SDs, cfg2.SDs...),
Recorders: append(cfg1.Recorders, cfg2.Recorders...),
Limiters: append(cfg1.Limiters, cfg2.Limiters...),
Quotas: append(cfg1.Quotas, cfg2.Quotas...),
CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...),
RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...),
Loggers: append(cfg1.Loggers, cfg2.Loggers...),
Routers: append(cfg1.Routers, cfg2.Routers...),
Observers: append(cfg1.Observers, cfg2.Observers...),
TLS: cfg1.TLS,
Log: cfg1.Log,
API: cfg1.API,
Metrics: cfg1.Metrics,
Profiling: cfg1.Profiling,
}
if cfg2.TLS != nil {
cfg.TLS = cfg2.TLS
}
if cfg2.Log != nil {
cfg.Log = cfg2.Log
}
if cfg2.API != nil {
cfg.API = cfg2.API
}
if cfg2.Metrics != nil {
cfg.Metrics = cfg2.Metrics
}
if cfg2.Profiling != nil {
cfg.Profiling = cfg2.Profiling
}
return cfg
}
-104
View File
@@ -1,104 +0,0 @@
package quota
import (
"strings"
"time"
"github.com/alecthomas/units"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xquota "github.com/go-gost/x/limiter/quota"
)
// ParseQuotaLimiter is best-effort: malformed fields are logged and treated as unset.
func ParseQuotaLimiter(cfg *config.QuotaConfig) *xquota.Limiter {
if cfg == nil {
return nil
}
log := logger.Default().WithFields(map[string]any{
"kind": "quota",
"quota": cfg.Name,
})
var limit uint64
if s := strings.TrimSpace(cfg.Limit); s != "" && s != "0" {
if v, err := units.ParseBase2Bytes(s); err != nil {
log.Warnf("quota: parse limit %q: %v", s, err)
} else if v > 0 {
limit = uint64(v)
}
}
var dir xquota.Direction
switch strings.ToLower(strings.TrimSpace(cfg.Direction)) {
case "in":
dir = xquota.DirectionIn
case "out":
dir = xquota.DirectionOut
default:
dir = xquota.DirectionTotal
}
var flush time.Duration
if s := strings.TrimSpace(cfg.Flush); s != "" {
if d, err := time.ParseDuration(s); err != nil {
log.Warnf("quota: parse flush %q: %v", s, err)
} else {
flush = d
}
}
return xquota.NewLimiter(cfg.Name, xquota.Options{
Limit: limit,
StartsAt: parseQuotaTime(cfg.StartsAt, log, "startsAt"),
ExpiresAt: parseQuotaTime(cfg.ExpiresAt, log, "expiresAt"),
Direction: dir,
Flush: flush,
Store: parseQuotaStore(cfg.Store, log),
Logger: log,
})
}
func parseQuotaTime(s string, log logger.Logger, field string) time.Time {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}
}
t, err := time.Parse(time.RFC3339, s)
if err != nil {
log.Warnf("quota: parse %s %q: %v", field, s, err)
return time.Time{}
}
return t
}
func parseQuotaStore(sc *config.QuotaStoreConfig, log logger.Logger) xquota.Store {
const defaultFile = "gost-quota.json"
if sc == nil || sc.Type == "" || strings.EqualFold(sc.Type, "file") {
path := defaultFile
if sc != nil && strings.TrimSpace(sc.File) != "" {
path = sc.File
}
return xquota.NewFileStore(path)
}
if strings.EqualFold(sc.Type, "redis") {
log.Warnf("quota: redis store not implemented; counter will not be persisted")
var rc config.QuotaRedisConfig
if sc.Redis != nil {
rc = *sc.Redis
}
return xquota.NewRedisStore(xquota.RedisConfig{
Addr: rc.Addr,
Username: rc.Username,
Password: rc.Password,
DB: rc.DB,
Key: rc.Key,
})
}
log.Warnf("quota: unknown store type %q; using file store %q", sc.Type, defaultFile)
return xquota.NewFileStore(defaultFile)
}
+6 -63
View File
@@ -2,31 +2,14 @@ package recorder
import (
"crypto/tls"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/recorder"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/plugin"
xrecorder "github.com/go-gost/x/recorder"
recorder_plugin "github.com/go-gost/x/recorder/plugin"
"gopkg.in/natefinch/lumberjack.v2"
)
type discardCloser struct{}
func (discardCloser) Write(p []byte) (n int, err error) { return len(p), nil }
func (discardCloser) Close() error { return nil }
// ParseRecorder converts a RecorderConfig into a recorder.Recorder. It
// supports plugin backends (HTTP or gRPC), file output (with optional
// rotation), TCP syslog-style delivery, HTTP POST delivery, and Redis storage
// (set, list, or sorted set). Returns nil when cfg is nil or no backend is
// configured.
func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
if cfg == nil {
return nil
@@ -42,14 +25,13 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return recorder_plugin.NewHTTPPlugin(
return xrecorder.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return recorder_plugin.NewGRPCPlugin(
return xrecorder.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
@@ -58,50 +40,17 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
}
if cfg.File != nil && cfg.File.Path != "" {
var out io.WriteCloser = discardCloser{}
if cfg.File.Rotation != nil {
out = &lumberjack.Logger{
Filename: cfg.File.Path,
MaxSize: cfg.File.Rotation.MaxSize,
MaxAge: cfg.File.Rotation.MaxAge,
MaxBackups: cfg.File.Rotation.MaxBackups,
LocalTime: cfg.File.Rotation.LocalTime,
Compress: cfg.File.Rotation.Compress,
}
} else {
os.MkdirAll(filepath.Dir(cfg.File.Path), 0755)
f, err := os.OpenFile(cfg.File.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Default().Warn(err)
} else {
out = f
}
}
return xrecorder.FileRecorder(out,
xrecorder.RecorderFileRecorderOption(cfg.Name),
xrecorder.SepFileRecorderOption(cfg.File.Sep),
return xrecorder.FileRecorder(cfg.File.Path,
xrecorder.SepRecorderOption(cfg.File.Sep),
)
}
if cfg.TCP != nil && cfg.TCP.Addr != "" {
return xrecorder.TCPRecorder(cfg.TCP.Addr,
xrecorder.RecorderTCPRecorderOption(cfg.Name),
xrecorder.TimeoutTCPRecorderOption(cfg.TCP.Timeout),
)
return xrecorder.TCPRecorder(cfg.TCP.Addr, xrecorder.TimeoutTCPRecorderOption(cfg.TCP.Timeout))
}
if cfg.HTTP != nil && cfg.HTTP.URL != "" {
h := http.Header{}
for k, v := range cfg.HTTP.Header {
h.Add(k, v)
}
return xrecorder.HTTPRecorder(cfg.HTTP.URL,
xrecorder.RecorderHTTPRecorderOption(cfg.Name),
xrecorder.TimeoutHTTPRecorderOption(cfg.HTTP.Timeout),
xrecorder.HeaderHTTPRecorderOption(h),
)
return xrecorder.HTTPRecorder(cfg.HTTP.URL, xrecorder.TimeoutHTTPRecorderOption(cfg.HTTP.Timeout))
}
if cfg.Redis != nil &&
@@ -110,26 +59,20 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
switch cfg.Redis.Type {
case "list": // redis list
return xrecorder.RedisListRecorder(cfg.Redis.Addr,
xrecorder.RecorderRedisRecorderOption(cfg.Name),
xrecorder.DBRedisRecorderOption(cfg.Redis.DB),
xrecorder.KeyRedisRecorderOption(cfg.Redis.Key),
xrecorder.UsernameRedisRecorderOption(cfg.Redis.Username),
xrecorder.PasswordRedisRecorderOption(cfg.Redis.Password),
)
case "sset": // sorted set
return xrecorder.RedisSortedSetRecorder(cfg.Redis.Addr,
xrecorder.RecorderRedisRecorderOption(cfg.Name),
xrecorder.DBRedisRecorderOption(cfg.Redis.DB),
xrecorder.KeyRedisRecorderOption(cfg.Redis.Key),
xrecorder.UsernameRedisRecorderOption(cfg.Redis.Username),
xrecorder.PasswordRedisRecorderOption(cfg.Redis.Password),
)
default: // redis set
return xrecorder.RedisSetRecorder(cfg.Redis.Addr,
xrecorder.RecorderRedisRecorderOption(cfg.Name),
xrecorder.DBRedisRecorderOption(cfg.Redis.DB),
xrecorder.KeyRedisRecorderOption(cfg.Redis.Key),
xrecorder.UsernameRedisRecorderOption(cfg.Redis.Username),
xrecorder.PasswordRedisRecorderOption(cfg.Redis.Password),
)
}
-199
View File
@@ -1,199 +0,0 @@
package recorder
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseRecorder_Nil(t *testing.T) {
r := ParseRecorder(nil)
if r != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseRecorder_PluginHTTP(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "http-rec",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if r == nil {
t.Fatal("expected non-nil recorder")
}
}
func TestParseRecorder_PluginGRPC(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "grpc-rec",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "recorder.local",
},
},
})
if r == nil {
t.Fatal("expected non-nil recorder")
}
}
func TestParseRecorder_PluginDefaultType(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "default-rec",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if r == nil {
t.Fatal("expected non-nil recorder")
}
}
func TestParseRecorder_TCP(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "tcp-rec",
TCP: &config.TCPRecorder{
Addr: "127.0.0.1:9999",
Timeout: 0,
},
})
if r == nil {
t.Fatal("expected non-nil recorder for TCP")
}
}
func TestParseRecorder_HTTP(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "http-rec",
HTTP: &config.HTTPRecorder{
URL: "http://localhost:8080/record",
Timeout: 0,
Header: map[string]string{"X-Test": "value"},
},
})
if r == nil {
t.Fatal("expected non-nil recorder for HTTP")
}
}
func TestParseRecorder_File(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "file-rec",
File: &config.FileRecorder{
Path: "/tmp/recorder.log",
Sep: "\n",
},
})
if r == nil {
t.Fatal("expected non-nil recorder for file")
}
}
func TestParseRecorder_FileWithRotation(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "file-rot-rec",
File: &config.FileRecorder{
Path: "/tmp/recorder-rot.log",
Rotation: &config.LogRotationConfig{
MaxSize: 100,
MaxAge: 30,
MaxBackups: 10,
LocalTime: true,
Compress: true,
},
},
})
if r == nil {
t.Fatal("expected non-nil recorder for file with rotation")
}
}
func TestParseRecorder_RedisSet(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "redis-set-rec",
Redis: &config.RedisRecorder{
Addr: "127.0.0.1:6379",
Key: "recorder-set",
Type: "set",
},
})
if r == nil {
t.Fatal("expected non-nil recorder for redis set")
}
}
func TestParseRecorder_RedisList(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "redis-list-rec",
Redis: &config.RedisRecorder{
Addr: "127.0.0.1:6379",
Key: "recorder-list",
Type: "list",
},
})
if r == nil {
t.Fatal("expected non-nil recorder for redis list")
}
}
func TestParseRecorder_RedisSortedSet(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "redis-sset-rec",
Redis: &config.RedisRecorder{
Addr: "127.0.0.1:6379",
Key: "recorder-sset",
Type: "sset",
},
})
if r == nil {
t.Fatal("expected non-nil recorder for redis sorted set")
}
}
func TestParseRecorder_EmptyFileReturnsNil(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "empty-rec",
})
if r != nil {
t.Fatal("expected nil recorder when no sub-config is provided")
}
}
func TestParseRecorder_FileWithEmptyPathReturnsNil(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "empty-path-rec",
File: &config.FileRecorder{
Path: "",
},
})
if r != nil {
t.Fatal("expected nil recorder when file path is empty")
}
}
func TestParseRecorder_TCPWithEmptyAddrReturnsNil(t *testing.T) {
r := ParseRecorder(&config.RecorderConfig{
Name: "empty-tcp-rec",
TCP: &config.TCPRecorder{
Addr: "",
},
})
if r != nil {
t.Fatal("expected nil recorder when TCP addr is empty")
}
}
+2 -8
View File
@@ -11,13 +11,8 @@ import (
"github.com/go-gost/x/internal/plugin"
"github.com/go-gost/x/registry"
xresolver "github.com/go-gost/x/resolver"
resolver_plugin "github.com/go-gost/x/resolver/plugin"
)
// ParseResolver converts a ResolverConfig into a resolver.Resolver. It
// supports plugin backends (HTTP or gRPC) and inline nameserver definitions
// with optional chain-based upstream routing, TTL, timeout, and
// prefer/async/only settings. Returns nil with no error when cfg is nil.
func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) {
if cfg == nil {
return nil, nil
@@ -33,14 +28,13 @@ func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return resolver_plugin.NewHTTPPlugin(
return xresolver.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
), nil
default:
return resolver_plugin.NewGRPCPlugin(
return xresolver.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
-146
View File
@@ -1,146 +0,0 @@
package resolver
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseResolver_Nil(t *testing.T) {
r, err := ParseResolver(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseResolver_PluginHTTP(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "http-res",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
func TestParseResolver_PluginGRPC(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "grpc-res",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "resolver.local",
},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
func TestParseResolver_PluginDefaultType(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "default-res",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
func TestParseResolver_WithNameservers(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "ns-res",
Nameservers: []*config.NameserverConfig{
{Addr: "8.8.8.8", Prefer: "ipv4"},
{Addr: "8.8.4.4", Prefer: "ipv4"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
func TestParseResolver_EmptyNameservers(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "empty-ns",
Nameservers: []*config.NameserverConfig{},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver even with empty nameservers")
}
}
func TestParseResolver_NameserverWithClientIP(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "clientip-res",
Nameservers: []*config.NameserverConfig{
{Addr: "8.8.8.8", ClientIP: "1.2.3.4"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
func TestParseResolver_NameserverWithAllFields(t *testing.T) {
r, err := ParseResolver(&config.ResolverConfig{
Name: "full-res",
Nameservers: []*config.NameserverConfig{
{
Addr: "8.8.8.8:53",
Chain: "chain1",
Prefer: "ipv4",
ClientIP: "1.2.3.4",
Hostname: "dns.example.com",
TTL: 300,
Timeout: 5,
Async: true,
Only: "A",
},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r == nil {
t.Fatal("expected non-nil resolver")
}
}
-48
View File
@@ -1,48 +0,0 @@
package rewriter
import (
"crypto/tls"
"strings"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/plugin"
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
)
// ParseRewriter converts a RewriterConfig into a rewriter.Rewriter.
// It currently supports plugin backends only (HTTP or gRPC).
// Returns nil when cfg is nil or no backend is configured.
func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter {
if cfg == nil {
return nil
}
if cfg.Plugin != nil {
var tlsCfg *tls.Config
if cfg.Plugin.TLS != nil {
tlsCfg = &tls.Config{
ServerName: cfg.Plugin.TLS.ServerName,
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
}
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return rewriter_plugin.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return rewriter_plugin.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
}
}
return nil
}
-123
View File
@@ -1,123 +0,0 @@
package router
import (
"crypto/tls"
"net"
"strings"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/router"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/loader"
"github.com/go-gost/x/internal/plugin"
xrouter "github.com/go-gost/x/router"
router_plugin "github.com/go-gost/x/router/plugin"
)
// ParseRouter converts a RouterConfig into a router.Router. It supports plugin
// backends (HTTP or gRPC) or inline static route definitions with optional
// file, Redis, and HTTP hot-reload support.
func ParseRouter(cfg *config.RouterConfig) router.Router {
if cfg == nil {
return nil
}
if cfg.Plugin != nil {
var tlsCfg *tls.Config
if cfg.Plugin.TLS != nil {
tlsCfg = &tls.Config{
ServerName: cfg.Plugin.TLS.ServerName,
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
}
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return router_plugin.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return router_plugin.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
)
}
}
var routes []*router.Route
for _, route := range cfg.Routes {
if route == nil {
continue
}
_, ipNet, _ := net.ParseCIDR(route.Net)
dst := route.Dst
if dst != "" {
if _, parsed, _ := net.ParseCIDR(dst); parsed != nil {
ipNet = parsed
}
} else {
if ipNet != nil {
dst = ipNet.String()
}
}
if dst == "" || route.Gateway == "" {
continue
}
routes = append(routes, &router.Route{
Net: ipNet,
Dst: dst,
Gateway: route.Gateway,
})
}
opts := []xrouter.Option{
xrouter.RoutesOption(routes),
xrouter.ReloadPeriodOption(cfg.Reload),
xrouter.LoggerOption(logger.Default().WithFields(map[string]any{
"kind": "router",
"router": cfg.Name,
})),
}
if cfg.File != nil && cfg.File.Path != "" {
opts = append(opts, xrouter.FileLoaderOption(loader.FileLoader(cfg.File.Path)))
}
if cfg.Redis != nil && cfg.Redis.Addr != "" {
switch cfg.Redis.Type {
case "list": // rediss list
opts = append(opts, xrouter.RedisLoaderOption(loader.RedisListLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
case "set": // redis set
opts = append(opts, xrouter.RedisLoaderOption(loader.RedisSetLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
default: // redis hash
opts = append(opts, xrouter.RedisLoaderOption(loader.RedisHashLoader(
cfg.Redis.Addr,
loader.DBRedisLoaderOption(cfg.Redis.DB),
loader.UsernameRedisLoaderOption(cfg.Redis.Username),
loader.PasswordRedisLoaderOption(cfg.Redis.Password),
loader.KeyRedisLoaderOption(cfg.Redis.Key),
)))
}
}
if cfg.HTTP != nil && cfg.HTTP.URL != "" {
opts = append(opts, xrouter.HTTPLoaderOption(loader.HTTPLoader(
cfg.HTTP.URL,
loader.TimeoutHTTPLoaderOption(cfg.HTTP.Timeout),
)))
}
return xrouter.NewRouter(opts...)
}
-237
View File
@@ -1,237 +0,0 @@
package router
import (
"context"
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseRouter_Nil(t *testing.T) {
r := ParseRouter(nil)
if r != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseRouter_PluginHTTP(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "http-rt",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_PluginGRPC(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "grpc-rt",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "router.local",
},
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_PluginDefaultType(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "default-rt",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_WithCIDRRoutes(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "cidr-rt",
Routes: []*config.RouterRouteConfig{
{Net: "192.168.1.0/24", Gateway: "10.0.0.1"},
{Net: "10.0.0.0/8", Gateway: "10.0.0.2"},
},
})
if r == nil {
t.Fatal("expected non-nil router with CIDR routes")
}
}
func TestParseRouter_WithDstRoutes(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "dst-rt",
Routes: []*config.RouterRouteConfig{
{Dst: "192.168.1.0/24", Gateway: "10.0.0.1"},
},
})
if r == nil {
t.Fatal("expected non-nil router with dst routes")
}
}
func TestParseRouter_MissingGatewaySkipped(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "skip-rt",
Routes: []*config.RouterRouteConfig{
{Net: "192.168.1.0/24", Gateway: ""}, // skipped: no gateway
{Net: "10.0.0.0/8", Gateway: "10.0.0.1"}, // kept
},
})
if r == nil {
t.Fatal("expected non-nil router (with one valid route)")
}
}
func TestParseRouter_MissingDstSkipped(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "no-dst-rt",
Routes: []*config.RouterRouteConfig{
{Dst: "", Gateway: "10.0.0.1"}, // skipped: no dst
},
})
if r == nil {
t.Fatal("expected non-nil router (all routes skipped)")
}
}
func TestParseRouter_InvalidCIDRSkipped(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "invalid-cidr-rt",
Routes: []*config.RouterRouteConfig{
{Net: "not-a-cidr", Gateway: "10.0.0.1"}, // invalid CIDR with empty Dst, skipped
{Net: "192.168.1.0/24", Gateway: "10.0.0.2"}, // valid
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_NilRouteSkipped(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "nil-route-rt",
Routes: []*config.RouterRouteConfig{
nil,
{Net: "192.168.1.0/24", Gateway: "10.0.0.1"},
},
})
if r == nil {
t.Fatal("expected non-nil router when nil route is in list")
}
}
func TestParseRouter_EmptyRoutes(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "empty-routes",
Routes: []*config.RouterRouteConfig{},
})
if r == nil {
t.Fatal("expected non-nil router with empty routes")
}
}
func TestParseRouter_FileLoader(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "file-rt",
File: &config.FileLoader{Path: "/tmp/routes.txt"},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_HTTPLoader(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "http-rt",
HTTP: &config.HTTPLoader{URL: "http://localhost:8080/routes"},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_RedisHashLoader(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "redis-hash-rt",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "routes-hash",
Type: "hash",
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_RedisListLoader(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "redis-list-rt",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "routes-list",
Type: "list",
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_RedisSetLoader(t *testing.T) {
r := ParseRouter(&config.RouterConfig{
Name: "redis-set-rt",
Redis: &config.RedisLoader{
Addr: "127.0.0.1:6379",
Key: "routes-set",
Type: "set",
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
}
func TestParseRouter_DstSyncedFromNet(t *testing.T) {
// When Dst is empty but Net is provided, Net is converted to Dst
r := ParseRouter(&config.RouterConfig{
Name: "net-to-dst-rt",
Routes: []*config.RouterRouteConfig{
{Net: "192.168.1.0/24", Gateway: "10.0.0.1"},
},
})
if r == nil {
t.Fatal("expected non-nil router")
}
route := r.GetRoute(context.Background(), "192.168.1.5")
if route == nil {
t.Fatal("expected non-nil route for IP in CIDR range")
}
if route.Dst != "192.168.1.0/24" {
t.Fatalf("Dst = %q, want %q", route.Dst, "192.168.1.0/24")
}
if route.Gateway != "10.0.0.1" {
t.Fatalf("Gateway = %q, want %q", route.Gateway, "10.0.0.1")
}
}
+3 -6
View File
@@ -7,11 +7,9 @@ import (
"github.com/go-gost/core/sd"
"github.com/go-gost/x/config"
"github.com/go-gost/x/internal/plugin"
sd_plugin "github.com/go-gost/x/sd/plugin"
xsd "github.com/go-gost/x/sd"
)
// ParseSD converts an SDConfig into an sd.SD. It only supports plugin backends
// (HTTP or gRPC); returns nil when cfg or cfg.Plugin is nil.
func ParseSD(cfg *config.SDConfig) sd.SD {
if cfg == nil || cfg.Plugin == nil {
return nil
@@ -26,14 +24,13 @@ func ParseSD(cfg *config.SDConfig) sd.SD {
}
switch strings.ToLower(cfg.Plugin.Type) {
case "http":
return sd_plugin.NewHTTPPlugin(
return xsd.NewHTTPPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
plugin.TimeoutOption(cfg.Plugin.Timeout),
)
default:
return sd_plugin.NewGRPCPlugin(
return xsd.NewGRPCPlugin(
cfg.Name, cfg.Plugin.Addr,
plugin.TokenOption(cfg.Plugin.Token),
plugin.TLSConfigOption(tlsCfg),
-75
View File
@@ -1,75 +0,0 @@
package sd
import (
"io"
"testing"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
)
func TestMain(m *testing.M) {
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
m.Run()
}
func TestParseSD_Nil(t *testing.T) {
s := ParseSD(nil)
if s != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseSD_NilPlugin(t *testing.T) {
s := ParseSD(&config.SDConfig{
Name: "no-plugin",
Plugin: nil,
})
if s != nil {
t.Fatal("expected nil when plugin is nil")
}
}
func TestParseSD_PluginHTTP(t *testing.T) {
s := ParseSD(&config.SDConfig{
Name: "http-sd",
Plugin: &config.PluginConfig{
Type: "http",
Addr: "127.0.0.1:9000",
},
})
if s == nil {
t.Fatal("expected non-nil SD")
}
}
func TestParseSD_PluginGRPC(t *testing.T) {
s := ParseSD(&config.SDConfig{
Name: "grpc-sd",
Plugin: &config.PluginConfig{
Type: "grpc",
Addr: "127.0.0.1:9001",
Token: "secret",
TLS: &config.TLSConfig{
Secure: true,
ServerName: "sd.local",
},
},
})
if s == nil {
t.Fatal("expected non-nil SD")
}
}
func TestParseSD_PluginDefaultType(t *testing.T) {
s := ParseSD(&config.SDConfig{
Name: "default-sd",
Plugin: &config.PluginConfig{
Addr: "127.0.0.1:9002",
},
})
if s == nil {
t.Fatal("expected non-nil SD")
}
}
-12
View File
@@ -7,9 +7,6 @@ import (
xs "github.com/go-gost/x/selector"
)
// ParseChainSelector creates a chain-level selector from a SelectorConfig. If
// cfg is nil it returns nil. Strategy defaults to round-robin when unrecognized
// or unset.
func ParseChainSelector(cfg *config.SelectorConfig) selector.Selector[chain.Chainer] {
if cfg == nil {
return nil
@@ -35,9 +32,6 @@ func ParseChainSelector(cfg *config.SelectorConfig) selector.Selector[chain.Chai
)
}
// ParseNodeSelector creates a node-level selector from a SelectorConfig. If
// cfg is nil it returns nil. Supported strategies: round/rr, random/rand,
// fifo/ha, hash, and parallel. Defaults to round-robin.
func ParseNodeSelector(cfg *config.SelectorConfig) selector.Selector[*chain.Node] {
if cfg == nil {
return nil
@@ -53,8 +47,6 @@ func ParseNodeSelector(cfg *config.SelectorConfig) selector.Selector[*chain.Node
strategy = xs.FIFOStrategy[*chain.Node]()
case "hash":
strategy = xs.HashStrategy[*chain.Node]()
case "parallel":
strategy = xs.ParallelStrategy[*chain.Node]()
default:
strategy = xs.RoundRobinStrategy[*chain.Node]()
}
@@ -66,8 +58,6 @@ func ParseNodeSelector(cfg *config.SelectorConfig) selector.Selector[*chain.Node
)
}
// DefaultNodeSelector returns a node selector using round-robin strategy with
// default fail filter settings.
func DefaultNodeSelector() selector.Selector[*chain.Node] {
return xs.NewSelector(
xs.RoundRobinStrategy[*chain.Node](),
@@ -76,8 +66,6 @@ func DefaultNodeSelector() selector.Selector[*chain.Node] {
)
}
// DefaultChainSelector returns a chain selector using round-robin strategy
// with default fail filter settings.
func DefaultChainSelector() selector.Selector[chain.Chainer] {
return xs.NewSelector(
xs.RoundRobinStrategy[chain.Chainer](),
-144
View File
@@ -1,144 +0,0 @@
package selector
import (
"testing"
"github.com/go-gost/x/config"
)
func TestParseChainSelector_Nil(t *testing.T) {
sel := ParseChainSelector(nil)
if sel != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseChainSelector_RoundRobin(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "round"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseChainSelector_RR(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "rr"})
if sel == nil {
t.Fatal("expected non-nil selector for rr strategy")
}
}
func TestParseChainSelector_Random(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "random"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseChainSelector_Rand(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "rand"})
if sel == nil {
t.Fatal("expected non-nil selector for rand strategy")
}
}
func TestParseChainSelector_FIFO(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "fifo"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseChainSelector_HA(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "ha"})
if sel == nil {
t.Fatal("expected non-nil selector for ha strategy")
}
}
func TestParseChainSelector_Hash(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "hash"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseChainSelector_DefaultStrategy(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{Strategy: "unknown"})
if sel == nil {
t.Fatal("expected non-nil selector for unknown strategy (should default to round-robin)")
}
}
func TestParseChainSelector_WithMaxFails(t *testing.T) {
sel := ParseChainSelector(&config.SelectorConfig{
Strategy: "round",
MaxFails: 3,
FailTimeout: 0,
})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseNodeSelector_Nil(t *testing.T) {
sel := ParseNodeSelector(nil)
if sel != nil {
t.Fatal("expected nil for nil config")
}
}
func TestParseNodeSelector_RoundRobin(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "round"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseNodeSelector_Random(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "random"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseNodeSelector_FIFO(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "fifo"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseNodeSelector_Hash(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "hash"})
if sel == nil {
t.Fatal("expected non-nil selector")
}
}
func TestParseNodeSelector_Parallel(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "parallel"})
if sel == nil {
t.Fatal("expected non-nil selector for parallel strategy")
}
}
func TestParseNodeSelector_DefaultStrategy(t *testing.T) {
sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "unknown"})
if sel == nil {
t.Fatal("expected non-nil selector for unknown strategy (should default to round-robin)")
}
}
func TestDefaultNodeSelector(t *testing.T) {
sel := DefaultNodeSelector()
if sel == nil {
t.Fatal("expected non-nil default node selector")
}
}
func TestDefaultChainSelector(t *testing.T) {
sel := DefaultChainSelector()
if sel == nil {
t.Fatal("expected non-nil default chain selector")
}
}
+53 -265
View File
@@ -2,24 +2,19 @@ package service
import (
"fmt"
"runtime"
"strings"
"time"
"github.com/go-gost/core/admission"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/listener"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/observer/stats"
mdutil "github.com/go-gost/core/metadata/util"
"github.com/go-gost/core/recorder"
"github.com/go-gost/core/selector"
"github.com/go-gost/core/service"
xadmission "github.com/go-gost/x/admission"
xauth "github.com/go-gost/x/auth"
xbypass "github.com/go-gost/x/bypass"
xchain "github.com/go-gost/x/chain"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
@@ -27,87 +22,46 @@ import (
auth_parser "github.com/go-gost/x/config/parsing/auth"
bypass_parser "github.com/go-gost/x/config/parsing/bypass"
hop_parser "github.com/go-gost/x/config/parsing/hop"
logger_parser "github.com/go-gost/x/config/parsing/logger"
selector_parser "github.com/go-gost/x/config/parsing/selector"
tls_util "github.com/go-gost/x/internal/util/tls"
quota_wrapper "github.com/go-gost/x/limiter/quota/wrapper"
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
"github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
xstats "github.com/go-gost/x/observer/stats"
xrecorder "github.com/go-gost/x/recorder"
"github.com/go-gost/x/registry"
xservice "github.com/go-gost/x/service"
"github.com/vishvananda/netns"
)
// plaintextListeners are listener types that never terminate TLS on their
// accepted connections, so any certFile/keyFile/caFile configured for them is
// silently ignored. This catches the common footgun where a user writes e.g.
// `tls+mws://...?certFile=...` expecting TLS: the "tls+" prefix is parsed as
// the handler scheme (see x/config/cmd/cmd.go buildServiceConfig), not a TLS
// wrapper, so the underlying mws listener stays plaintext. Extend this set when
// new plaintext listeners are added.
var plaintextListeners = map[string]bool{
"tcp": true, "udp": true,
"ws": true, "mws": true,
"redirect": true, "tproxy": true,
"rtcp": true, "rudp": true,
"unix": true, "runix": true, "serial": true, "stdio": true,
}
// ParseService constructs a fully-wired service.Service from a ServiceConfig.
// It defaults the listener to "tcp" and the handler to "auto", resolves named
// components from the registry (authers, admissions, bypasses, resolvers,
// hosts, chains, hops, limiters, recorders, observers), composes TLS settings,
// and applies metadata-driven options (proxy protocol, netns, sockopts,
// pre/post hooks, stats, etc.). It returns an error if the listener or handler
// type is unknown or if construction of any sub-component fails.
func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
if cfg.Listener == nil {
cfg.Listener = &config.ListenerConfig{}
cfg.Listener = &config.ListenerConfig{
Type: "tcp",
}
}
if strings.TrimSpace(cfg.Listener.Type) == "" {
cfg.Listener.Type = "tcp"
}
if cfg.Handler == nil {
cfg.Handler = &config.HandlerConfig{}
cfg.Handler = &config.HandlerConfig{
Type: "auto",
}
}
if strings.TrimSpace(cfg.Handler.Type) == "" {
cfg.Handler.Type = "auto"
}
log := logger.Default()
if loggers := logger_parser.List(cfg.Logger, cfg.Loggers...); len(loggers) > 0 {
log = logger.LoggerGroup(loggers...)
}
serviceLogger := log.WithFields(map[string]any{
serviceLogger := logger.Default().WithFields(map[string]any{
"kind": "service",
"service": cfg.Name,
"listener": cfg.Listener.Type,
"handler": cfg.Handler.Type,
})
listenerLogger := serviceLogger.WithFields(map[string]any{
"kind": "listener",
})
tlsCfg := cfg.Listener.TLS
if tlsCfg == nil {
tlsCfg = &config.TLSConfig{}
}
tlsConfig, err := tls_util.LoadServerConfig(tlsCfg)
if err != nil {
serviceLogger.Error(err)
listenerLogger.Error(err)
return nil, err
}
if tlsConfig == nil {
tlsConfig = parsing.DefaultTLSConfig().Clone()
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
}
if (tlsCfg.CertFile != "" || tlsCfg.KeyFile != "" || tlsCfg.CAFile != "") && plaintextListeners[cfg.Listener.Type] {
serviceLogger.Warnf("TLS certificate options are configured but the %q listener does not use TLS and will ignore them; the connection will be plaintext. Use a TLS-capable listener (e.g. mwss, wss, tls, quic, grpc, http2, http3) to enable TLS.",
cfg.Listener.Type)
}
authers := auth_parser.List(cfg.Listener.Auther, cfg.Listener.Authers...)
@@ -118,7 +72,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
}
var auther auth.Authenticator
if len(authers) > 0 {
auther = xauth.AuthenticatorGroup(authers...)
auther = auth.AuthenticatorGroup(authers...)
}
admissions := admission_parser.List(cfg.Admission, cfg.Admissions...)
@@ -134,16 +88,6 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
ifce := cfg.Interface
var preUp, preDown, postUp, postDown []string
var ignoreChain bool
var pStats stats.Stats
var observerPeriod time.Duration
var netnsIn, netnsOut string
var dialTimeout time.Duration
var labels map[string]string
var limiterRefreshInterval time.Duration
var limiterCleanupInterval time.Duration
var limiterScope string
if cfg.Metadata != nil {
md := metadata.NewMetadata(cfg.Metadata)
ppv = mdutil.GetInt(md, parsing.MDKeyProxyProtocol)
@@ -160,104 +104,31 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
postUp = mdutil.GetStrings(md, parsing.MDKeyPostUp)
postDown = mdutil.GetStrings(md, parsing.MDKeyPostDown)
ignoreChain = mdutil.GetBool(md, parsing.MDKeyIgnoreChain)
if mdutil.GetBool(md, parsing.MDKeyEnableStats) {
pStats = xstats.NewStats(mdutil.GetBool(md, parsing.MDKeyObserverResetTraffic))
}
observerPeriod = mdutil.GetDuration(md, parsing.MDKeyObserverPeriod, "observePeriod")
netnsIn = mdutil.GetString(md, parsing.MDKeyNetns)
netnsOut = mdutil.GetString(md, parsing.MDKeyNetnsOut)
dialTimeout = mdutil.GetDuration(md, parsing.MDKeyDialTimeout)
limiterRefreshInterval = mdutil.GetDuration(md, parsing.MDKeyLimiterRefreshInterval)
limiterCleanupInterval = mdutil.GetDuration(md, parsing.MDKeyLimiterCleanupInterval)
limiterScope = mdutil.GetString(md, parsing.MDKeyLimiterScope)
labels = mdutil.GetStringMapString(md, parsing.MDKeyLabels)
}
if len(labels) > 0 {
serviceLogger = serviceLogger.WithFields(map[string]any{
"labels": labels,
})
}
listenerLogger := serviceLogger.WithFields(map[string]any{
"kind": "listener",
})
routerOpts := []chain.RouterOption{
chain.TimeoutRouterOption(dialTimeout),
chain.InterfaceRouterOption(ifce),
chain.NetnsRouterOption(netnsOut),
chain.SockOptsRouterOption(sockOpts),
chain.ResolverRouterOption(registry.ResolverRegistry().Get(cfg.Resolver)),
chain.HostMapperRouterOption(registry.HostsRegistry().Get(cfg.Hosts)),
chain.LoggerRouterOption(listenerLogger),
}
if !ignoreChain {
routerOpts = append(routerOpts,
chain.ChainRouterOption(chainGroup(cfg.Listener.Chain, cfg.Listener.ChainGroup)),
)
}
listenOpts := []listener.Option{
listener.AddrOption(cfg.Addr),
listener.RouterOption(xchain.NewRouter(routerOpts...)),
listener.AutherOption(auther),
listener.AuthOption(auth_parser.Info(cfg.Listener.Auth)),
listener.TLSConfigOption(tlsConfig),
listener.AdmissionOption(xadmission.AdmissionGroup(admissions...)),
listener.TrafficLimiterOption(
cache_limiter.NewCachedTrafficLimiter(
registry.TrafficLimiterRegistry().Get(cfg.Limiter),
cache_limiter.RefreshIntervalOption(limiterRefreshInterval),
cache_limiter.CleanupIntervalOption(limiterCleanupInterval),
cache_limiter.ScopeOption(limiterScope),
),
),
listener.AdmissionOption(admission.AdmissionGroup(admissions...)),
listener.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Limiter)),
listener.ConnLimiterOption(registry.ConnLimiterRegistry().Get(cfg.CLimiter)),
listener.LoggerOption(listenerLogger),
listener.ServiceOption(cfg.Name),
listener.ProxyProtocolOption(ppv),
listener.StatsOption(pStats),
listener.NetnsOption(netnsIn),
listener.LoggerOption(listenerLogger),
}
if netnsIn != "" {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
originNs, err := netns.Get()
if err != nil {
return nil, fmt.Errorf("netns.Get(): %v", err)
}
defer netns.Set(originNs)
var ns netns.NsHandle
if strings.HasPrefix(netnsIn, "/") {
ns, err = netns.GetFromPath(netnsIn)
} else {
ns, err = netns.GetFromName(netnsIn)
}
if err != nil {
return nil, fmt.Errorf("netns.Get(%s): %v", netnsIn, err)
}
defer ns.Close()
if err := netns.Set(ns); err != nil {
return nil, fmt.Errorf("netns.Set(%s): %v", netnsIn, err)
}
if !ignoreChain {
listenOpts = append(listenOpts,
listener.ChainOption(chainGroup(cfg.Listener.Chain, cfg.Listener.ChainGroup)),
)
}
var ln listener.Listener
if rf := registry.ListenerRegistry().Get(cfg.Listener.Type); rf != nil {
ln = rf(listenOpts...)
} else {
return nil, fmt.Errorf("unknown listener: %s", cfg.Listener.Type)
return nil, fmt.Errorf("unregistered listener: %s", cfg.Listener.Type)
}
if cfg.Listener.Metadata == nil {
@@ -269,10 +140,6 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
return nil, err
}
for _, qname := range cfg.Quotas {
ln = quota_wrapper.WrapListener(ln, strings.TrimSpace(qname))
}
handlerLogger := serviceLogger.WithFields(map[string]any{
"kind": "handler",
})
@@ -288,8 +155,6 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
}
if tlsConfig == nil {
tlsConfig = parsing.DefaultTLSConfig().Clone()
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
}
authers = auth_parser.List(cfg.Handler.Auther, cfg.Handler.Authers...)
@@ -301,72 +166,27 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
auther = nil
if len(authers) > 0 {
auther = xauth.AuthenticatorGroup(authers...)
}
// Parse skipauth from handler metadata to whitelist client IPs that
// should bypass authentication. Accepts both YAML arrays
// (skipauth: ["10.0.0.0/8"]) and comma-separated URL query values
// (?skipauth=10.0.0.0/8,192.168.1.1).
if cfg.Handler.Metadata != nil {
hmd := metadata.NewMetadata(cfg.Handler.Metadata)
skipauth := mdutil.GetStrings(hmd, "skipauth")
if len(skipauth) == 0 {
if s := mdutil.GetString(hmd, "skipauth"); s != "" {
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
skipauth = append(skipauth, p)
}
}
}
}
if len(skipauth) > 0 {
if auther != nil {
auther = xauth.WhitelistedAuthenticator(auther, skipauth)
handlerLogger.Debugf("skipauth whitelist applied: %v", skipauth)
} else {
handlerLogger.Warnf("skipauth configured but no auther set — authentication is already disabled for all clients")
}
}
auther = auth.AuthenticatorGroup(authers...)
}
var recorders []recorder.RecorderObject
for _, r := range cfg.Recorders {
md := metadata.NewMetadata(r.Metadata)
rec := registry.RecorderRegistry().Get(r.Name)
if r.Metadata != nil {
rec = &xrecorder.MetadataRecorder{
Recorder: rec,
Metadata: r.Metadata,
}
}
recorders = append(recorders, recorder.RecorderObject{
Recorder: rec,
Recorder: registry.RecorderRegistry().Get(r.Name),
Record: r.Record,
Options: &recorder.Options{
Direction: mdutil.GetBool(md, parsing.MDKeyRecorderDirection),
TimestampFormat: mdutil.GetString(md, parsing.MDKeyRecorderTimestampFormat),
Hexdump: mdutil.GetBool(md, parsing.MDKeyRecorderHexdump),
HTTPBody: mdutil.GetBool(md, parsing.MDKeyRecorderHTTPBody),
MaxBodySize: mdutil.GetInt(md, parsing.MDKeyRecorderHTTPMaxBodySize),
},
Metadata: r.Metadata,
})
}
var rew rewriter.Rewriter
if cfg.Rewriter != "" {
if !registry.RewriterRegistry().IsRegistered(cfg.Rewriter) {
serviceLogger.Warnf("rewriter %q not found in registry", cfg.Rewriter)
}
rew = registry.RewriterRegistry().Get(cfg.Rewriter)
}
routerOpts = []chain.RouterOption{
routerOpts := []chain.RouterOption{
chain.RetriesRouterOption(cfg.Handler.Retries),
chain.TimeoutRouterOption(dialTimeout),
// chain.TimeoutRouterOption(10*time.Second),
chain.InterfaceRouterOption(ifce),
chain.NetnsRouterOption(netnsOut),
chain.SockOptsRouterOption(sockOpts),
chain.ResolverRouterOption(registry.ResolverRegistry().Get(cfg.Resolver)),
chain.HostMapperRouterOption(registry.HostsRegistry().Get(cfg.Hosts)),
@@ -378,30 +198,26 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
chain.ChainRouterOption(chainGroup(cfg.Handler.Chain, cfg.Handler.ChainGroup)),
)
}
router := chain.NewRouter(routerOpts...)
var h handler.Handler
if rf := registry.HandlerRegistry().Get(cfg.Handler.Type); rf != nil {
h = rf(
handler.RouterOption(xchain.NewRouter(routerOpts...)),
handler.RouterOption(router),
handler.AutherOption(auther),
handler.AuthOption(auth_parser.Info(cfg.Handler.Auth)),
handler.BypassOption(xbypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
handler.BypassOption(bypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
handler.TLSConfigOption(tlsConfig),
handler.RateLimiterOption(registry.RateLimiterRegistry().Get(cfg.RLimiter)),
handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)),
handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)),
handler.RecordersOption(recorders...),
handler.RewriterOption(rew),
handler.LoggerOption(handlerLogger),
handler.ServiceOption(cfg.Name),
handler.NetnsOption(netnsIn),
)
} else {
return nil, fmt.Errorf("unknown handler: %s", cfg.Handler.Type)
return nil, fmt.Errorf("unregistered handler: %s", cfg.Handler.Type)
}
if forwarder, ok := h.(handler.Forwarder); ok {
hop, err := parseForwarder(cfg.Forwarder, log)
hop, err := parseForwarder(cfg.Forwarder)
if err != nil {
return nil, err
}
@@ -418,79 +234,51 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
}
s := xservice.NewService(cfg.Name, ln, h,
xservice.AdmissionOption(xadmission.AdmissionGroup(admissions...)),
xservice.AdmissionOption(admission.AdmissionGroup(admissions...)),
xservice.PreUpOption(preUp),
xservice.PreDownOption(preDown),
xservice.PostUpOption(postUp),
xservice.PostDownOption(postDown),
xservice.RecordersOption(recorders...),
xservice.StatsOption(pStats),
xservice.ObserverOption(registry.ObserverRegistry().Get(cfg.Observer)),
xservice.ObserverPeriodOption(observerPeriod),
xservice.LoggerOption(serviceLogger),
xservice.LabelsOption(labels),
)
serviceLogger.Infof("listening on %s/%s", s.Addr().String(), s.Addr().Network())
return s, nil
}
func parseForwarder(cfg *config.ForwarderConfig, log logger.Logger) (hop.Hop, error) {
func parseForwarder(cfg *config.ForwarderConfig) (hop.Hop, error) {
if cfg == nil {
return nil, nil
}
hopName := cfg.Hop
if hopName == "" {
hopName = cfg.Name
}
if hopName != "" {
return registry.HopRegistry().Get(hopName), nil
}
hc := config.HopConfig{
Name: cfg.Name,
Selector: cfg.Selector,
}
for _, node := range cfg.Nodes {
if node == nil {
continue
}
filter := node.Filter
if filter == nil {
if node.Protocol != "" || node.Host != "" || node.Path != "" {
filter = &config.NodeFilterConfig{
Protocol: node.Protocol,
if node != nil {
hc.Nodes = append(hc.Nodes,
&config.NodeConfig{
Name: node.Name,
Addr: node.Addr,
Host: node.Host,
Network: node.Network,
Protocol: node.Protocol,
Path: node.Path,
}
}
Bypass: node.Bypass,
Bypasses: node.Bypasses,
HTTP: node.HTTP,
TLS: node.TLS,
Auth: node.Auth,
},
)
}
httpCfg := node.HTTP
if node.Auth != nil {
if httpCfg == nil {
httpCfg = &config.HTTPNodeConfig{}
}
if httpCfg.Auth == nil {
httpCfg.Auth = node.Auth
}
}
hc.Nodes = append(hc.Nodes, &config.NodeConfig{
Name: node.Name,
Addr: node.Addr,
Network: node.Network,
Bypass: node.Bypass,
Bypasses: node.Bypasses,
Filter: filter,
Matcher: node.Matcher,
HTTP: httpCfg,
TLS: node.TLS,
Metadata: node.Metadata,
})
}
return hop_parser.ParseHop(&hc, log)
if len(hc.Nodes) > 0 {
return hop_parser.ParseHop(&hc)
}
return registry.HopRegistry().Get(hc.Name), nil
}
func chainGroup(name string, group *config.ChainGroupConfig) chain.Chainer {
+24 -117
View File
@@ -12,9 +12,6 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"sync/atomic"
"time"
"github.com/go-gost/core/logger"
@@ -23,136 +20,47 @@ import (
)
var (
defaultTLSConfig atomic.Value
defaultTLSConfig *tls.Config
)
// testDefaultCertDir overrides the default certificate directory in tests.
// It is set only from same-package test code and left empty in production.
var testDefaultCertDir string
// defaultCertDir returns the directory used for persisting auto-generated
// certificates. It uses testDefaultCertDir when set (for testing), otherwise
// returns $HOME/.gost/.
func defaultCertDir() string {
if testDefaultCertDir != "" {
return testDefaultCertDir
}
dir, err := os.UserHomeDir()
if err != nil {
return ".gost"
}
return filepath.Join(dir, ".gost")
}
// DefaultTLSConfig returns the global default TLS configuration used as a
// fallback when a listener or handler does not specify its own TLS settings.
// The returned config is a shared instance; callers that need to mutate it
// should clone it first.
func DefaultTLSConfig() *tls.Config {
v, _ := defaultTLSConfig.Load().(*tls.Config)
return v
return defaultTLSConfig
}
// SetDefaultTLSConfig replaces the global default TLS configuration. It is safe
// to call from multiple goroutines.
func SetDefaultTLSConfig(cfg *tls.Config) {
defaultTLSConfig.Store(cfg)
}
// BuildDefaultTLSConfig loads or generates the default TLS certificate and key
// from the given config.
//
// When explicit certificate files are configured (CertFile, KeyFile, or CAFile
// is non-empty) it loads them via tls_util.LoadDefaultConfig — the original
// behaviour preserved for backward compatibility.
//
// When no certificate files are configured (all three empty), it tries sources
// in this order:
// 1. cert.pem / key.pem in the current working directory (backward compatible)
// 2. auto-ca-cert.pem / auto-ca-key.pem under $HOME/.gost/ (persisted)
// 3. If none found, generates a new ECDSA P-256 CA certificate, persists it
// to $HOME/.gost/, and uses it.
//
// If cfg is nil an empty TLSConfig is used (equivalent to no explicit files),
// so the CWD → persisted → generate path is taken.
func BuildDefaultTLSConfig(cfg *config.TLSConfig) (*tls.Config, error) {
func BuildDefaultTLSConfig(cfg *config.TLSConfig) {
log := logger.Default()
if cfg == nil {
cfg = &config.TLSConfig{}
}
if cfg.CertFile != "" || cfg.KeyFile != "" || cfg.CAFile != "" {
tlsConfig, err := tls_util.LoadDefaultConfig(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
if err != nil {
return nil, err
cfg = &config.TLSConfig{
CertFile: "cert.pem",
KeyFile: "key.pem",
CAFile: "ca.pem",
}
log.Debug("load global TLS certificate files OK")
return tlsConfig, nil
}
tlsConfig, err := loadOrGeneratePersistentTLSConfig(cfg)
tlsConfig, err := tls_util.LoadDefaultConfig(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
if err != nil {
return nil, err
// generate random self-signed certificate.
cert, err := genCertificate(cfg.Validity, cfg.Organization, cfg.CommonName)
if err != nil {
log.Fatal(err)
}
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
log.Debug("load global TLS certificate files failed, use random generated certificate")
} else {
log.Debug("load global TLS certificate files OK")
}
return tlsConfig, nil
defaultTLSConfig = tlsConfig
}
// loadOrGeneratePersistentTLSConfig tries loading an auto-generated CA
// certificate from these locations (in order):
// 1. cert.pem / key.pem in the current working directory (backward compatible)
// 2. auto-ca-cert.pem / auto-ca-key.pem under defaultCertDir() (persisted)
//
// If none is found it generates a new one, persists it to defaultCertDir()
// (best-effort), and returns it.
func loadOrGeneratePersistentTLSConfig(cfg *config.TLSConfig) (*tls.Config, error) {
log := logger.Default()
// 1. Try CWD cert.pem / key.pem first (backward compatible).
cwdCert, cwdErr := tls.LoadX509KeyPair("cert.pem", "key.pem")
if cwdErr == nil {
log.Debug("loaded default certificate from current working directory (cert.pem / key.pem)")
return &tls.Config{Certificates: []tls.Certificate{cwdCert}}, nil
}
// 2. Try persisted certificate in defaultCertDir().
dir := defaultCertDir()
certFile := filepath.Join(dir, "auto-ca-cert.pem")
keyFile := filepath.Join(dir, "auto-ca-key.pem")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err == nil {
log.Debugf("loaded persisted default certificate from %s", dir)
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
log.Debug("generating new default certificate (no persisted certificate found)")
// 3. Generate a new certificate.
rawCert, rawKey, err := generateKeyPair(cfg.Validity, cfg.Organization, cfg.CommonName)
func genCertificate(validity time.Duration, org string, cn string) (cert tls.Certificate, err error) {
rawCert, rawKey, err := generateKeyPair(validity, org, cn)
if err != nil {
return nil, err
return
}
// Best-effort persist to disk. If it fails the in-memory certificate is
// still usable.
if err := os.MkdirAll(dir, 0700); err != nil {
log.Warnf("failed to create certificate directory %s: %v", dir, err)
} else {
if err := os.WriteFile(certFile, rawCert, 0644); err != nil {
log.Warnf("failed to persist certificate: %v", err)
} else if err := os.WriteFile(keyFile, rawKey, 0600); err != nil {
log.Warnf("failed to persist private key: %v", err)
} else {
log.Debugf("persisted default certificate to %s", dir)
}
}
cert, err = tls.X509KeyPair(rawCert, rawKey)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
return tls.X509KeyPair(rawCert, rawKey)
}
func generateKeyPair(validity time.Duration, org string, cn string) (rawCert, rawKey []byte, err error) {
@@ -199,7 +107,6 @@ func generateKeyPair(validity time.Duration, org string, cn string) (rawCert, ra
x509.ExtKeyUsageServerAuth,
},
BasicConstraintsValid: true,
IsCA: true,
}
if _, isRSA := priv.(*rsa.PrivateKey); isRSA {
template.KeyUsage |= x509.KeyUsageKeyEncipherment
Loaded 100 of 724 files, more files were not shown because too many files have changed in this diff. Show more