Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func applyProxyConfig(opt *proxyOptions, pc *ProxyConfig, setFlags map[string]bo
}
if pc.WebLogMax != nil && !setFlags["web-log-max"] {
opt.webLogMax = *pc.WebLogMax
opt.webLogMaxSet = true
}
return nil
}
Expand Down
97 changes: 74 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ type proxyOptions struct {
webAddr string
webAllowPublic bool
webLogMax int
webLogMaxSet bool
}

// runProxy 实现 proxy 子命令:单端口 SOCKS5 + HTTP 应用层代理。
Expand Down Expand Up @@ -1070,6 +1071,64 @@ func defaultProxyOptions() proxyOptions {
}
}

type proxyAdminOptions struct {
statsAddr string
statsAllowPublic bool
webAddr string
webAllowPublic bool
webLogMax int
}

// collectProxyAdminOptions validates and consolidates the process-wide admin
// endpoints used by a multi-instance proxy. Every instance that enables an
// endpoint must agree on its address and security settings; silently selecting
// the first declaration would make behavior depend on YAML ordering.
func collectProxyAdminOptions(instances []proxyOptions) (proxyAdminOptions, error) {
out := proxyAdminOptions{webLogMax: 1000}
statsSet := false
webSet := false
webLogMaxSet := false
webLogMaxConflict := false
conflictingWebLogMax := 0
for _, opt := range instances {
if addr := strings.TrimSpace(opt.statsAddr); addr != "" {
if statsSet && out.statsAddr != addr {
return out, errors.New(i18n.T(i18n.KeyErrConflictingMultiOption, "stats_addr", out.statsAddr, addr))
}
if statsSet && out.statsAllowPublic != opt.statsAllowPublic {
return out, errors.New(i18n.T(i18n.KeyErrConflictingMultiOption, "stats_allow_public", out.statsAllowPublic, opt.statsAllowPublic))
}
out.statsAddr = addr
out.statsAllowPublic = opt.statsAllowPublic
statsSet = true
}
if addr := strings.TrimSpace(opt.webAddr); addr != "" {
if webSet && out.webAddr != addr {
return out, errors.New(i18n.T(i18n.KeyErrConflictingMultiOption, "web_addr", out.webAddr, addr))
}
if webSet && out.webAllowPublic != opt.webAllowPublic {
return out, errors.New(i18n.T(i18n.KeyErrConflictingMultiOption, "web_allow_public", out.webAllowPublic, opt.webAllowPublic))
}
out.webAddr = addr
out.webAllowPublic = opt.webAllowPublic
webSet = true
}
if opt.webLogMaxSet {
if webLogMaxSet && out.webLogMax != opt.webLogMax {
webLogMaxConflict = true
conflictingWebLogMax = opt.webLogMax
continue
}
out.webLogMax = opt.webLogMax
webLogMaxSet = true
}
}
if out.webAddr != "" && webLogMaxConflict {
return out, errors.New(i18n.T(i18n.KeyErrConflictingMultiOption, "web_log_max", out.webLogMax, conflictingWebLogMax))
}
return out, nil
}

// runProxyMulti 处理 proxy 多实例(多端口映射):逐个实例从默认值出发叠加配置
// 文件字段(per-instance 不应用 CLI 覆盖),校验并按监听地址去重后并发启动。
func runProxyMulti(base proxyOptions, cfg *Config, setFlags map[string]bool) error {
Expand Down Expand Up @@ -1115,6 +1174,7 @@ func runProxyMulti(base proxyOptions, cfg *Config, setFlags map[string]bool) err
}
if setFlags["web-log-max"] {
opt.webLogMax = base.webLogMax
opt.webLogMaxSet = true
}
if err := validateProxyOptions(&opt); err != nil {
return err
Expand All @@ -1131,6 +1191,15 @@ func runProxyMulti(base proxyOptions, cfg *Config, setFlags map[string]bool) err
instances = append(instances, proxyInstance{opt: opt, upstream: upstream})
}

instanceOptions := make([]proxyOptions, 0, len(instances))
for _, inst := range instances {
instanceOptions = append(instanceOptions, inst.opt)
}
admin, err := collectProxyAdminOptions(instanceOptions)
if err != nil {
return err
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

Expand All @@ -1141,35 +1210,17 @@ func runProxyMulti(base proxyOptions, cfg *Config, setFlags map[string]bool) err
errCh := make(chan error, len(instances))
var wg sync.WaitGroup
providers := make([]stats.Provider, 0, len(instances))
statsAddr := ""
statsAllowPublic := false
// Web 面板:若任一实例设置了 -web-addr,则创建一份共享事件日志并注入所有
// 实例,聚合到同一个面板。取第一个非空 webAddr 及其 webLogMax/allowPublic。
webAddr := ""
webAllowPublic := false
webLogMax := 1000
for _, inst := range instances {
if strings.TrimSpace(inst.opt.webAddr) != "" {
webAddr = inst.opt.webAddr
webAllowPublic = inst.opt.webAllowPublic
webLogMax = inst.opt.webLogMax
break
}
}
// All instances share one validated admin surface and one event log.
var events *stats.EventLog
if webAddr != "" {
events = stats.NewEventLog(webLogMax)
if admin.webAddr != "" {
events = stats.NewEventLog(admin.webLogMax)
}
for i, inst := range instances {
i, inst := i, inst
log.Print(i18n.T(i18n.KeyLogInstanceStarting, i+1, inst.opt.addr))
srv := newProxyServer(inst.opt, inst.upstream)
srv.Events = events
providers = append(providers, srv)
if statsAddr == "" && strings.TrimSpace(inst.opt.statsAddr) != "" {
statsAddr = inst.opt.statsAddr
statsAllowPublic = inst.opt.statsAllowPublic
}
wg.Add(1)
go func() {
defer wg.Done()
Expand All @@ -1183,8 +1234,8 @@ func runProxyMulti(base proxyOptions, cfg *Config, setFlags map[string]bool) err
}()
}

statsWait, statsErrCh := startStatsEndpoint(ctx, cancel, statsAddr, statsAllowPublic, providers...)
webWait, webErrCh := startWebEndpoint(ctx, cancel, webAddr, webAllowPublic, events, providers...)
statsWait, statsErrCh := startStatsEndpoint(ctx, cancel, admin.statsAddr, admin.statsAllowPublic, providers...)
webWait, webErrCh := startWebEndpoint(ctx, cancel, admin.webAddr, admin.webAllowPublic, events, providers...)

wg.Wait()
statsWait()
Expand Down
96 changes: 96 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,99 @@ func TestStartForwardInstancesWithStatsEndpoint(t *testing.T) {
t.Fatal("未在超时内退出")
}
}

func TestCollectProxyAdminOptionsRejectsConflicts(t *testing.T) {
tests := []struct {
name string
instances []proxyOptions
}{
{
name: "stats addresses",
instances: []proxyOptions{
{statsAddr: "127.0.0.1:9090"},
{statsAddr: "127.0.0.1:9091"},
},
},
{
name: "stats public policy",
instances: []proxyOptions{
{statsAddr: "0.0.0.0:9090", statsAllowPublic: true},
{statsAddr: "0.0.0.0:9090", statsAllowPublic: false},
},
},
{
name: "web addresses",
instances: []proxyOptions{
{webAddr: "127.0.0.1:8080", webLogMax: 1000},
{webAddr: "127.0.0.1:8081", webLogMax: 1000},
},
},
{
name: "web public policy",
instances: []proxyOptions{
{webAddr: "0.0.0.0:8080", webAllowPublic: true, webLogMax: 1000},
{webAddr: "0.0.0.0:8080", webAllowPublic: false, webLogMax: 1000},
},
},
{
name: "web log capacity",
instances: []proxyOptions{
{webAddr: "127.0.0.1:8080", webLogMax: 100, webLogMaxSet: true},
{webAddr: "127.0.0.1:8080", webLogMax: 200, webLogMaxSet: true},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := collectProxyAdminOptions(tt.instances); err == nil {
t.Fatal("expected conflicting admin options to be rejected")
}
})
}
}

func TestCollectProxyAdminOptionsAcceptsConsistentSettings(t *testing.T) {
instances := []proxyOptions{
{
statsAddr: "127.0.0.1:9090",
statsAllowPublic: false,
webAddr: "127.0.0.1:8080",
webAllowPublic: false,
webLogMax: 200,
webLogMaxSet: true,
},
{
statsAddr: "127.0.0.1:9090",
statsAllowPublic: false,
webAddr: "127.0.0.1:8080",
webAllowPublic: false,
webLogMax: 200,
},
}
got, err := collectProxyAdminOptions(instances)
if err != nil {
t.Fatal(err)
}
if got.statsAddr != "127.0.0.1:9090" || got.webAddr != "127.0.0.1:8080" || got.webLogMax != 200 {
t.Fatalf("unexpected consolidated options: %+v", got)
}

for _, instances := range [][]proxyOptions{
{
{webAddr: "127.0.0.1:8080"},
{webAddr: "127.0.0.1:8080", webLogMax: 200, webLogMaxSet: true},
},
{
{webAddr: "127.0.0.1:8080", webLogMax: 200, webLogMaxSet: true},
{webAddr: "127.0.0.1:8080"},
},
} {
got, err := collectProxyAdminOptions(instances)
if err != nil {
t.Fatalf("omitted capacity should not conflict: %v", err)
}
if got.webLogMax != 200 {
t.Fatalf("webLogMax=%d, want explicit value 200", got.webLogMax)
}
}
}