init
This commit is contained in:
224
internal/acme/client.go
Normal file
224
internal/acme/client.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
// CertResult represents the result of a certificate issuance.
|
||||
type CertResult struct {
|
||||
Domain string
|
||||
CertPath string
|
||||
KeyPath string
|
||||
CertPEM string
|
||||
KeyPEM string
|
||||
IssueDate time.Time
|
||||
ExpiryDate time.Time
|
||||
}
|
||||
|
||||
// User implements the acme.User interface for lego.
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func (u *User) GetEmail() string { return u.Email }
|
||||
func (u *User) GetRegistration() *registration.Resource { return u.Registration }
|
||||
func (u *User) GetPrivateKey() crypto.PrivateKey { return u.key }
|
||||
|
||||
// Client wraps the lego ACME client.
|
||||
type Client struct {
|
||||
certDir string
|
||||
staging bool
|
||||
httpPort string
|
||||
webrootDir string
|
||||
}
|
||||
|
||||
// ClientOption configures the Client.
|
||||
type ClientOption func(*Client)
|
||||
|
||||
// WithCertDir sets the certificate storage directory.
|
||||
func WithCertDir(dir string) ClientOption {
|
||||
return func(c *Client) { c.certDir = dir }
|
||||
}
|
||||
|
||||
// WithStaging enables the Let's Encrypt staging environment.
|
||||
func WithStaging(staging bool) ClientOption {
|
||||
return func(c *Client) { c.staging = staging }
|
||||
}
|
||||
|
||||
// WithHTTPPort sets the port for HTTP-01 challenge (default: ":80").
|
||||
func WithHTTPPort(port string) ClientOption {
|
||||
return func(c *Client) { c.httpPort = port }
|
||||
}
|
||||
|
||||
// WithWebrootDir sets the webroot directory for webroot challenge mode.
|
||||
func WithWebrootDir(dir string) ClientOption {
|
||||
return func(c *Client) { c.webrootDir = dir }
|
||||
}
|
||||
|
||||
// NewClient creates a new ACME client.
|
||||
func NewClient(opts ...ClientOption) *Client {
|
||||
c := &Client{
|
||||
certDir: "/etc/miaomiaowu/certs",
|
||||
staging: false,
|
||||
httpPort: ":80",
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ObtainCertificate requests a new certificate for the given domain.
|
||||
func (c *Client) ObtainCertificate(ctx context.Context, email, domain string, useWebroot bool) (*CertResult, error) {
|
||||
if email == "" {
|
||||
return nil, errors.New("email is required")
|
||||
}
|
||||
if domain == "" {
|
||||
return nil, errors.New("domain is required")
|
||||
}
|
||||
|
||||
// Generate a new private key for the user
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate private key: %w", err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Email: email,
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(user)
|
||||
if c.staging {
|
||||
config.CADirURL = lego.LEDirectoryStaging
|
||||
} else {
|
||||
config.CADirURL = lego.LEDirectoryProduction
|
||||
}
|
||||
config.Certificate.KeyType = certcrypto.EC256
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create lego client: %w", err)
|
||||
}
|
||||
|
||||
// Set up HTTP-01 challenge provider
|
||||
if useWebroot && c.webrootDir != "" {
|
||||
// Webroot mode: write challenge files to the specified directory
|
||||
provider, err := NewWebrootProvider(c.webrootDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create webroot provider: %w", err)
|
||||
}
|
||||
if err := client.Challenge.SetHTTP01Provider(provider); err != nil {
|
||||
return nil, fmt.Errorf("set webroot provider: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Standalone mode: lego starts its own HTTP server
|
||||
provider := http01.NewProviderServer("", c.httpPort)
|
||||
if err := client.Challenge.SetHTTP01Provider(provider); err != nil {
|
||||
return nil, fmt.Errorf("set http01 provider: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register the user
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("register with ACME: %w", err)
|
||||
}
|
||||
user.Registration = reg
|
||||
|
||||
// Request the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{domain},
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("obtain certificate: %w", err)
|
||||
}
|
||||
|
||||
// Parse the certificate to get expiry date
|
||||
expiryDate, issueDate, err := parseCertificateDates(certificates.Certificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
|
||||
// Save the certificate to disk
|
||||
certPath, keyPath, err := c.saveCertificate(domain, certificates.Certificate, certificates.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("save certificate: %w", err)
|
||||
}
|
||||
|
||||
return &CertResult{
|
||||
Domain: domain,
|
||||
CertPath: certPath,
|
||||
KeyPath: keyPath,
|
||||
CertPEM: string(certificates.Certificate),
|
||||
KeyPEM: string(certificates.PrivateKey),
|
||||
IssueDate: issueDate,
|
||||
ExpiryDate: expiryDate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) saveCertificate(domain string, certPEM, keyPEM []byte) (string, string, error) {
|
||||
// Ensure directory exists
|
||||
domainDir := filepath.Join(c.certDir, domain)
|
||||
if err := os.MkdirAll(domainDir, 0700); err != nil {
|
||||
return "", "", fmt.Errorf("create cert directory: %w", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(domainDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(domainDir, "privkey.pem")
|
||||
|
||||
// Write certificate
|
||||
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
||||
return "", "", fmt.Errorf("write certificate: %w", err)
|
||||
}
|
||||
|
||||
// Write private key with restrictive permissions
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
return "", "", fmt.Errorf("write private key: %w", err)
|
||||
}
|
||||
|
||||
return certPath, keyPath, nil
|
||||
}
|
||||
|
||||
func parseCertificateDates(certPEM []byte) (expiryDate, issueDate time.Time, err error) {
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return time.Time{}, time.Time{}, errors.New("failed to decode PEM block")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
|
||||
return cert.NotAfter, cert.NotBefore, nil
|
||||
}
|
||||
|
||||
// GetCertDir returns the certificate storage directory.
|
||||
func (c *Client) GetCertDir() string {
|
||||
return c.certDir
|
||||
}
|
||||
55
internal/acme/webroot.go
Normal file
55
internal/acme/webroot.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
)
|
||||
|
||||
// WebrootProvider implements the HTTP-01 challenge using a webroot directory.
|
||||
type WebrootProvider struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewWebrootProvider creates a new webroot provider.
|
||||
func NewWebrootProvider(path string) (*WebrootProvider, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("webroot path is required")
|
||||
}
|
||||
|
||||
// Ensure the webroot directory exists
|
||||
challengeDir := filepath.Join(path, http01.ChallengePath(""))
|
||||
if err := os.MkdirAll(challengeDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create challenge directory: %w", err)
|
||||
}
|
||||
|
||||
return &WebrootProvider{path: path}, nil
|
||||
}
|
||||
|
||||
// Present writes the challenge token to the webroot directory.
|
||||
func (w *WebrootProvider) Present(domain, token, keyAuth string) error {
|
||||
challengePath := filepath.Join(w.path, http01.ChallengePath(token))
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(challengePath), 0755); err != nil {
|
||||
return fmt.Errorf("create challenge directory: %w", err)
|
||||
}
|
||||
|
||||
// Write the key authorization to the challenge file
|
||||
if err := os.WriteFile(challengePath, []byte(keyAuth), 0644); err != nil {
|
||||
return fmt.Errorf("write challenge file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the challenge token file.
|
||||
func (w *WebrootProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
challengePath := filepath.Join(w.path, http01.ChallengePath(token))
|
||||
if err := os.Remove(challengePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove challenge file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
990
internal/agent/client.go
Normal file
990
internal/agent/client.go
Normal file
@@ -0,0 +1,990 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mmw-agent/internal/acme"
|
||||
"mmw-agent/internal/collector"
|
||||
"mmw-agent/internal/config"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ConnectionMode represents the current connection mode
|
||||
type ConnectionMode string
|
||||
|
||||
const (
|
||||
ModeWebSocket ConnectionMode = "websocket"
|
||||
ModeHTTP ConnectionMode = "http"
|
||||
ModePull ConnectionMode = "pull"
|
||||
ModeAuto ConnectionMode = "auto"
|
||||
)
|
||||
|
||||
// Client represents an agent client that connects to a master server
|
||||
type Client struct {
|
||||
config *config.Config
|
||||
collector *collector.Collector
|
||||
xrayServers []config.XrayServer
|
||||
wsConn *websocket.Conn
|
||||
wsMu sync.Mutex
|
||||
connected bool
|
||||
reconnects int
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Connection state
|
||||
currentMode ConnectionMode
|
||||
httpClient *http.Client
|
||||
httpAvailable bool
|
||||
modeMu sync.RWMutex
|
||||
|
||||
// Speed calculation (from system network interface)
|
||||
lastRxBytes int64
|
||||
lastTxBytes int64
|
||||
lastSampleTime time.Time
|
||||
speedMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient creates a new agent client
|
||||
func NewClient(cfg *config.Config) *Client {
|
||||
return &Client{
|
||||
config: cfg,
|
||||
collector: collector.NewCollector(),
|
||||
xrayServers: cfg.XrayServers,
|
||||
stopCh: make(chan struct{}),
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
currentMode: ModePull, // Default to pull mode
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the agent client with automatic mode selection
|
||||
func (c *Client) Start(ctx context.Context) {
|
||||
log.Printf("[Agent] Starting in %s mode", c.config.ConnectionMode)
|
||||
|
||||
mode := ConnectionMode(c.config.ConnectionMode)
|
||||
|
||||
switch mode {
|
||||
case ModeWebSocket:
|
||||
c.wg.Add(1)
|
||||
go c.runWebSocket(ctx)
|
||||
|
||||
case ModeHTTP:
|
||||
c.wg.Add(1)
|
||||
go c.runHTTPReporter(ctx)
|
||||
|
||||
case ModePull:
|
||||
c.setCurrentMode(ModePull)
|
||||
log.Printf("[Agent] Pull mode enabled - API will be served at /api/child/traffic and /api/child/speed")
|
||||
|
||||
case ModeAuto:
|
||||
fallthrough
|
||||
default:
|
||||
c.wg.Add(1)
|
||||
go c.runAutoMode(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the agent client
|
||||
func (c *Client) Stop() {
|
||||
close(c.stopCh)
|
||||
c.wg.Wait()
|
||||
|
||||
c.wsMu.Lock()
|
||||
if c.wsConn != nil {
|
||||
c.wsConn.Close()
|
||||
}
|
||||
c.wsMu.Unlock()
|
||||
|
||||
log.Printf("[Agent] Stopped")
|
||||
}
|
||||
|
||||
// IsConnected returns whether the WebSocket is connected
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.wsMu.Lock()
|
||||
defer c.wsMu.Unlock()
|
||||
return c.connected
|
||||
}
|
||||
|
||||
// GetCurrentMode returns the current connection mode
|
||||
func (c *Client) GetCurrentMode() ConnectionMode {
|
||||
c.modeMu.RLock()
|
||||
defer c.modeMu.RUnlock()
|
||||
return c.currentMode
|
||||
}
|
||||
|
||||
// setCurrentMode sets the current connection mode
|
||||
func (c *Client) setCurrentMode(mode ConnectionMode) {
|
||||
c.modeMu.Lock()
|
||||
defer c.modeMu.Unlock()
|
||||
c.currentMode = mode
|
||||
}
|
||||
|
||||
// runWebSocket manages the WebSocket connection lifecycle with fallback to auto mode
|
||||
func (c *Client) runWebSocket(ctx context.Context) {
|
||||
defer c.wg.Done()
|
||||
|
||||
maxConsecutiveFailures := 5
|
||||
consecutiveFailures := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
c.setCurrentMode(ModeWebSocket)
|
||||
if err := c.connectAndRun(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Printf("[Agent] Context canceled, stopping gracefully")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Agent] WebSocket error: %v", err)
|
||||
consecutiveFailures++
|
||||
|
||||
if consecutiveFailures >= maxConsecutiveFailures {
|
||||
log.Printf("[Agent] Too many WebSocket failures (%d), switching to auto mode for fallback...", consecutiveFailures)
|
||||
c.runAutoModeLoop(ctx)
|
||||
consecutiveFailures = 0
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures = 0
|
||||
}
|
||||
|
||||
backoff := c.calculateBackoff()
|
||||
log.Printf("[Agent] Reconnecting in %v...", backoff)
|
||||
c.waitWithTrafficReport(ctx, backoff)
|
||||
}
|
||||
}
|
||||
|
||||
// calculateBackoff calculates the reconnection backoff duration
|
||||
func (c *Client) calculateBackoff() time.Duration {
|
||||
c.reconnects++
|
||||
backoff := time.Duration(c.reconnects) * 5 * time.Second
|
||||
if backoff > 5*time.Minute {
|
||||
backoff = 5 * time.Minute
|
||||
}
|
||||
return backoff
|
||||
}
|
||||
|
||||
// connectAndRun establishes and maintains a WebSocket connection
|
||||
func (c *Client) connectAndRun(ctx context.Context) error {
|
||||
masterURL := c.config.MasterURL
|
||||
u, err := url.Parse(masterURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
u.Scheme = "ws"
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
}
|
||||
|
||||
u.Path = "/api/remote/ws"
|
||||
|
||||
log.Printf("[Agent] Connecting to %s", u.String())
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, _, err := dialer.DialContext(ctx, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
c.wsConn = conn
|
||||
c.wsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
c.wsMu.Lock()
|
||||
c.wsConn = nil
|
||||
c.connected = false
|
||||
c.wsMu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
if err := c.authenticate(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
c.connected = true
|
||||
c.reconnects = 0
|
||||
c.wsMu.Unlock()
|
||||
|
||||
log.Printf("[Agent] Connected and authenticated")
|
||||
|
||||
return c.runMessageLoop(ctx, conn)
|
||||
}
|
||||
|
||||
// authenticate sends the authentication message
|
||||
func (c *Client) authenticate(conn *websocket.Conn) error {
|
||||
authPayload, _ := json.Marshal(map[string]string{
|
||||
"token": c.config.Token,
|
||||
})
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "auth",
|
||||
"payload": json.RawMessage(authPayload),
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Type string `json:"type"`
|
||||
Payload struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.Type != "auth_result" || !result.Payload.Success {
|
||||
return &AuthError{Message: result.Payload.Message}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMessageLoop handles sending traffic data, speed data, and heartbeats
|
||||
func (c *Client) runMessageLoop(ctx context.Context, conn *websocket.Conn) error {
|
||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
||||
heartbeatTicker := time.NewTicker(30 * time.Second)
|
||||
defer trafficTicker.Stop()
|
||||
defer speedTicker.Stop()
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
msgCh := make(chan []byte, 10)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
// Send message to processing channel
|
||||
select {
|
||||
case msgCh <- message:
|
||||
default:
|
||||
log.Printf("[Agent] Message queue full, dropping message")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
c.sendTrafficData(conn)
|
||||
c.sendSpeedData(conn)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.stopCh:
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case msg := <-msgCh:
|
||||
c.handleMessage(conn, msg)
|
||||
case <-trafficTicker.C:
|
||||
if err := c.sendTrafficData(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-speedTicker.C:
|
||||
if err := c.sendSpeedData(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-heartbeatTicker.C:
|
||||
if err := c.sendHeartbeat(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendTrafficData collects and sends traffic data to the master
|
||||
func (c *Client) sendTrafficData(conn *websocket.Conn) error {
|
||||
stats, err := c.collectLocalMetrics()
|
||||
if err != nil {
|
||||
log.Printf("[Agent] Failed to collect metrics: %v", err)
|
||||
stats = &collector.XrayStats{}
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"stats": stats,
|
||||
})
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "traffic",
|
||||
"payload": json.RawMessage(payload),
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
err = conn.WriteJSON(msg)
|
||||
c.wsMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Agent] Sent traffic data: %d inbounds, %d outbounds, %d users",
|
||||
len(stats.Inbound), len(stats.Outbound), len(stats.User))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendHeartbeat sends a heartbeat message
|
||||
func (c *Client) sendHeartbeat(conn *websocket.Conn) error {
|
||||
now := time.Now()
|
||||
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"boot_time": now,
|
||||
"listen_port": listenPort,
|
||||
})
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "heartbeat",
|
||||
"payload": json.RawMessage(payload),
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
err := conn.WriteJSON(msg)
|
||||
c.wsMu.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// collectLocalMetrics collects traffic metrics from local Xray servers
|
||||
func (c *Client) collectLocalMetrics() (*collector.XrayStats, error) {
|
||||
stats := &collector.XrayStats{
|
||||
Inbound: make(map[string]collector.TrafficData),
|
||||
Outbound: make(map[string]collector.TrafficData),
|
||||
User: make(map[string]collector.TrafficData),
|
||||
}
|
||||
|
||||
for _, server := range c.xrayServers {
|
||||
host, port, err := c.collector.GetMetricsPortFromConfig(server.ConfigPath)
|
||||
if err != nil {
|
||||
log.Printf("[Agent] Failed to get metrics config for %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
metrics, err := c.collector.FetchMetrics(host, port)
|
||||
if err != nil {
|
||||
log.Printf("[Agent] Failed to fetch metrics for %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if metrics.Stats != nil {
|
||||
collector.MergeStats(stats, metrics.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStats returns the current traffic stats (for pull mode)
|
||||
func (c *Client) GetStats() (*collector.XrayStats, error) {
|
||||
return c.collectLocalMetrics()
|
||||
}
|
||||
|
||||
// GetSpeed returns the current speed data (for pull mode)
|
||||
func (c *Client) GetSpeed() (uploadSpeed, downloadSpeed int64) {
|
||||
return c.collectSpeed()
|
||||
}
|
||||
|
||||
// runAutoMode implements the three-tier fallback: WebSocket -> HTTP -> Pull
|
||||
func (c *Client) runAutoMode(ctx context.Context) {
|
||||
defer c.wg.Done()
|
||||
c.runAutoModeLoop(ctx)
|
||||
}
|
||||
|
||||
// runAutoModeLoop is the internal loop for auto mode fallback
|
||||
func (c *Client) runAutoModeLoop(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
log.Printf("[Agent] Trying WebSocket connection...")
|
||||
if err := c.tryWebSocketOnce(ctx); err == nil {
|
||||
c.setCurrentMode(ModeWebSocket)
|
||||
log.Printf("[Agent] WebSocket mode active")
|
||||
if err := c.connectAndRun(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Printf("[Agent] Context canceled, stopping gracefully")
|
||||
return
|
||||
}
|
||||
log.Printf("[Agent] WebSocket disconnected: %v", err)
|
||||
}
|
||||
c.reconnects = 0
|
||||
continue
|
||||
} else {
|
||||
log.Printf("[Agent] WebSocket failed: %v, trying HTTP...", err)
|
||||
}
|
||||
|
||||
if c.tryHTTPOnce(ctx) {
|
||||
c.setCurrentMode(ModeHTTP)
|
||||
log.Printf("[Agent] HTTP mode active")
|
||||
c.runHTTPReporterLoop(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
c.setCurrentMode(ModePull)
|
||||
log.Printf("[Agent] Falling back to pull mode - API available at /api/child/traffic and /api/child/speed")
|
||||
|
||||
c.runPullModeWithTrafficReport(ctx, 30*time.Second)
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[Agent] Retrying higher-priority connection modes...")
|
||||
}
|
||||
}
|
||||
|
||||
// tryWebSocketOnce attempts a single WebSocket connection test
|
||||
func (c *Client) tryWebSocketOnce(ctx context.Context) error {
|
||||
masterURL := c.config.MasterURL
|
||||
u, err := url.Parse(masterURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
u.Scheme = "ws"
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
}
|
||||
u.Path = "/api/remote/ws"
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, _, err := dialer.DialContext(ctx, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryHTTPOnce tests if HTTP push is available
|
||||
func (c *Client) tryHTTPOnce(ctx context.Context) bool {
|
||||
u, err := url.Parse(c.config.MasterURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
u.Path = "/api/remote/heartbeat"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader([]byte("{}")))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.Token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[Agent] HTTP test failed: %v", err)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
c.httpAvailable = resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized
|
||||
return c.httpAvailable
|
||||
}
|
||||
|
||||
// runHTTPReporter runs the HTTP push reporter
|
||||
func (c *Client) runHTTPReporter(ctx context.Context) {
|
||||
defer c.wg.Done()
|
||||
c.setCurrentMode(ModeHTTP)
|
||||
c.runHTTPReporterLoop(ctx)
|
||||
}
|
||||
|
||||
// runHTTPReporterLoop runs the HTTP reporting loop
|
||||
func (c *Client) runHTTPReporterLoop(ctx context.Context) {
|
||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
||||
heartbeatTicker := time.NewTicker(30 * time.Second)
|
||||
defer trafficTicker.Stop()
|
||||
defer speedTicker.Stop()
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
c.sendTrafficHTTP(ctx)
|
||||
c.sendSpeedHTTP(ctx)
|
||||
|
||||
consecutiveErrors := 0
|
||||
maxErrors := 5
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.stopCh:
|
||||
return
|
||||
case <-trafficTicker.C:
|
||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||
consecutiveErrors++
|
||||
if consecutiveErrors >= maxErrors {
|
||||
log.Printf("[Agent] Too many HTTP errors, will retry connection modes")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
consecutiveErrors = 0
|
||||
}
|
||||
case <-speedTicker.C:
|
||||
if err := c.sendSpeedHTTP(ctx); err != nil {
|
||||
log.Printf("[Agent] Failed to send speed via HTTP: %v", err)
|
||||
}
|
||||
case <-heartbeatTicker.C:
|
||||
if err := c.sendHeartbeatHTTP(ctx); err != nil {
|
||||
consecutiveErrors++
|
||||
if consecutiveErrors >= maxErrors {
|
||||
log.Printf("[Agent] Too many HTTP errors, will retry connection modes")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
consecutiveErrors = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendTrafficHTTP sends traffic data via HTTP POST
|
||||
func (c *Client) sendTrafficHTTP(ctx context.Context) error {
|
||||
stats, err := c.collectLocalMetrics()
|
||||
if err != nil {
|
||||
stats = &collector.XrayStats{}
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"stats": stats,
|
||||
})
|
||||
|
||||
u, err := url.Parse(c.config.MasterURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Path = "/api/remote/traffic"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.Token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Printf("[Agent] Sent traffic data via HTTP: %d inbounds, %d outbounds, %d users",
|
||||
len(stats.Inbound), len(stats.Outbound), len(stats.User))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendSpeedHTTP sends speed data via HTTP POST
|
||||
func (c *Client) sendSpeedHTTP(ctx context.Context) error {
|
||||
uploadSpeed, downloadSpeed := c.collectSpeed()
|
||||
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"upload_speed": uploadSpeed,
|
||||
"download_speed": downloadSpeed,
|
||||
})
|
||||
|
||||
u, err := url.Parse(c.config.MasterURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Path = "/api/remote/speed"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.Token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Printf("[Agent] Sent speed via HTTP: ↑%d B/s ↓%d B/s", uploadSpeed, downloadSpeed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendHeartbeatHTTP sends heartbeat via HTTP POST
|
||||
func (c *Client) sendHeartbeatHTTP(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"boot_time": now,
|
||||
"listen_port": listenPort,
|
||||
})
|
||||
|
||||
u, err := url.Parse(c.config.MasterURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Path = "/api/remote/heartbeat"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.Token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runPullModeWithTrafficReport runs pull mode while sending traffic data to keep server online
|
||||
func (c *Client) runPullModeWithTrafficReport(ctx context.Context, duration time.Duration) {
|
||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||
defer trafficTicker.Stop()
|
||||
|
||||
timeout := time.After(duration)
|
||||
|
||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||
log.Printf("[Agent] Pull mode traffic report failed: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.stopCh:
|
||||
return
|
||||
case <-timeout:
|
||||
return
|
||||
case <-trafficTicker.C:
|
||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||
log.Printf("[Agent] Pull mode traffic report failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitWithTrafficReport waits for the specified duration while sending traffic data
|
||||
func (c *Client) waitWithTrafficReport(ctx context.Context, duration time.Duration) {
|
||||
if duration <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if duration > 30*time.Second {
|
||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||
log.Printf("[Agent] Traffic report during backoff failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||
defer trafficTicker.Stop()
|
||||
|
||||
timeout := time.After(duration)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.stopCh:
|
||||
return
|
||||
case <-timeout:
|
||||
return
|
||||
case <-trafficTicker.C:
|
||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||
log.Printf("[Agent] Traffic report during backoff failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendSpeedData sends speed data via WebSocket
|
||||
func (c *Client) sendSpeedData(conn *websocket.Conn) error {
|
||||
uploadSpeed, downloadSpeed := c.collectSpeed()
|
||||
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"upload_speed": uploadSpeed,
|
||||
"download_speed": downloadSpeed,
|
||||
})
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "speed",
|
||||
"payload": json.RawMessage(payload),
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
err := conn.WriteJSON(msg)
|
||||
c.wsMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Agent] Sent speed data: ↑%d B/s ↓%d B/s", uploadSpeed, downloadSpeed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectSpeed calculates the current upload and download speed from system network interface
|
||||
func (c *Client) collectSpeed() (uploadSpeed, downloadSpeed int64) {
|
||||
c.speedMu.Lock()
|
||||
defer c.speedMu.Unlock()
|
||||
|
||||
rxBytes, txBytes := c.getSystemNetworkStats()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if !c.lastSampleTime.IsZero() && c.lastRxBytes > 0 {
|
||||
elapsed := now.Sub(c.lastSampleTime).Seconds()
|
||||
if elapsed > 0 {
|
||||
uploadSpeed = int64(float64(txBytes-c.lastTxBytes) / elapsed)
|
||||
downloadSpeed = int64(float64(rxBytes-c.lastRxBytes) / elapsed)
|
||||
|
||||
if uploadSpeed < 0 {
|
||||
uploadSpeed = 0
|
||||
}
|
||||
if downloadSpeed < 0 {
|
||||
downloadSpeed = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.lastRxBytes = rxBytes
|
||||
c.lastTxBytes = txBytes
|
||||
c.lastSampleTime = now
|
||||
|
||||
return uploadSpeed, downloadSpeed
|
||||
}
|
||||
|
||||
// getSystemNetworkStats reads network statistics from /proc/net/dev
|
||||
func (c *Client) getSystemNetworkStats() (rxBytes, txBytes int64) {
|
||||
data, err := os.ReadFile("/proc/net/dev")
|
||||
if err != nil {
|
||||
log.Printf("[Agent] Failed to read /proc/net/dev: %v", err)
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Inter") || strings.HasPrefix(line, "face") || strings.HasPrefix(line, "lo:") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(parts[1])
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
rx, err1 := strconv.ParseInt(fields[0], 10, 64)
|
||||
tx, err2 := strconv.ParseInt(fields[8], 10, 64)
|
||||
if err1 == nil && err2 == nil {
|
||||
rxBytes += rx
|
||||
txBytes += tx
|
||||
}
|
||||
}
|
||||
|
||||
return rxBytes, txBytes
|
||||
}
|
||||
|
||||
// AuthError represents an authentication error
|
||||
type AuthError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return "authentication failed: " + e.Message
|
||||
}
|
||||
|
||||
// WebSocket message types
|
||||
const (
|
||||
WSMsgTypeCertRequest = "cert_request"
|
||||
WSMsgTypeCertUpdate = "cert_update"
|
||||
)
|
||||
|
||||
// WSCertRequestPayload represents a certificate request from master
|
||||
type WSCertRequestPayload struct {
|
||||
CertID int64 `json:"cert_id"`
|
||||
Domain string `json:"domain"`
|
||||
Email string `json:"email"`
|
||||
Provider string `json:"provider"`
|
||||
ChallengeMode string `json:"challenge_mode"`
|
||||
WebrootPath string `json:"webroot_path,omitempty"`
|
||||
}
|
||||
|
||||
// WSCertUpdatePayload represents a certificate update response to master
|
||||
type WSCertUpdatePayload struct {
|
||||
CertID int64 `json:"cert_id"`
|
||||
Domain string `json:"domain"`
|
||||
Success bool `json:"success"`
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
CertPEM string `json:"cert_pem,omitempty"`
|
||||
KeyPEM string `json:"key_pem,omitempty"`
|
||||
IssueDate time.Time `json:"issue_date,omitempty"`
|
||||
ExpiryDate time.Time `json:"expiry_date,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// handleMessage processes incoming messages from master
|
||||
func (c *Client) handleMessage(conn *websocket.Conn, message []byte) {
|
||||
var msg struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
log.Printf("[Agent] Failed to parse message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case WSMsgTypeCertRequest:
|
||||
var payload WSCertRequestPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
log.Printf("[Agent] Failed to parse cert_request payload: %v", err)
|
||||
return
|
||||
}
|
||||
go c.handleCertRequest(conn, payload)
|
||||
default:
|
||||
// Ignore unknown message types
|
||||
}
|
||||
}
|
||||
|
||||
// handleCertRequest processes a certificate request from master
|
||||
func (c *Client) handleCertRequest(conn *websocket.Conn, req WSCertRequestPayload) {
|
||||
log.Printf("[Agent] Received certificate request for domain: %s", req.Domain)
|
||||
|
||||
result := c.requestCertificate(req)
|
||||
|
||||
// Send result back to master
|
||||
payload, _ := json.Marshal(result)
|
||||
msg := map[string]interface{}{
|
||||
"type": WSMsgTypeCertUpdate,
|
||||
"payload": json.RawMessage(payload),
|
||||
}
|
||||
|
||||
c.wsMu.Lock()
|
||||
err := conn.WriteJSON(msg)
|
||||
c.wsMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[Agent] Failed to send cert_update: %v", err)
|
||||
} else {
|
||||
log.Printf("[Agent] Sent cert_update for domain %s, success=%v", req.Domain, result.Success)
|
||||
}
|
||||
}
|
||||
|
||||
// requestCertificate performs the actual certificate request using ACME
|
||||
func (c *Client) requestCertificate(req WSCertRequestPayload) WSCertUpdatePayload {
|
||||
result := WSCertUpdatePayload{
|
||||
CertID: req.CertID,
|
||||
Domain: req.Domain,
|
||||
}
|
||||
|
||||
// Create ACME client with appropriate options
|
||||
opts := []acme.ClientOption{}
|
||||
if req.ChallengeMode == "webroot" && req.WebrootPath != "" {
|
||||
opts = append(opts, acme.WithWebrootDir(req.WebrootPath))
|
||||
}
|
||||
|
||||
acmeClient := acme.NewClient(opts...)
|
||||
|
||||
// Determine if we should use webroot mode
|
||||
useWebroot := req.ChallengeMode == "webroot" && req.WebrootPath != ""
|
||||
|
||||
// Request the certificate
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
certResult, err := acmeClient.ObtainCertificate(ctx, req.Email, req.Domain, useWebroot)
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
log.Printf("[Agent] Certificate request failed for %s: %v", req.Domain, err)
|
||||
return result
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
result.CertPath = certResult.CertPath
|
||||
result.KeyPath = certResult.KeyPath
|
||||
result.CertPEM = certResult.CertPEM
|
||||
result.KeyPEM = certResult.KeyPEM
|
||||
result.IssueDate = certResult.IssueDate
|
||||
result.ExpiryDate = certResult.ExpiryDate
|
||||
|
||||
log.Printf("[Agent] Certificate obtained for %s, expires: %s", req.Domain, certResult.ExpiryDate)
|
||||
return result
|
||||
}
|
||||
176
internal/collector/metrics.go
Normal file
176
internal/collector/metrics.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// XrayMetrics represents the metrics response from Xray's /debug/vars endpoint
|
||||
type XrayMetrics struct {
|
||||
Stats *XrayStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
// XrayStats contains inbound, outbound, and user traffic stats
|
||||
type XrayStats struct {
|
||||
Inbound map[string]TrafficData `json:"inbound,omitempty"`
|
||||
Outbound map[string]TrafficData `json:"outbound,omitempty"`
|
||||
User map[string]TrafficData `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// TrafficData contains uplink and downlink traffic in bytes
|
||||
type TrafficData struct {
|
||||
Uplink int64 `json:"uplink"`
|
||||
Downlink int64 `json:"downlink"`
|
||||
}
|
||||
|
||||
// XrayConfig represents the structure of xray config.json for reading metrics port
|
||||
type XrayConfig struct {
|
||||
Log json.RawMessage `json:"log,omitempty"`
|
||||
DNS json.RawMessage `json:"dns,omitempty"`
|
||||
API json.RawMessage `json:"api,omitempty"`
|
||||
Stats json.RawMessage `json:"stats,omitempty"`
|
||||
Policy json.RawMessage `json:"policy,omitempty"`
|
||||
Routing json.RawMessage `json:"routing,omitempty"`
|
||||
Inbounds json.RawMessage `json:"inbounds,omitempty"`
|
||||
Outbounds json.RawMessage `json:"outbounds,omitempty"`
|
||||
Metrics *MetricsConfig `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
// MetricsConfig represents the metrics section in xray config
|
||||
type MetricsConfig struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Listen string `json:"listen,omitempty"` // Format: "127.0.0.1:38889"
|
||||
}
|
||||
|
||||
// Collector collects traffic metrics from Xray servers
|
||||
type Collector struct {
|
||||
httpClient *http.Client
|
||||
defaultMetricsPort int
|
||||
defaultMetricsHost string
|
||||
}
|
||||
|
||||
// NewCollector creates a new metrics collector
|
||||
func NewCollector() *Collector {
|
||||
return &Collector{
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
defaultMetricsPort: 38889,
|
||||
defaultMetricsHost: "127.0.0.1",
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetricsPortFromConfig reads the metrics port from xray config file
|
||||
func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, error) {
|
||||
if configPath == "" {
|
||||
return "127.0.0.1", c.defaultMetricsPort, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return "127.0.0.1", c.defaultMetricsPort, fmt.Errorf("read config file: %w", err)
|
||||
}
|
||||
|
||||
var config XrayConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return "127.0.0.1", c.defaultMetricsPort, fmt.Errorf("parse config file: %w", err)
|
||||
}
|
||||
|
||||
if config.Metrics == nil || config.Metrics.Listen == "" {
|
||||
return "", 0, fmt.Errorf("metrics not configured in xray config")
|
||||
}
|
||||
|
||||
// Parse listen address (format: "127.0.0.1:38889" or ":38889")
|
||||
listen := config.Metrics.Listen
|
||||
host := "127.0.0.1"
|
||||
var port int
|
||||
|
||||
if strings.Contains(listen, ":") {
|
||||
parts := strings.Split(listen, ":")
|
||||
if len(parts) == 2 {
|
||||
if parts[0] != "" {
|
||||
host = parts[0]
|
||||
}
|
||||
p, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid metrics port: %s", parts[1])
|
||||
}
|
||||
port = p
|
||||
}
|
||||
} else {
|
||||
// Try to parse as port only
|
||||
p, err := strconv.Atoi(listen)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid metrics listen format: %s", listen)
|
||||
}
|
||||
port = p
|
||||
}
|
||||
|
||||
if port <= 0 || port > 65535 {
|
||||
return "", 0, fmt.Errorf("invalid metrics port: %d", port)
|
||||
}
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// FetchMetrics fetches metrics from Xray's /debug/vars endpoint
|
||||
func (c *Collector) FetchMetrics(host string, port int) (*XrayMetrics, error) {
|
||||
url := fmt.Sprintf("http://%s:%d/debug/vars", host, port)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
|
||||
var metrics XrayMetrics
|
||||
if err := json.Unmarshal(body, &metrics); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal metrics: %w", err)
|
||||
}
|
||||
|
||||
return &metrics, nil
|
||||
}
|
||||
|
||||
// MergeStats merges source stats into dest stats
|
||||
func MergeStats(dest, source *XrayStats) {
|
||||
if source == nil {
|
||||
return
|
||||
}
|
||||
if dest.Inbound == nil {
|
||||
dest.Inbound = make(map[string]TrafficData)
|
||||
}
|
||||
if dest.Outbound == nil {
|
||||
dest.Outbound = make(map[string]TrafficData)
|
||||
}
|
||||
if dest.User == nil {
|
||||
dest.User = make(map[string]TrafficData)
|
||||
}
|
||||
|
||||
for k, v := range source.Inbound {
|
||||
dest.Inbound[k] = v
|
||||
}
|
||||
for k, v := range source.Outbound {
|
||||
dest.Outbound[k] = v
|
||||
}
|
||||
for k, v := range source.User {
|
||||
dest.User[k] = v
|
||||
}
|
||||
}
|
||||
152
internal/config/config.go
Normal file
152
internal/config/config.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds the agent configuration
|
||||
type Config struct {
|
||||
MasterURL string `yaml:"master_url"`
|
||||
Token string `yaml:"token"`
|
||||
ConnectionMode string `yaml:"connection_mode"`
|
||||
ListenPort string `yaml:"listen_port"`
|
||||
XrayServers []XrayServer `yaml:"xray_servers"`
|
||||
TrafficReportInterval time.Duration `yaml:"traffic_report_interval"`
|
||||
SpeedReportInterval time.Duration `yaml:"speed_report_interval"`
|
||||
}
|
||||
|
||||
// XrayServer represents a local Xray server configuration
|
||||
type XrayServer struct {
|
||||
Name string `yaml:"name"`
|
||||
ConfigPath string `yaml:"config_path"`
|
||||
}
|
||||
|
||||
// DefaultXrayConfigPaths are the default paths to search for Xray config
|
||||
var DefaultXrayConfigPaths = []string{
|
||||
"/usr/local/etc/xray/config.json",
|
||||
"/etc/xray/config.json",
|
||||
"/opt/xray/config.json",
|
||||
}
|
||||
|
||||
// Load loads configuration from a YAML file
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
config.applyDefaults()
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// FromEnv creates configuration from environment variables
|
||||
func FromEnv() *Config {
|
||||
config := &Config{
|
||||
MasterURL: os.Getenv("MMWX_MASTER_URL"),
|
||||
Token: os.Getenv("MMWX_TOKEN"),
|
||||
ConnectionMode: os.Getenv("MMWX_CONNECTION_MODE"),
|
||||
ListenPort: os.Getenv("MMWX_LISTEN_PORT"),
|
||||
}
|
||||
|
||||
// Parse Xray config path from env
|
||||
if xrayConfig := os.Getenv("MMWX_XRAY_CONFIG"); xrayConfig != "" {
|
||||
config.XrayServers = []XrayServer{
|
||||
{Name: "primary", ConfigPath: xrayConfig},
|
||||
}
|
||||
}
|
||||
|
||||
// Parse intervals
|
||||
if interval := os.Getenv("MMWX_TRAFFIC_INTERVAL"); interval != "" {
|
||||
if d, err := time.ParseDuration(interval); err == nil {
|
||||
config.TrafficReportInterval = d
|
||||
}
|
||||
}
|
||||
if interval := os.Getenv("MMWX_SPEED_INTERVAL"); interval != "" {
|
||||
if d, err := time.ParseDuration(interval); err == nil {
|
||||
config.SpeedReportInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
config.applyDefaults()
|
||||
return config
|
||||
}
|
||||
|
||||
// Merge merges environment config into file config (env takes precedence)
|
||||
func (c *Config) Merge(env *Config) {
|
||||
if env.MasterURL != "" {
|
||||
c.MasterURL = env.MasterURL
|
||||
}
|
||||
if env.Token != "" {
|
||||
c.Token = env.Token
|
||||
}
|
||||
if env.ConnectionMode != "" {
|
||||
c.ConnectionMode = env.ConnectionMode
|
||||
}
|
||||
if env.ListenPort != "" {
|
||||
c.ListenPort = env.ListenPort
|
||||
}
|
||||
if len(env.XrayServers) > 0 {
|
||||
c.XrayServers = env.XrayServers
|
||||
}
|
||||
if env.TrafficReportInterval > 0 {
|
||||
c.TrafficReportInterval = env.TrafficReportInterval
|
||||
}
|
||||
if env.SpeedReportInterval > 0 {
|
||||
c.SpeedReportInterval = env.SpeedReportInterval
|
||||
}
|
||||
}
|
||||
|
||||
// applyDefaults sets default values for unset fields
|
||||
func (c *Config) applyDefaults() {
|
||||
if c.ConnectionMode == "" {
|
||||
c.ConnectionMode = "auto"
|
||||
}
|
||||
if c.ListenPort == "" {
|
||||
c.ListenPort = "8081"
|
||||
}
|
||||
if c.TrafficReportInterval == 0 {
|
||||
c.TrafficReportInterval = 1 * time.Minute
|
||||
}
|
||||
if c.SpeedReportInterval == 0 {
|
||||
c.SpeedReportInterval = 3 * time.Second
|
||||
}
|
||||
|
||||
// Auto-discover Xray servers if not configured
|
||||
if len(c.XrayServers) == 0 {
|
||||
c.XrayServers = c.discoverXrayServers()
|
||||
}
|
||||
}
|
||||
|
||||
// discoverXrayServers scans default paths for Xray config files
|
||||
func (c *Config) discoverXrayServers() []XrayServer {
|
||||
var servers []XrayServer
|
||||
for i, path := range DefaultXrayConfigPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
servers = append(servers, XrayServer{
|
||||
Name: "xray-" + strconv.Itoa(i+1),
|
||||
ConfigPath: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
// Token is required for non-pull modes
|
||||
if c.ConnectionMode != "pull" && c.Token == "" {
|
||||
// Allow empty token, will work in pull mode only
|
||||
}
|
||||
return nil
|
||||
}
|
||||
106
internal/handler/api.go
Normal file
106
internal/handler/api.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"mmw-agent/internal/agent"
|
||||
)
|
||||
|
||||
// APIHandler handles API requests from the master server (for pull mode)
|
||||
type APIHandler struct {
|
||||
client *agent.Client
|
||||
configToken string
|
||||
}
|
||||
|
||||
// NewAPIHandler creates a new API handler
|
||||
func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
|
||||
return &APIHandler{
|
||||
client: client,
|
||||
configToken: configToken,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles the HTTP request for traffic data
|
||||
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.authenticate(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := h.client.GetStats()
|
||||
if err != nil {
|
||||
log.Printf("[API] Failed to get stats: %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Failed to collect stats",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
// ServeSpeedHTTP handles the HTTP request for speed data
|
||||
func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.authenticate(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uploadSpeed, downloadSpeed := h.client.GetSpeed()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"upload_speed": uploadSpeed,
|
||||
"download_speed": downloadSpeed,
|
||||
})
|
||||
}
|
||||
|
||||
// authenticate checks if the request is authorized
|
||||
func (h *APIHandler) authenticate(r *http.Request) bool {
|
||||
if h.configToken == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
return token == h.configToken
|
||||
}
|
||||
|
||||
return auth == h.configToken
|
||||
}
|
||||
2368
internal/handler/manage.go
Normal file
2368
internal/handler/manage.go
Normal file
File diff suppressed because it is too large
Load Diff
41
internal/xrpc/client.go
Normal file
41
internal/xrpc/client.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package xrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
loggerpb "github.com/xtls/xray-core/app/log/command"
|
||||
handlerpb "github.com/xtls/xray-core/app/proxyman/command"
|
||||
statspb "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// Clients groups the gRPC stubs that the samples rely on.
|
||||
type Clients struct {
|
||||
Connection *grpc.ClientConn
|
||||
Handler handlerpb.HandlerServiceClient
|
||||
Logger loggerpb.LoggerServiceClient
|
||||
Stats statspb.StatsServiceClient
|
||||
}
|
||||
|
||||
// New establishes an insecure (plaintext) connection against a running Xray API endpoint.
|
||||
func New(ctx context.Context, addr string, port uint16, dialOpts ...grpc.DialOption) (*Clients, error) {
|
||||
target := fmt.Sprintf("%s:%d", addr, port)
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
}
|
||||
opts = append(opts, dialOpts...)
|
||||
|
||||
conn, err := grpc.DialContext(ctx, target, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Clients{
|
||||
Connection: conn,
|
||||
Handler: handlerpb.NewHandlerServiceClient(conn),
|
||||
Logger: loggerpb.NewLoggerServiceClient(conn),
|
||||
Stats: statspb.NewStatsServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
76
internal/xrpc/services/handler/common.go
Normal file
76
internal/xrpc/services/handler/common.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
cnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func receiverSettings(port uint32, enableSniff bool) *serial.TypedMessage {
|
||||
pr := cnet.SinglePortRange(cnet.Port(port))
|
||||
rc := &proxyman.ReceiverConfig{
|
||||
PortList: &cnet.PortList{Range: []*cnet.PortRange{pr}},
|
||||
Listen: cnet.NewIPOrDomain(cnet.AnyIP),
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
},
|
||||
}
|
||||
if enableSniff {
|
||||
rc.SniffingSettings = &proxyman.SniffingConfig{
|
||||
Enabled: true,
|
||||
DestinationOverride: []string{"http", "tls"},
|
||||
}
|
||||
}
|
||||
return serial.ToTypedMessage(rc)
|
||||
}
|
||||
|
||||
func senderSettings() *serial.TypedMessage {
|
||||
return serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
},
|
||||
MultiplexSettings: &proxyman.MultiplexingConfig{
|
||||
Enabled: true,
|
||||
Concurrency: 8,
|
||||
XudpProxyUDP443: "reject",
|
||||
},
|
||||
TargetStrategy: internet.DomainStrategy_USE_IP,
|
||||
})
|
||||
}
|
||||
|
||||
func endpoint(address string, port uint32, user *protocol.User) *protocol.ServerEndpoint {
|
||||
return &protocol.ServerEndpoint{
|
||||
Address: cnet.NewIPOrDomain(cnet.ParseAddress(address)),
|
||||
Port: port,
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
||||
func randomUUID() string {
|
||||
u := uuid.New()
|
||||
return (&u).String()
|
||||
}
|
||||
|
||||
func cnetOrDomain(value string) *cnet.IPOrDomain {
|
||||
return cnet.NewIPOrDomain(cnet.ParseAddress(value))
|
||||
}
|
||||
|
||||
func inboundConfig(tag string, receiver *serial.TypedMessage, proxy *serial.TypedMessage) *core.InboundHandlerConfig {
|
||||
return &core.InboundHandlerConfig{
|
||||
Tag: tag,
|
||||
ReceiverSettings: receiver,
|
||||
ProxySettings: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
func outboundConfig(tag string, sender *serial.TypedMessage, proxy *serial.TypedMessage) *core.OutboundHandlerConfig {
|
||||
return &core.OutboundHandlerConfig{
|
||||
Tag: tag,
|
||||
SenderSettings: sender,
|
||||
ProxySettings: proxy,
|
||||
}
|
||||
}
|
||||
274
internal/xrpc/services/handler/inbound.go
Normal file
274
internal/xrpc/services/handler/inbound.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/xtls/xray-core/app/proxyman/command"
|
||||
cnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
"github.com/xtls/xray-core/proxy/dokodemo"
|
||||
"github.com/xtls/xray-core/proxy/http"
|
||||
"github.com/xtls/xray-core/proxy/loopback"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
ss2022 "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/proxy/socks"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
vlessin "github.com/xtls/xray-core/proxy/vless/inbound"
|
||||
"github.com/xtls/xray-core/proxy/vmess"
|
||||
vmessin "github.com/xtls/xray-core/proxy/vmess/inbound"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
)
|
||||
|
||||
// AddVMessInbound demonstrates HandlerServiceClient.AddInbound for VMess inbound.
|
||||
func AddVMessInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, true),
|
||||
serial.ToTypedMessage(&vmessin.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Email: "demo@vmess.local",
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: randomUUID(),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AUTO,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Default: &vmessin.DefaultConfig{Level: 0},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddVLESSInbound adds a VLESS inbound with Vision style fallbacks.
|
||||
func AddVLESSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, true),
|
||||
serial.ToTypedMessage(&vlessin.Config{
|
||||
Clients: []*protocol.User{
|
||||
{
|
||||
Level: 1,
|
||||
Email: "client@vless.local",
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: randomUUID(),
|
||||
Encryption: "none",
|
||||
}),
|
||||
},
|
||||
},
|
||||
Fallbacks: []*vlessin.Fallback{
|
||||
{
|
||||
Name: "websocket",
|
||||
Alpn: "h2",
|
||||
Path: "/ws",
|
||||
Type: "http",
|
||||
Dest: "127.0.0.1:8080",
|
||||
Xver: 1,
|
||||
},
|
||||
},
|
||||
Decryption: "none",
|
||||
Padding: "enable",
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTrojanInbound registers a Trojan inbound with two users and ALPN fallback.
|
||||
func AddTrojanInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, true),
|
||||
serial.ToTypedMessage(&trojan.ServerConfig{
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Email: "alice@trojan.local",
|
||||
Account: serial.ToTypedMessage(&trojan.Account{
|
||||
Password: randomUUID(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
Level: 0,
|
||||
Email: "bob@trojan.local",
|
||||
Account: serial.ToTypedMessage(&trojan.Account{
|
||||
Password: randomUUID(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
Fallbacks: []*trojan.Fallback{
|
||||
{
|
||||
Name: "http",
|
||||
Alpn: "http/1.1",
|
||||
Dest: "127.0.0.1:8081",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddShadowsocksInbound adds an AEAD Shadowsocks inbound supporting both TCP and UDP.
|
||||
func AddShadowsocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&shadowsocks.ServerConfig{
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Email: "ss@demo.local",
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: "s3cret-pass",
|
||||
CipherType: shadowsocks.CipherType_AES_128_GCM,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddShadowsocks2022Inbound covers both single user and multi-user deployment.
|
||||
func AddShadowsocks2022Inbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
server := &ss2022.MultiUserServerConfig{
|
||||
Method: "2022-blake3-aes-128-gcm",
|
||||
Key: "0123456789abcdef0123456789abcdef",
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Email: "user1@ss2022.local",
|
||||
Account: serial.ToTypedMessage(&ss2022.Account{
|
||||
Key: randomUUID(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
Level: 0,
|
||||
Email: "user2@ss2022.local",
|
||||
Account: serial.ToTypedMessage(&ss2022.Account{
|
||||
Key: randomUUID(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(server),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddSocksInbound exposes a SOCKS5 server with username/password authentication.
|
||||
func AddSocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&socks.ServerConfig{
|
||||
AuthType: socks.AuthType_PASSWORD,
|
||||
Accounts: map[string]string{"demo": "passw0rd"},
|
||||
UdpEnabled: true,
|
||||
UserLevel: 0,
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddHTTPInbound adds an HTTP proxy inbound with basic auth.
|
||||
func AddHTTPInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&http.ServerConfig{
|
||||
Accounts: map[string]string{"demo": "http-pass"},
|
||||
AllowTransparent: true,
|
||||
UserLevel: 0,
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddDokodemoInbound configures a dokodemo-door mirror port.
|
||||
func AddDokodemoInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetPort uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: cnetOrDomain("example.com"),
|
||||
Port: targetPort,
|
||||
Networks: []cnet.Network{cnet.Network_TCP, cnet.Network_UDP},
|
||||
FollowRedirect: false,
|
||||
UserLevel: 0,
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddDNSInbound exposes the built-in DNS server on an API port.
|
||||
func AddDNSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&dns.Config{
|
||||
Server: &cnet.Endpoint{
|
||||
Network: cnet.Network_UDP,
|
||||
Address: cnetOrDomain("1.1.1.1"),
|
||||
Port: 53,
|
||||
},
|
||||
Non_IPQuery: "drop",
|
||||
BlockTypes: []int32{65, 28},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddLoopbackInbound ties an inbound to an existing outbound chain.
|
||||
func AddLoopbackInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetInbound string) error {
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(&loopback.Config{InboundTag: targetInbound}),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddWireGuardInbound sets up a WireGuard entry point with a single peer.
|
||||
func AddWireGuardInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||
cfg := &wireguard.DeviceConfig{
|
||||
SecretKey: "yAnExampleSecretKeyBase64==",
|
||||
Endpoint: []string{":51820"},
|
||||
Mtu: 1420,
|
||||
NumWorkers: 2,
|
||||
DomainStrategy: wireguard.DeviceConfig_FORCE_IP46,
|
||||
Peers: []*wireguard.PeerConfig{
|
||||
{
|
||||
PublicKey: "peerPublicKeyBase64==",
|
||||
Endpoint: "203.0.113.1:51820",
|
||||
KeepAlive: 25,
|
||||
AllowedIps: []string{"0.0.0.0/0", "::/0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
inbound := inboundConfig(
|
||||
tag,
|
||||
receiverSettings(port, false),
|
||||
serial.ToTypedMessage(cfg),
|
||||
)
|
||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||
return err
|
||||
}
|
||||
59
internal/xrpc/services/handler/management.go
Normal file
59
internal/xrpc/services/handler/management.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/xtls/xray-core/app/proxyman/command"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
)
|
||||
|
||||
func RemoveInbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
_, err := client.RemoveInbound(ctx, &command.RemoveInboundRequest{Tag: tag})
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
_, err := client.RemoveOutbound(ctx, &command.RemoveOutboundRequest{Tag: tag})
|
||||
return err
|
||||
}
|
||||
|
||||
func ListInboundTags(ctx context.Context, client command.HandlerServiceClient) ([]string, error) {
|
||||
resp, err := client.ListInbounds(ctx, &command.ListInboundsRequest{IsOnlyTags: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags := make([]string, 0, len(resp.GetInbounds()))
|
||||
for _, inbound := range resp.GetInbounds() {
|
||||
tags = append(tags, inbound.GetTag())
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func GetInboundUsers(ctx context.Context, client command.HandlerServiceClient, inboundTag string) ([]*protocol.User, error) {
|
||||
resp, err := client.GetInboundUsers(ctx, &command.GetInboundUserRequest{
|
||||
Tag: inboundTag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.GetUsers(), nil
|
||||
}
|
||||
|
||||
func GetInboundUsersCount(ctx context.Context, client command.HandlerServiceClient, inboundTag string) (int64, error) {
|
||||
resp, err := client.GetInboundUsersCount(ctx, &command.GetInboundUserRequest{
|
||||
Tag: inboundTag,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resp.GetCount(), nil
|
||||
}
|
||||
|
||||
func AlterOutbound(ctx context.Context, client command.HandlerServiceClient, tag string, operation *serial.TypedMessage) error {
|
||||
_, err := client.AlterOutbound(ctx, &command.AlterOutboundRequest{
|
||||
Tag: tag,
|
||||
Operation: operation,
|
||||
})
|
||||
return err
|
||||
}
|
||||
221
internal/xrpc/services/handler/outbound.go
Normal file
221
internal/xrpc/services/handler/outbound.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/xtls/xray-core/app/proxyman/command"
|
||||
cnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/proxy/blackhole"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/http"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
ss2022 "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/proxy/socks"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
vlessout "github.com/xtls/xray-core/proxy/vless/outbound"
|
||||
"github.com/xtls/xray-core/proxy/vmess"
|
||||
vmessout "github.com/xtls/xray-core/proxy/vmess/outbound"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func AddFreedomOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&freedom.Config{
|
||||
DomainStrategy: internet.DomainStrategy_AS_IS,
|
||||
UserLevel: 0,
|
||||
Fragment: &freedom.Fragment{
|
||||
PacketsFrom: 5,
|
||||
PacketsTo: 10,
|
||||
LengthMin: 50,
|
||||
LengthMax: 150,
|
||||
IntervalMin: 10,
|
||||
IntervalMax: 20,
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddBlackholeOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&blackhole.Config{
|
||||
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddDNSOutbound(ctx context.Context, client command.HandlerServiceClient, tag string, upstream string) error {
|
||||
endpointCfg := &cnet.Endpoint{
|
||||
Network: cnet.Network_UDP,
|
||||
Address: cnet.NewIPOrDomain(cnet.ParseAddress(upstream)),
|
||||
Port: 53,
|
||||
}
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&dns.Config{
|
||||
Server: endpointCfg,
|
||||
UserLevel: 0,
|
||||
BlockTypes: []int32{1, 28},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddHTTPOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&http.ClientConfig{
|
||||
Server: endpoint("example.com", 80, nil),
|
||||
Header: []*http.Header{
|
||||
{Key: "User-Agent", Value: "miaomiaowu"},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddSocksOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&socks.ClientConfig{
|
||||
Server: endpoint("127.0.0.1", 1080, nil),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddTrojanOutbound(ctx context.Context, client command.HandlerServiceClient, tag string, password string) error {
|
||||
user := &protocol.User{
|
||||
Email: "trojan@client.local",
|
||||
Level: 0,
|
||||
Account: serial.ToTypedMessage(&trojan.Account{
|
||||
Password: password,
|
||||
}),
|
||||
}
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&trojan.ClientConfig{
|
||||
Server: endpoint("trojan.example.com", 443, user),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddShadowsocksOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
user := &protocol.User{
|
||||
Email: "ss@client.local",
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: "client-pass",
|
||||
CipherType: shadowsocks.CipherType_AES_256_GCM,
|
||||
}),
|
||||
}
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&shadowsocks.ClientConfig{
|
||||
Server: endpoint("ss.example.com", 8388, user),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddShadowsocks2022Outbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&ss2022.ClientConfig{
|
||||
Address: cnetOrDomain("203.0.113.2"),
|
||||
Port: 8389,
|
||||
Method: "2022-blake3-aes-256-gcm",
|
||||
Key: "clientkeybase64==",
|
||||
UdpOverTcp: true,
|
||||
UdpOverTcpVersion: 2,
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddVLESSOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
user := &protocol.User{
|
||||
Email: "vless@client.local",
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: randomUUID(),
|
||||
Encryption: "none",
|
||||
}),
|
||||
}
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&vlessout.Config{
|
||||
Vnext: endpoint("vless.example.com", 443, user),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddVMessOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
user := &protocol.User{
|
||||
Email: "vmess@client.local",
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: randomUUID(),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AUTO,
|
||||
},
|
||||
}),
|
||||
}
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&vmessout.Config{
|
||||
Receiver: endpoint("vmess.example.com", 443, user),
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
|
||||
func AddWireGuardOutbound(ctx context.Context, client command.HandlerServiceClient, tag string) error {
|
||||
cfg := outboundConfig(
|
||||
tag,
|
||||
senderSettings(),
|
||||
serial.ToTypedMessage(&wireguard.DeviceConfig{
|
||||
SecretKey: "clientSecretKeyBase64==",
|
||||
Endpoint: []string{"198.51.100.2:51820"},
|
||||
IsClient: true,
|
||||
Mtu: 1420,
|
||||
DomainStrategy: wireguard.DeviceConfig_FORCE_IP4,
|
||||
Peers: []*wireguard.PeerConfig{
|
||||
{
|
||||
PublicKey: "serverPublicKeyBase64==",
|
||||
AllowedIps: []string{"0.0.0.0/0"},
|
||||
KeepAlive: 30,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||
return err
|
||||
}
|
||||
120
internal/xrpc/services/handler/users.go
Normal file
120
internal/xrpc/services/handler/users.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/xtls/xray-core/app/proxyman/command"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
ss2022 "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"github.com/xtls/xray-core/proxy/vmess"
|
||||
)
|
||||
|
||||
// AddVMessUser demonstrates AlterInbound(AddUserOperation) for VMess.
|
||||
func AddVMessUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Level: 0,
|
||||
Email: email,
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: randomUUID(),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AUTO,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddVLESSUser shows how to add VLESS users dynamically.
|
||||
func AddVLESSUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Level: 0,
|
||||
Email: email,
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: randomUUID(),
|
||||
Encryption: "none",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTrojanUser adds a Trojan password to an inbound handler.
|
||||
func AddTrojanUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Level: 0,
|
||||
Email: email,
|
||||
Account: serial.ToTypedMessage(&trojan.Account{
|
||||
Password: password,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddShadowsocksUser sets up a Shadowsocks AEAD credential.
|
||||
func AddShadowsocksUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Level: 0,
|
||||
Email: email,
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: password,
|
||||
CipherType: shadowsocks.CipherType_CHACHA20_POLY1305,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddShadowsocks2022User covers key rotation for SS2022.
|
||||
func AddShadowsocks2022User(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Email: email,
|
||||
Account: serial.ToTypedMessage(&ss2022.Account{
|
||||
Key: randomUUID(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveUser removes any user (identified by email) from an inbound.
|
||||
func RemoveUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||
req := &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.RemoveUserOperation{
|
||||
Email: email,
|
||||
}),
|
||||
}
|
||||
_, err := client.AlterInbound(ctx, req)
|
||||
return err
|
||||
}
|
||||
16
internal/xrpc/services/logger/logger.go
Normal file
16
internal/xrpc/services/logger/logger.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
loggerpb "github.com/xtls/xray-core/app/log/command"
|
||||
)
|
||||
|
||||
// RestartLogger triggers the LoggerService restartLogger RPC and waits for completion.
|
||||
func RestartLogger(ctx context.Context, client loggerpb.LoggerServiceClient) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
_, err := client.RestartLogger(ctx, &loggerpb.RestartLoggerRequest{})
|
||||
return err
|
||||
}
|
||||
30
internal/xrpc/services/stats/stats.go
Normal file
30
internal/xrpc/services/stats/stats.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
statspb "github.com/xtls/xray-core/app/stats/command"
|
||||
)
|
||||
|
||||
func QueryTraffic(ctx context.Context, client statspb.StatsServiceClient, pattern string, reset bool) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
resp, err := client.QueryStats(ctx, &statspb.QueryStatsRequest{
|
||||
Pattern: pattern,
|
||||
Reset_: reset,
|
||||
})
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if len(resp.GetStat()) == 0 {
|
||||
return -1, nil
|
||||
}
|
||||
return resp.GetStat()[0].GetValue(), nil
|
||||
}
|
||||
|
||||
func GetSystemStats(ctx context.Context, client statspb.StatsServiceClient) (*statspb.SysStatsResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
return client.GetSysStats(ctx, &statspb.SysStatsRequest{})
|
||||
}
|
||||
Reference in New Issue
Block a user