添加对于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 {
|
type ManagedSSMServer interface {
|
||||||
Inbound
|
Inbound
|
||||||
SetTracker(tracker SSMTracker)
|
SetTracker(tracker SSMTracker)
|
||||||
UpdateUsers(users []string, uPSKs []string) error
|
UpdateUsers(users []string, uPSKs []string, flows []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSMTracker interface {
|
type SSMTracker interface {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
originca "github.com/sagernet/sing-box/service/origin_ca"
|
originca "github.com/sagernet/sing-box/service/origin_ca"
|
||||||
"github.com/sagernet/sing-box/service/resolved"
|
"github.com/sagernet/sing-box/service/resolved"
|
||||||
"github.com/sagernet/sing-box/service/ssmapi"
|
"github.com/sagernet/sing-box/service/ssmapi"
|
||||||
|
"github.com/sagernet/sing-box/service/xboard"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ func ServiceRegistry() *service.Registry {
|
|||||||
|
|
||||||
resolved.RegisterService(registry)
|
resolved.RegisterService(registry)
|
||||||
ssmapi.RegisterService(registry)
|
ssmapi.RegisterService(registry)
|
||||||
|
xboard.RegisterService(registry)
|
||||||
|
|
||||||
registerDERPService(registry)
|
registerDERPService(registry)
|
||||||
registerCCMService(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"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
@@ -35,6 +36,47 @@ type Inbound struct {
|
|||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
service *anytls.Service
|
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) {
|
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),
|
Adapter: inbound.NewAdapter(C.TypeAnyTLS, tag),
|
||||||
router: uot.NewRouter(router, logger),
|
router: uot.NewRouter(router, logger),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
options: options,
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.TLS != nil && options.TLS.Enabled {
|
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
|
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 {
|
if err != nil {
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
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
|
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 {
|
err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
|
||||||
return index
|
return index
|
||||||
}), uPSKs)
|
}), uPSKs)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
@@ -43,6 +44,45 @@ type Inbound struct {
|
|||||||
service *vless.Service[int]
|
service *vless.Service[int]
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
transport adapter.V2RayServerTransport
|
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) {
|
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
|
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)
|
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||||
@@ -203,6 +249,12 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
} else {
|
} else {
|
||||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
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)
|
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
@@ -44,6 +45,36 @@ type Inbound struct {
|
|||||||
users []option.VMessUser
|
users []option.VMessUser
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
transport adapter.V2RayServerTransport
|
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) {
|
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
|
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)
|
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||||
@@ -209,6 +246,12 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
} else {
|
} else {
|
||||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
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)
|
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (m *UserManager) postUpdate(updated bool) error {
|
|||||||
users = append(users, username)
|
users = append(users, username)
|
||||||
uPSKs = append(uPSKs, password)
|
uPSKs = append(uPSKs, password)
|
||||||
}
|
}
|
||||||
err := m.server.UpdateUsers(users, uPSKs)
|
err := m.server.UpdateUsers(users, uPSKs, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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