3x-ui/trusttunnel/process.go

268 lines
7 KiB
Go
Raw Normal View History

// Package trusttunnel manages TrustTunnel endpoint processes alongside Xray.
// TrustTunnel is a separate VPN protocol binary that runs independently of xray-core.
package trusttunnel
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/logger"
)
const (
DefaultInstallPath = "/opt/trusttunnel"
BinaryName = "trusttunnel_endpoint"
)
// Settings holds the TrustTunnel configuration parsed from the inbound's Settings JSON.
type Settings struct {
Hostname string `json:"hostname"`
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
EnableHTTP1 bool `json:"enableHttp1"`
EnableHTTP2 bool `json:"enableHttp2"`
EnableQUIC bool `json:"enableQuic"`
IPv6Available bool `json:"ipv6Available"`
AllowPrivateNetwork bool `json:"allowPrivateNetwork"`
Clients []Client `json:"clients"`
}
// Client represents a TrustTunnel user credential.
type Client struct {
Email string `json:"email"`
Password string `json:"password"`
Enable bool `json:"enable"`
LimitIP int `json:"limitIp"`
TotalGB int64 `json:"totalGB"`
ExpiryTime int64 `json:"expiryTime"`
TgID int64 `json:"tgId"`
SubID string `json:"subId"`
Comment string `json:"comment"`
Reset int `json:"reset"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
// GetInstallPath returns the TrustTunnel binary installation directory.
func GetInstallPath() string {
p := os.Getenv("TRUSTTUNNEL_PATH")
if p != "" {
return p
}
return DefaultInstallPath
}
// GetBinaryPath returns the full path to the TrustTunnel endpoint binary.
func GetBinaryPath() string {
return filepath.Join(GetInstallPath(), BinaryName)
}
// GetConfigDir returns the directory for TrustTunnel config files for a given inbound tag.
func GetConfigDir(tag string) string {
return filepath.Join(config.GetBinFolderPath(), "trusttunnel", tag)
}
// IsBinaryInstalled checks if the TrustTunnel binary exists.
func IsBinaryInstalled() bool {
_, err := os.Stat(GetBinaryPath())
return err == nil
}
// GetVersion returns the TrustTunnel binary version string.
func GetVersion() string {
cmd := exec.Command(GetBinaryPath(), "--version")
data, err := cmd.Output()
if err != nil {
return "Unknown"
}
return strings.TrimSpace(string(data))
}
// Process manages a single TrustTunnel endpoint process.
type Process struct {
cmd *exec.Cmd
tag string
configDir string
logWriter *logWriter
exitErr error
startTime time.Time
}
type logWriter struct {
buf bytes.Buffer
lastLine string
}
func (w *logWriter) Write(p []byte) (n int, err error) {
n, err = w.buf.Write(p)
lines := strings.Split(strings.TrimSpace(w.buf.String()), "\n")
if len(lines) > 0 {
w.lastLine = lines[len(lines)-1]
}
return
}
// NewProcess creates a new TrustTunnel process for the given inbound tag.
func NewProcess(tag string) *Process {
return &Process{
tag: tag,
configDir: GetConfigDir(tag),
logWriter: &logWriter{},
startTime: time.Now(),
}
}
func (p *Process) IsRunning() bool {
if p.cmd == nil || p.cmd.Process == nil {
return false
}
return p.cmd.ProcessState == nil
}
func (p *Process) GetErr() error {
return p.exitErr
}
func (p *Process) GetResult() string {
if p.logWriter.lastLine == "" && p.exitErr != nil {
return p.exitErr.Error()
}
return p.logWriter.lastLine
}
func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds())
}
// WriteConfig generates TOML configuration files from an inbound's settings.
func (p *Process) WriteConfig(listen string, port int, settings Settings) error {
if err := os.MkdirAll(p.configDir, 0o750); err != nil {
return fmt.Errorf("failed to create config dir: %w", err)
}
listenAddr := listen
if listenAddr == "" {
listenAddr = "0.0.0.0"
}
listenAddr = fmt.Sprintf("%s:%d", listenAddr, port)
// vpn.toml
vpnToml := fmt.Sprintf(`listen_address = "%s"
ipv6_available = %t
allow_private_network_connections = %t
credentials_file = "credentials.toml"
`, listenAddr, settings.IPv6Available, settings.AllowPrivateNetwork)
// Add protocol sections based on user selection
if settings.EnableHTTP1 {
vpnToml += "\n[listen_protocols.http1]\n"
}
if settings.EnableHTTP2 {
vpnToml += "\n[listen_protocols.http2]\n"
}
if settings.EnableQUIC {
vpnToml += "\n[listen_protocols.quic]\n"
}
if err := os.WriteFile(filepath.Join(p.configDir, "vpn.toml"), []byte(vpnToml), 0o640); err != nil {
return fmt.Errorf("failed to write vpn.toml: %w", err)
}
// hosts.toml
if settings.Hostname != "" && settings.CertFile != "" && settings.KeyFile != "" {
hostsToml := fmt.Sprintf(`[[main_hosts]]
hostname = "%s"
cert_chain_path = "%s"
private_key_path = "%s"
`, settings.Hostname, settings.CertFile, settings.KeyFile)
if err := os.WriteFile(filepath.Join(p.configDir, "hosts.toml"), []byte(hostsToml), 0o640); err != nil {
return fmt.Errorf("failed to write hosts.toml: %w", err)
}
}
// credentials.toml
var credBuf strings.Builder
for _, client := range settings.Clients {
if !client.Enable {
continue
}
credBuf.WriteString(fmt.Sprintf("[[client]]\nusername = \"%s\"\npassword = \"%s\"\n\n",
escapeToml(client.Email), escapeToml(client.Password)))
}
if err := os.WriteFile(filepath.Join(p.configDir, "credentials.toml"), []byte(credBuf.String()), 0o640); err != nil {
return fmt.Errorf("failed to write credentials.toml: %w", err)
}
return nil
}
// Start launches the TrustTunnel endpoint process.
// The binary expects positional args: trusttunnel_endpoint vpn.toml hosts.toml
func (p *Process) Start() error {
if p.IsRunning() {
return fmt.Errorf("trusttunnel %s is already running", p.tag)
}
if !IsBinaryInstalled() {
return fmt.Errorf("trusttunnel binary not found at %s", GetBinaryPath())
}
cmd := exec.Command(GetBinaryPath(), "vpn.toml", "hosts.toml")
cmd.Dir = p.configDir
cmd.Stdout = p.logWriter
cmd.Stderr = p.logWriter
p.cmd = cmd
p.startTime = time.Now()
p.exitErr = nil
go func() {
err := cmd.Run()
if err != nil {
logger.Errorf("TrustTunnel process %s exited: %v", p.tag, err)
p.exitErr = err
}
}()
return nil
}
// Stop terminates the TrustTunnel endpoint process.
func (p *Process) Stop() error {
if !p.IsRunning() {
return nil
}
if runtime.GOOS == "windows" {
return p.cmd.Process.Kill()
}
return p.cmd.Process.Signal(syscall.SIGTERM)
}
// ParseSettings parses TrustTunnel settings from the inbound's Settings JSON string.
func ParseSettings(settingsJSON string) (Settings, error) {
var s Settings
s.EnableHTTP1 = true
s.EnableHTTP2 = true
s.EnableQUIC = true
s.IPv6Available = true
if err := json.Unmarshal([]byte(settingsJSON), &s); err != nil {
return s, fmt.Errorf("failed to parse trusttunnel settings: %w", err)
}
return s, nil
}
func escapeToml(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\"", "\\\"")
return s
}