使用热重载
This commit is contained in:
20
api.md
20
api.md
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
186
common/listener/proxy_protocol.go
Normal file
186
common/listener/proxy_protocol.go
Normal 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
|
||||
}
|
||||
114
common/listener/proxy_protocol_test.go
Normal file
114
common/listener/proxy_protocol_test.go
Normal 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
|
||||
}
|
||||
15
install.sh
15
install.sh
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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...")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user