1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2026-10-10 12:13:10 +08:00

fix: align HelloRetryRequest handling with JLS v3

JLS v3 does not permit HelloRetryRequest during JLS authentication. Keep the ClientHello random unchanged across HRR and make clients continue as ordinary TLS with certificate verification.

When an authenticated ClientHello would require HRR, fail before writing handshake data and wrap ErrJLSAuthFailed. This allows stream and QUIC callers to forward the untouched handshake to the camouflage fallback instead of accepting a non-standard JLS exchange.
This commit is contained in:
wwqgtxx
2026-07-16 22:17:24 +08:00
parent 423611628c
commit f86bc224dc
7 changed files with 90 additions and 36 deletions
+2 -2
View File
@@ -23,8 +23,8 @@ require (
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/http v0.1.6
github.com/metacubex/jls-quic-go v0.0.0-20260716020038-12287ee553d3
github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728
github.com/metacubex/jls-quic-go v0.0.0-20260716141257-bee72c5b0a6c
github.com/metacubex/jls-tls v0.0.0-20260716140250-3d001a2e3788
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
github.com/metacubex/mhurl v0.1.0
github.com/metacubex/mlkem v0.1.0
+4 -4
View File
@@ -131,10 +131,10 @@ github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I=
github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/jls-quic-go v0.0.0-20260716020038-12287ee553d3 h1:JuRPjylaOoVvSR9g/fuePbquEzHEgbeAeKSa1aaJ7bI=
github.com/metacubex/jls-quic-go v0.0.0-20260716020038-12287ee553d3/go.mod h1:6Ge9WhYeId1yJLD4kgyMDgFmcPJzmDAkHNc9TpzCCXw=
github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728 h1:4IbvO5xjKMJLs5GmLl0fYI+lDcJxAoZ3hJtaqq2/YC0=
github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8=
github.com/metacubex/jls-quic-go v0.0.0-20260716141257-bee72c5b0a6c h1:TK7l/879ivHrWnRQ+IZAuLCpJH4eTvo1ylNxtWF3nVk=
github.com/metacubex/jls-quic-go v0.0.0-20260716141257-bee72c5b0a6c/go.mod h1:q4vsumnl4LVzMuVawpwvnWRIHSyOzp3b0n1ZY9A9wvo=
github.com/metacubex/jls-tls v0.0.0-20260716140250-3d001a2e3788 h1:vqtCy6N0WiYwk4/4vL3qibvWbBjoQOxBgNRLeNRFXuQ=
github.com/metacubex/jls-tls v0.0.0-20260716140250-3d001a2e3788/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8=
github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df h1:S0vBzqjXok24VopstOgPd1JdgglW9tXehrqvwpQWbQ8=
github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df/go.mod h1:F4sVXat6QjPXkNsKRDyyG3BhSkxPFFnRPEIwmmyCgbg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
+7 -5
View File
@@ -112,7 +112,7 @@ func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Co
if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err
}
if !tlsConn.ConnectionState().JLS.Authenticated {
if tlsConn.ConnectionState().JLS.Status != tls.JLSAuthenticated {
return nil, ErrJLSAuthFailed
}
return tlsConn, nil
@@ -181,14 +181,14 @@ func Server(ctx context.Context, conn net.Conn, config *ServerConfig) (net.Conn,
tlsConn := tls.Server(recorder, config.TLSConfig.Clone())
if err := tlsConn.HandshakeContext(ctx); err != nil {
// Forwarding after authentication or a local write would mix two server handshakes.
if tlsConn.ConnectionState().JLS.Authenticated || recorder.wroteToClient() {
if tlsConn.ConnectionState().JLS.Status == tls.JLSAuthenticated || recorder.wroteToClient() {
recorder.discard()
return nil, err
}
return nil, relayFallback(ctx, conn, recorder.stop(), config)
}
recorder.discard()
if !tlsConn.ConnectionState().JLS.Authenticated {
if tlsConn.ConnectionState().JLS.Status != tls.JLSAuthenticated {
return nil, ErrJLSAuthFailed
}
return tlsConn, nil
@@ -196,13 +196,15 @@ func Server(ctx context.Context, conn net.Conn, config *ServerConfig) (net.Conn,
func UserFromConn(conn net.Conn) (string, bool) {
tlsConn, ok := N.FindUpstream(conn, func(tlsConn *tls.Conn) bool {
state := tlsConn.ConnectionState().JLS
return state.Authenticated && state.User != ""
return tlsConn.ConnectionState().JLS.Status != tls.JLSDisabled
})
if !ok {
return "", false
}
state := tlsConn.ConnectionState().JLS
if state.Status != tls.JLSAuthenticated || state.User == "" {
return "", false
}
return state.User, true
}
+68 -15
View File
@@ -15,6 +15,7 @@ import (
"github.com/metacubex/http"
"github.com/metacubex/http/httptest"
tls "github.com/metacubex/jls-tls"
httpTLS "github.com/metacubex/tls"
)
func TestJLSClientServer(t *testing.T) {
@@ -53,7 +54,7 @@ func testJLSClientServer(t *testing.T, clientFingerprint string) {
}
defer conn.Close()
state := conn.(*tls.Conn).ConnectionState()
if !state.JLS.Authenticated || state.JLS.User != user.Username {
if state.JLS.Status != tls.JLSAuthenticated || state.JLS.User != user.Username {
serverDone <- errors.New("server did not authenticate JLS user")
return
}
@@ -62,6 +63,11 @@ func testJLSClientServer(t *testing.T, clientFingerprint string) {
serverDone <- errors.New("server did not expose JLS user")
return
}
outerJLS := tls.Server(outerTLS, &tls.Config{JLSConfig: &tls.JLSConfig{Enable: true}})
if _, ok := UserFromConn(outerJLS); ok {
serverDone <- errors.New("server exposed an inner JLS user through an unauthenticated outer JLS connection")
return
}
_, err = io.Copy(conn, conn)
serverDone <- err
}()
@@ -112,14 +118,32 @@ func TestJLSUTLSClientRejectsInvalidFallbackCertificate(t *testing.T) {
}
func TestJLSUTLSClientFallback(t *testing.T) {
for _, protocol := range []string{"http/1.1", "h2"} {
t.Run(protocol, func(t *testing.T) {
for _, test := range []struct {
name string
protocol string
hrr bool
}{
{name: "HTTP/1.1", protocol: "http/1.1"},
{name: "HTTP/2", protocol: "h2"},
{name: "HelloRetryRequest", protocol: "http/1.1", hrr: true},
} {
t.Run(test.name, func(t *testing.T) {
requestProtocol := make(chan string, 1)
hrrObserved := make(chan bool, 1)
server := httptest.NewUnstartedServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
requestProtocol <- request.Proto
writer.WriteHeader(http.StatusNoContent)
}))
server.EnableHTTP2 = protocol == "h2"
server.EnableHTTP2 = test.protocol == "h2"
if test.hrr {
server.TLS = &httpTLS.Config{
CurvePreferences: []httpTLS.CurveID{httpTLS.CurveP256},
VerifyConnection: func(state httpTLS.ConnectionState) error {
hrrObserved <- state.HelloRetryRequest
return nil
},
}
}
server.StartTLS()
defer func() {
server.CloseClientConnections()
@@ -131,7 +155,7 @@ func TestJLSUTLSClientFallback(t *testing.T) {
if err != nil {
t.Fatal(err)
}
config, err := NewClientConfig("example.com", "user", "password", []string{protocol})
config, err := NewClientConfig("example.com", "user", "password", []string{test.protocol})
if err != nil {
t.Fatal(err)
}
@@ -144,7 +168,7 @@ func TestJLSUTLSClientFallback(t *testing.T) {
}
wantProtocol := "HTTP/1.1"
if protocol == "h2" {
if test.protocol == "h2" {
wantProtocol = "HTTP/2.0"
}
select {
@@ -155,14 +179,31 @@ func TestJLSUTLSClientFallback(t *testing.T) {
default:
t.Fatal("fallback HTTP request was not sent")
}
if test.hrr && !<-hrrObserved {
t.Fatal("ordinary TLS fallback did not exercise HelloRetryRequest")
}
})
}
}
func TestJLSUTLSClientRejectsHelloRetryRequest(t *testing.T) {
func TestJLSServerFallsBackInsteadOfHelloRetryRequest(t *testing.T) {
user := User{Username: "user", Password: "password"}
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
return nil, errors.New("HRR failure dialed fallback")
fallbackRequest := make(chan struct{}, 1)
fallback := httptest.NewUnstartedServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
fallbackRequest <- struct{}{}
writer.WriteHeader(http.StatusNoContent)
}))
fallback.StartTLS()
defer func() {
fallback.CloseClientConnections()
fallback.Close()
}()
ca.GetCertPool().AddCert(fallback.Certificate())
fallbackDialed := make(chan struct{}, 1)
serverConfig, err := NewServerConfig("example.com", fallback.Listener.Addr().String(), []User{user}, nil, 0, func(ctx context.Context, network, _ string) (net.Conn, error) {
fallbackDialed <- struct{}{}
return (&net.Dialer{}).DialContext(ctx, network, fallback.Listener.Addr().String())
})
if err != nil {
t.Fatal(err)
@@ -177,18 +218,30 @@ func TestJLSUTLSClientRejectsHelloRetryRequest(t *testing.T) {
serverDone <- serverErr
}()
clientConfig, err := NewClientConfig("camouflage.example", user.Username, user.Password, nil)
clientConfig, err := NewClientConfig("example.com", user.Username, user.Password, nil)
if err != nil {
t.Fatal(err)
}
clientConfig.ClientFingerprint = "chrome"
if conn, clientErr := NewClient(context.Background(), clientSide, clientConfig); clientErr == nil {
_ = conn.Close()
t.Fatal("uTLS client unexpectedly completed a HelloRetryRequest handshake")
if conn, clientErr := NewClient(context.Background(), clientSide, clientConfig); !errors.Is(clientErr, ErrJLSAuthFailed) {
if conn != nil {
_ = conn.Close()
}
t.Fatalf("client error = %v, want %v", clientErr, ErrJLSAuthFailed)
}
_ = clientSide.Close()
if err = <-serverDone; err == nil {
t.Fatal("JLS server unexpectedly completed a HelloRetryRequest handshake")
if err = <-serverDone; !errors.Is(err, ErrFallbackCompleted) {
t.Fatalf("server error = %v, want %v", err, ErrFallbackCompleted)
}
select {
case <-fallbackDialed:
default:
t.Fatal("JLS server did not dial fallback")
}
select {
case <-fallbackRequest:
default:
t.Fatal("fallback HTTP request was not sent")
}
}
+7 -8
View File
@@ -93,7 +93,7 @@ func newUTLSClient(ctx context.Context, conn net.Conn, config *ClientConfig) (ne
if err = uConn.BuildHandshakeState(); err != nil {
return nil, true, err
}
sentClientHello := append([]byte(nil), uConn.HandshakeState.Hello.Raw...)
verifier.clientHello = append([]byte(nil), uConn.HandshakeState.Hello.Raw...)
if err = uConn.HandshakeContext(ctx); err != nil {
return nil, true, err
@@ -105,13 +105,6 @@ func newUTLSClient(ctx context.Context, conn net.Conn, config *ClientConfig) (ne
jlsClientHTTPFallback(ctx, uConn, config.ServerName, fingerprint)
return nil, true, ErrJLSAuthFailed
}
// A HelloRetryRequest makes uTLS generate another ClientHello. Its public API
// cannot recalculate the JLS random at that point, so reject it instead of
// accepting a connection whose second ClientHello was not authenticated.
if !bytes.Equal(sentClientHello, uConn.HandshakeState.Hello.Raw) {
_ = uConn.Close()
return nil, true, errors.New("jls: uTLS HelloRetryRequest is not supported")
}
if uConn.ConnectionState().Version != utls.VersionTLS13 {
_ = uConn.Close()
return nil, true, ErrJLSAuthFailed
@@ -164,10 +157,16 @@ type utlsJLSVerifier struct {
*utls.UConn
user User
serverName string
clientHello []byte
authenticated bool
}
func (v *utlsJLSVerifier) VerifyConnection(state utls.ConnectionState) error {
// JLS v3 does not permit HelloRetryRequest at any stage. uTLS replaces
// HandshakeState.Hello when it sends the second ClientHello.
if !bytes.Equal(v.clientHello, v.HandshakeState.Hello.Raw) {
return verifyUTLSCertificate(state, v.serverName)
}
serverHello := v.HandshakeState.ServerHello
if serverHello == nil {
return errors.New("jls: uTLS server hello is unavailable")
+1 -1
View File
@@ -72,7 +72,7 @@ func monitorJLSAuth(quicConn *quic.Conn, packetConn net.PacketConn, tlsConf *tls
_ = packetConn.Close()
}
checkAuth := func() bool {
return quicConn.ConnectionState().TLS.JLS.Authenticated
return quicConn.ConnectionState().TLS.JLS.Status == tls.JLSAuthenticated
}
if !early {
if checkAuth() {
+1 -1
View File
@@ -210,7 +210,7 @@ func (s *Server) jlsAdditions(state *connState) []inbound.Addition {
func (s *Server) jlsUser(state *connState) string {
tlsState := state.quicConn.ConnectionState().TLS
if tlsState.JLS.Authenticated {
if tlsState.JLS.Status == tls.JLSAuthenticated {
return tlsState.JLS.User
}
return ""