From 74c726ff62e5f772436432e96aaa85c468d704dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 4 Feb 2026 05:03:48 +0800 Subject: [PATCH] Commands: Print CA cert's SHA256 in `tls ping` (#5644) And https://github.com/XTLS/Xray-core/issues/5642#issuecomment-3840806246 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- common/utils/access_field.go | 17 +++++++++ infra/conf/transport_internet.go | 9 ++++- main/commands/all/tls/ping.go | 57 ++++++++++++++++++----------- transport/internet/tls/config.go | 1 + transport/internet/tls/config.pb.go | 15 ++++++-- transport/internet/tls/config.proto | 2 + transport/internet/tls/tls.go | 4 ++ 7 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 common/utils/access_field.go diff --git a/common/utils/access_field.go b/common/utils/access_field.go new file mode 100644 index 00000000..bc42e67c --- /dev/null +++ b/common/utils/access_field.go @@ -0,0 +1,17 @@ +package utils + +import ( + "reflect" + "unsafe" +) + +// AccessField can used to access unexported field of a struct +// valueType must be the exact type of the field or it will panic +func AccessField[valueType any](obj any, fieldName string) *valueType { + field := reflect.ValueOf(obj).Elem().FieldByName(fieldName) + if field.Type() != reflect.TypeOf(*new(valueType)) { + panic("field type: " + field.Type().String() + ", valueType: " + reflect.TypeOf(*new(valueType)).String()) + } + v := (*valueType)(unsafe.Pointer(field.UnsafeAddr())) + return v +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 9357aa79..965a05a0 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1,6 +1,7 @@ package conf import ( + "context" "encoding/base64" "encoding/hex" "encoding/json" @@ -10,6 +11,7 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" @@ -747,7 +749,12 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.MasterKeyLog = c.MasterKeyLog if c.AllowInsecure { - return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`) + if time.Now().After(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) { + return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`) + } else { + errors.LogWarning(context.Background(), `"allowInsecure" will be removed automatically after 2026-06-01, please use "pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn) instead, PLEASE CONTACT YOUR SERVICE PROVIDER (AIRPORT)`) + config.AllowInsecure = true + } } if c.PinnedPeerCertSha256 != "" { for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { diff --git a/main/commands/all/tls/ping.go b/main/commands/all/tls/ping.go index e340fb07..49501e9b 100644 --- a/main/commands/all/tls/ping.go +++ b/main/commands/all/tls/ping.go @@ -6,8 +6,13 @@ import ( "encoding/hex" "fmt" "net" + "os" "strconv" + "text/tabwriter" + utls "github.com/refraction-networking/utls" + + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/main/commands/base" . "github.com/xtls/xray-core/transport/internet/tls" ) @@ -46,6 +51,7 @@ func executePing(cmd *base.Command, args []string) { } else { TargetPort, _ = strconv.Atoi(port) } + tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) var ip net.IP if len(*pingIPStr) > 0 { @@ -70,7 +76,7 @@ func executePing(cmd *base.Command, args []string) { if err != nil { base.Fatalf("Failed to dial tcp: %s", err) } - tlsConn := gotls.Client(tcpConn, &gotls.Config{ + tlsConn := GeneraticUClient(tcpConn, &gotls.Config{ InsecureSkipVerify: true, NextProtos: []string{"h2", "http/1.1"}, MaxVersion: gotls.VersionTLS13, @@ -81,8 +87,9 @@ func executePing(cmd *base.Command, args []string) { fmt.Println("Handshake failure: ", err) } else { fmt.Println("Handshake succeeded") - printTLSConnDetail(tlsConn) - printCertificates(tlsConn.ConnectionState().PeerCertificates) + printTLSConnDetail(tabWriter, tlsConn) + printCertificates(tabWriter, tlsConn.ConnectionState().PeerCertificates) + tabWriter.Flush() } tlsConn.Close() } @@ -94,7 +101,7 @@ func executePing(cmd *base.Command, args []string) { if err != nil { base.Fatalf("Failed to dial tcp: %s", err) } - tlsConn := gotls.Client(tcpConn, &gotls.Config{ + tlsConn := GeneraticUClient(tcpConn, &gotls.Config{ ServerName: domain, NextProtos: []string{"h2", "http/1.1"}, MaxVersion: gotls.VersionTLS13, @@ -105,8 +112,9 @@ func executePing(cmd *base.Command, args []string) { fmt.Println("Handshake failure: ", err) } else { fmt.Println("Handshake succeeded") - printTLSConnDetail(tlsConn) - printCertificates(tlsConn.ConnectionState().PeerCertificates) + printTLSConnDetail(tabWriter, tlsConn) + printCertificates(tabWriter, tlsConn.ConnectionState().PeerCertificates) + tabWriter.Flush() } tlsConn.Close() } @@ -115,38 +123,45 @@ func executePing(cmd *base.Command, args []string) { fmt.Println("TLS ping finished") } -func printCertificates(certs []*x509.Certificate) { +func printCertificates(tabWriter *tabwriter.Writer, certs []*x509.Certificate) { var leaf *x509.Certificate + var CAs []*x509.Certificate var length int for _, cert := range certs { length += len(cert.Raw) if len(cert.DNSNames) != 0 { leaf = cert + } else { + CAs = append(CAs, cert) } } - fmt.Println("Certificate chain's total length: ", length, "(certs count: "+strconv.Itoa(len(certs))+")") + fmt.Fprintf(tabWriter, "Certificate chain's total length: \t %d (certs count: %s)\n", length, strconv.Itoa(len(certs))) if leaf != nil { - fmt.Println("Cert's signature algorithm: ", leaf.SignatureAlgorithm.String()) - fmt.Println("Cert's publicKey algorithm: ", leaf.PublicKeyAlgorithm.String()) - fmt.Println("Cert's allowed domains: ", leaf.DNSNames) - fmt.Println("Cert's leaf SHA256: ", hex.EncodeToString(GenerateCertHash(leaf))) + fmt.Fprintf(tabWriter, "Cert's signature algorithm: \t %s\n", leaf.SignatureAlgorithm.String()) + fmt.Fprintf(tabWriter, "Cert's publicKey algorithm: \t %s\n", leaf.PublicKeyAlgorithm.String()) + fmt.Fprintf(tabWriter, "Cert's leaf SHA256: \t %s\n", hex.EncodeToString(GenerateCertHash(leaf))) + for _, ca := range CAs { + fmt.Fprintf(tabWriter, "Cert's CA: %s SHA256: \t %s\n", ca.Subject.CommonName, hex.EncodeToString(GenerateCertHash(ca))) + } + fmt.Fprintf(tabWriter, "Cert's allowed domains: \t %v\n", leaf.DNSNames) } } -func printTLSConnDetail(tlsConn *gotls.Conn) { +func printTLSConnDetail(tabWriter *tabwriter.Writer, tlsConn *utls.UConn) { connectionState := tlsConn.ConnectionState() var tlsVersion string - if connectionState.Version == gotls.VersionTLS13 { + switch connectionState.Version { + case gotls.VersionTLS13: tlsVersion = "TLS 1.3" - } else if connectionState.Version == gotls.VersionTLS12 { + case gotls.VersionTLS12: tlsVersion = "TLS 1.2" } - fmt.Println("TLS Version: ", tlsVersion) - curveID := connectionState.CurveID - if curveID != 0 { - PostQuantum := (curveID == gotls.X25519MLKEM768) - fmt.Println("TLS Post-Quantum key exchange: ", PostQuantum, "("+curveID.String()+")") + fmt.Fprintf(tabWriter, "TLS Version: \t %s\n", tlsVersion) + curveID := utils.AccessField[utls.CurveID](tlsConn.Conn, "curveID") + if curveID != nil { + PostQuantum := (*curveID == utls.X25519MLKEM768) + fmt.Fprintf(tabWriter, "TLS Post-Quantum key exchange: \t %t (%s)\n", PostQuantum, curveID.String()) } else { - fmt.Println("TLS Post-Quantum key exchange: false (RSA Exchange)") + fmt.Fprintf(tabWriter, "TLS Post-Quantum key exchange: false (RSA Exchange)\n") } } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 1b80f439..678a9b2f 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -384,6 +384,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { PinnedPeerCertSha256: c.PinnedPeerCertSha256, } config := &tls.Config{ + InsecureSkipVerify: c.AllowInsecure, Rand: randCarrier, ClientSessionCache: globalSessionCache, RootCAs: root, diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index e14839eb..37628755 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -177,7 +177,8 @@ func (x *Certificate) GetBuildChain() bool { } type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` // List of certificates to be served on server. Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` // Override server name. @@ -241,6 +242,13 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1} } +func (x *Config) GetAllowInsecure() bool { + if x != nil { + return x.AllowInsecure + } + return false +} + func (x *Config) GetCertificate() []*Certificate { if x != nil { return x.Certificate @@ -385,8 +393,9 @@ const file_transport_internet_tls_config_proto_rawDesc = "" + "\x05Usage\x12\x10\n" + "\fENCIPHERMENT\x10\x00\x12\x14\n" + "\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" + - "\x0fAUTHORITY_ISSUE\x10\x02\"\xce\x06\n" + - "\x06Config\x12J\n" + + "\x0fAUTHORITY_ISSUE\x10\x02\"\xf5\x06\n" + + "\x06Config\x12%\n" + + "\x0eallow_insecure\x18\x01 \x01(\bR\rallowInsecure\x12J\n" + "\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" + "\vserver_name\x18\x03 \x01(\tR\n" + "serverName\x12#\n" + diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 225206b2..57cd7866 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -38,6 +38,8 @@ message Certificate { } message Config { + bool allow_insecure = 1; + // List of certificates to be served on server. repeated Certificate certificate = 2; diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 20b9716d..cbc80bf5 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -126,6 +126,10 @@ func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) ne return &UConn{UConn: utlsConn} } +func GeneraticUClient(c net.Conn, config *tls.Config) *utls.UConn { + return utls.UClient(c, copyConfig(config), utls.HelloChrome_Auto) +} + func copyConfig(c *tls.Config) *utls.Config { return &utls.Config{ Rand: c.Rand,