|
|
@@ -4,7 +4,6 @@ import (
|
|
|
"archive/zip"
|
|
|
"bufio"
|
|
|
"bytes"
|
|
|
- "context"
|
|
|
"crypto/sha256"
|
|
|
"crypto/x509"
|
|
|
"encoding/hex"
|
|
|
@@ -13,6 +12,7 @@ import (
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"mime/multipart"
|
|
|
+ stdnet "net"
|
|
|
"net/http"
|
|
|
"net/url"
|
|
|
"os"
|
|
|
@@ -34,6 +34,7 @@ import (
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
+ utls "github.com/refraction-networking/utls"
|
|
|
"github.com/shirou/gopsutil/v4/cpu"
|
|
|
"github.com/shirou/gopsutil/v4/disk"
|
|
|
"github.com/shirou/gopsutil/v4/host"
|
|
|
@@ -158,12 +159,15 @@ const xrayVersionsCacheTTL = 15 * time.Minute
|
|
|
// callers from triggering arbitrary aggregation work and keeps the
|
|
|
// frontend's bucket selector self-documenting.
|
|
|
var allowedHistoryBuckets = map[int]bool{
|
|
|
- 2: true, // Real-time view
|
|
|
- 30: true, // 30s intervals
|
|
|
- 60: true, // 1m intervals
|
|
|
- 120: true, // 2m intervals
|
|
|
- 180: true, // 3m intervals
|
|
|
- 300: true, // 5m intervals
|
|
|
+ 2: true, // Real-time view
|
|
|
+ 30: true, // 30s intervals
|
|
|
+ 60: true, // 1m intervals
|
|
|
+ 120: true, // 2m intervals
|
|
|
+ 180: true, // 3m intervals
|
|
|
+ 300: true, // 5m intervals
|
|
|
+ 720: true, // 12m intervals
|
|
|
+ 1440: true, // 24m intervals
|
|
|
+ 2880: true, // 48m intervals
|
|
|
}
|
|
|
|
|
|
// IsAllowedHistoryBucket reports whether a bucket-seconds value is in the
|
|
|
@@ -1732,7 +1736,16 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
|
|
|
func (s *ServerService) GetCertHash(certFile string, certContent string) ([]string, error) {
|
|
|
var certBytes []byte
|
|
|
if path := strings.TrimSpace(certFile); path != "" {
|
|
|
- b, err := os.ReadFile(path)
|
|
|
+ // Guard against path traversal: only hash certificate files the panel
|
|
|
+ // already references in its own configuration (an inbound's TLS
|
|
|
+ // certificateFile or the panel's own web cert). The path handed to
|
|
|
+ // os.ReadFile comes from that allow-list, never directly from the
|
|
|
+ // caller-supplied value.
|
|
|
+ known, ok := s.resolveKnownCertFile(path)
|
|
|
+ if !ok {
|
|
|
+ return nil, common.NewError("certificate file is not referenced by any inbound or panel setting")
|
|
|
+ }
|
|
|
+ b, err := os.ReadFile(known)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
@@ -1778,46 +1791,119 @@ func (s *ServerService) GetCertHash(certFile string, certContent string) ([]stri
|
|
|
return hashes, nil
|
|
|
}
|
|
|
|
|
|
-// GetRemoteCertHash runs `xray tls ping <server>` to fetch the live certificate
|
|
|
-// SHA-256 of a remote endpoint — the value to put in pinnedPeerCertSha256 (pcs)
|
|
|
-// when pinning a server whose certificate file you don't hold (a CDN front, a
|
|
|
-// REALITY dest, an external proxy). Returns the unique leaf-certificate hashes.
|
|
|
+// resolveKnownCertFile checks the caller-supplied certificate path against the
|
|
|
+// set of certificate files the panel already references (inbound TLS configs
|
|
|
+// plus the panel's own web cert) and, on a match, returns the path taken from
|
|
|
+// that configuration — not the caller's value. This both confines reads to
|
|
|
+// known certificates and breaks the user-input-to-filesystem taint flow.
|
|
|
+func (s *ServerService) resolveKnownCertFile(certFile string) (string, bool) {
|
|
|
+ want := filepath.Clean(certFile)
|
|
|
+ for _, known := range s.knownCertFiles() {
|
|
|
+ if filepath.Clean(known) == want {
|
|
|
+ return known, true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return "", false
|
|
|
+}
|
|
|
+
|
|
|
+// knownCertFiles collects every certificate file path the panel legitimately
|
|
|
+// references: the certificateFile of each inbound's TLS settings and the
|
|
|
+// panel's own web TLS certificate.
|
|
|
+func (s *ServerService) knownCertFiles() []string {
|
|
|
+ var files []string
|
|
|
+ if cert, err := s.settingService.GetCertFile(); err == nil {
|
|
|
+ if cert = strings.TrimSpace(cert); cert != "" {
|
|
|
+ files = append(files, cert)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if inbounds, err := s.inboundService.GetAllInbounds(); err == nil {
|
|
|
+ for _, inbound := range inbounds {
|
|
|
+ files = collectCertFiles(inbound.StreamSettings, files)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return files
|
|
|
+}
|
|
|
+
|
|
|
+// collectCertFiles walks a stream-settings JSON document and appends the value
|
|
|
+// of every "certificateFile" field it finds (TLS settings may nest them under
|
|
|
+// several keys depending on the security type).
|
|
|
+func collectCertFiles(streamSettings string, out []string) []string {
|
|
|
+ streamSettings = strings.TrimSpace(streamSettings)
|
|
|
+ if streamSettings == "" {
|
|
|
+ return out
|
|
|
+ }
|
|
|
+ var parsed any
|
|
|
+ if err := json.Unmarshal([]byte(streamSettings), &parsed); err != nil {
|
|
|
+ return out
|
|
|
+ }
|
|
|
+ return walkCertFiles(parsed, out)
|
|
|
+}
|
|
|
+
|
|
|
+func walkCertFiles(node any, out []string) []string {
|
|
|
+ switch v := node.(type) {
|
|
|
+ case map[string]any:
|
|
|
+ for key, val := range v {
|
|
|
+ if key == "certificateFile" {
|
|
|
+ if path, ok := val.(string); ok {
|
|
|
+ if path = strings.TrimSpace(path); path != "" {
|
|
|
+ out = append(out, path)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ out = walkCertFiles(val, out)
|
|
|
+ }
|
|
|
+ case []any:
|
|
|
+ for _, item := range v {
|
|
|
+ out = walkCertFiles(item, out)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+// GetRemoteCertHash opens a uTLS (Chrome fingerprint) handshake to a remote
|
|
|
+// endpoint and returns the hex-encoded SHA-256 of its leaf certificate — the
|
|
|
+// value to put in pinnedPeerCertSha256 (pcs) when pinning a server whose
|
|
|
+// certificate file you don't hold (a CDN front, a REALITY dest, an external
|
|
|
+// proxy). A native handshake replaces the old `xray tls ping` subprocess so the
|
|
|
+// real dial/handshake failure (connection refused, timeout, …) surfaces
|
|
|
+// verbatim. `server` may be host or host:port; the port defaults to 443.
|
|
|
func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) {
|
|
|
server = strings.TrimSpace(server)
|
|
|
if server == "" {
|
|
|
return nil, common.NewError("no server provided")
|
|
|
}
|
|
|
|
|
|
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
|
- defer cancel()
|
|
|
- cmd := exec.CommandContext(ctx, xray.GetBinaryPath(), "tls", "ping", server)
|
|
|
- var out bytes.Buffer
|
|
|
- cmd.Stdout = &out
|
|
|
- cmd.Stderr = &out
|
|
|
- if err := cmd.Run(); err != nil && out.Len() == 0 {
|
|
|
- return nil, err
|
|
|
+ host, port := server, "443"
|
|
|
+ if h, p, err := stdnet.SplitHostPort(server); err == nil {
|
|
|
+ host, port = h, p
|
|
|
}
|
|
|
|
|
|
- hexRe := regexp.MustCompile(`[0-9a-fA-F]{64}`)
|
|
|
- seen := make(map[string]struct{})
|
|
|
- var leaves []string
|
|
|
- for _, line := range strings.Split(out.String(), "\n") {
|
|
|
- if !strings.Contains(line, "leaf SHA256") {
|
|
|
- continue
|
|
|
- }
|
|
|
- hash := strings.ToLower(hexRe.FindString(line))
|
|
|
- if hash == "" {
|
|
|
- continue
|
|
|
- }
|
|
|
- if _, ok := seen[hash]; !ok {
|
|
|
- seen[hash] = struct{}{}
|
|
|
- leaves = append(leaves, hash)
|
|
|
- }
|
|
|
+ dialer := stdnet.Dialer{Timeout: 10 * time.Second}
|
|
|
+ tcpConn, err := dialer.Dial("tcp", stdnet.JoinHostPort(host, port))
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.NewErrorf("failed to dial %s: %s", stdnet.JoinHostPort(host, port), err)
|
|
|
}
|
|
|
- if len(leaves) == 0 {
|
|
|
- return nil, common.NewError("no certificate hash found for ", server)
|
|
|
+ defer tcpConn.Close()
|
|
|
+ _ = tcpConn.SetDeadline(time.Now().Add(15 * time.Second))
|
|
|
+
|
|
|
+ tlsConn := utls.UClient(tcpConn, &utls.Config{
|
|
|
+ ServerName: host,
|
|
|
+ InsecureSkipVerify: true,
|
|
|
+ NextProtos: []string{"h2", "http/1.1"},
|
|
|
+ }, utls.HelloChrome_Auto)
|
|
|
+ defer tlsConn.Close()
|
|
|
+ if err := tlsConn.Handshake(); err != nil {
|
|
|
+ return nil, common.NewErrorf("tls handshake with %s failed: %s", host, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ certs := tlsConn.ConnectionState().PeerCertificates
|
|
|
+ if len(certs) == 0 {
|
|
|
+ return nil, common.NewError("no certificate returned by ", host)
|
|
|
}
|
|
|
- return leaves, nil
|
|
|
+ // PeerCertificates[0] is always the leaf the connection verifies against —
|
|
|
+ // robust for IP-only self-signed certs that carry no DNS SANs.
|
|
|
+ sum := sha256.Sum256(certs[0].Raw)
|
|
|
+ return []string{hex.EncodeToString(sum[:])}, nil
|
|
|
}
|
|
|
|
|
|
func (s *ServerService) GetNewEchCert(sni string) (any, error) {
|