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:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user