Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial session ticket support #14

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions common/tls/std_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,55 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net"
"net/netip"
"os"
"strings"
"sync"
"time"

"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
)

type ClientSessionCache struct {
cache map[string]*tls.ClientSessionState
mutex sync.Mutex
}

func NewClientSessionCache() *ClientSessionCache {
return &ClientSessionCache{
cache: make(map[string]*tls.ClientSessionState),
}
}

func (c *ClientSessionCache) Get(sessionKey string) (*tls.ClientSessionState, bool) {
_ = sessionKey // To stop linter from complaining
sessionKey = "unused"
c.mutex.Lock()
session, ok := c.cache[sessionKey]
c.mutex.Unlock()
return session, ok
}

func (c *ClientSessionCache) Put(sessionKey string, cs *tls.ClientSessionState) {
_ = sessionKey
sessionKey = "unused"
c.mutex.Lock()
if cs == nil {
delete(c.cache, sessionKey)
} else {
c.cache[sessionKey] = cs
}
c.mutex.Unlock()
}

type STDClientConfig struct {
config *tls.Config
config *tls.Config
obSessionTicketOpts *option.OutboundSessionTicketOptions
}

func (s *STDClientConfig) ServerName() string {
Expand All @@ -39,11 +76,62 @@ func (s *STDClientConfig) Config() (*STDConfig, error) {
}

func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
return tls.Client(conn, s.config), nil
tlsConfig := s.config.Clone()
if s.obSessionTicketOpts != nil && s.obSessionTicketOpts.Enabled && tlsConfig.ServerName == s.obSessionTicketOpts.RealDomain {
s.obSessionTicketOpts.Mutex.Lock()
t := time.Now().Unix()
if (t - s.obSessionTicketOpts.LastUpdate) >= s.obSessionTicketOpts.TimeoutSecs {
s.obSessionTicketOpts.SessionState = NewClientSessionCache()
tlsConfig.ClientSessionCache = s.obSessionTicketOpts.SessionState
tlsConfig.SessionTicketsDisabled = false
client := tls.Client(conn, tlsConfig)
err := client.Handshake()
if err != nil {
client.Close()
s.obSessionTicketOpts.Mutex.Unlock()
return nil, E.New(fmt.Sprintf("Failed to obtain session ticket: %v", err))
}
_, err = client.Write([]byte{1, 2, 3})
if err != nil {
client.Close()
s.obSessionTicketOpts.Mutex.Unlock()
return nil, E.New(fmt.Sprintf("Failed to obtain session ticket: %v", err))
}
_, err = io.ReadAll(client)
if err != nil {
client.Close()
s.obSessionTicketOpts.Mutex.Unlock()
return nil, E.New(fmt.Sprintf("Failed to obtain session ticket: %v", err))
}
client.Close()
s.obSessionTicketOpts.LastUpdate = t
s.obSessionTicketOpts.Mutex.Unlock()
return nil, E.New("Got the session ticket, attempting to connect...")
}
s.obSessionTicketOpts.Mutex.Unlock()
tlsConfig.InsecureSkipVerify = true // We are using custom verification, this is fine
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
DNSName: s.config.ServerName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
tlsConfig.SessionTicketsDisabled = false
tlsConfig.ClientSessionCache = s.obSessionTicketOpts.SessionState
tlsConfig.ServerName = s.obSessionTicketOpts.FakeDomain
//tlsConfig.ServerName = s.obSessionTicketOpts.RealDomain
//tlsConfig.ServerName = ""
}
return tls.Client(conn, tlsConfig), nil
}

func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{s.config.Clone()}
return &STDClientConfig{s.config.Clone(), s.obSessionTicketOpts}
}

func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
Expand Down Expand Up @@ -132,5 +220,5 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
}
tlsConfig.RootCAs = certPool
}
return &STDClientConfig{&tlsConfig}, nil
return &STDClientConfig{&tlsConfig, options.SessionTicket}, nil
}
44 changes: 30 additions & 14 deletions option/tls.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package option

import (
"crypto/tls"
"sync"
)

type InboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
ServerName string `json:"server_name,omitempty"`
Expand Down Expand Up @@ -35,20 +40,31 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
}

type OutboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
Certificate Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
TLSTricks *TLSTricksOptions `json:"tls_tricks,omitempty"`
Enabled bool `json:"enabled,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
Certificate Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
TLSTricks *TLSTricksOptions `json:"tls_tricks,omitempty"`
SessionTicket *OutboundSessionTicketOptions `json:"session_ticket,omitempty"`
}

type OutboundSessionTicketOptions struct {
Enabled bool `json:"enabled,omitempty"`
FakeDomain string `json:"fake_domain,omitempty"`
RealDomain string `json:"real_domain,omitempty"`
TimeoutSecs int64 `json:"timeout_secs,omitempty"`
SessionState tls.ClientSessionCache
LastUpdate int64
Mutex sync.Mutex
}

type OutboundTLSOptionsContainer struct {
Expand Down