feat: add Mieru protocol and major system performance optimizations

This commit is contained in:
xboard
2026-03-16 22:22:04 +08:00
parent 7a37c15fdc
commit 3e457e8ac6
13 changed files with 343 additions and 337 deletions
+19
View File
@@ -2,6 +2,10 @@ package kernel
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
"github.com/cedar2025/xboard-node/internal/panel"
"golang.org/x/time/rate"
@@ -60,6 +64,21 @@ type Kernel interface {
SetSpeedLimitFunc(fn func(uuid string) *rate.Limiter)
}
// ComputeHash returns a hash of config + user identities that would
// require a kernel restart/reconstruction if changed.
func ComputeHash(nc *panel.NodeConfig, users []panel.User) string {
h := sha256.New()
configData, _ := json.Marshal(nc)
h.Write(configData)
sorted := make([]panel.User, len(users))
copy(sorted, users)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID })
for _, u := range sorted {
fmt.Fprintf(h, "%d:%s,", u.ID, u.UUID)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
// Connection represents an active proxy connection reported by the kernel.
type Connection struct {
ID string
+55
View File
@@ -351,11 +351,33 @@ func buildInbound(nc *panel.NodeConfig, users []panel.User, certFile, keyFile st
return buildSocks(base, users)
case "http":
return buildHTTP(base, nc, users, certFile, keyFile)
case "mieru":
return buildMieru(base, nc, users)
default:
return nil
}
}
func buildMieru(base M, nc *panel.NodeConfig, users []panel.User) M {
base["type"] = "mieru"
if nc.Transport != "" {
base["transport"] = nc.Transport
}
if nc.TrafficPattern != "" {
base["traffic_pattern"] = nc.TrafficPattern
}
userList := make([]M, 0, len(users))
for _, u := range users {
userList = append(userList, M{
"name": u.UUID,
"password": u.UUID,
})
}
base["users"] = userList
return base
}
func buildShadowsocks(base M, nc *panel.NodeConfig, users []panel.User) M {
base["type"] = "shadowsocks"
base["method"] = nc.Cipher
@@ -394,6 +416,8 @@ func buildVMess(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyF
base["users"] = userList
applyTransport(base, nc)
applyMultiplex(base, nc)
if nc.TLS == 1 {
base["tls"] = buildTLSConfig(nc, certFile, keyFile)
}
@@ -418,6 +442,7 @@ func buildVLESS(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyF
base["users"] = userList
applyTransport(base, nc)
applyMultiplex(base, nc)
if nc.TLS == 1 {
base["tls"] = buildTLSConfig(nc, certFile, keyFile)
@@ -441,6 +466,8 @@ func buildTrojan(base M, nc *panel.NodeConfig, users []panel.User, certFile, key
base["users"] = userList
applyTransport(base, nc)
applyMultiplex(base, nc)
if nc.TLS == 1 {
base["tls"] = buildTLSConfig(nc, certFile, keyFile)
} else if nc.TLS == 2 {
@@ -742,3 +769,31 @@ func buildRealityConfig(nc *panel.NodeConfig) M {
tls["reality"] = reality
return tls
}
func applyMultiplex(base M, nc *panel.NodeConfig) {
if nc.Multiplex == nil || !nc.Multiplex.Enabled {
return
}
mux := M{
"enabled": true,
}
if nc.Multiplex.Padding {
mux["padding"] = true
}
if nc.Multiplex.Brutal != nil && nc.Multiplex.Brutal.Enabled {
brutal := M{
"enabled": true,
}
if nc.Multiplex.Brutal.UpMbps > 0 {
brutal["up_mbps"] = nc.Multiplex.Brutal.UpMbps
}
if nc.Multiplex.Brutal.DownMbps > 0 {
brutal["down_mbps"] = nc.Multiplex.Brutal.DownMbps
}
mux["brutal"] = brutal
}
base["multiplex"] = mux
}
+49 -43
View File
@@ -70,7 +70,7 @@ func (s *SingBox) Name() string { return "sing-box" }
func (s *SingBox) Protocols() []string {
return []string{
"vmess", "vless", "trojan", "shadowsocks",
"hysteria2", "tuic", "naive", "socks", "http", "anytls",
"hysteria2", "tuic", "naive", "socks", "http", "anytls", "mieru",
}
}
@@ -175,55 +175,61 @@ func (s *SingBox) Reload(nodeConfig *panel.NodeConfig, users []panel.User, certF
nopFactory := singLog.NewNOPFactory()
// Configuration hash check for inbound reconstruction
configChanged := s.nodeConfig == nil || kernel.ComputeHash(nodeConfig, users) != kernel.ComputeHash(s.nodeConfig, s.users)
for _, inb := range opts.Inbounds {
tag := inb.Tag
if existing, ok := im.Get(tag); ok && existing.Type() == inb.Type {
var err error
switch v := existing.(type) {
case adapter.UpdatableInbound[option.VMessUser]:
if opts, ok := inb.Options.(*option.VMessInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
if !configChanged {
if existing, ok := im.Get(tag); ok && existing.Type() == inb.Type {
var err error
switch v := existing.(type) {
case adapter.UpdatableInbound[option.VMessUser]:
if opts, ok := inb.Options.(*option.VMessInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.VLESSUser]:
if opts, ok := inb.Options.(*option.VLESSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.TrojanUser]:
if opts, ok := inb.Options.(*option.TrojanInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.Hysteria2User]:
if opts, ok := inb.Options.(*option.Hysteria2InboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableShadowsocksInbound:
if opts, ok := inb.Options.(*option.ShadowsocksInboundOptions); ok {
err = v.UpdateUsersByOptions(opts.Users)
}
case adapter.UpdatableInbound[option.TUICUser]:
if opts, ok := inb.Options.(*option.TUICInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.AnyTLSUser]:
if opts, ok := inb.Options.(*option.AnyTLSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[auth.User]:
switch opts := inb.Options.(type) {
case *option.NaiveInboundOptions:
err = v.UpdateUsers(opts.Users)
case *option.SocksInboundOptions:
err = v.UpdateUsers(opts.Users)
case *option.HTTPMixedInboundOptions:
err = v.UpdateUsers(opts.Users)
}
}
case adapter.UpdatableInbound[option.VLESSUser]:
if opts, ok := inb.Options.(*option.VLESSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.TrojanUser]:
if opts, ok := inb.Options.(*option.TrojanInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.Hysteria2User]:
if opts, ok := inb.Options.(*option.Hysteria2InboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableShadowsocksInbound:
if opts, ok := inb.Options.(*option.ShadowsocksInboundOptions); ok {
err = v.UpdateUsersByOptions(opts.Users)
}
case adapter.UpdatableInbound[option.TUICUser]:
if opts, ok := inb.Options.(*option.TUICInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.AnyTLSUser]:
if opts, ok := inb.Options.(*option.AnyTLSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[auth.User]:
switch opts := inb.Options.(type) {
case *option.NaiveInboundOptions:
err = v.UpdateUsers(opts.Users)
case *option.SocksInboundOptions:
err = v.UpdateUsers(opts.Users)
case *option.HTTPMixedInboundOptions:
err = v.UpdateUsers(opts.Users)
if err == nil {
continue
}
slog.Warn("incremental update failed, falling back to recreate", "tag", tag, "error", err)
}
if err == nil {
continue
}
slog.Warn("incremental update failed, falling back to recreate", "tag", tag, "error", err)
}
// Config mismatch or incremental update failure: Reconstruct Inbound.
// Remove the existing inbound first so im.Create() can bind the same port.
// im.Create() cannot atomically swap a TCP listener — it tries to start the
// new socket before the old one is closed, causing "address already in use".
+10
View File
@@ -460,6 +460,16 @@ func applyStreamSettings(base M, nc *panel.NodeConfig, certFile, keyFile string)
ss["realitySettings"] = buildRealitySettings(nc)
}
// Proxy Protocol
if nc.AcceptProxyProtocol {
sockopt, ok := base["streamSettings"].(M)["sockopt"].(M)
if !ok {
sockopt = M{}
}
sockopt["acceptProxyProtocol"] = true
ss["sockopt"] = sockopt
}
base["streamSettings"] = ss
}
+8 -21
View File
@@ -8,7 +8,6 @@ import (
"fmt"
"log/slog"
"os"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -152,7 +151,7 @@ func (x *Xray) Start(nodeConfig *panel.NodeConfig, users []panel.User, certFile,
x.protocol = nodeConfig.Protocol
x.inboundTag = nodeConfig.Protocol + "-in"
x.cumTraffic = make(map[int][2]int64)
x.lastKernelHash = computeKernelHash(nodeConfig, users)
x.lastKernelHash = kernel.ComputeHash(nodeConfig, users)
x.running.Store(true)
x.mu.Unlock()
@@ -165,23 +164,26 @@ func (x *Xray) Start(nodeConfig *panel.NodeConfig, users []panel.User, certFile,
return nil
}
// Reload hot-swaps dispatcher limits and, only when kernel-affecting fields
// have changed (user identities, routes, protocol), triggers a full restart.
// Reload updates dispatcher limits and handles configuration changes.
// Since Xray does not support granular inbound reconstruction without an
// instance restart for most transport/TLS settings, it triggers a full restart
// if any kernel-affecting fields (hash mismatch) have changed.
func (x *Xray) Reload(nodeConfig *panel.NodeConfig, users []panel.User, certFile, keyFile string) error {
x.updateDispatcherLimits(users)
newHash := computeKernelHash(nodeConfig, users)
newHash := kernel.ComputeHash(nodeConfig, users)
x.mu.Lock()
same := x.lastKernelHash == newHash
if same {
x.users = users
x.mu.Unlock()
slog.Info("xray: limits updated, kernel unchanged")
slog.Debug("xray: limits updated, kernel configuration unchanged")
return nil
}
x.mu.Unlock()
slog.Info("xray: configuration changed, performing full restart")
return x.Start(nodeConfig, users, certFile, keyFile)
}
@@ -745,18 +747,3 @@ func (x *Xray) updateDispatcherLimits(users []panel.User) {
// so that concurrent Xray instances in multi-node mode each capture their
// own LimitDispatcher.
var xrayCreationMu sync.Mutex
// computeKernelHash returns a hash of config + user identities that would
// require a kernel restart if changed. Limit-only changes are excluded.
func computeKernelHash(nc *panel.NodeConfig, users []panel.User) string {
h := sha256.New()
configData, _ := json.Marshal(nc)
h.Write(configData)
sorted := make([]panel.User, len(users))
copy(sorted, users)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID })
for _, u := range sorted {
fmt.Fprintf(h, "%d:%s,", u.ID, u.UUID)
}
return fmt.Sprintf("%x", h.Sum(nil))
}