mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-17 08:40:59 +00:00
fix(tls): pin remote cert via native uTLS handshake instead of xray subprocess
GetRemoteCertHash shelled out to 'xray tls ping' and scraped its stdout, which swallowed the real failure (a refused dial surfaced only as 'no certificate hash found'). Replace it with a native uTLS Chrome handshake: dial/handshake errors now surface verbatim, host:port is honoured, and the leaf is taken from PeerCertificates[0] so IP-only self-signed certs (no DNS SANs) hash correctly. Mirrors alireza0/x-ui@1372ad0 without its nil-leaf panic.
This commit is contained in:
@@ -86,7 +86,7 @@ require (
|
|||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
github.com/quic-go/quic-go v0.60.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
|
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||||
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
||||||
github.com/sagernet/sing v0.8.10 // indirect
|
github.com/sagernet/sing v0.8.10 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
stdnet "net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
utls "github.com/refraction-networking/utls"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
@@ -1859,55 +1860,50 @@ func walkCertFiles(node any, out []string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRemoteCertHash runs `xray tls ping <server>` to fetch the live certificate
|
// GetRemoteCertHash opens a uTLS (Chrome fingerprint) handshake to a remote
|
||||||
// SHA-256 of a remote endpoint — the value to put in pinnedPeerCertSha256 (pcs)
|
// endpoint and returns the hex-encoded SHA-256 of its leaf certificate — the
|
||||||
// when pinning a server whose certificate file you don't hold (a CDN front, a
|
// value to put in pinnedPeerCertSha256 (pcs) when pinning a server whose
|
||||||
// REALITY dest, an external proxy). Returns the unique leaf-certificate hashes.
|
// 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) {
|
func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) {
|
||||||
server = strings.TrimSpace(server)
|
server = strings.TrimSpace(server)
|
||||||
if server == "" {
|
if server == "" {
|
||||||
return nil, common.NewError("no server provided")
|
return nil, common.NewError("no server provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
host, port := server, "443"
|
||||||
defer cancel()
|
if h, p, err := stdnet.SplitHostPort(server); err == nil {
|
||||||
cmd := exec.CommandContext(ctx, xray.GetBinaryPath(), "tls", "ping", server)
|
host, port = h, p
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &out
|
|
||||||
if err := cmd.Run(); err != nil && out.Len() == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hexRe := regexp.MustCompile(`[0-9a-fA-F]{64}`)
|
dialer := stdnet.Dialer{Timeout: 10 * time.Second}
|
||||||
seen := make(map[string]struct{})
|
tcpConn, err := dialer.Dial("tcp", stdnet.JoinHostPort(host, port))
|
||||||
var leaves []string
|
if err != nil {
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
return nil, common.NewErrorf("failed to dial %s: %s", stdnet.JoinHostPort(host, port), err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(leaves) == 0 {
|
defer tcpConn.Close()
|
||||||
// Surface why the ping produced no cert (dial refused, timeout, …)
|
_ = tcpConn.SetDeadline(time.Now().Add(15 * time.Second))
|
||||||
// instead of the bare "not found" — the inbound is usually just not
|
|
||||||
// listening for TLS on the pinged port.
|
tlsConn := utls.UClient(tcpConn, &utls.Config{
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
ServerName: host,
|
||||||
line = strings.TrimSpace(line)
|
InsecureSkipVerify: true,
|
||||||
if strings.Contains(line, "Failed") || strings.Contains(line, "error") {
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
return nil, common.NewError("no certificate hash for ", server, ": ", line)
|
}, utls.HelloChrome_Auto)
|
||||||
}
|
defer tlsConn.Close()
|
||||||
}
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
return nil, common.NewError("no certificate hash found for ", server)
|
return nil, common.NewErrorf("tls handshake with %s failed: %s", host, err)
|
||||||
}
|
}
|
||||||
return leaves, nil
|
|
||||||
|
certs := tlsConn.ConnectionState().PeerCertificates
|
||||||
|
if len(certs) == 0 {
|
||||||
|
return nil, common.NewError("no certificate returned by ", host)
|
||||||
|
}
|
||||||
|
// 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) {
|
func (s *ServerService) GetNewEchCert(sni string) (any, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user