多节点聚合
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
|
||||
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
|
||||
PANEL_TOKEN=${INPUT_TOKEN:-$PANEL_TOKEN}
|
||||
|
||||
# Consolidation
|
||||
CONFIG_PANEL_URL=$PANEL_URL
|
||||
CONFIG_NODE_ID=$NODE_ID
|
||||
USER_PANEL_URL=$PANEL_URL
|
||||
USER_NODE_ID=$NODE_ID
|
||||
read -p "Enter Node Count [${NODE_COUNT:-1}]: " INPUT_COUNT
|
||||
NODE_COUNT=${INPUT_COUNT:-${NODE_COUNT:-1}}
|
||||
|
||||
if ! [[ "$NODE_COUNT" =~ ^[0-9]+$ ]] || [[ "$NODE_COUNT" -lt 1 ]]; then
|
||||
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)
|
||||
echo -e "${YELLOW}Syncing system time...${NC}"
|
||||
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}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up trailing slash
|
||||
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
|
||||
echo -e "${YELLOW}Generating configuration...${NC}"
|
||||
@@ -175,19 +249,7 @@ cat > "$CONFIG_FILE" <<EOF
|
||||
]
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
${SERVICE_JSON}
|
||||
],
|
||||
"inbounds": [],
|
||||
"outbounds": [
|
||||
|
||||
@@ -15,4 +15,19 @@ type XBoardServiceOptions struct {
|
||||
NodeType string `json:"node_type"`
|
||||
SyncInterval badoption.Duration `json:"sync_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) {
|
||||
flow = flows[i]
|
||||
}
|
||||
if flow == "" {
|
||||
flow = "xtls-rprx-vision"
|
||||
}
|
||||
newUsers[i] = option.VLESSUser{
|
||||
Name: users[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
|
||||
router adapter.Router
|
||||
inboundManager adapter.InboundManager
|
||||
protocol string
|
||||
vlessFlow string
|
||||
ssCipher string // stored for user key derivation in syncUsers
|
||||
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 {
|
||||
NodeType string `json:"node_type"`
|
||||
NodeType_ string `json:"nodeType"`
|
||||
@@ -107,6 +96,13 @@ type XNodeConfig struct {
|
||||
Flow string `json:"flow"`
|
||||
TLSSettings *XTLSSettings `json:"tls_settings"`
|
||||
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"`
|
||||
NetworkSettings json.RawMessage `json:"network_settings"`
|
||||
NetworkSettings_ json.RawMessage `json:"networkSettings"`
|
||||
@@ -124,6 +120,24 @@ type XNodeConfig struct {
|
||||
|
||||
// AnyTls
|
||||
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 {
|
||||
@@ -138,6 +152,12 @@ type XInnerConfig struct {
|
||||
Flow string `json:"flow"`
|
||||
TLSSettings *XTLSSettings `json:"tls_settings"`
|
||||
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"`
|
||||
NetworkSettings json.RawMessage `json:"network_settings"`
|
||||
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) {
|
||||
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)
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeXBoard, tag),
|
||||
@@ -277,6 +304,105 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
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 {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
@@ -393,7 +519,7 @@ func (s *Service) setupNode() error {
|
||||
return err
|
||||
}
|
||||
|
||||
inboundTag := "xboard-inbound"
|
||||
inboundTag := s.inboundTag()
|
||||
|
||||
// Resolve nested config (V2bX compatibility: server_config / serverConfig / config)
|
||||
var inner XInnerConfig
|
||||
@@ -472,6 +598,8 @@ func (s *Service) setupNode() error {
|
||||
}
|
||||
|
||||
s.logger.Info("Xboard protocol identified: ", protocol)
|
||||
s.protocol = protocol
|
||||
s.vlessFlow = inner.Flow
|
||||
|
||||
var listenAddr badoption.Addr
|
||||
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
|
||||
var tlsOptions option.InboundTLSOptions
|
||||
securityType := inner.TLS
|
||||
tlsSettings := inner.TLSSettings
|
||||
if tlsSettings == nil {
|
||||
tlsSettings = inner.TLSSettings_
|
||||
tlsSettings := mergedTLSSettings(inner, config)
|
||||
hasCertificate := applyCertConfig(&tlsOptions, config.CertConfig)
|
||||
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 {
|
||||
case 1: // TLS
|
||||
tlsOptions.Enabled = true
|
||||
if tlsSettings != nil {
|
||||
tlsOptions.ServerName = tlsSettings.ServerName
|
||||
}
|
||||
tlsOptions.Enabled = hasCertificate
|
||||
case 2: // Reality
|
||||
if tlsSettings != nil {
|
||||
tlsOptions.Enabled = true
|
||||
tlsOptions.ServerName = tlsSettings.ServerName
|
||||
shortIDs := tlsSettings.ShortIDs
|
||||
if len(shortIDs) == 0 && 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)
|
||||
tlsOptions.Enabled = true
|
||||
tlsOptions.ServerName = tlsSettings.ServerName
|
||||
shortIDs := tlsSettings.ShortIDs
|
||||
if len(shortIDs) == 0 && 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),
|
||||
}
|
||||
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)
|
||||
@@ -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) ──
|
||||
networkType := inner.Network
|
||||
networkSettings := inner.NetworkSettings
|
||||
@@ -757,6 +891,10 @@ func (s *Service) setupNode() error {
|
||||
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
|
||||
s.inboundManager.Remove(inboundTag)
|
||||
|
||||
@@ -920,11 +1058,16 @@ func (s *Service) syncUsers() {
|
||||
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{
|
||||
ID: u.ID,
|
||||
Email: userName,
|
||||
Key: key,
|
||||
Flow: u.Flow,
|
||||
Flow: flow,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package xboard
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) {
|
||||
user := XUser{
|
||||
@@ -66,3 +70,26 @@ func TestXUserIdentifierFallsBackToEmailThenID(t *testing.T) {
|
||||
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