mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-27 20:53:01 +00:00
Add TrustTunnel protocol support as a separate managed process
TrustTunnel (by AdGuard) is an independent VPN protocol binary that runs alongside Xray. This integrates it into the 3x-ui panel so users can create TrustTunnel inbounds through the same UI. Architecture: - TrustTunnel runs as a separate process (not an xray inbound) - Each TrustTunnel inbound gets its own TOML config and process - TrustTunnel inbounds are skipped during xray config generation - Periodic health checks restart crashed TrustTunnel processes New files (isolated, minimal merge conflict risk): - trusttunnel/process.go: process lifecycle and TOML config generation - web/service/trusttunnel.go: service layer with start/stop/restart - web/html/form/protocol/trusttunnel.html: UI form template Modified files (minimal, targeted changes): - database/model/model.go: add TrustTunnel protocol constant - web/service/xray.go: skip trusttunnel inbounds in xray config - web/service/inbound.go: validation + TrustTunnel process triggers - web/web.go: startup/shutdown integration - web/assets/js/model/inbound.js: protocol enum + settings class - web/assets/js/model/dbinbound.js: isTrustTunnel helper - web/html/form/inbound.html: form conditional - web/html/form/client.html: password field for TrustTunnel clients https://claude.ai/code/session_01RQBndg4ZPmYAToK4KKcBzp
This commit is contained in:
parent
37f0880f8f
commit
10eab4cb06
11 changed files with 824 additions and 21 deletions
|
|
@ -21,6 +21,7 @@ const (
|
||||||
Shadowsocks Protocol = "shadowsocks"
|
Shadowsocks Protocol = "shadowsocks"
|
||||||
Mixed Protocol = "mixed"
|
Mixed Protocol = "mixed"
|
||||||
WireGuard Protocol = "wireguard"
|
WireGuard Protocol = "wireguard"
|
||||||
|
TrustTunnel Protocol = "trusttunnel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents a user account in the 3x-ui panel.
|
// User represents a user account in the 3x-ui panel.
|
||||||
|
|
|
||||||
267
trusttunnel/process.go
Normal file
267
trusttunnel/process.go
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,10 @@ class DBInbound {
|
||||||
return this.protocol === Protocols.WIREGUARD;
|
return this.protocol === Protocols.WIREGUARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isTrustTunnel() {
|
||||||
|
return this.protocol === Protocols.TRUSTTUNNEL;
|
||||||
|
}
|
||||||
|
|
||||||
get address() {
|
get address() {
|
||||||
let address = location.hostname;
|
let address = location.hostname;
|
||||||
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
|
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
|
||||||
|
|
@ -124,6 +128,7 @@ class DBInbound {
|
||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
|
case Protocols.TRUSTTUNNEL:
|
||||||
return true;
|
return true;
|
||||||
case Protocols.SHADOWSOCKS:
|
case Protocols.SHADOWSOCKS:
|
||||||
return this.toInbound().isSSMultiUser;
|
return this.toInbound().isSSMultiUser;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const Protocols = {
|
||||||
HTTP: 'http',
|
HTTP: 'http',
|
||||||
WIREGUARD: 'wireguard',
|
WIREGUARD: 'wireguard',
|
||||||
TUN: 'tun',
|
TUN: 'tun',
|
||||||
|
TRUSTTUNNEL: 'trusttunnel',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSMethods = {
|
const SSMethods = {
|
||||||
|
|
@ -1201,6 +1202,7 @@ class Inbound extends XrayCommonClass {
|
||||||
case Protocols.VLESS: return this.settings.vlesses;
|
case Protocols.VLESS: return this.settings.vlesses;
|
||||||
case Protocols.TROJAN: return this.settings.trojans;
|
case Protocols.TROJAN: return this.settings.trojans;
|
||||||
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
|
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
|
||||||
|
case Protocols.TRUSTTUNNEL: return this.settings.clients;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1827,6 +1829,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||||
case Protocols.TUN: return new Inbound.TunSettings(protocol);
|
case Protocols.TUN: return new Inbound.TunSettings(protocol);
|
||||||
|
case Protocols.TRUSTTUNNEL: return new Inbound.TrustTunnelSettings(protocol);
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1842,6 +1845,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||||
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
|
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
|
||||||
|
case Protocols.TRUSTTUNNEL: return Inbound.TrustTunnelSettings.fromJson(json);
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2708,3 +2712,123 @@ Inbound.TunSettings = class extends Inbound.Settings {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Inbound.TrustTunnelSettings = class extends Inbound.Settings {
|
||||||
|
constructor(
|
||||||
|
protocol,
|
||||||
|
hostname = '',
|
||||||
|
certFile = '',
|
||||||
|
keyFile = '',
|
||||||
|
enableHttp1 = true,
|
||||||
|
enableHttp2 = true,
|
||||||
|
enableQuic = true,
|
||||||
|
ipv6Available = true,
|
||||||
|
allowPrivateNetwork = false,
|
||||||
|
clients = [new Inbound.TrustTunnelSettings.Client()],
|
||||||
|
) {
|
||||||
|
super(protocol);
|
||||||
|
this.hostname = hostname;
|
||||||
|
this.certFile = certFile;
|
||||||
|
this.keyFile = keyFile;
|
||||||
|
this.enableHttp1 = enableHttp1;
|
||||||
|
this.enableHttp2 = enableHttp2;
|
||||||
|
this.enableQuic = enableQuic;
|
||||||
|
this.ipv6Available = ipv6Available;
|
||||||
|
this.allowPrivateNetwork = allowPrivateNetwork;
|
||||||
|
this.clients = clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient() {
|
||||||
|
this.clients.push(new Inbound.TrustTunnelSettings.Client());
|
||||||
|
}
|
||||||
|
|
||||||
|
delClient(index) {
|
||||||
|
this.clients.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json = {}) {
|
||||||
|
return new Inbound.TrustTunnelSettings(
|
||||||
|
Protocols.TRUSTTUNNEL,
|
||||||
|
json.hostname,
|
||||||
|
json.certFile,
|
||||||
|
json.keyFile,
|
||||||
|
json.enableHttp1 ?? true,
|
||||||
|
json.enableHttp2 ?? true,
|
||||||
|
json.enableQuic ?? true,
|
||||||
|
json.ipv6Available ?? true,
|
||||||
|
json.allowPrivateNetwork ?? false,
|
||||||
|
(json.clients || []).map(c => Inbound.TrustTunnelSettings.Client.fromJson(c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
hostname: this.hostname,
|
||||||
|
certFile: this.certFile,
|
||||||
|
keyFile: this.keyFile,
|
||||||
|
enableHttp1: this.enableHttp1,
|
||||||
|
enableHttp2: this.enableHttp2,
|
||||||
|
enableQuic: this.enableQuic,
|
||||||
|
ipv6Available: this.ipv6Available,
|
||||||
|
allowPrivateNetwork: this.allowPrivateNetwork,
|
||||||
|
clients: this.clients.map(c => c.toJson()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Inbound.TrustTunnelSettings.Client = class extends XrayCommonClass {
|
||||||
|
constructor(
|
||||||
|
email = RandomUtil.randomText(8) + '@trusttunnel',
|
||||||
|
password = RandomUtil.randomSeq(16),
|
||||||
|
enable = true,
|
||||||
|
limitIp = 0,
|
||||||
|
totalGB = 0,
|
||||||
|
expiryTime = 0,
|
||||||
|
tgId = '',
|
||||||
|
subId = RandomUtil.randomText(16),
|
||||||
|
comment = '',
|
||||||
|
reset = 0,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.email = email;
|
||||||
|
this.password = password;
|
||||||
|
this.enable = enable;
|
||||||
|
this.limitIp = limitIp;
|
||||||
|
this.totalGB = totalGB;
|
||||||
|
this.expiryTime = expiryTime;
|
||||||
|
this.tgId = tgId;
|
||||||
|
this.subId = subId;
|
||||||
|
this.comment = comment;
|
||||||
|
this.reset = reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json = {}) {
|
||||||
|
return new Inbound.TrustTunnelSettings.Client(
|
||||||
|
json.email,
|
||||||
|
json.password,
|
||||||
|
json.enable ?? true,
|
||||||
|
json.limitIp,
|
||||||
|
json.totalGB,
|
||||||
|
json.expiryTime,
|
||||||
|
json.tgId,
|
||||||
|
json.subId,
|
||||||
|
json.comment,
|
||||||
|
json.reset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
email: this.email,
|
||||||
|
password: this.password,
|
||||||
|
enable: this.enable,
|
||||||
|
limitIp: this.limitIp,
|
||||||
|
totalGB: this.totalGB,
|
||||||
|
expiryTime: this.expiryTime,
|
||||||
|
tgId: this.tgId,
|
||||||
|
subId: this.subId,
|
||||||
|
comment: this.comment,
|
||||||
|
reset: this.reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<a-input v-model.trim="client.email"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS || inbound.protocol === Protocols.TRUSTTUNNEL">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
{{ i18n "password" }}
|
{{ i18n "password" }}
|
||||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
|
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
|
||||||
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
|
<a-icon v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.TRUSTTUNNEL" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="client.password"></a-input>
|
<a-input v-model.trim="client.password"></a-input>
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,11 @@
|
||||||
{{template "form/tun"}}
|
{{template "form/tun"}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- trusttunnel -->
|
||||||
|
<template v-if="inbound.protocol === Protocols.TRUSTTUNNEL">
|
||||||
|
{{template "form/trusttunnel"}}
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- stream settings -->
|
<!-- stream settings -->
|
||||||
<template v-if="inbound.canEnableStream()">
|
<template v-if="inbound.canEnableStream()">
|
||||||
{{template "form/streamSettings"}}
|
{{template "form/streamSettings"}}
|
||||||
|
|
|
||||||
100
web/html/form/protocol/trusttunnel.html
Normal file
100
web/html/form/protocol/trusttunnel.html
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
{{define "form/trusttunnel"}}
|
||||||
|
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||||
|
:wrapper-col="{ md: {span:14} }">
|
||||||
|
|
||||||
|
<a-form-item label="Hostname">
|
||||||
|
<a-input v-model.trim="inbound.settings.hostname"
|
||||||
|
placeholder="example.com"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.certificate" }}</span>
|
||||||
|
</template>
|
||||||
|
Certificate Path
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="inbound.settings.certFile"
|
||||||
|
placeholder="/path/to/fullchain.pem"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.key" }}</span>
|
||||||
|
</template>
|
||||||
|
Private Key Path
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="inbound.settings.keyFile"
|
||||||
|
placeholder="/path/to/privkey.pem"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '10px 0' }">Transport Protocols</a-divider>
|
||||||
|
|
||||||
|
<a-form-item label="HTTP/1.1">
|
||||||
|
<a-switch v-model="inbound.settings.enableHttp1"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="HTTP/2">
|
||||||
|
<a-switch v-model="inbound.settings.enableHttp2"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="QUIC (HTTP/3)">
|
||||||
|
<a-switch v-model="inbound.settings.enableQuic"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '10px 0' }">Network</a-divider>
|
||||||
|
|
||||||
|
<a-form-item label="IPv6">
|
||||||
|
<a-switch v-model="inbound.settings.ipv6Available"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>Allow connections to private network addresses (10.x, 192.168.x, etc.)</span>
|
||||||
|
</template>
|
||||||
|
Private Network
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-switch v-model="inbound.settings.allowPrivateNetwork"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '10px 0' }">
|
||||||
|
Clients
|
||||||
|
<a-button icon="plus" type="primary" size="small"
|
||||||
|
@click="inbound.settings.addClient()"></a-button>
|
||||||
|
</a-divider>
|
||||||
|
|
||||||
|
<a-form v-for="(client, index) in inbound.settings.clients" :colon="false"
|
||||||
|
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
|
<a-divider :style="{ margin: '0' }">
|
||||||
|
Client [[ index + 1 ]]
|
||||||
|
<a-icon v-if="inbound.settings.clients.length > 1" type="delete"
|
||||||
|
@click="() => inbound.settings.delClient(index)"
|
||||||
|
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
|
||||||
|
</a-icon>
|
||||||
|
</a-divider>
|
||||||
|
|
||||||
|
<a-form-item label="Username">
|
||||||
|
<a-input v-model.trim="client.email"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label='{{ i18n "password" }}'>
|
||||||
|
<a-input v-model.trim="client.password"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label='{{ i18n "enable" }}'>
|
||||||
|
<a-switch v-model="client.enable"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-form>
|
||||||
|
{{end}}
|
||||||
|
|
@ -270,6 +270,10 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||||
if client.Email == "" {
|
if client.Email == "" {
|
||||||
return inbound, false, common.NewError("empty client ID")
|
return inbound, false, common.NewError("empty client ID")
|
||||||
}
|
}
|
||||||
|
case "trusttunnel":
|
||||||
|
if client.Password == "" {
|
||||||
|
return inbound, false, common.NewError("empty client password")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if client.ID == "" {
|
if client.ID == "" {
|
||||||
return inbound, false, common.NewError("empty client ID")
|
return inbound, false, common.NewError("empty client ID")
|
||||||
|
|
@ -299,7 +303,14 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := false
|
needRestart := false
|
||||||
if inbound.Enable {
|
if inbound.Enable && inbound.Protocol == "trusttunnel" {
|
||||||
|
// TrustTunnel inbounds are managed by TrustTunnelService, not Xray
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(inbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to start TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if inbound.Enable {
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ")
|
inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ")
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
|
|
@ -327,19 +338,33 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
|
|
||||||
var tag string
|
var tag string
|
||||||
needRestart := false
|
needRestart := false
|
||||||
result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag)
|
|
||||||
if result.Error == nil {
|
// Check if this is a TrustTunnel inbound (skip xray API for those)
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
var protocol string
|
||||||
err1 := s.xrayApi.DelInbound(tag)
|
db.Model(model.Inbound{}).Select("protocol").Where("id = ?", id).First(&protocol)
|
||||||
if err1 == nil {
|
isTrustTunnel := protocol == string(model.TrustTunnel)
|
||||||
logger.Debug("Inbound deleted by api:", tag)
|
|
||||||
} else {
|
if isTrustTunnel {
|
||||||
logger.Debug("Unable to delete inbound by api:", err1)
|
// Stop TrustTunnel process for this inbound
|
||||||
needRestart = true
|
db.Model(model.Inbound{}).Select("tag").Where("id = ?", id).First(&tag)
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil && tag != "" {
|
||||||
|
tt.StopForInbound(tag)
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("No enabled inbound founded to removing by api", tag)
|
result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag)
|
||||||
|
if result.Error == nil {
|
||||||
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
|
err1 := s.xrayApi.DelInbound(tag)
|
||||||
|
if err1 == nil {
|
||||||
|
logger.Debug("Inbound deleted by api:", tag)
|
||||||
|
} else {
|
||||||
|
logger.Debug("Unable to delete inbound by api:", err1)
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
s.xrayApi.Close()
|
||||||
|
} else {
|
||||||
|
logger.Debug("No enabled inbound founded to removing by api", tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete client traffics of inbounds
|
// Delete client traffics of inbounds
|
||||||
|
|
@ -489,6 +514,20 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := false
|
needRestart := false
|
||||||
|
if oldInbound.Protocol == "trusttunnel" {
|
||||||
|
// TrustTunnel inbounds are managed by TrustTunnelService
|
||||||
|
err = tx.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return inbound, false, err
|
||||||
|
}
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(oldInbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to restart TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inbound, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
if s.xrayApi.DelInbound(tag) == nil {
|
if s.xrayApi.DelInbound(tag) == nil {
|
||||||
logger.Debug("Old inbound deleted by api:", tag)
|
logger.Debug("Old inbound deleted by api:", tag)
|
||||||
|
|
@ -606,6 +645,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
if client.Email == "" {
|
if client.Email == "" {
|
||||||
return false, common.NewError("empty client ID")
|
return false, common.NewError("empty client ID")
|
||||||
}
|
}
|
||||||
|
case "trusttunnel":
|
||||||
|
if client.Password == "" {
|
||||||
|
return false, common.NewError("empty client password")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if client.ID == "" {
|
if client.ID == "" {
|
||||||
return false, common.NewError("empty client ID")
|
return false, common.NewError("empty client ID")
|
||||||
|
|
@ -643,6 +686,25 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
needRestart := false
|
needRestart := false
|
||||||
|
if oldInbound.Protocol == "trusttunnel" {
|
||||||
|
// TrustTunnel: just save client stats and restart the TrustTunnel process
|
||||||
|
for _, client := range clients {
|
||||||
|
if len(client.Email) > 0 {
|
||||||
|
s.AddClientStat(tx, data.Id, &client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(oldInbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to restart TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if len(client.Email) > 0 {
|
if len(client.Email) > 0 {
|
||||||
|
|
@ -693,7 +755,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
if oldInbound.Protocol == "trojan" {
|
if oldInbound.Protocol == "trojan" {
|
||||||
client_key = "password"
|
client_key = "password"
|
||||||
}
|
}
|
||||||
if oldInbound.Protocol == "shadowsocks" {
|
if oldInbound.Protocol == "shadowsocks" || oldInbound.Protocol == "trusttunnel" {
|
||||||
client_key = "email"
|
client_key = "email"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -745,6 +807,19 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if needApiDel && notDepleted {
|
if needApiDel && notDepleted {
|
||||||
|
if oldInbound.Protocol == "trusttunnel" {
|
||||||
|
// TrustTunnel: save and restart process instead of xray API
|
||||||
|
err = db.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(oldInbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to restart TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)
|
err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)
|
||||||
if err1 == nil {
|
if err1 == nil {
|
||||||
|
|
@ -798,7 +873,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
case "trojan":
|
case "trojan":
|
||||||
oldClientId = oldClient.Password
|
oldClientId = oldClient.Password
|
||||||
newClientId = clients[0].Password
|
newClientId = clients[0].Password
|
||||||
case "shadowsocks":
|
case "shadowsocks", "trusttunnel":
|
||||||
oldClientId = oldClient.Email
|
oldClientId = oldClient.Email
|
||||||
newClientId = clients[0].Email
|
newClientId = clients[0].Email
|
||||||
default:
|
default:
|
||||||
|
|
@ -896,6 +971,20 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
needRestart := false
|
needRestart := false
|
||||||
|
if oldInbound.Protocol == "trusttunnel" {
|
||||||
|
// TrustTunnel: save and restart process instead of xray API
|
||||||
|
err = tx.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(oldInbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to restart TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(oldEmail) > 0 {
|
if len(oldEmail) > 0 {
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
if oldClients[clientIndex].Enable {
|
if oldClients[clientIndex].Enable {
|
||||||
|
|
@ -2490,6 +2579,18 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
if needApiDel {
|
if needApiDel {
|
||||||
|
if oldInbound.Protocol == "trusttunnel" {
|
||||||
|
err = db.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if tt := GetTrustTunnelService(); tt != nil {
|
||||||
|
if err1 := tt.RestartForInbound(oldInbound); err1 != nil {
|
||||||
|
logger.Warning("Failed to restart TrustTunnel:", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {
|
if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {
|
||||||
logger.Debug("Client deleted by api:", email)
|
logger.Debug("Client deleted by api:", email)
|
||||||
|
|
|
||||||
180
web/service/trusttunnel.go
Normal file
180
web/service/trusttunnel.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/trusttunnel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package-level TrustTunnel service instance, set during server startup.
|
||||||
|
// Used by InboundService to trigger TrustTunnel restarts on inbound changes.
|
||||||
|
var ttService *TrustTunnelService
|
||||||
|
|
||||||
|
// SetTrustTunnelService sets the package-level TrustTunnel service instance.
|
||||||
|
func SetTrustTunnelService(s *TrustTunnelService) {
|
||||||
|
ttService = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTrustTunnelService returns the package-level TrustTunnel service instance.
|
||||||
|
func GetTrustTunnelService() *TrustTunnelService {
|
||||||
|
return ttService
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrustTunnelService manages TrustTunnel endpoint processes.
|
||||||
|
// Each TrustTunnel inbound runs its own process alongside Xray.
|
||||||
|
type TrustTunnelService struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
processes map[string]*trusttunnel.Process // keyed by inbound tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrustTunnelService() *TrustTunnelService {
|
||||||
|
return &TrustTunnelService{
|
||||||
|
processes: make(map[string]*trusttunnel.Process),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAll starts TrustTunnel processes for all enabled TrustTunnel inbounds.
|
||||||
|
func (s *TrustTunnelService) StartAll() {
|
||||||
|
if !trusttunnel.IsBinaryInstalled() {
|
||||||
|
logger.Debug("TrustTunnel binary not installed, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inbounds, err := s.getTrustTunnelInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get TrustTunnel inbounds:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if !inbound.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.startInbound(inbound); err != nil {
|
||||||
|
logger.Warningf("Failed to start TrustTunnel for %s: %v", inbound.Tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAll stops all running TrustTunnel processes.
|
||||||
|
func (s *TrustTunnelService) StopAll() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
for tag, proc := range s.processes {
|
||||||
|
if proc.IsRunning() {
|
||||||
|
if err := proc.Stop(); err != nil {
|
||||||
|
logger.Warningf("Failed to stop TrustTunnel %s: %v", tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.processes = make(map[string]*trusttunnel.Process)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartForInbound restarts the TrustTunnel process for a specific inbound.
|
||||||
|
func (s *TrustTunnelService) RestartForInbound(inbound *model.Inbound) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// Stop existing process for this tag
|
||||||
|
if proc, ok := s.processes[inbound.Tag]; ok && proc.IsRunning() {
|
||||||
|
proc.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inbound.Enable {
|
||||||
|
delete(s.processes, inbound.Tag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.startInboundLocked(inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopForInbound stops the TrustTunnel process for a specific inbound tag.
|
||||||
|
func (s *TrustTunnelService) StopForInbound(tag string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if proc, ok := s.processes[tag]; ok {
|
||||||
|
if proc.IsRunning() {
|
||||||
|
proc.Stop()
|
||||||
|
}
|
||||||
|
delete(s.processes, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunning checks if a TrustTunnel process is running for the given tag.
|
||||||
|
func (s *TrustTunnelService) IsRunning(tag string) bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if proc, ok := s.processes[tag]; ok {
|
||||||
|
return proc.IsRunning()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAndRestart checks for crashed TrustTunnel processes and restarts them.
|
||||||
|
func (s *TrustTunnelService) CheckAndRestart() {
|
||||||
|
if !trusttunnel.IsBinaryInstalled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inbounds, err := s.getTrustTunnelInbounds()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if !inbound.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proc, ok := s.processes[inbound.Tag]
|
||||||
|
if !ok || !proc.IsRunning() {
|
||||||
|
logger.Infof("TrustTunnel %s not running, restarting...", inbound.Tag)
|
||||||
|
s.startInboundLocked(inbound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrustTunnelService) startInbound(inbound *model.Inbound) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.startInboundLocked(inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrustTunnelService) startInboundLocked(inbound *model.Inbound) error {
|
||||||
|
settings, err := trusttunnel.ParseSettings(inbound.Settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc := trusttunnel.NewProcess(inbound.Tag)
|
||||||
|
|
||||||
|
if err := proc.WriteConfig(inbound.Listen, inbound.Port, settings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proc.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.processes[inbound.Tag] = proc
|
||||||
|
logger.Infof("TrustTunnel started for inbound %s on port %d", inbound.Tag, inbound.Port)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrustTunnelService) getTrustTunnelInbounds() ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Where("protocol = ?", model.TrustTunnel).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|
@ -113,6 +113,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
if !inbound.Enable {
|
if !inbound.Enable {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Skip TrustTunnel inbounds — they are managed by TrustTunnelService, not Xray
|
||||||
|
if inbound.Protocol == "trusttunnel" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// get settings clients
|
// get settings clients
|
||||||
settings := map[string]any{}
|
settings := map[string]any{}
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
|
|
|
||||||
26
web/web.go
26
web/web.go
|
|
@ -101,9 +101,10 @@ type Server struct {
|
||||||
api *controller.APIController
|
api *controller.APIController
|
||||||
ws *controller.WebSocketController
|
ws *controller.WebSocketController
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
|
trustTunnelService *service.TrustTunnelService
|
||||||
|
|
||||||
wsHub *websocket.Hub
|
wsHub *websocket.Hub
|
||||||
|
|
||||||
|
|
@ -117,8 +118,9 @@ type Server struct {
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &Server{
|
return &Server{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
trustTunnelService: service.NewTrustTunnelService(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,6 +301,11 @@ func (s *Server) startTask() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("start xray failed:", err)
|
logger.Warning("start xray failed:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start TrustTunnel processes for all enabled TrustTunnel inbounds
|
||||||
|
service.SetTrustTunnelService(s.trustTunnelService)
|
||||||
|
s.trustTunnelService.StartAll()
|
||||||
|
|
||||||
// Check whether xray is running every second
|
// Check whether xray is running every second
|
||||||
s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob())
|
s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob())
|
||||||
|
|
||||||
|
|
@ -318,6 +325,12 @@ func (s *Server) startTask() {
|
||||||
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Check for crashed TrustTunnel processes every 30 seconds
|
||||||
|
ttService := s.trustTunnelService
|
||||||
|
s.cron.AddFunc("@every 30s", func() {
|
||||||
|
ttService.CheckAndRestart()
|
||||||
|
})
|
||||||
|
|
||||||
// check client ips from log file every 10 sec
|
// check client ips from log file every 10 sec
|
||||||
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
|
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
|
||||||
|
|
||||||
|
|
@ -455,6 +468,9 @@ func (s *Server) Start() (err error) {
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
s.cancel()
|
s.cancel()
|
||||||
s.xrayService.StopXray()
|
s.xrayService.StopXray()
|
||||||
|
if s.trustTunnelService != nil {
|
||||||
|
s.trustTunnelService.StopAll()
|
||||||
|
}
|
||||||
if s.cron != nil {
|
if s.cron != nil {
|
||||||
s.cron.Stop()
|
s.cron.Stop()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue