多节点聚合
This commit is contained in:
112
install.sh
112
install.sh
@@ -123,32 +123,106 @@ fi
|
|||||||
read -p "Enter Panel URL [${PANEL_URL}]: " INPUT_URL
|
read -p "Enter Panel URL [${PANEL_URL}]: " INPUT_URL
|
||||||
PANEL_URL=${INPUT_URL:-$PANEL_URL}
|
PANEL_URL=${INPUT_URL:-$PANEL_URL}
|
||||||
|
|
||||||
read -p "Enter Node ID [${NODE_ID}]: " INPUT_ID
|
|
||||||
NODE_ID=${INPUT_ID:-$NODE_ID}
|
|
||||||
|
|
||||||
read -p "Enter Node Type (e.g., v2ray) [${NODE_TYPE:-v2ray}]: " INPUT_TYPE
|
|
||||||
NODE_TYPE=${INPUT_TYPE:-${NODE_TYPE:-v2ray}}
|
|
||||||
|
|
||||||
read -p "Enter Panel Token (Node Key) [${PANEL_TOKEN}]: " INPUT_TOKEN
|
read -p "Enter Panel Token (Node Key) [${PANEL_TOKEN}]: " INPUT_TOKEN
|
||||||
PANEL_TOKEN=${INPUT_TOKEN:-$PANEL_TOKEN}
|
PANEL_TOKEN=${INPUT_TOKEN:-$PANEL_TOKEN}
|
||||||
|
|
||||||
# Consolidation
|
read -p "Enter Node Count [${NODE_COUNT:-1}]: " INPUT_COUNT
|
||||||
CONFIG_PANEL_URL=$PANEL_URL
|
NODE_COUNT=${INPUT_COUNT:-${NODE_COUNT:-1}}
|
||||||
CONFIG_NODE_ID=$NODE_ID
|
|
||||||
USER_PANEL_URL=$PANEL_URL
|
if ! [[ "$NODE_COUNT" =~ ^[0-9]+$ ]] || [[ "$NODE_COUNT" -lt 1 ]]; then
|
||||||
USER_NODE_ID=$NODE_ID
|
echo -e "${RED}Node Count must be a positive integer${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -a NODE_IDS
|
||||||
|
declare -a NODE_TYPES
|
||||||
|
declare -a NODE_TAGS
|
||||||
|
|
||||||
|
for ((i=1; i<=NODE_COUNT; i++)); do
|
||||||
|
DEFAULT_NODE_ID=""
|
||||||
|
DEFAULT_NODE_TYPE="${NODE_TYPE:-vless}"
|
||||||
|
DEFAULT_NODE_TAG=""
|
||||||
|
if [[ "$i" -eq 1 && -n "$NODE_ID" ]]; then
|
||||||
|
DEFAULT_NODE_ID="$NODE_ID"
|
||||||
|
fi
|
||||||
|
read -p "Enter Node ID for node #$i [${DEFAULT_NODE_ID}]: " INPUT_ID
|
||||||
|
CURRENT_NODE_ID=${INPUT_ID:-$DEFAULT_NODE_ID}
|
||||||
|
if [[ -z "$CURRENT_NODE_ID" ]]; then
|
||||||
|
echo -e "${RED}Node ID is required for node #$i${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
read -p "Enter Node Type for node #$i [${DEFAULT_NODE_TYPE}]: " INPUT_TYPE
|
||||||
|
CURRENT_NODE_TYPE=${INPUT_TYPE:-$DEFAULT_NODE_TYPE}
|
||||||
|
read -p "Enter Tag for node #$i (optional) [${DEFAULT_NODE_TAG}]: " INPUT_TAG
|
||||||
|
CURRENT_NODE_TAG=${INPUT_TAG:-$DEFAULT_NODE_TAG}
|
||||||
|
NODE_IDS+=("$CURRENT_NODE_ID")
|
||||||
|
NODE_TYPES+=("$CURRENT_NODE_TYPE")
|
||||||
|
NODE_TAGS+=("$CURRENT_NODE_TAG")
|
||||||
|
done
|
||||||
|
|
||||||
# Sync time (Critical for SS 2022)
|
# Sync time (Critical for SS 2022)
|
||||||
echo -e "${YELLOW}Syncing system time...${NC}"
|
echo -e "${YELLOW}Syncing system time...${NC}"
|
||||||
timedatectl set-ntp true || true
|
timedatectl set-ntp true || true
|
||||||
|
|
||||||
if [[ -z "$PANEL_URL" || -z "$NODE_ID" || -z "$PANEL_TOKEN" ]]; then
|
if [[ -z "$PANEL_URL" || -z "$PANEL_TOKEN" ]]; then
|
||||||
echo -e "${RED}All fields are required!${NC}"
|
echo -e "${RED}All fields are required!${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up trailing slash
|
# Clean up trailing slash
|
||||||
PANEL_URL="${PANEL_URL%/}"
|
PANEL_URL="${PANEL_URL%/}"
|
||||||
|
CONFIG_PANEL_URL=$PANEL_URL
|
||||||
|
USER_PANEL_URL=$PANEL_URL
|
||||||
|
|
||||||
|
SERVICE_JSON=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"type": "xboard",
|
||||||
|
"panel_url": "$PANEL_URL",
|
||||||
|
"config_panel_url": "$CONFIG_PANEL_URL",
|
||||||
|
"user_panel_url": "$USER_PANEL_URL",
|
||||||
|
"key": "$PANEL_TOKEN",
|
||||||
|
"sync_interval": "1m",
|
||||||
|
"report_interval": "1m"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "$NODE_COUNT" -eq 1 ]]; then
|
||||||
|
SERVICE_JSON+=$(cat <<EOF
|
||||||
|
,
|
||||||
|
"node_id": ${NODE_IDS[0]},
|
||||||
|
"node_type": "${NODE_TYPES[0]}",
|
||||||
|
"config_node_id": ${NODE_IDS[0]},
|
||||||
|
"user_node_id": ${NODE_IDS[0]}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SERVICE_JSON+=$',
|
||||||
|
"nodes": ['
|
||||||
|
for ((i=0; i<NODE_COUNT; i++)); do
|
||||||
|
NODE_BLOCK=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"node_id": ${NODE_IDS[$i]},
|
||||||
|
"config_node_id": ${NODE_IDS[$i]},
|
||||||
|
"user_node_id": ${NODE_IDS[$i]},
|
||||||
|
"node_type": "${NODE_TYPES[$i]}"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
if [[ -n "${NODE_TAGS[$i]}" ]]; then
|
||||||
|
NODE_BLOCK+=$(cat <<EOF
|
||||||
|
,
|
||||||
|
"tag": "${NODE_TAGS[$i]}"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
NODE_BLOCK+=$'\n }'
|
||||||
|
if [[ "$i" -gt 0 ]]; then
|
||||||
|
SERVICE_JSON+=$','
|
||||||
|
fi
|
||||||
|
SERVICE_JSON+=$'\n'"$NODE_BLOCK"
|
||||||
|
done
|
||||||
|
SERVICE_JSON+=$'\n ]'
|
||||||
|
fi
|
||||||
|
SERVICE_JSON+=$'\n }'
|
||||||
|
|
||||||
# Generate Configuration
|
# Generate Configuration
|
||||||
echo -e "${YELLOW}Generating configuration...${NC}"
|
echo -e "${YELLOW}Generating configuration...${NC}"
|
||||||
@@ -175,19 +249,7 @@ cat > "$CONFIG_FILE" <<EOF
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"services": [
|
"services": [
|
||||||
{
|
${SERVICE_JSON}
|
||||||
"type": "xboard",
|
|
||||||
"panel_url": "$PANEL_URL",
|
|
||||||
"config_panel_url": "$CONFIG_PANEL_URL",
|
|
||||||
"user_panel_url": "$USER_PANEL_URL",
|
|
||||||
"key": "$PANEL_TOKEN",
|
|
||||||
"node_id": $NODE_ID,
|
|
||||||
"node_type": "$NODE_TYPE",
|
|
||||||
"config_node_id": ${CONFIG_NODE_ID:-0},
|
|
||||||
"user_node_id": ${USER_NODE_ID:-0},
|
|
||||||
"sync_interval": "1m",
|
|
||||||
"report_interval": "1m"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"inbounds": [],
|
"inbounds": [],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
|
|||||||
@@ -15,4 +15,19 @@ type XBoardServiceOptions struct {
|
|||||||
NodeType string `json:"node_type"`
|
NodeType string `json:"node_type"`
|
||||||
SyncInterval badoption.Duration `json:"sync_interval,omitempty"`
|
SyncInterval badoption.Duration `json:"sync_interval,omitempty"`
|
||||||
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
||||||
|
Nodes []XBoardNodeOptions `json:"nodes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XBoardNodeOptions struct {
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
PanelURL string `json:"panel_url,omitempty"`
|
||||||
|
ConfigPanelURL string `json:"config_panel_url,omitempty"`
|
||||||
|
UserPanelURL string `json:"user_panel_url,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
NodeID int `json:"node_id,omitempty"`
|
||||||
|
ConfigNodeID int `json:"config_node_id,omitempty"`
|
||||||
|
UserNodeID int `json:"user_node_id,omitempty"`
|
||||||
|
NodeType string `json:"node_type,omitempty"`
|
||||||
|
SyncInterval badoption.Duration `json:"sync_interval,omitempty"`
|
||||||
|
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ func (h *Inbound) UpdateUsers(users []string, uuids []string, flows []string) er
|
|||||||
if i < len(flows) {
|
if i < len(flows) {
|
||||||
flow = flows[i]
|
flow = flows[i]
|
||||||
}
|
}
|
||||||
if flow == "" {
|
|
||||||
flow = "xtls-rprx-vision"
|
|
||||||
}
|
|
||||||
newUsers[i] = option.VLESSUser{
|
newUsers[i] = option.VLESSUser{
|
||||||
Name: users[i],
|
Name: users[i],
|
||||||
UUID: uuids[i],
|
UUID: uuids[i],
|
||||||
|
|||||||
112
service/xboard/multi_service.go
Normal file
112
service/xboard/multi_service.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package xboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiNodeService struct {
|
||||||
|
boxService.Adapter
|
||||||
|
services []adapter.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMultiNodeService(ctx context.Context, logger log.ContextLogger, tag string, options option.XBoardServiceOptions) (adapter.Service, error) {
|
||||||
|
expanded := expandNodeOptions(options)
|
||||||
|
services := make([]adapter.Service, 0, len(expanded))
|
||||||
|
for i, node := range expanded {
|
||||||
|
nodeTag := expandedNodeTag(tag, i, options.Nodes[i], node)
|
||||||
|
service, err := newSingleService(ctx, logger, nodeTag, node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create xboard node service %s: %w", nodeTag, err)
|
||||||
|
}
|
||||||
|
services = append(services, service)
|
||||||
|
}
|
||||||
|
return &multiNodeService{
|
||||||
|
Adapter: boxService.NewAdapter(C.TypeXBoard, tag),
|
||||||
|
services: services,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandNodeOptions(base option.XBoardServiceOptions) []option.XBoardServiceOptions {
|
||||||
|
if len(base.Nodes) == 0 {
|
||||||
|
return []option.XBoardServiceOptions{base}
|
||||||
|
}
|
||||||
|
result := make([]option.XBoardServiceOptions, 0, len(base.Nodes))
|
||||||
|
for _, node := range base.Nodes {
|
||||||
|
child := base
|
||||||
|
child.Nodes = nil
|
||||||
|
if node.PanelURL != "" {
|
||||||
|
child.PanelURL = node.PanelURL
|
||||||
|
}
|
||||||
|
if node.ConfigPanelURL != "" {
|
||||||
|
child.ConfigPanelURL = node.ConfigPanelURL
|
||||||
|
}
|
||||||
|
if node.UserPanelURL != "" {
|
||||||
|
child.UserPanelURL = node.UserPanelURL
|
||||||
|
}
|
||||||
|
if node.Key != "" {
|
||||||
|
child.Key = node.Key
|
||||||
|
}
|
||||||
|
if node.NodeID != 0 {
|
||||||
|
child.NodeID = node.NodeID
|
||||||
|
}
|
||||||
|
if node.ConfigNodeID != 0 {
|
||||||
|
child.ConfigNodeID = node.ConfigNodeID
|
||||||
|
}
|
||||||
|
if node.UserNodeID != 0 {
|
||||||
|
child.UserNodeID = node.UserNodeID
|
||||||
|
}
|
||||||
|
if node.NodeType != "" {
|
||||||
|
child.NodeType = node.NodeType
|
||||||
|
}
|
||||||
|
if node.SyncInterval != 0 {
|
||||||
|
child.SyncInterval = node.SyncInterval
|
||||||
|
}
|
||||||
|
if node.ReportInterval != 0 {
|
||||||
|
child.ReportInterval = node.ReportInterval
|
||||||
|
}
|
||||||
|
result = append(result, child)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandedNodeTag(baseTag string, index int, entry option.XBoardNodeOptions, node option.XBoardServiceOptions) string {
|
||||||
|
nodeTag := ""
|
||||||
|
if entry.Tag != "" {
|
||||||
|
nodeTag = entry.Tag
|
||||||
|
}
|
||||||
|
if nodeTag == "" && node.NodeID != 0 {
|
||||||
|
nodeTag = fmt.Sprintf("%d", node.NodeID)
|
||||||
|
}
|
||||||
|
if nodeTag == "" {
|
||||||
|
nodeTag = fmt.Sprintf("%d", index+1)
|
||||||
|
}
|
||||||
|
if baseTag == "" {
|
||||||
|
return "xboard-" + nodeTag
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s", baseTag, nodeTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *multiNodeService) Start(stage adapter.StartStage) error {
|
||||||
|
for _, service := range s.services {
|
||||||
|
if err := service.Start(stage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *multiNodeService) Close() error {
|
||||||
|
for i := len(s.services) - 1; i >= 0; i-- {
|
||||||
|
if err := s.services[i].Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -74,23 +74,12 @@ type Service struct {
|
|||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
router adapter.Router
|
router adapter.Router
|
||||||
inboundManager adapter.InboundManager
|
inboundManager adapter.InboundManager
|
||||||
|
protocol string
|
||||||
|
vlessFlow string
|
||||||
ssCipher string // stored for user key derivation in syncUsers
|
ssCipher string // stored for user key derivation in syncUsers
|
||||||
ssServerKey string // stored for SS2022 per-user key extraction
|
ssServerKey string // stored for SS2022 per-user key extraction
|
||||||
}
|
}
|
||||||
|
|
||||||
type XBoardServiceOptions struct {
|
|
||||||
PanelURL string `json:"panel_url"`
|
|
||||||
ConfigPanelURL string `json:"config_panel_url,omitempty"`
|
|
||||||
UserPanelURL string `json:"user_panel_url,omitempty"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
NodeID int `json:"node_id"`
|
|
||||||
ConfigNodeID int `json:"config_node_id,omitempty"`
|
|
||||||
UserNodeID int `json:"user_node_id,omitempty"`
|
|
||||||
NodeType string `json:"node_type"`
|
|
||||||
SyncInterval badoption.Duration `json:"sync_interval,omitempty"`
|
|
||||||
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type XNodeConfig struct {
|
type XNodeConfig struct {
|
||||||
NodeType string `json:"node_type"`
|
NodeType string `json:"node_type"`
|
||||||
NodeType_ string `json:"nodeType"`
|
NodeType_ string `json:"nodeType"`
|
||||||
@@ -107,6 +96,13 @@ type XNodeConfig struct {
|
|||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
TLSSettings *XTLSSettings `json:"tls_settings"`
|
TLSSettings *XTLSSettings `json:"tls_settings"`
|
||||||
TLSSettings_ *XTLSSettings `json:"tlsSettings"`
|
TLSSettings_ *XTLSSettings `json:"tlsSettings"`
|
||||||
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
|
PrivateKey string `json:"private_key,omitempty"`
|
||||||
|
ShortID string `json:"short_id,omitempty"`
|
||||||
|
ShortIDs []string `json:"short_ids,omitempty"`
|
||||||
|
Dest string `json:"dest,omitempty"`
|
||||||
|
ServerName string `json:"server_name,omitempty"`
|
||||||
|
ServerPortText string `json:"server_port_text,omitempty"`
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
NetworkSettings json.RawMessage `json:"network_settings"`
|
NetworkSettings json.RawMessage `json:"network_settings"`
|
||||||
NetworkSettings_ json.RawMessage `json:"networkSettings"`
|
NetworkSettings_ json.RawMessage `json:"networkSettings"`
|
||||||
@@ -124,6 +120,24 @@ type XNodeConfig struct {
|
|||||||
|
|
||||||
// AnyTls
|
// AnyTls
|
||||||
PaddingScheme []string `json:"padding_scheme"`
|
PaddingScheme []string `json:"padding_scheme"`
|
||||||
|
|
||||||
|
// TLS certificate settings
|
||||||
|
CertConfig *XCertConfig `json:"cert_config,omitempty"`
|
||||||
|
AutoTLS bool `json:"auto_tls,omitempty"`
|
||||||
|
Domain string `json:"domain,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XCertConfig struct {
|
||||||
|
CertMode string `json:"cert_mode"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
DNSProvider string `json:"dns_provider"`
|
||||||
|
DNSEnv map[string]string `json:"dns_env"`
|
||||||
|
HTTPPort int `json:"http_port"`
|
||||||
|
CertFile string `json:"cert_file"`
|
||||||
|
KeyFile string `json:"key_file"`
|
||||||
|
CertContent string `json:"cert_content"`
|
||||||
|
KeyContent string `json:"key_content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type XInnerConfig struct {
|
type XInnerConfig struct {
|
||||||
@@ -138,6 +152,12 @@ type XInnerConfig struct {
|
|||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
TLSSettings *XTLSSettings `json:"tls_settings"`
|
TLSSettings *XTLSSettings `json:"tls_settings"`
|
||||||
TLSSettings_ *XTLSSettings `json:"tlsSettings"`
|
TLSSettings_ *XTLSSettings `json:"tlsSettings"`
|
||||||
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
|
PrivateKey string `json:"private_key,omitempty"`
|
||||||
|
ShortID string `json:"short_id,omitempty"`
|
||||||
|
ShortIDs []string `json:"short_ids,omitempty"`
|
||||||
|
Dest string `json:"dest,omitempty"`
|
||||||
|
ServerName string `json:"server_name,omitempty"`
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
NetworkSettings json.RawMessage `json:"network_settings"`
|
NetworkSettings json.RawMessage `json:"network_settings"`
|
||||||
NetworkSettings_ json.RawMessage `json:"networkSettings"`
|
NetworkSettings_ json.RawMessage `json:"networkSettings"`
|
||||||
@@ -245,6 +265,13 @@ func (s *XStreamSettings) GetReality() *XRealitySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.XBoardServiceOptions) (adapter.Service, error) {
|
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.XBoardServiceOptions) (adapter.Service, error) {
|
||||||
|
if len(options.Nodes) > 0 {
|
||||||
|
return newMultiNodeService(ctx, logger, tag, options)
|
||||||
|
}
|
||||||
|
return newSingleService(ctx, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSingleService(ctx context.Context, logger log.ContextLogger, tag string, options option.XBoardServiceOptions) (adapter.Service, error) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
s := &Service{
|
s := &Service{
|
||||||
Adapter: boxService.NewAdapter(C.TypeXBoard, tag),
|
Adapter: boxService.NewAdapter(C.TypeXBoard, tag),
|
||||||
@@ -277,6 +304,105 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) inboundTag() string {
|
||||||
|
if s.Tag() != "" {
|
||||||
|
return "xboard-inbound-" + s.Tag()
|
||||||
|
}
|
||||||
|
if s.options.NodeID != 0 {
|
||||||
|
return "xboard-inbound-" + strconv.Itoa(s.options.NodeID)
|
||||||
|
}
|
||||||
|
return "xboard-inbound"
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyCertConfig(tlsOptions *option.InboundTLSOptions, certConfig *XCertConfig) bool {
|
||||||
|
if certConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch certConfig.CertMode {
|
||||||
|
case "", "none":
|
||||||
|
return false
|
||||||
|
case "file":
|
||||||
|
if certConfig.CertFile == "" || certConfig.KeyFile == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tlsOptions.CertificatePath = certConfig.CertFile
|
||||||
|
tlsOptions.KeyPath = certConfig.KeyFile
|
||||||
|
return true
|
||||||
|
case "content":
|
||||||
|
if certConfig.CertContent == "" || certConfig.KeyContent == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tlsOptions.Certificate = badoption.Listable[string]{certConfig.CertContent}
|
||||||
|
tlsOptions.Key = badoption.Listable[string]{certConfig.KeyContent}
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergedTLSSettings(inner XInnerConfig, config *XNodeConfig) *XTLSSettings {
|
||||||
|
tlsSettings := inner.TLSSettings
|
||||||
|
if tlsSettings == nil {
|
||||||
|
tlsSettings = inner.TLSSettings_
|
||||||
|
}
|
||||||
|
if tlsSettings == nil && config != nil {
|
||||||
|
tlsSettings = config.TLSSettings
|
||||||
|
}
|
||||||
|
if tlsSettings == nil && config != nil {
|
||||||
|
tlsSettings = config.TLSSettings_
|
||||||
|
}
|
||||||
|
if tlsSettings == nil {
|
||||||
|
tlsSettings = &XTLSSettings{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsSettings.PublicKey == "" {
|
||||||
|
if inner.PublicKey != "" {
|
||||||
|
tlsSettings.PublicKey = inner.PublicKey
|
||||||
|
} else if config != nil {
|
||||||
|
tlsSettings.PublicKey = config.PublicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsSettings.PrivateKey == "" {
|
||||||
|
if inner.PrivateKey != "" {
|
||||||
|
tlsSettings.PrivateKey = inner.PrivateKey
|
||||||
|
} else if config != nil {
|
||||||
|
tlsSettings.PrivateKey = config.PrivateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsSettings.ShortID == "" {
|
||||||
|
if inner.ShortID != "" {
|
||||||
|
tlsSettings.ShortID = inner.ShortID
|
||||||
|
} else if config != nil {
|
||||||
|
tlsSettings.ShortID = config.ShortID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tlsSettings.ShortIDs) == 0 {
|
||||||
|
if len(inner.ShortIDs) > 0 {
|
||||||
|
tlsSettings.ShortIDs = inner.ShortIDs
|
||||||
|
} else if config != nil && len(config.ShortIDs) > 0 {
|
||||||
|
tlsSettings.ShortIDs = config.ShortIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsSettings.Dest == "" {
|
||||||
|
if inner.Dest != "" {
|
||||||
|
tlsSettings.Dest = inner.Dest
|
||||||
|
} else if config != nil {
|
||||||
|
tlsSettings.Dest = config.Dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsSettings.ServerName == "" {
|
||||||
|
if inner.ServerName != "" {
|
||||||
|
tlsSettings.ServerName = inner.ServerName
|
||||||
|
} else if config != nil {
|
||||||
|
tlsSettings.ServerName = config.ServerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsSettings.ServerPort == "" && config != nil && config.ServerPortText != "" {
|
||||||
|
tlsSettings.ServerPort = config.ServerPortText
|
||||||
|
}
|
||||||
|
return tlsSettings
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Start(stage adapter.StartStage) error {
|
func (s *Service) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
@@ -393,7 +519,7 @@ func (s *Service) setupNode() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
inboundTag := "xboard-inbound"
|
inboundTag := s.inboundTag()
|
||||||
|
|
||||||
// Resolve nested config (V2bX compatibility: server_config / serverConfig / config)
|
// Resolve nested config (V2bX compatibility: server_config / serverConfig / config)
|
||||||
var inner XInnerConfig
|
var inner XInnerConfig
|
||||||
@@ -472,6 +598,8 @@ func (s *Service) setupNode() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("Xboard protocol identified: ", protocol)
|
s.logger.Info("Xboard protocol identified: ", protocol)
|
||||||
|
s.protocol = protocol
|
||||||
|
s.vlessFlow = inner.Flow
|
||||||
|
|
||||||
var listenAddr badoption.Addr
|
var listenAddr badoption.Addr
|
||||||
if addr, err := netip.ParseAddr(inner.ListenIP); err == nil {
|
if addr, err := netip.ParseAddr(inner.ListenIP); err == nil {
|
||||||
@@ -489,51 +617,53 @@ func (s *Service) setupNode() error {
|
|||||||
// V2bX: 0=None, 1=TLS, 2=Reality
|
// V2bX: 0=None, 1=TLS, 2=Reality
|
||||||
var tlsOptions option.InboundTLSOptions
|
var tlsOptions option.InboundTLSOptions
|
||||||
securityType := inner.TLS
|
securityType := inner.TLS
|
||||||
tlsSettings := inner.TLSSettings
|
tlsSettings := mergedTLSSettings(inner, config)
|
||||||
if tlsSettings == nil {
|
hasCertificate := applyCertConfig(&tlsOptions, config.CertConfig)
|
||||||
tlsSettings = inner.TLSSettings_
|
if config.CertConfig != nil && !hasCertificate && config.CertConfig.CertMode != "" && config.CertConfig.CertMode != "none" {
|
||||||
|
s.logger.Warn("Xboard cert_config present but unsupported or incomplete for local TLS. cert_mode=", config.CertConfig.CertMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch securityType {
|
switch securityType {
|
||||||
case 1: // TLS
|
case 1: // TLS
|
||||||
tlsOptions.Enabled = true
|
|
||||||
if tlsSettings != nil {
|
if tlsSettings != nil {
|
||||||
tlsOptions.ServerName = tlsSettings.ServerName
|
tlsOptions.ServerName = tlsSettings.ServerName
|
||||||
}
|
}
|
||||||
|
tlsOptions.Enabled = hasCertificate
|
||||||
case 2: // Reality
|
case 2: // Reality
|
||||||
if tlsSettings != nil {
|
tlsOptions.Enabled = true
|
||||||
tlsOptions.Enabled = true
|
tlsOptions.ServerName = tlsSettings.ServerName
|
||||||
tlsOptions.ServerName = tlsSettings.ServerName
|
shortIDs := tlsSettings.ShortIDs
|
||||||
shortIDs := tlsSettings.ShortIDs
|
if len(shortIDs) == 0 && tlsSettings.ShortID != "" {
|
||||||
if len(shortIDs) == 0 && tlsSettings.ShortID != "" {
|
shortIDs = []string{tlsSettings.ShortID}
|
||||||
shortIDs = []string{tlsSettings.ShortID}
|
|
||||||
}
|
|
||||||
dest := tlsSettings.Dest
|
|
||||||
if dest == "" {
|
|
||||||
dest = tlsSettings.ServerName
|
|
||||||
}
|
|
||||||
if dest == "" {
|
|
||||||
dest = "www.microsoft.com"
|
|
||||||
}
|
|
||||||
serverPort := uint16(443)
|
|
||||||
if tlsSettings.ServerPort != "" {
|
|
||||||
if port, err := strconv.Atoi(tlsSettings.ServerPort); err == nil && port > 0 {
|
|
||||||
serverPort = uint16(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tlsOptions.Reality = &option.InboundRealityOptions{
|
|
||||||
Enabled: true,
|
|
||||||
Handshake: option.InboundRealityHandshakeOptions{
|
|
||||||
ServerOptions: option.ServerOptions{
|
|
||||||
Server: dest,
|
|
||||||
ServerPort: serverPort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PrivateKey: tlsSettings.PrivateKey,
|
|
||||||
ShortID: badoption.Listable[string](shortIDs),
|
|
||||||
}
|
|
||||||
s.logger.Info("Xboard REALITY configured. Dest: ", dest, ":", serverPort)
|
|
||||||
}
|
}
|
||||||
|
dest := tlsSettings.Dest
|
||||||
|
if dest == "" {
|
||||||
|
dest = tlsSettings.ServerName
|
||||||
|
}
|
||||||
|
if dest == "" {
|
||||||
|
dest = "www.microsoft.com"
|
||||||
|
}
|
||||||
|
serverPort := uint16(443)
|
||||||
|
if tlsSettings.ServerPort != "" {
|
||||||
|
if port, err := strconv.Atoi(tlsSettings.ServerPort); err == nil && port > 0 {
|
||||||
|
serverPort = uint16(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsOptions.Reality = &option.InboundRealityOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Handshake: option.InboundRealityHandshakeOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: dest,
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateKey: tlsSettings.PrivateKey,
|
||||||
|
ShortID: badoption.Listable[string](shortIDs),
|
||||||
|
}
|
||||||
|
if tlsSettings.PublicKey != "" {
|
||||||
|
s.logger.Debug("Xboard REALITY public_key received from panel")
|
||||||
|
}
|
||||||
|
s.logger.Info("Xboard REALITY configured. Dest: ", dest, ":", serverPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check streamSettings for Reality (legacy Xboard format)
|
// Also check streamSettings for Reality (legacy Xboard format)
|
||||||
@@ -567,6 +697,10 @@ func (s *Service) setupNode() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if securityType == 1 && !tlsOptions.Enabled {
|
||||||
|
s.logger.Warn("Xboard TLS enabled by panel but no usable certificate material found; inbound will run without local TLS")
|
||||||
|
}
|
||||||
|
|
||||||
// ── Resolve network transport settings (V2bX style) ──
|
// ── Resolve network transport settings (V2bX style) ──
|
||||||
networkType := inner.Network
|
networkType := inner.Network
|
||||||
networkSettings := inner.NetworkSettings
|
networkSettings := inner.NetworkSettings
|
||||||
@@ -757,6 +891,10 @@ func (s *Service) setupNode() error {
|
|||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
return fmt.Errorf("unsupported protocol: %s", protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tlsOptions.Enabled && securityType == 1 && (protocol == "trojan" || protocol == "tuic" || protocol == "hysteria" || protocol == "hysteria2" || protocol == "anytls") {
|
||||||
|
s.logger.Warn("Xboard ", protocol, " usually requires local TLS certificates, but no local certificate material was configured")
|
||||||
|
}
|
||||||
|
|
||||||
// Remove old if exists
|
// Remove old if exists
|
||||||
s.inboundManager.Remove(inboundTag)
|
s.inboundManager.Remove(inboundTag)
|
||||||
|
|
||||||
@@ -920,11 +1058,16 @@ func (s *Service) syncUsers() {
|
|||||||
s.logger.Info("User [", u.ID, "] ID[:", ss2022KeyLen, "]=", originalKey, " → b64_PSK=", key)
|
s.logger.Info("User [", u.ID, "] ID[:", ss2022KeyLen, "]=", originalKey, " → b64_PSK=", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flow := u.Flow
|
||||||
|
if s.protocol == "vless" && flow == "" {
|
||||||
|
flow = s.vlessFlow
|
||||||
|
}
|
||||||
|
|
||||||
newUsers[userName] = userData{
|
newUsers[userName] = userData{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Email: userName,
|
Email: userName,
|
||||||
Key: key,
|
Key: key,
|
||||||
Flow: u.Flow,
|
Flow: flow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package xboard
|
package xboard
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) {
|
func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) {
|
||||||
user := XUser{
|
user := XUser{
|
||||||
@@ -66,3 +70,26 @@ func TestXUserIdentifierFallsBackToEmailThenID(t *testing.T) {
|
|||||||
t.Fatalf("Identifier() = %q, want %q", got, "9")
|
t.Fatalf("Identifier() = %q, want %q", got, "9")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpandNodeOptions(t *testing.T) {
|
||||||
|
base := option.XBoardServiceOptions{
|
||||||
|
PanelURL: "https://panel.example",
|
||||||
|
Key: "shared-token",
|
||||||
|
NodeType: "vless",
|
||||||
|
Nodes: []option.XBoardNodeOptions{
|
||||||
|
{NodeID: 1},
|
||||||
|
{NodeID: 2, NodeType: "anytls"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := expandNodeOptions(base)
|
||||||
|
if len(nodes) != 2 {
|
||||||
|
t.Fatalf("expandNodeOptions() len = %d, want 2", len(nodes))
|
||||||
|
}
|
||||||
|
if nodes[0].NodeID != 1 || nodes[0].NodeType != "vless" {
|
||||||
|
t.Fatalf("first node = %+v", nodes[0])
|
||||||
|
}
|
||||||
|
if nodes[1].NodeID != 2 || nodes[1].NodeType != "anytls" {
|
||||||
|
t.Fatalf("second node = %+v", nodes[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user