Native Xray

This commit is contained in:
2026-07-04 17:26:01 -03:00
parent 6cd9626db9
commit 0eaa48ffd0
23 changed files with 7058 additions and 312 deletions
+155 -9
View File
@@ -64,6 +64,17 @@ type TLSForwarderConfig struct {
type Config struct {
Listen string `json:"listen"`
// ProxyAutoRestartInterval controls a watchdog that periodically hard-restarts
// the public proxy listeners (listen, extra_listen, and TLS forwarders) and
// closes active SSH sessions. Empty, "0s", "off", or "disabled" turns it off.
// Valid examples: "30m", "6h", "24h". Minimum accepted value is 1m.
ProxyAutoRestartInterval string `json:"proxy_auto_restart_interval,omitempty"`
// ProxyAutoRestartGrace is the delay between closing proxy listeners/sessions
// and binding them again during an auto restart. Empty defaults to "2s".
ProxyAutoRestartGrace string `json:"proxy_auto_restart_grace,omitempty"`
// Optional extra public listen addresses (multiport). These
// addresses use the same HTTPcleanup and SSH handler as the
// primary Listen address. For IPv6, use bracket form, e.g.
@@ -126,14 +137,46 @@ type Config struct {
// the tunnelled zone, and loads its private key from PrivKeyFile. The
// corresponding public key must be distributed to clients.
type DNSTTConfig struct {
// Domain is the root of the DNS zone reserved for the tunnel (e.g. "t.example.com").
// Domain is the primary/root DNS zone reserved for the tunnel (e.g. "t.example.com").
// It is kept for backward compatibility and is also used as the first client
// domain when Domains is empty.
Domain string `json:"domain"`
// UDPListen is the UDP address to listen on for incoming DNS queries.
// The address should be IPv6formatted (e.g. "[::]:5300") and reachable by
// recursive resolvers. Note: port 53 may require root privileges; binding
// to an unprivileged port and using iptables to redirect port 53 is
// recommended【561853413345496†L97-L109】.
// Domains optionally lists all DNS root zones/NS domains accepted by this
// server. This allows the same DNSTT listener/key to answer for public and
// local DNS deployments at the same time, for example:
// ["t.example.com", "t.local.lan"]. The first normalized value is mirrored
// into Domain for older clients/UI code.
Domains []string `json:"domains,omitempty"`
// UDPListen is the UDP address to listen on for incoming DNS tunnel queries.
// IPv6 addresses must use bracket form, for example "[::]:5300" or
// "[2001:db8::1234]:53". IPv6 listeners are opened with udp6 so they do
// not require a spare IPv4 address on the same port.
UDPListen string `json:"udp_listen"`
// FakeDNSEnabled starts an extra built-in DNS listener for local/LAN testing.
// It uses the same DNSTT private key and session pool, but only accepts the
// FakeDNSDomain zone. This avoids needing a second DNS server for tests such
// as t.local.lan over a dedicated IPv6 address.
FakeDNSEnabled bool `json:"fake_dns_enabled,omitempty"`
// FakeDNSListen is the IPv4/IPv6 UDP address for the built-in local DNS
// listener. For your case use an IPv6 address, for example
// "[2001:db8::1234]:53" or "[::]:53".
FakeDNSListen string `json:"fake_dns_listen,omitempty"`
// FakeDNSDomain is the local DNSTT zone accepted by the built-in DNS listener.
// If empty while FakeDNSEnabled is true, it defaults to "t.local.lan".
FakeDNSDomain string `json:"fake_dns_domain,omitempty"`
// FakeDNSWorkers controls how many concurrent UDP read/parse workers are used
// by the built-in local DNS listener. Zero uses a safe automatic default.
FakeDNSWorkers int `json:"fake_dns_workers,omitempty"`
// DNSResponseWorkers controls how many DNS response sender shards are used.
// Zero keeps the safest default of one sender. Increase carefully only when
// the DNSTT pending queue grows under load.
DNSResponseWorkers int `json:"dns_response_workers,omitempty"`
// PrivKeyFile is the path to the Noise server private key. Generate a
// keypair with the dnstt tool (use -gen-key) and copy the resulting
// private key here; the matching public key must be distributed to
@@ -158,6 +201,41 @@ type DNSTTConfig struct {
// printed to the console. Set this to true in combination with
// disable_stats_log if you want a fully quiet DNSTT server.
DisableConsoleLog bool `json:"disable_console_log"`
// AutoRestartInterval controls a watchdog that periodically cycles only the
// integrated DNSTT UDP listener. Empty, "0s", "off", or "disabled" turns it
// off. Valid examples: "30m", "2h", "6h". Minimum accepted value is 1m.
AutoRestartInterval string `json:"auto_restart_interval,omitempty"`
// AutoRestartGrace is the delay between closing the old DNSTT UDP socket and
// binding a new one during an auto restart. Empty defaults to "2s".
AutoRestartGrace string `json:"auto_restart_grace,omitempty"`
// MaxSessions limits concurrently open DNSTT/KCP sessions. Zero uses a safe
// default for large public servers. Negative disables the limit.
MaxSessions int `json:"max_sessions,omitempty"`
// MaxStreams limits concurrently open smux/SSH streams across all DNSTT
// sessions. Zero uses a safe default. Negative disables the limit.
MaxStreams int `json:"max_streams,omitempty"`
// PendingResponses controls the buffer of queued DNS responses waiting for
// sendLoop. Zero uses the default. Keeping it bounded prevents RAM spikes.
PendingResponses int `json:"pending_responses,omitempty"`
// StreamBuffer sets smux MaxStreamBuffer in bytes. Zero uses the default.
// Lower values reduce RAM use when thousands of clients are connected.
StreamBuffer int `json:"stream_buffer,omitempty"`
// UDPReadBuffer and UDPWriteBuffer request OS socket buffers in bytes. Zero
// uses the default. The kernel may clamp these unless sysctl limits are raised.
UDPReadBuffer int `json:"udp_read_buffer,omitempty"`
UDPWriteBuffer int `json:"udp_write_buffer,omitempty"`
// LogConnections enables per-session and per-stream DNSTT logs. Leave off on
// servers with thousands of users because connection logging can become the
// bottleneck and make crashes more likely.
LogConnections bool `json:"log_connections,omitempty"`
}
// UDPGWConfig defines the settings for the integrated UDP gateway. The
@@ -214,6 +292,16 @@ type UDPGWConfig struct {
// growth if a client sprays packets to many unique destinations.
// Default is 32768.
MaxMapEntries int `json:"max_map_entries"`
// AutoRestartInterval controls a watchdog that periodically hard-restarts
// the integrated UDPGW listener and closes all connected UDPGW clients. Empty,
// "0s", "off", or "disabled" turns it off. Valid examples: "30m", "6h", "24h".
// Minimum accepted value is 1m.
AutoRestartInterval string `json:"auto_restart_interval,omitempty"`
// AutoRestartGrace is the delay between closing the old UDPGW listener/clients
// and binding a new one during an auto restart. Empty defaults to "2s".
AutoRestartGrace string `json:"auto_restart_grace,omitempty"`
}
type UserConfig struct {
@@ -341,6 +429,35 @@ func (m *UserManager) DisconnectUser(username string) {
}
}
// DisconnectAll closes every authenticated SSH connection currently tracked by
// the user manager. It is used by the proxy hard auto-restart to mimic a real
// service reboot instead of only reopening listening sockets.
func (m *UserManager) DisconnectAll() int {
m.mu.RLock()
states := make([]*UserState, 0, len(m.users))
for _, u := range m.users {
states = append(states, u)
}
m.mu.RUnlock()
seen := make(map[*ssh.ServerConn]struct{})
for _, u := range states {
if u == nil {
continue
}
u.mu.Lock()
for c := range u.conns {
seen[c] = struct{}{}
}
u.mu.Unlock()
}
for c := range seen {
_ = c.Close()
}
return len(seen)
}
// Global state
var (
userMgr = &UserManager{users: make(map[string]*UserState)}
@@ -1030,6 +1147,19 @@ func readNetDev() (map[string]ifaceCounters, error) {
// ---------- Config loading ----------
const defaultInstalledConfigPath = "/opt/sshpanel/config.json"
func resolveMainConfigPath(path string) string {
path = strings.TrimSpace(path)
if path != "" {
return path
}
if _, err := os.Stat("config.json"); err == nil {
return "config.json"
}
return defaultInstalledConfigPath
}
func loadConfig(path string) (*Config, map[string]*UserState, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -1040,6 +1170,9 @@ func loadConfig(path string) (*Config, map[string]*UserState, error) {
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, nil, fmt.Errorf("parse config: %w", err)
}
if cfg.Xray != nil {
cfg.Xray.NormalizeDefaults()
}
if cfg.Listen == "" {
cfg.Listen = ":2222"
@@ -1721,6 +1854,9 @@ func handleDnsttStats(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if proxyManagedServerFromRequest(w, r, statsStore, "/api/dnstt", nil, "") {
return
}
// Obtain a copy of the current snapshot.
stats := GetDNSTTStatsSnapshot()
w.Header().Set("Content-Type", "application/json")
@@ -2621,19 +2757,20 @@ func main() {
log.Printf("GOMEMLIMIT auto-set to 80%% of RAM: %d MB", limit/1024/1024)
}
configPath := flag.String("config", "config.json", "path to JSON config file")
configPath := flag.String("config", "", "path to JSON config file (default: ./config.json if present, otherwise /opt/sshpanel/config.json)")
quietFlag := flag.Bool("quiet", false, "override config and disable logs")
userCountFlag := flag.Bool("usercount", false, "show per-user connection counters (single line)")
flag.Parse()
cfg, userMap, err := loadConfig(*configPath)
resolvedConfigPath := resolveMainConfigPath(*configPath)
cfg, userMap, err := loadConfig(resolvedConfigPath)
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
userMgr.ReplaceAll(userMap)
// Store config path and live config for hot-reload via admin API.
globalCfgPath = *configPath
globalCfgPath = resolvedConfigPath
setGlobalCfg(cfg)
userCountEnabled = cfg.UserCount || *userCountFlag
@@ -2677,8 +2814,14 @@ func main() {
if err := store.EnsureXrayClientsSchema(ctx); err != nil {
log.Printf("xray clients table: %v", err)
} else {
if err := store.ResetXrayActiveConnections(ctx); err != nil {
log.Printf("xray active connection reset: %v", err)
}
startXrayClientExpiryChecker(store)
}
if err := store.EnsureXrayConfigSchema(ctx); err != nil {
log.Printf("xray config table disabled: %v", err)
}
if err := store.EnsureIfaceUsageTables(ctx); err != nil {
log.Printf("vnstat usage tables disabled: %v", err)
}
@@ -2892,6 +3035,9 @@ func main() {
log.Printf("failed to start TLS listener: %v", e)
}
// Start proxy hard auto-restart after the initial listeners are bound.
startProxyAutoRestart(cfg)
// Print user counts once at startup.
updateUserDisplay()