添加对于Xboard的完整支持
This commit is contained in:
12
.config.example
Normal file
12
.config.example
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "xboard",
|
||||
"panel_url": "https://your-panel.com",
|
||||
"key": "your-node-key",
|
||||
"node_id": 1,
|
||||
"sync_interval": "1m",
|
||||
"report_interval": "1m"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type ManagedSSMServer interface {
|
||||
Inbound
|
||||
SetTracker(tracker SSMTracker)
|
||||
UpdateUsers(users []string, uPSKs []string) error
|
||||
UpdateUsers(users []string, uPSKs []string, flows []string) error
|
||||
}
|
||||
|
||||
type SSMTracker interface {
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
originca "github.com/sagernet/sing-box/service/origin_ca"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-box/service/ssmapi"
|
||||
"github.com/sagernet/sing-box/service/xboard"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
@@ -133,6 +134,7 @@ func ServiceRegistry() *service.Registry {
|
||||
|
||||
resolved.RegisterService(registry)
|
||||
ssmapi.RegisterService(registry)
|
||||
xboard.RegisterService(registry)
|
||||
|
||||
registerDERPService(registry)
|
||||
registerCCMService(registry)
|
||||
|
||||
201
install.sh
Normal file
201
install.sh
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
|
||||
# sing-box Xboard Integration Installation Script
|
||||
# This script automates the installation and configuration of sing-box with Xboard support.
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
CONFIG_DIR="/etc/sing-box"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
BINARY_PATH="/usr/local/bin/sing-box"
|
||||
SERVICE_FILE="/etc/systemd/system/sing-box.service"
|
||||
|
||||
echo -e "${GREEN}Welcome to sing-box Xboard Installation Script${NC}"
|
||||
|
||||
# Check root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}This script must be run as root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect Architecture
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64) BINARY_ARCH="amd64" ;;
|
||||
aarch64) BINARY_ARCH="arm64" ;;
|
||||
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Interactive Prompts
|
||||
read -p "Enter Panel URL (e.g., https://yourbase.com): " PANEL_URL
|
||||
read -p "Enter Node ID: " NODE_ID
|
||||
read -p "Enter Panel Token (Node Key): " PANEL_TOKEN
|
||||
|
||||
if [[ -z "$PANEL_URL" || -z "$NODE_ID" || -z "$PANEL_TOKEN" ]]; then
|
||||
echo -e "${RED}All fields are required!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up trailing slash
|
||||
PANEL_URL="${PANEL_URL%/}"
|
||||
|
||||
# Prepare directories
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p "/var/lib/sing-box"
|
||||
|
||||
# Check and Install Go
|
||||
install_go() {
|
||||
echo -e "${YELLOW}Checking Go environment...${NC}"
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//' | cut -d. -f1,2)
|
||||
if [[ "$(printf '%s\n' "1.24" "$GO_VERSION" | sort -V | head -n1)" == "1.24" ]]; then
|
||||
echo -e "${GREEN}Go $GO_VERSION already installed.${NC}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Installing Go 1.24.7...${NC}"
|
||||
GO_TAR="go1.24.7.linux-$BINARY_ARCH.tar.gz"
|
||||
curl -L "https://golang.org/dl/$GO_TAR" -o "$GO_TAR"
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf "$GO_TAR"
|
||||
rm "$GO_TAR"
|
||||
|
||||
# Add to PATH for current session
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
if ! grep -q "/usr/local/go/bin" ~/.bashrc; then
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
|
||||
fi
|
||||
echo -e "${GREEN}Go installed successfully.${NC}"
|
||||
}
|
||||
|
||||
# Build sing-box
|
||||
build_sing_box() {
|
||||
echo -e "${YELLOW}Building sing-box from source...${NC}"
|
||||
|
||||
# Check if we are in the source directory
|
||||
if [[ ! -f "go.mod" ]]; then
|
||||
echo -e "${YELLOW}Source not found in current directory. Cloning repository...${NC}"
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Installing git...${NC}"
|
||||
apt-get update && apt-get install -y git || yum install -y git
|
||||
fi
|
||||
# Replace with your repository URL
|
||||
git clone https://github.com/sagernet/sing-box.git sing-box-src
|
||||
cd sing-box-src
|
||||
fi
|
||||
|
||||
# Build params from Makefile
|
||||
VERSION=$(git rev-parse --short HEAD 2>/dev/null || echo "custom")
|
||||
TAGS="with_quic,with_shadowsocks,with_v2rayapi,with_utls,with_clash_api,with_gvisor"
|
||||
|
||||
echo -e "${YELLOW}Running go build...${NC}"
|
||||
go build -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$VERSION' -s -w" -tags "$TAGS" ./cmd/sing-box
|
||||
|
||||
mv sing-box "$BINARY_PATH"
|
||||
chmod +x "$BINARY_PATH"
|
||||
echo -e "${GREEN}sing-box built and installed to $BINARY_PATH${NC}"
|
||||
}
|
||||
|
||||
install_go
|
||||
build_sing_box
|
||||
|
||||
# Generate Configuration
|
||||
echo -e "${YELLOW}Generating configuration...${NC}"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
{
|
||||
"log": {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"path": "/var/lib/sing-box/cache.db"
|
||||
}
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"type": "xboard",
|
||||
"panel_url": "$PANEL_URL",
|
||||
"key": "$PANEL_TOKEN",
|
||||
"node_id": $NODE_ID,
|
||||
"sync_interval": "1m",
|
||||
"report_interval": "1m"
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"sniff": true,
|
||||
"sniff_override_destination": true,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "www.google.com",
|
||||
"reality": {
|
||||
"enabled": true,
|
||||
"handshake": {
|
||||
"server": "www.google.com",
|
||||
"server_port": 443
|
||||
},
|
||||
"private_key": "YOUR_PRIVATE_KEY",
|
||||
"short_id": ["YOUR_SHORT_ID"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo -e "${YELLOW}NOTE: VLESS+Reality template created. Please update 'private_key' and 'short_id' in $CONFIG_FILE if you use Reality.${NC}"
|
||||
|
||||
# Create Systemd Service
|
||||
echo -e "${YELLOW}Creating systemd service...${NC}"
|
||||
cat > "$SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=sing-box service
|
||||
After=network.target nss-lookup.target
|
||||
|
||||
[Service]
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||
ExecStart=$BINARY_PATH run -c $CONFIG_FILE
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
LimitNOFILE=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Reload and Start
|
||||
systemctl daemon-reload
|
||||
echo -e "${GREEN}Service created. You can start it with: systemctl start sing-box${NC}"
|
||||
echo -e "${GREEN}Check status with: systemctl status sing-box${NC}"
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
@@ -35,6 +36,47 @@ type Inbound struct {
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service *anytls.Service
|
||||
options option.AnyTLSInboundOptions
|
||||
tracker adapter.SSMTracker
|
||||
ssmMutex sync.RWMutex
|
||||
}
|
||||
|
||||
var _ adapter.ManagedSSMServer = (*Inbound)(nil)
|
||||
|
||||
func (h *Inbound) SetTracker(tracker adapter.SSMTracker) {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
h.tracker = tracker
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []string, passwords []string, flows []string) error {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
|
||||
paddingScheme := padding.DefaultPaddingScheme
|
||||
if len(h.options.PaddingScheme) > 0 {
|
||||
paddingScheme = []byte(strings.Join(h.options.PaddingScheme, "\n"))
|
||||
}
|
||||
|
||||
anytlsUsers := make([]anytls.User, len(users))
|
||||
for i := range users {
|
||||
anytlsUsers[i] = anytls.User{
|
||||
Name: users[i],
|
||||
Password: passwords[i],
|
||||
}
|
||||
}
|
||||
|
||||
service, err := anytls.NewService(anytls.ServiceConfig{
|
||||
Users: anytlsUsers,
|
||||
PaddingScheme: paddingScheme,
|
||||
Handler: (*inboundHandler)(h),
|
||||
Logger: h.logger,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.service = service
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSInboundOptions) (adapter.Inbound, error) {
|
||||
@@ -42,6 +84,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
Adapter: inbound.NewAdapter(C.TypeAnyTLS, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
options: options,
|
||||
}
|
||||
|
||||
if options.TLS != nil && options.TLS.Enabled {
|
||||
@@ -106,7 +149,14 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||
h.ssmMutex.RLock()
|
||||
tracker := h.tracker
|
||||
service := h.service
|
||||
h.ssmMutex.RUnlock()
|
||||
if tracker != nil {
|
||||
conn = tracker.TrackConnection(conn, metadata)
|
||||
}
|
||||
err := service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
|
||||
@@ -122,7 +122,7 @@ func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) {
|
||||
h.tracker = tracker
|
||||
}
|
||||
|
||||
func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error {
|
||||
func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string, flows []string) error {
|
||||
err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
|
||||
return index
|
||||
}), uPSKs)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
@@ -43,6 +44,45 @@ type Inbound struct {
|
||||
service *vless.Service[int]
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
tracker adapter.SSMTracker
|
||||
ssmMutex sync.RWMutex
|
||||
}
|
||||
|
||||
var _ adapter.ManagedSSMServer = (*Inbound)(nil)
|
||||
|
||||
func (h *Inbound) SetTracker(tracker adapter.SSMTracker) {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
h.tracker = tracker
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []string, uuids []string, flows []string) error {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
newUsers := make([]option.VLESSUser, len(users))
|
||||
for i := range users {
|
||||
flow := ""
|
||||
if i < len(flows) {
|
||||
flow = flows[i]
|
||||
}
|
||||
if flow == "" {
|
||||
flow = "xtls-rprx-vision"
|
||||
}
|
||||
newUsers[i] = option.VLESSUser{
|
||||
Name: users[i],
|
||||
UUID: uuids[i],
|
||||
Flow: flow,
|
||||
}
|
||||
}
|
||||
h.users = newUsers
|
||||
h.service.UpdateUsers(common.MapIndexed(h.users, func(index int, _ option.VLESSUser) int {
|
||||
return index
|
||||
}), common.Map(h.users, func(it option.VLESSUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(h.users, func(it option.VLESSUser) string {
|
||||
return it.Flow
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
|
||||
@@ -157,6 +197,12 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
h.ssmMutex.RLock()
|
||||
tracker := h.tracker
|
||||
h.ssmMutex.RUnlock()
|
||||
if tracker != nil {
|
||||
conn = tracker.TrackConnection(conn, metadata)
|
||||
}
|
||||
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
@@ -203,6 +249,12 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
||||
}
|
||||
h.ssmMutex.RLock()
|
||||
tracker := h.tracker
|
||||
h.ssmMutex.RUnlock()
|
||||
if tracker != nil {
|
||||
conn = tracker.TrackPacketConnection(conn, metadata)
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
@@ -44,6 +45,36 @@ type Inbound struct {
|
||||
users []option.VMessUser
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
tracker adapter.SSMTracker
|
||||
ssmMutex sync.RWMutex
|
||||
}
|
||||
|
||||
var _ adapter.ManagedSSMServer = (*Inbound)(nil)
|
||||
|
||||
func (h *Inbound) SetTracker(tracker adapter.SSMTracker) {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
h.tracker = tracker
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []string, uuids []string, flows []string) error {
|
||||
h.ssmMutex.Lock()
|
||||
defer h.ssmMutex.Unlock()
|
||||
newUsers := make([]option.VMessUser, len(users))
|
||||
for i := range users {
|
||||
newUsers[i] = option.VMessUser{
|
||||
Name: users[i],
|
||||
UUID: uuids[i],
|
||||
}
|
||||
}
|
||||
h.users = newUsers
|
||||
return h.service.UpdateUsers(common.MapIndexed(h.users, func(index int, it option.VMessUser) int {
|
||||
return index
|
||||
}), common.Map(h.users, func(it option.VMessUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(h.users, func(it option.VMessUser) int {
|
||||
return it.AlterId
|
||||
}))
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) {
|
||||
@@ -163,6 +194,12 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
h.ssmMutex.RLock()
|
||||
tracker := h.tracker
|
||||
h.ssmMutex.RUnlock()
|
||||
if tracker != nil {
|
||||
conn = tracker.TrackConnection(conn, metadata)
|
||||
}
|
||||
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
@@ -209,6 +246,12 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
||||
}
|
||||
h.ssmMutex.RLock()
|
||||
tracker := h.tracker
|
||||
h.ssmMutex.RUnlock()
|
||||
if tracker != nil {
|
||||
conn = tracker.TrackPacketConnection(conn, metadata)
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func (m *UserManager) postUpdate(updated bool) error {
|
||||
users = append(users, username)
|
||||
uPSKs = append(uPSKs, password)
|
||||
}
|
||||
err := m.server.UpdateUsers(users, uPSKs)
|
||||
err := m.server.UpdateUsers(users, uPSKs, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
294
service/xboard/service.go
Normal file
294
service/xboard/service.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package xboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/ssmapi"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.XBoardServiceOptions](registry, C.TypeXBoard, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
options option.XBoardServiceOptions
|
||||
traffics map[string]*ssmapi.TrafficManager
|
||||
users map[string]*ssmapi.UserManager
|
||||
servers map[string]adapter.ManagedSSMServer
|
||||
localUsers map[string]userData
|
||||
inboundTags []string
|
||||
syncTicker *time.Ticker
|
||||
reportTicker *time.Ticker
|
||||
access sync.Mutex
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.XBoardServiceOptions) (adapter.Service, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeXBoard, tag),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
options: options,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
traffics: make(map[string]*ssmapi.TrafficManager),
|
||||
users: make(map[string]*ssmapi.UserManager),
|
||||
servers: make(map[string]adapter.ManagedSSMServer),
|
||||
syncTicker: time.NewTicker(time.Duration(options.SyncInterval)),
|
||||
reportTicker: time.NewTicker(time.Duration(options.ReportInterval)),
|
||||
}
|
||||
|
||||
if s.options.SyncInterval == 0 {
|
||||
s.syncTicker.Stop()
|
||||
s.syncTicker = time.NewTicker(1 * time.Minute)
|
||||
}
|
||||
if s.options.ReportInterval == 0 {
|
||||
s.reportTicker.Stop()
|
||||
s.reportTicker = time.NewTicker(1 * time.Minute)
|
||||
}
|
||||
|
||||
inboundManager := service.FromContext[adapter.InboundManager](ctx)
|
||||
allInbounds := inboundManager.List()
|
||||
for _, inbound := range allInbounds {
|
||||
managedServer, isManaged := inbound.(adapter.ManagedSSMServer)
|
||||
if isManaged {
|
||||
traffic := ssmapi.NewTrafficManager()
|
||||
managedServer.SetTracker(traffic)
|
||||
user := ssmapi.NewUserManager(managedServer, traffic)
|
||||
s.traffics[inbound.Tag()] = traffic
|
||||
s.users[inbound.Tag()] = user
|
||||
s.servers[inbound.Tag()] = managedServer
|
||||
s.inboundTags = append(s.inboundTags, inbound.Tag())
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.inboundTags) == 0 {
|
||||
logger.Warn("Xboard service: no managed inbounds found")
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
go s.loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) loop() {
|
||||
// Initial sync
|
||||
s.syncUsers()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.syncTicker.C:
|
||||
s.syncUsers()
|
||||
case <-s.reportTicker.C:
|
||||
s.reportTraffic()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type userData struct {
|
||||
ID int
|
||||
Email string
|
||||
Key string
|
||||
Flow string
|
||||
}
|
||||
|
||||
func (s *Service) syncUsers() {
|
||||
s.logger.Info("Xboard sync users...")
|
||||
xUsers, err := s.fetchUsers()
|
||||
if err != nil {
|
||||
s.logger.Error("Xboard sync error: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
|
||||
newUsers := make(map[string]userData)
|
||||
for _, u := range xUsers {
|
||||
key := u.UUID
|
||||
if key == "" {
|
||||
key = u.Passwd
|
||||
}
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
newUsers[u.Email] = userData{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Key: key,
|
||||
Flow: u.Flow,
|
||||
}
|
||||
}
|
||||
|
||||
for tag, server := range s.servers {
|
||||
// Update users in each manager
|
||||
users := make([]string, 0, len(newUsers))
|
||||
keys := make([]string, 0, len(newUsers))
|
||||
flows := make([]string, 0, len(newUsers))
|
||||
for _, u := range newUsers {
|
||||
users = append(users, u.Email)
|
||||
keys = append(keys, u.Key)
|
||||
flows = append(flows, u.Flow)
|
||||
}
|
||||
err = server.UpdateUsers(users, keys, flows)
|
||||
if err != nil {
|
||||
s.logger.Error("Update users for inbound ", tag, ": ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update local ID mapping
|
||||
s.localUsers = newUsers
|
||||
|
||||
s.logger.Info("Xboard sync completed, total users: ", len(xUsers))
|
||||
}
|
||||
|
||||
func (s *Service) reportTraffic() {
|
||||
s.logger.Trace("Xboard reporting traffic...")
|
||||
|
||||
s.access.Lock()
|
||||
localUsers := s.localUsers
|
||||
s.access.Unlock()
|
||||
|
||||
if len(localUsers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type pushItem struct {
|
||||
UserID int `json:"user_id"`
|
||||
U int64 `json:"u"`
|
||||
D int64 `json:"d"`
|
||||
}
|
||||
|
||||
usageMap := make(map[int]*pushItem)
|
||||
|
||||
for _, trafficManager := range s.traffics {
|
||||
users := make([]*ssmapi.UserObject, 0, len(localUsers))
|
||||
for email := range localUsers {
|
||||
users = append(users, &ssmapi.UserObject{UserName: email})
|
||||
}
|
||||
|
||||
// Read incremental usage
|
||||
trafficManager.ReadUsers(users, true)
|
||||
|
||||
for _, u := range users {
|
||||
if u.UplinkBytes == 0 && u.DownlinkBytes == 0 {
|
||||
continue
|
||||
}
|
||||
meta, ok := localUsers[u.UserName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
item, ok := usageMap[meta.ID]
|
||||
if !ok {
|
||||
item = &pushItem{UserID: meta.ID}
|
||||
usageMap[meta.ID] = item
|
||||
}
|
||||
item.U += u.UplinkBytes
|
||||
item.D += u.DownlinkBytes
|
||||
}
|
||||
}
|
||||
|
||||
if len(usageMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pushData := make([]*pushItem, 0, len(usageMap))
|
||||
for _, item := range usageMap {
|
||||
pushData = append(pushData, item)
|
||||
}
|
||||
|
||||
err := s.pushTraffic(pushData)
|
||||
if err != nil {
|
||||
s.logger.Error("Xboard report error: ", err)
|
||||
} else {
|
||||
s.logger.Info("Xboard report completed, users reported: ", len(pushData))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) pushTraffic(data any) error {
|
||||
url := fmt.Sprintf("%s/api/v1/server/UniProxy/push?node_id=%d", s.options.PanelURL, s.options.NodeID)
|
||||
body, _ := json.Marshal(data)
|
||||
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body))
|
||||
req.Header.Set("Authorization", s.options.Key)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return E.New("failed to push traffic, status: ", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
s.cancel()
|
||||
s.syncTicker.Stop()
|
||||
s.reportTicker.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Xboard User Model
|
||||
type XUser struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
UUID string `json:"uuid"`
|
||||
Passwd string `json:"passwd"`
|
||||
Flow string `json:"flow"`
|
||||
}
|
||||
|
||||
func (s *Service) fetchUsers() ([]XUser, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/server/UniProxy/user?node_id=%d", s.options.PanelURL, s.options.NodeID)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", s.options.Key)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, E.New("failed to fetch users, status: ", resp.Status)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data []XUser `json:"data"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user