使用热重载

This commit is contained in:
CN-JS-HuiBai
2026-04-15 15:40:38 +08:00
parent 27ed827903
commit 1647cda714
9 changed files with 614 additions and 24 deletions

22
api.md
View File

@@ -331,6 +331,26 @@ Same endpoints as V1 but under `/api/v2/passport/` prefix.
- **Purpose**: Get server configuration
- **Returns**: Server configuration
- **Data**: Server settings and parameters
- **Example Response**:
```json
{
"protocol": "vless",
"listen_ip": "0.0.0.0",
"server_port": 18443,
"network": "tcp",
"tls": 2,
"server_name": "git.example.com",
"dest": "www.cloudflare.com:443",
"private_key": "YOUR_REALITY_PRIVATE_KEY",
"short_id": "01234567",
"accept_proxy_protocol": true,
"base_config": {
"push_interval": 60,
"pull_interval": 60
}
}
```
- **Proxy Protocol Note**: Set `accept_proxy_protocol` to `true` only when this node is behind an L4 proxy or load balancer that really sends PROXY protocol headers. Direct client connections will fail if this is enabled without an upstream PROXY sender.
- **GET** `/api/v1/server/UniProxy/user`
- **Purpose**: Get user data for server
@@ -952,4 +972,4 @@ Same endpoints as V1 but under `/api/v2/passport/` prefix.
6. **Filtering**: Admin endpoints often support filtering and sorting parameters.
This documentation provides a comprehensive overview of all available API endpoints in the Xboard system. Each endpoint serves specific functionality within the VPN service management platform.
This documentation provides a comprehensive overview of all available API endpoints in the Xboard system. Each endpoint serves specific functionality within the VPN service management platform.

View File

@@ -21,10 +21,6 @@ import (
)
func (l *Listener) ListenTCP() (net.Listener, error) {
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var listenConfig net.ListenConfig
@@ -100,6 +96,18 @@ func (l *Listener) loopTCPIn() {
l.logger.Error("tcp listener closed: ", err)
continue
}
remoteAddr := conn.RemoteAddr()
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
//nolint:staticcheck
wrappedConn, wrapErr := wrapProxyProtocolConn(conn, l.listenOptions.ProxyProtocolAcceptNoHeader)
if wrapErr != nil {
conn.Close()
l.logger.Error("process connection from ", remoteAddr, ": PROXY protocol: ", wrapErr)
continue
}
conn = wrappedConn
}
//nolint:staticcheck
metadata.InboundDetour = l.listenOptions.Detour
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()

View File

@@ -0,0 +1,186 @@
package listener
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
)
var errProxyProtocolHeaderNotPresent = errors.New("proxy protocol header not present")
var proxyProtocolV2Signature = []byte{
0x0d, 0x0a, 0x0d, 0x0a,
0x00, 0x0d, 0x0a, 0x51,
0x55, 0x49, 0x54, 0x0a,
}
type proxyProtocolConn struct {
net.Conn
reader *bufio.Reader
remoteAddr net.Addr
}
func (c *proxyProtocolConn) Read(p []byte) (int, error) {
return c.reader.Read(p)
}
func (c *proxyProtocolConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
func wrapProxyProtocolConn(conn net.Conn, allowNoHeader bool) (net.Conn, error) {
reader := bufio.NewReader(conn)
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
if err != nil {
if allowNoHeader && errors.Is(err, errProxyProtocolHeaderNotPresent) {
return &proxyProtocolConn{
Conn: conn,
reader: reader,
remoteAddr: conn.RemoteAddr(),
}, nil
}
return nil, err
}
if remoteAddr == nil {
remoteAddr = conn.RemoteAddr()
}
return &proxyProtocolConn{
Conn: conn,
reader: reader,
remoteAddr: remoteAddr,
}, nil
}
func readProxyProtocolRemoteAddr(reader *bufio.Reader) (net.Addr, error) {
firstByte, err := reader.Peek(1)
if err != nil {
return nil, err
}
switch firstByte[0] {
case 'P':
return readProxyProtocolV1RemoteAddr(reader)
case '\r':
signature, err := reader.Peek(len(proxyProtocolV2Signature))
if err != nil {
return nil, err
}
if !bytes.Equal(signature, proxyProtocolV2Signature) {
return nil, errProxyProtocolHeaderNotPresent
}
return readProxyProtocolV2RemoteAddr(reader)
default:
return nil, errProxyProtocolHeaderNotPresent
}
}
func readProxyProtocolV1RemoteAddr(reader *bufio.Reader) (net.Addr, error) {
prefix, err := reader.Peek(6)
if err != nil {
return nil, err
}
if !bytes.Equal(prefix, []byte("PROXY ")) {
return nil, errProxyProtocolHeaderNotPresent
}
line, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
if len(line) < 2 || line[len(line)-2:] != "\r\n" {
return nil, fmt.Errorf("invalid PROXY protocol v1 line ending")
}
fields := bytes.Fields([]byte(line[:len(line)-2]))
if len(fields) < 2 {
return nil, fmt.Errorf("invalid PROXY protocol v1 header")
}
if string(fields[1]) == "UNKNOWN" {
return nil, nil
}
if len(fields) != 6 {
return nil, fmt.Errorf("invalid PROXY protocol v1 field count")
}
sourceIP := net.ParseIP(string(fields[2]))
if sourceIP == nil {
return nil, fmt.Errorf("invalid PROXY protocol source ip")
}
sourcePort, err := parseProxyProtocolPort(fields[4])
if err != nil {
return nil, err
}
return &net.TCPAddr{
IP: sourceIP,
Port: sourcePort,
}, nil
}
func readProxyProtocolV2RemoteAddr(reader *bufio.Reader) (net.Addr, error) {
header := make([]byte, 16)
if _, err := io.ReadFull(reader, header); err != nil {
return nil, err
}
if !bytes.Equal(header[:12], proxyProtocolV2Signature) {
return nil, errProxyProtocolHeaderNotPresent
}
version := header[12] >> 4
command := header[12] & 0x0f
if version != 0x2 {
return nil, fmt.Errorf("invalid PROXY protocol v2 version")
}
addressDataLen := int(binary.BigEndian.Uint16(header[14:16]))
addressData := make([]byte, addressDataLen)
if _, err := io.ReadFull(reader, addressData); err != nil {
return nil, err
}
if command == 0x0 {
return nil, nil
}
if command != 0x1 {
return nil, fmt.Errorf("unsupported PROXY protocol v2 command")
}
switch header[13] {
case 0x11, 0x12:
if len(addressData) < 12 {
return nil, fmt.Errorf("short PROXY protocol v2 ipv4 header")
}
return &net.TCPAddr{
IP: net.IP(addressData[:4]),
Port: int(binary.BigEndian.Uint16(addressData[8:10])),
}, nil
case 0x21, 0x22:
if len(addressData) < 36 {
return nil, fmt.Errorf("short PROXY protocol v2 ipv6 header")
}
return &net.TCPAddr{
IP: net.IP(addressData[:16]),
Port: int(binary.BigEndian.Uint16(addressData[32:34])),
}, nil
case 0x31, 0x32:
return nil, nil
default:
return nil, fmt.Errorf("unsupported PROXY protocol v2 family")
}
}
func parseProxyProtocolPort(raw []byte) (int, error) {
port := 0
for _, ch := range raw {
if ch < '0' || ch > '9' {
return 0, fmt.Errorf("invalid PROXY protocol port")
}
port = port*10 + int(ch-'0')
if port > 65535 {
return 0, fmt.Errorf("invalid PROXY protocol port")
}
}
return port, nil
}

View File

@@ -0,0 +1,114 @@
package listener
import (
"bufio"
"encoding/binary"
"net"
"strings"
"testing"
"time"
)
func TestReadProxyProtocolV1RemoteAddr(t *testing.T) {
reader := bufio.NewReaderSize(newStaticConn("PROXY TCP4 203.0.113.10 192.0.2.1 45678 443\r\npayload"), 128)
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tcpAddr, ok := remoteAddr.(*net.TCPAddr)
if !ok {
t.Fatalf("unexpected addr type: %T", remoteAddr)
}
if got := tcpAddr.IP.String(); got != "203.0.113.10" {
t.Fatalf("unexpected ip: %s", got)
}
if tcpAddr.Port != 45678 {
t.Fatalf("unexpected port: %d", tcpAddr.Port)
}
}
func TestReadProxyProtocolV2RemoteAddr(t *testing.T) {
header := make([]byte, 28)
copy(header[:12], proxyProtocolV2Signature)
header[12] = 0x21
header[13] = 0x11
binary.BigEndian.PutUint16(header[14:16], 12)
copy(header[16:20], net.ParseIP("198.51.100.12").To4())
copy(header[20:24], net.ParseIP("192.0.2.8").To4())
binary.BigEndian.PutUint16(header[24:26], 50000)
binary.BigEndian.PutUint16(header[26:28], 443)
reader := bufio.NewReaderSize(newStaticConn(string(header)+"payload"), 128)
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tcpAddr, ok := remoteAddr.(*net.TCPAddr)
if !ok {
t.Fatalf("unexpected addr type: %T", remoteAddr)
}
if got := tcpAddr.IP.String(); got != "198.51.100.12" {
t.Fatalf("unexpected ip: %s", got)
}
if tcpAddr.Port != 50000 {
t.Fatalf("unexpected port: %d", tcpAddr.Port)
}
}
func TestWrapProxyProtocolConnAllowNoHeader(t *testing.T) {
rawConn := newStaticConn("hello")
conn, err := wrapProxyProtocolConn(rawConn, true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conn.RemoteAddr().String() != rawConn.RemoteAddr().String() {
t.Fatalf("remote addr changed unexpectedly: %s", conn.RemoteAddr())
}
}
type staticConn struct {
net.Conn
reader *bufio.Reader
local net.Addr
remote net.Addr
}
func newStaticConn(payload string) *staticConn {
return &staticConn{
reader: bufio.NewReaderSize(strings.NewReader(payload), len(payload)+16),
local: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 443},
remote: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345},
}
}
func (c *staticConn) Read(p []byte) (int, error) {
return c.reader.Read(p)
}
func (c *staticConn) Write(p []byte) (int, error) {
return len(p), nil
}
func (c *staticConn) Close() error {
return nil
}
func (c *staticConn) LocalAddr() net.Addr {
return c.local
}
func (c *staticConn) RemoteAddr() net.Addr {
return c.remote
}
func (c *staticConn) SetDeadline(t time.Time) error {
return nil
}
func (c *staticConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *staticConn) SetWriteDeadline(t time.Time) error {
return nil
}

View File

@@ -145,6 +145,9 @@ PANEL_URL=${INPUT_URL:-$PANEL_URL}
read -p "Enter Panel Token (Node Key) [${PANEL_TOKEN}]: " INPUT_TOKEN
PANEL_TOKEN=${INPUT_TOKEN:-$PANEL_TOKEN}
read -p "This node is behind an L4 proxy/LB that sends PROXY protocol? [${ENABLE_PROXY_PROTOCOL_HINT:-n}]: " INPUT_PROXY_PROTOCOL
ENABLE_PROXY_PROTOCOL_HINT=${INPUT_PROXY_PROTOCOL:-${ENABLE_PROXY_PROTOCOL_HINT:-n}}
read -p "Enter Node Count [${NODE_COUNT:-1}]: " INPUT_COUNT
NODE_COUNT=${INPUT_COUNT:-${NODE_COUNT:-1}}
@@ -284,6 +287,17 @@ EOF
echo -e "${GREEN}Configuration written to $CONFIG_FILE${NC}"
if [[ "$ENABLE_PROXY_PROTOCOL_HINT" =~ ^([yY][eE][sS]|[yY]|1|true|TRUE)$ ]]; then
echo -e "${YELLOW}Proxy Protocol deployment hint enabled.${NC}"
echo -e "${YELLOW}To make real client IP reporting work, your panel node config response must include:${NC}"
echo -e "${YELLOW} \"accept_proxy_protocol\": true${NC}"
echo -e "${YELLOW}Only enable this when the upstream L4 proxy or load balancer actually sends PROXY protocol headers.${NC}"
echo -e "${YELLOW}If clients connect directly without a PROXY header, connections will fail after enabling it on the panel.${NC}"
else
echo -e "${YELLOW}Proxy Protocol is not expected for this deployment.${NC}"
echo -e "${YELLOW}Keep panel field \"accept_proxy_protocol\" disabled or absent unless you are using an L4 proxy/LB that sends it.${NC}"
fi
# Create Systemd Service
echo -e "${YELLOW}Creating systemd service...${NC}"
cat > "$SERVICE_FILE" <<EOF
@@ -311,3 +325,4 @@ systemctl restart "$SERVICE_NAME"
echo -e "${GREEN}Service installed and started successfully.${NC}"
echo -e "${GREEN}Check status with: systemctl status ${SERVICE_NAME}${NC}"
echo -e "${GREEN}View logs with: journalctl -u ${SERVICE_NAME} -f${NC}"
echo -e "${GREEN}Panel config endpoint must control PROXY protocol via accept_proxy_protocol when needed.${NC}"

View File

@@ -4,6 +4,7 @@ import (
"context"
"net"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -42,6 +43,7 @@ type MultiInbound struct {
service shadowsocks.MultiService[int]
users []option.ShadowsocksUser
tracker adapter.SSMTracker
ssmMutex sync.RWMutex
}
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
@@ -119,10 +121,14 @@ func (h *MultiInbound) Close() error {
}
func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) {
h.ssmMutex.Lock()
defer h.ssmMutex.Unlock()
h.tracker = tracker
}
func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string, flows []string) error {
h.ssmMutex.Lock()
defer h.ssmMutex.Unlock()
err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
return index
}), uPSKs)
@@ -163,7 +169,15 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
if !loaded {
return os.ErrInvalid
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
return os.ErrInvalid
}
userEntry := h.users[userIndex]
tracker := h.tracker
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
@@ -175,8 +189,8 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
if h.tracker != nil {
conn = h.tracker.TrackConnection(conn, metadata)
if tracker != nil {
conn = tracker.TrackConnection(conn, metadata)
}
return h.router.RouteConnection(ctx, conn, metadata)
}
@@ -186,7 +200,15 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
if !loaded {
return os.ErrInvalid
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
return os.ErrInvalid
}
userEntry := h.users[userIndex]
tracker := h.tracker
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
@@ -200,8 +222,8 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
if h.tracker != nil {
conn = h.tracker.TrackPacketConnection(conn, metadata)
if tracker != nil {
conn = tracker.TrackPacketConnection(conn, metadata)
}
return h.router.RoutePacketConnection(ctx, conn, metadata)
}

View File

@@ -209,16 +209,22 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
userEntry := h.users[userIndex]
tracker := h.tracker
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
metadata.User = user
}
h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
h.ssmMutex.RLock()
tracker := h.tracker
h.ssmMutex.RUnlock()
if tracker != nil {
conn = tracker.TrackConnection(conn, metadata)
}
@@ -233,7 +239,16 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
userEntry := h.users[userIndex]
tracker := h.tracker
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
@@ -246,9 +261,6 @@ 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)
}

View File

@@ -215,7 +215,15 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
userEntry := h.users[userIndex]
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
@@ -233,7 +241,16 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
h.ssmMutex.RLock()
if userIndex < 0 || userIndex >= len(h.users) {
h.ssmMutex.RUnlock()
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
userEntry := h.users[userIndex]
tracker := h.tracker
h.ssmMutex.RUnlock()
user := userEntry.Name
if user == "" {
user = F.ToString(userIndex)
} else {
@@ -246,9 +263,6 @@ 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)
}

View File

@@ -106,6 +106,8 @@ type XNodeConfig struct {
ServerName string `json:"server_name,omitempty"`
ServerPortText string `json:"server_port_text,omitempty"`
Network string `json:"network"`
AcceptProxyProtocol bool `json:"accept_proxy_protocol,omitempty"`
AcceptProxyProtocol_ bool `json:"acceptProxyProtocol,omitempty"`
Multiplex *XMultiplexConfig `json:"multiplex,omitempty"`
NetworkSettings json.RawMessage `json:"network_settings"`
NetworkSettings_ json.RawMessage `json:"networkSettings"`
@@ -162,6 +164,8 @@ type XInnerConfig struct {
Dest string `json:"dest,omitempty"`
ServerName string `json:"server_name,omitempty"`
Network string `json:"network"`
AcceptProxyProtocol bool `json:"accept_proxy_protocol,omitempty"`
AcceptProxyProtocol_ bool `json:"acceptProxyProtocol,omitempty"`
Multiplex *XMultiplexConfig `json:"multiplex,omitempty"`
NetworkSettings json.RawMessage `json:"network_settings"`
NetworkSettings_ json.RawMessage `json:"networkSettings"`
@@ -212,6 +216,67 @@ type GrpcNetworkConfig struct {
ServiceName string `json:"serviceName"`
}
func unmarshalNetworkSettings(settings json.RawMessage) map[string]any {
if len(settings) == 0 {
return nil
}
var raw map[string]any
if err := json.Unmarshal(settings, &raw); err != nil {
return nil
}
return raw
}
func readNetworkString(raw map[string]any, keys ...string) string {
for _, key := range keys {
value, exists := raw[key]
if !exists {
continue
}
if stringValue, ok := value.(string); ok && stringValue != "" {
return stringValue
}
}
return ""
}
func readNetworkBool(raw map[string]any, keys ...string) (bool, bool) {
for _, key := range keys {
value, exists := raw[key]
if !exists {
continue
}
if boolValue, ok := value.(bool); ok {
return boolValue, true
}
}
return false, false
}
func readNetworkDuration(raw map[string]any, keys ...string) (badoption.Duration, bool) {
for _, key := range keys {
value, exists := raw[key]
if !exists {
continue
}
switch typedValue := value.(type) {
case float64:
return badoption.Duration(time.Duration(typedValue) * time.Second), true
case string:
if typedValue == "" {
continue
}
if durationValue, err := time.ParseDuration(typedValue); err == nil {
return badoption.Duration(durationValue), true
}
if secondsValue, err := strconv.ParseInt(typedValue, 10, 64); err == nil {
return badoption.Duration(time.Duration(secondsValue) * time.Second), true
}
}
}
return 0, false
}
type HttpupgradeNetworkConfig struct {
Path string `json:"path"`
Host string `json:"host"`
@@ -466,6 +531,7 @@ func getInboundTransport(network string, settings json.RawMessage) (*option.V2Ra
t := &option.V2RayTransportOptions{
Type: network,
}
rawSettings := unmarshalNetworkSettings(settings)
switch network {
case "tcp":
if len(settings) != 0 {
@@ -535,6 +601,18 @@ func getInboundTransport(network string, settings json.RawMessage) (*option.V2Ra
t.GRPCOptions = option.V2RayGRPCOptions{
ServiceName: networkConfig.ServiceName,
}
if serviceName := readNetworkString(rawSettings, "service_name"); serviceName != "" && t.GRPCOptions.ServiceName == "" {
t.GRPCOptions.ServiceName = serviceName
}
if idleTimeout, ok := readNetworkDuration(rawSettings, "idle_timeout", "idleTimeout"); ok {
t.GRPCOptions.IdleTimeout = idleTimeout
}
if pingTimeout, ok := readNetworkDuration(rawSettings, "ping_timeout", "pingTimeout"); ok {
t.GRPCOptions.PingTimeout = pingTimeout
}
if permitWithoutStream, ok := readNetworkBool(rawSettings, "permit_without_stream", "permitWithoutStream"); ok {
t.GRPCOptions.PermitWithoutStream = permitWithoutStream
}
}
case "httpupgrade":
if len(settings) != 0 {
@@ -548,10 +626,106 @@ func getInboundTransport(network string, settings json.RawMessage) (*option.V2Ra
Host: networkConfig.Host,
}
}
case "h2", "http":
t.Type = "http"
if rawSettings != nil {
t.HTTPOptions = option.V2RayHTTPOptions{
Path: readNetworkString(rawSettings, "path"),
Method: readNetworkString(rawSettings, "method"),
Headers: nil,
}
if idleTimeout, ok := readNetworkDuration(rawSettings, "idle_timeout", "idleTimeout"); ok {
t.HTTPOptions.IdleTimeout = idleTimeout
}
if pingTimeout, ok := readNetworkDuration(rawSettings, "ping_timeout", "pingTimeout"); ok {
t.HTTPOptions.PingTimeout = pingTimeout
}
if hostValue, exists := rawSettings["host"]; exists {
switch typedHost := hostValue.(type) {
case string:
if typedHost != "" {
t.HTTPOptions.Host = badoption.Listable[string]{typedHost}
}
case []any:
hostList := make(badoption.Listable[string], 0, len(typedHost))
for _, item := range typedHost {
if host, ok := item.(string); ok && host != "" {
hostList = append(hostList, host)
}
}
if len(hostList) > 0 {
t.HTTPOptions.Host = hostList
}
}
}
if headersValue, exists := rawSettings["headers"]; exists {
if headersMap, ok := headersValue.(map[string]any); ok {
headerOptions := make(badoption.HTTPHeader)
for headerKey, headerValue := range headersMap {
switch typedHeader := headerValue.(type) {
case string:
if typedHeader != "" {
headerOptions[headerKey] = badoption.Listable[string]{typedHeader}
}
case []any:
values := make(badoption.Listable[string], 0, len(typedHeader))
for _, item := range typedHeader {
if headerString, ok := item.(string); ok && headerString != "" {
values = append(values, headerString)
}
}
if len(values) > 0 {
headerOptions[headerKey] = values
}
}
}
if len(headerOptions) > 0 {
t.HTTPOptions.Headers = headerOptions
}
}
}
}
}
return t, nil
}
func networkAcceptProxyProtocol(settings json.RawMessage) bool {
if len(settings) == 0 {
return false
}
var raw map[string]any
if err := json.Unmarshal(settings, &raw); err != nil {
return false
}
for _, key := range []string{"acceptProxyProtocol", "accept_proxy_protocol"} {
value, exists := raw[key]
if !exists {
continue
}
enabled, ok := value.(bool)
if ok && enabled {
return true
}
}
return false
}
func acceptProxyProtocolEnabled(inner XInnerConfig, config *XNodeConfig) bool {
if inner.AcceptProxyProtocol || inner.AcceptProxyProtocol_ {
return true
}
if config != nil && (config.AcceptProxyProtocol || config.AcceptProxyProtocol_) {
return true
}
if networkAcceptProxyProtocol(inner.NetworkSettings) || networkAcceptProxyProtocol(inner.NetworkSettings_) {
return true
}
if config != nil && (networkAcceptProxyProtocol(config.NetworkSettings) || networkAcceptProxyProtocol(config.NetworkSettings_)) {
return true
}
return false
}
func (s *Service) setupNode() error {
s.logger.Info("Xboard fetching node config...")
config, err := s.fetchConfig()
@@ -675,6 +849,10 @@ func (s *Service) setupNode() error {
Listen: &listenAddr,
ListenPort: uint16(inner.Port),
}
if acceptProxyProtocolEnabled(inner, config) {
listen.ProxyProtocol = true
s.logger.Info("Xboard PROXY protocol enabled for inbound on ", inner.ListenIP, ":", inner.Port)
}
// ── TLS / Reality handling (matching V2bX panel.Security constants) ──
// V2bX: 0=None, 1=TLS, 2=Reality
@@ -1264,6 +1442,11 @@ func (s *Service) syncUsers() {
}
}
if sameUserSet(s.localUsers, newUsers) {
s.logger.Trace("Xboard sync skipped: users unchanged")
return
}
for tag, server := range s.servers {
// Update users in each manager
users := make([]string, 0, len(newUsers))
@@ -1286,6 +1469,22 @@ func (s *Service) syncUsers() {
s.logger.Info("Xboard sync completed, total users: ", len(users))
}
func sameUserSet(current map[string]userData, next map[string]userData) bool {
if len(current) != len(next) {
return false
}
for userName, currentUser := range current {
nextUser, exists := next[userName]
if !exists {
return false
}
if currentUser != nextUser {
return false
}
}
return true
}
func (s *Service) reportTraffic() {
s.logger.Trace("Xboard reporting traffic...")