Compare commits

..

7 Commits

Author SHA1 Message Date
世界
83238d6a81
Fix time service wrapper 2025-07-23 18:23:07 +08:00
dyhkwong
51e4330b8c
Fix disable_sni nil time func 2025-07-23 17:51:39 +08:00
世界
082572433e
documentation: Bump version 2025-07-23 11:26:14 +08:00
世界
0f2035149c
Remove dependency on circl 2025-07-23 11:26:06 +08:00
世界
cba364204a
Fix VectorisedReadWaiter on windows 2025-07-23 11:26:06 +08:00
世界
4e17788549
Update dependencies 2025-07-23 11:26:06 +08:00
世界
18a6719893
Fix IndexTLSServerName 2025-07-23 11:26:06 +08:00
8 changed files with 90 additions and 170 deletions

View File

@ -1,14 +1,11 @@
package tls
import (
"bytes"
"encoding/binary"
"crypto/ecdh"
"crypto/rand"
"encoding/pem"
E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
"golang.org/x/crypto/cryptobyte"
)
type ECHCapableConfig interface {
@ -17,145 +14,68 @@ type ECHCapableConfig interface {
SetECHConfigList([]byte)
}
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_AES128GCM,
}, {
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
var configBuffer bytes.Buffer
var totalLen uint16
for _, keyPair := range keyPairs {
totalLen += uint16(len(keyPair.rawConf))
echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
if err != nil {
return
}
binary.Write(&configBuffer, binary.BigEndian, totalLen)
for _, keyPair := range keyPairs {
configBuffer.Write(keyPair.rawConf)
configBuilder := cryptobyte.NewBuilder(nil)
configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
configBytes, err := configBuilder.Bytes()
if err != nil {
return
}
var keyBuffer bytes.Buffer
for _, keyPair := range keyPairs {
keyBuffer.Write(keyPair.rawKey)
keyBuilder := cryptobyte.NewBuilder(nil)
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
keyBytes, err := keyBuilder.Bytes()
if err != nil {
return
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes}))
return
}
type echKeyConfigPair struct {
id uint8
rawKey []byte
conf myECHKeyConfig
rawConf []byte
}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) {
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
type echCipherSuite struct {
kdf hpke.KDF
aead hpke.AEAD
}
type myECHKeyConfig struct {
id uint8
kem hpke.KEM
seed []byte
}
func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
be := binary.BigEndian
// prepare for future update
if version != 0xfe0d {
return nil, E.New("unsupported ECH version", version)
}
suiteBuf := make([]byte, 0, len(suite)*4+2)
suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
for _, s := range suite {
if !s.kdf.IsValid() || !s.aead.IsValid() {
return nil, E.New("invalid HPKE cipher suite")
}
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
}
pairs := []echKeyConfigPair{}
for _, c := range conf {
pair := echKeyConfigPair{}
pair.id = c.id
pair.conf = c
if !c.kem.IsValid() {
return nil, E.New("invalid HPKE KEM")
}
kpGenerator := c.kem.Scheme().GenerateKeyPair
if len(c.seed) > 0 {
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
return pub, sec, nil
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
return nil, E.New("HPKE KEM seed too short")
}
}
pub, sec, err := kpGenerator()
if err != nil {
return nil, E.Cause(err, "generate ECH config key pair")
}
b := []byte{}
b = be.AppendUint16(b, version)
b = be.AppendUint16(b, 0) // length field
// contents
// key config
b = append(b, c.id)
b = be.AppendUint16(b, uint16(c.kem))
pubBuf, err := pub.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH public key")
}
b = be.AppendUint16(b, uint16(len(pubBuf)))
b = append(b, pubBuf...)
b = append(b, suiteBuf...)
// end key config
// max name len, not supported
b = append(b, 0)
// server name
b = append(b, byte(len(serverName)))
b = append(b, []byte(serverName)...)
// extensions, not supported
b = be.AppendUint16(b, 0)
be.PutUint16(b[2:], uint16(len(b)-4))
pair.rawConf = b
secBuf, err := sec.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH private key")
}
sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
pair.rawKey = sk
pairs = append(pairs, pair)
}
return pairs, nil
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.Bytes()
}

View File

@ -87,13 +87,15 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
CurrentTime: tlsConfig.Time(),
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}

View File

@ -11,10 +11,13 @@ type TimeServiceWrapper struct {
}
func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
if w.TimeService == nil {
return nil
return func() time.Time {
if w.TimeService != nil {
return w.TimeService.TimeFunc()()
} else {
return time.Now()
}
}
return w.TimeService.TimeFunc()
}
func (w *TimeServiceWrapper) Upstream() any {

View File

@ -36,47 +36,48 @@ func IndexTLSServerName(payload []byte) *MyServerName {
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
return nil
}
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:])
if serverName == nil {
return nil
}
serverName.Length += recordLayerHeaderLen
serverName.Index += recordLayerHeaderLen
return serverName
}
func indexTLSServerNameFromHandshake(hs []byte) *MyServerName {
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName {
if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return nil
}
if hs[0] != handshakeType {
if handshake[0] != handshakeType {
return nil
}
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3])
if len(handshake[4:]) != int(handshakeLen) {
return nil
}
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return nil
}
sessionIDLen := hs[38]
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
sessionIDLen := handshake[38]
currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen)
if len(handshake) < currentIndex {
return nil
}
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
if len(cs) < cipherSuiteHeaderLen {
cipherSuites := handshake[currentIndex:]
if len(cipherSuites) < cipherSuiteHeaderLen {
return nil
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1])
if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return nil
}
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)])
currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
if len(handshake) < currentIndex {
return nil
}
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:])
if serverName == nil {
return nil
}
@ -118,6 +119,7 @@ func indexTLSServerNameFromExtensions(exs []byte) *MyServerName {
}
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
return &MyServerName{
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
Length: int(sniLen),

View File

@ -15,5 +15,6 @@ func TestIndexTLSServerName(t *testing.T) {
require.NoError(t, err)
serverName := tf.IndexTLSServerName(payload)
require.NotNil(t, serverName)
require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length]))
require.Equal(t, "github.com", serverName.ServerName)
}

View File

@ -2,7 +2,7 @@
icon: material/alert-decagram
---
#### 1.12.0-rc.1
#### 1.12.0-rc.2
* Fixes and improvements

3
go.mod
View File

@ -5,7 +5,6 @@ go 1.23.1
require (
github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0
github.com/cloudflare/circl v1.6.1
github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0
github.com/go-chi/chi/v5 v5.2.2
@ -28,7 +27,7 @@ require (
github.com/sagernet/gomobile v0.1.7
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.7.0-beta.1.0.20250720120749-5ee6ddd30ca3
github.com/sagernet/sing v0.7.0-beta.1.0.20250722151551-64142925accb
github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.5.0-beta.3
github.com/sagernet/sing-shadowsocks v0.2.8

11
go.sum
View File

@ -20,8 +20,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
@ -107,15 +105,10 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/libdns/alidns v1.0.4-libdns.v1.beta1 h1:ods22gD4PcT0g4qRX77ucykjz7Rppnkz3vQoxDbbKTM=
github.com/libdns/alidns v1.0.4-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=
github.com/libdns/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ=
github.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=
github.com/libdns/cloudflare v0.2.2-0.20250430151523-b46a2b0885f6 h1:0dlpPjNr8TaYZbkpwCiee4udBNrYrWG8EZPYEbjHEn8=
github.com/libdns/cloudflare v0.2.2-0.20250430151523-b46a2b0885f6/go.mod h1:Aq4IXdjalB6mD0ELvKqJiIGim8zSC6mlIshRPMOAb5w=
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8=
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
@ -174,8 +167,8 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.0-beta.1.0.20250720120749-5ee6ddd30ca3 h1:/STH8/x0clwkDLq53f0H2T3oxX62SH65Wl8zWxo7/lE=
github.com/sagernet/sing v0.7.0-beta.1.0.20250720120749-5ee6ddd30ca3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.0-beta.1.0.20250722151551-64142925accb h1:9DU5JA9Cow/bUfdP1v1pYMbAkFiW17UbI4b/iEPjVnc=
github.com/sagernet/sing v0.7.0-beta.1.0.20250722151551-64142925accb/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.0-beta.3 h1:X/acRNsqQNfDlmwE7SorHfaZiny5e67hqIzM/592ric=