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
+29 -4
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -17,6 +18,12 @@ import (
"github.com/go-viper/mapstructure/v2"
)
var (
trafficMapPool = sync.Pool{New: func() interface{} { return make(map[string][2]int64) }}
aliveMapPool = sync.Pool{New: func() interface{} { return make(map[string][]string) }}
onlineMapPool = sync.Pool{New: func() interface{} { return make(map[string]int) }}
)
// Client communicates with the Xboard panel API
type Client struct {
baseURL string
@@ -78,30 +85,48 @@ func (c *Client) Report(traffic map[int][2]int64, alive map[int][]string, online
cpu float64, mem, swap, disk [2]uint64,
metrics map[string]interface{},
) error {
payload := map[string]interface{}{}
payload := make(map[string]interface{})
if len(traffic) > 0 {
t := make(map[string][2]int64, len(traffic))
t := trafficMapPool.Get().(map[string][2]int64)
for uid, d := range traffic {
t[strconv.Itoa(uid)] = d
}
payload["traffic"] = t
defer func() {
for k := range t {
delete(t, k)
}
trafficMapPool.Put(t)
}()
}
if len(alive) > 0 {
a := make(map[string][]string, len(alive))
a := aliveMapPool.Get().(map[string][]string)
for uid, ips := range alive {
a[strconv.Itoa(uid)] = ips
}
payload["alive"] = a
defer func() {
for k := range a {
delete(a, k)
}
aliveMapPool.Put(a)
}()
}
if len(online) > 0 {
o := make(map[string]int, len(online))
o := onlineMapPool.Get().(map[string]int)
for uid, count := range online {
o[strconv.Itoa(uid)] = count
}
payload["online"] = o
defer func() {
for k := range o {
delete(o, k)
}
onlineMapPool.Put(o)
}()
}
status := map[string]interface{}{
+26
View File
@@ -110,6 +110,32 @@ type NodeConfig struct {
// AnyTLS
PaddingScheme StringOrArray `json:"padding_scheme,omitempty"`
// Mieru
Transport string `json:"transport,omitempty"`
TrafficPattern string `json:"traffic_pattern,omitempty"`
// Multiplex
Multiplex *MultiplexConfig `json:"multiplex,omitempty"`
// Proxy Protocol
AcceptProxyProtocol bool `json:"accept_proxy_protocol,omitempty"`
}
type MultiplexConfig struct {
Enabled bool `json:"enabled"`
Protocol string `json:"protocol"`
MaxConnections int `json:"max_connections"`
MinStreams int `json:"min_streams"`
MaxStreams int `json:"max_streams"`
Padding bool `json:"padding"`
Brutal *BrutalConfig `json:"brutal,omitempty"`
}
type BrutalConfig struct {
Enabled bool `json:"enabled"`
UpMbps int `json:"up_mbps"`
DownMbps int `json:"down_mbps"`
}
// CertConfig holds certificate automation settings from the panel
+55 -22
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"math/rand"
"net/url"
"strconv"
"sync/atomic"
@@ -67,6 +68,7 @@ type WSClient struct {
nodeID int
onEvent func(WSEvent)
onStatus func(WSStatusChange)
onPing func() map[string]interface{}
connected atomic.Bool
}
@@ -74,13 +76,14 @@ type WSClient struct {
// NewWSClient creates a new WebSocket client.
// wsURL is the base WebSocket URL (e.g. "ws://panel.example.com:8076").
// token and nodeID are used for authentication via query parameters.
func NewWSClient(wsURL string, token string, nodeID int, onEvent func(WSEvent), onStatus func(WSStatusChange)) *WSClient {
func NewWSClient(wsURL string, token string, nodeID int, onEvent func(WSEvent), onStatus func(WSStatusChange), onPing func() map[string]interface{}) *WSClient {
return &WSClient{
wsURL: wsURL,
token: token,
nodeID: nodeID,
onEvent: onEvent,
onStatus: onStatus,
onPing: onPing,
}
}
@@ -122,15 +125,19 @@ func (w *WSClient) Run(ctx context.Context) {
backoff = time.Second
}
timer := time.NewTimer(backoff)
// Apply exponential backoff with jitter to prevent thundering herd.
jitter := time.Duration(rand.Int63n(int64(backoff / 5)))
wait := backoff + jitter
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
}
if backoff < 30*time.Second {
backoff = min(backoff*2, 30*time.Second)
if backoff < 60*time.Second {
backoff = min(backoff*2, 60*time.Second)
}
}
}
@@ -186,8 +193,9 @@ func (w *WSClient) connect(ctx context.Context) error {
slog.Info("ws connected and authenticated")
}
// Ping interval: send pong responses to server pings
const pingInterval = 50 * time.Second
// Ping interval: send pong responses to server pings.
// We also use this timer to trigger periodic status pushes (10s interval).
const reportInterval = 10 * time.Second
msgCh := make(chan wsMessage, 16)
errCh := make(chan error, 1)
@@ -211,8 +219,11 @@ func (w *WSClient) connect(ctx context.Context) error {
}
}()
pingTimer := time.NewTimer(pingInterval)
defer pingTimer.Stop()
reportTicker := time.NewTicker(reportInterval)
defer reportTicker.Stop()
// writeCh decouples data collection from network I/O.
writeCh := make(chan wsMessage, 8)
for {
select {
@@ -227,16 +238,38 @@ func (w *WSClient) connect(ctx context.Context) error {
return fmt.Errorf("read: %w", err)
case msg := <-msgCh:
pingTimer.Reset(pingInterval)
w.handleMessage(msg)
case <-pingTimer.C:
// Send pong to keep connection alive
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteJSON(wsMessage{Event: "pong"}); err != nil {
return fmt.Errorf("pong: %w", err)
if msg.Event == "ping" {
// Queue an immediate pong
select {
case writeCh <- wsMessage{Event: "pong"}:
default:
slog.Warn("ws write channel full, skipping pong")
}
}
case <-reportTicker.C:
// Send periodic node.status via WebSocket
if w.onPing != nil {
msg := wsMessage{Event: "node.status"}
if stats := w.onPing(); stats != nil {
data, _ := json.Marshal(stats)
msg.Data = data
msg.Timestamp = time.Now().Unix()
}
select {
case writeCh <- msg:
default:
slog.Warn("ws write channel full, skipping status push (network slow?)")
}
}
case msg := <-writeCh:
// Perform the actual network write asynchronously in this loop.
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteJSON(msg); err != nil {
return fmt.Errorf("write: %w", err)
}
pingTimer.Reset(pingInterval)
}
}
}
@@ -270,7 +303,7 @@ func (w *WSClient) handleDataEvent(msg wsMessage) {
// Helper to unmarshal and decode with weak Typing
decodeData := func(data []byte, target interface{}) error {
var raw map[string]interface{}
var raw any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
@@ -279,25 +312,25 @@ func (w *WSClient) handleDataEvent(msg wsMessage) {
switch msg.Event {
case WSEventSyncConfig:
slog.Info("ws sync config event received")
slog.Debug("ws sync config event received")
var p syncConfigPayload
if err := decodeData(msg.Data, &p); err != nil {
if err := decodeData(msg.Data, &p); err != nil && p.Config.Protocol != "" {
slog.Warn("ws: cannot decode config payload", "error", err)
return
}
event.Config = &p.Config
case WSEventSyncUsers:
slog.Info("ws sync users event received")
slog.Debug("ws sync users event received")
var p syncUsersPayload
if err := decodeData(msg.Data, &p); err != nil {
if err := decodeData(msg.Data, &p); err != nil && len(p.Users) > 0 {
slog.Warn("ws: cannot decode users payload", "error", err)
return
}
event.Users = p.Users
case WSEventSyncUserDelta:
slog.Info("ws sync user delta event received")
slog.Debug("ws sync user delta event received")
var p syncUserDeltaPayload
if err := decodeData(msg.Data, &p); err != nil {
slog.Warn("ws: cannot decode user delta payload", "error", err)
+4 -4
View File
@@ -90,7 +90,7 @@ func TestWSClient_ConnectAndReceiveDataEvents(t *testing.T) {
mu.Lock()
received = append(received, event)
mu.Unlock()
}, nil)
}, nil, func() map[string]interface{} { return nil })
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -187,7 +187,7 @@ func TestWSClient_ReconnectOnDisconnect(t *testing.T) {
defer server.Close()
host := strings.TrimPrefix(server.URL, "http://")
ws := NewWSClient("ws://"+host, "reconnect-token", 1, func(WSEvent) {}, nil)
ws := NewWSClient("ws://"+host, "reconnect-token", 1, func(WSEvent) {}, nil, func() map[string]interface{} { return nil })
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -218,7 +218,7 @@ func TestWSClient_ReconnectOnDisconnect(t *testing.T) {
}
func TestWSClient_FallbackWhenNoServer(t *testing.T) {
ws := NewWSClient("ws://127.0.0.1:19999", "fallback-token", 1, func(WSEvent) {}, nil)
ws := NewWSClient("ws://127.0.0.1:19999", "fallback-token", 1, func(WSEvent) {}, nil, func() map[string]interface{} { return nil })
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
@@ -254,7 +254,7 @@ func TestWSClient_UserDeltaEvent(t *testing.T) {
mu.Lock()
received = append(received, event)
mu.Unlock()
}, nil)
}, nil, func() map[string]interface{} { return nil })
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()