454 lines
13 KiB
Go
454 lines
13 KiB
Go
//go:build darwin && cgo
|
|
|
|
package tls
|
|
|
|
import (
|
|
"context"
|
|
stdtls "crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/option"
|
|
"github.com/sagernet/sing/common/json/badoption"
|
|
"github.com/sagernet/sing/common/logger"
|
|
)
|
|
|
|
const appleTLSTestTimeout = 5 * time.Second
|
|
|
|
const (
|
|
appleTLSSuccessHandshakeLoops = 20
|
|
appleTLSFailureRecoveryLoops = 10
|
|
)
|
|
|
|
type appleTLSServerResult struct {
|
|
state stdtls.ConnectionState
|
|
err error
|
|
}
|
|
|
|
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
for index := 0; index < appleTLSSuccessHandshakeLoops; index++ {
|
|
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS12,
|
|
MaxVersion: stdtls.VersionTLS12,
|
|
NextProtos: []string{"h2"},
|
|
})
|
|
|
|
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.2",
|
|
ALPN: badoption.Listable[string]{"h2"},
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("iteration %d: %v", index, err)
|
|
}
|
|
|
|
clientState := clientConn.ConnectionState()
|
|
if clientState.Version != stdtls.VersionTLS12 {
|
|
_ = clientConn.Close()
|
|
t.Fatalf("iteration %d: unexpected negotiated version: %x", index, clientState.Version)
|
|
}
|
|
if clientState.NegotiatedProtocol != "h2" {
|
|
_ = clientConn.Close()
|
|
t.Fatalf("iteration %d: unexpected negotiated protocol: %q", index, clientState.NegotiatedProtocol)
|
|
}
|
|
_ = clientConn.Close()
|
|
|
|
result := <-serverResult
|
|
if result.err != nil {
|
|
t.Fatalf("iteration %d: %v", index, result.err)
|
|
}
|
|
if result.state.Version != stdtls.VersionTLS12 {
|
|
t.Fatalf("iteration %d: server negotiated unexpected version: %x", index, result.state.Version)
|
|
}
|
|
if result.state.NegotiatedProtocol != "h2" {
|
|
t.Fatalf("iteration %d: server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS13,
|
|
MaxVersion: stdtls.VersionTLS13,
|
|
})
|
|
|
|
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MaxVersion: "1.2",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
})
|
|
if err == nil {
|
|
clientConn.Close()
|
|
t.Fatal("expected version mismatch handshake to fail")
|
|
}
|
|
|
|
if result := <-serverResult; result.err == nil {
|
|
t.Fatal("expected server handshake to fail on version mismatch")
|
|
}
|
|
}
|
|
|
|
func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
})
|
|
|
|
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "example.com",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
})
|
|
if err == nil {
|
|
clientConn.Close()
|
|
t.Fatal("expected server name mismatch handshake to fail")
|
|
}
|
|
|
|
if result := <-serverResult; result.err == nil {
|
|
t.Fatal("expected server handshake to fail on server name mismatch")
|
|
}
|
|
}
|
|
|
|
func TestAppleClientHandshakeRecoversAfterFailure(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
testCases := []struct {
|
|
name string
|
|
serverConfig *stdtls.Config
|
|
clientOptions option.OutboundTLSOptions
|
|
}{
|
|
{
|
|
name: "version mismatch",
|
|
serverConfig: &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS13,
|
|
MaxVersion: stdtls.VersionTLS13,
|
|
},
|
|
clientOptions: option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MaxVersion: "1.2",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
},
|
|
},
|
|
{
|
|
name: "server name mismatch",
|
|
serverConfig: &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
},
|
|
clientOptions: option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "example.com",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
},
|
|
},
|
|
}
|
|
successClientOptions := option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.2",
|
|
ALPN: badoption.Listable[string]{"h2"},
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
for index := 0; index < appleTLSFailureRecoveryLoops; index++ {
|
|
failedResult, failedAddress := startAppleTLSTestServer(t, testCase.serverConfig)
|
|
failedConn, err := newAppleTestClientConn(t, failedAddress, testCase.clientOptions)
|
|
if err == nil {
|
|
_ = failedConn.Close()
|
|
t.Fatalf("iteration %d: expected handshake failure", index)
|
|
}
|
|
if result := <-failedResult; result.err == nil {
|
|
t.Fatalf("iteration %d: expected server handshake failure", index)
|
|
}
|
|
|
|
successResult, successAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS12,
|
|
MaxVersion: stdtls.VersionTLS12,
|
|
NextProtos: []string{"h2"},
|
|
})
|
|
successConn, err := newAppleTestClientConn(t, successAddress, successClientOptions)
|
|
if err != nil {
|
|
t.Fatalf("iteration %d: follow-up handshake failed: %v", index, err)
|
|
}
|
|
clientState := successConn.ConnectionState()
|
|
if clientState.NegotiatedProtocol != "h2" {
|
|
_ = successConn.Close()
|
|
t.Fatalf("iteration %d: unexpected negotiated protocol after failure: %q", index, clientState.NegotiatedProtocol)
|
|
}
|
|
_ = successConn.Close()
|
|
|
|
result := <-successResult
|
|
if result.err != nil {
|
|
t.Fatalf("iteration %d: follow-up server handshake failed: %v", index, result.err)
|
|
}
|
|
if result.state.NegotiatedProtocol != "h2" {
|
|
t.Fatalf("iteration %d: follow-up server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppleClientReadDeadline(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS12,
|
|
MaxVersion: stdtls.VersionTLS12,
|
|
})
|
|
|
|
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.2",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer clientConn.Close()
|
|
defer close(serverDone)
|
|
|
|
err = clientConn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
|
if err != nil {
|
|
t.Fatalf("SetReadDeadline: %v", err)
|
|
}
|
|
|
|
readDone := make(chan error, 1)
|
|
buffer := make([]byte, 64)
|
|
go func() {
|
|
_, readErr := clientConn.Read(buffer)
|
|
readDone <- readErr
|
|
}()
|
|
|
|
select {
|
|
case readErr := <-readDone:
|
|
if !errors.Is(readErr, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", readErr)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Read did not return within 2s after deadline")
|
|
}
|
|
|
|
_, err = clientConn.Read(buffer)
|
|
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("sticky deadline: expected os.ErrDeadlineExceeded, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAppleClientSetDeadlineClearsPreExpiredSticky(t *testing.T) {
|
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{
|
|
Certificates: []stdtls.Certificate{serverCertificate},
|
|
MinVersion: stdtls.VersionTLS12,
|
|
MaxVersion: stdtls.VersionTLS12,
|
|
})
|
|
|
|
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
Engine: "apple",
|
|
ServerName: "localhost",
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.2",
|
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer clientConn.Close()
|
|
defer close(serverDone)
|
|
|
|
err = clientConn.SetReadDeadline(time.Now().Add(-time.Second))
|
|
if err != nil {
|
|
t.Fatalf("SetReadDeadline past: %v", err)
|
|
}
|
|
|
|
// Pre-expired deadline trips sticky flag without cancelling nw_connection
|
|
// (prepareReadTimeout short-circuits before the C read is issued).
|
|
buffer := make([]byte, 64)
|
|
_, err = clientConn.Read(buffer)
|
|
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("pre-expired: expected os.ErrDeadlineExceeded, got %v", err)
|
|
}
|
|
|
|
err = clientConn.SetReadDeadline(time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("SetReadDeadline zero: %v", err)
|
|
}
|
|
|
|
newDeadline := 300 * time.Millisecond
|
|
err = clientConn.SetReadDeadline(time.Now().Add(newDeadline))
|
|
if err != nil {
|
|
t.Fatalf("SetReadDeadline future: %v", err)
|
|
}
|
|
|
|
readStart := time.Now()
|
|
_, err = clientConn.Read(buffer)
|
|
readElapsed := time.Since(readStart)
|
|
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("after clear: expected os.ErrDeadlineExceeded, got %v", err)
|
|
}
|
|
if readElapsed < newDeadline-50*time.Millisecond {
|
|
t.Fatalf("sticky flag was not cleared: Read returned after %v, expected ~%v", readElapsed, newDeadline)
|
|
}
|
|
}
|
|
|
|
func startAppleTLSSilentServer(t *testing.T, tlsConfig *stdtls.Config) (chan<- struct{}, string) {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
listener.Close()
|
|
})
|
|
|
|
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
|
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
conn, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
handshakeErr := conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
if handshakeErr != nil {
|
|
return
|
|
}
|
|
tlsConn := stdtls.Server(conn, tlsConfig)
|
|
defer tlsConn.Close()
|
|
handshakeErr = tlsConn.Handshake()
|
|
if handshakeErr != nil {
|
|
return
|
|
}
|
|
handshakeErr = conn.SetDeadline(time.Time{})
|
|
if handshakeErr != nil {
|
|
return
|
|
}
|
|
<-done
|
|
}()
|
|
return done, listener.Addr().String()
|
|
}
|
|
|
|
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
|
t.Helper()
|
|
|
|
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return certificate, string(certificatePEM)
|
|
}
|
|
|
|
func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
listener.Close()
|
|
})
|
|
|
|
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
|
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
result := make(chan appleTLSServerResult, 1)
|
|
go func() {
|
|
defer close(result)
|
|
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
result <- appleTLSServerResult{err: err}
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
if err != nil {
|
|
result <- appleTLSServerResult{err: err}
|
|
return
|
|
}
|
|
|
|
tlsConn := stdtls.Server(conn, tlsConfig)
|
|
defer tlsConn.Close()
|
|
|
|
err = tlsConn.Handshake()
|
|
if err != nil {
|
|
result <- appleTLSServerResult{err: err}
|
|
return
|
|
}
|
|
|
|
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
|
|
}()
|
|
|
|
return result, listener.Addr().String()
|
|
}
|
|
|
|
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
|
t.Cleanup(cancel)
|
|
|
|
clientConfig, err := NewClientWithOptions(ClientOptions{
|
|
Context: ctx,
|
|
Logger: logger.NOP(),
|
|
ServerAddress: "",
|
|
Options: options,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
return tlsConn, nil
|
|
}
|