Add nested rules Add backend action, allow wildcard in backends Remove poison from tree, update README with action table Allow defining pass/fail actions on challenge, Remove redirect/referer parameters on backend pass Set challenge cookie tied to host Rewrite DNSBL condition into a challenge Allow passing an arbitrary path for assets to js challenges Optimize programs exhaustively on compilation Activation instead of map for CEL context, faster map access, new network override Return valid host on cookie setting in case Host is an IP address. bug: does not work with IPv6, see https://github.com/golang/go/issues/65521 Apply TLS fingerprinter on GetConfigForClient instead of GetCertificate Cleanup go-away cookies before passing to backend Code action for specifically replying with an HTTP code
336 lines
8.0 KiB
Go
336 lines
8.0 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
)
|
|
|
|
func applyTLSFingerprinter(server *http.Server) {
|
|
if server.TLSConfig == nil {
|
|
return
|
|
}
|
|
server.TLSConfig = server.TLSConfig.Clone()
|
|
|
|
getConfigForClient := server.TLSConfig.GetConfigForClient
|
|
|
|
if getConfigForClient == nil {
|
|
getConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
server.TLSConfig.GetConfigForClient = func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
|
ja3n, ja4 := buildTLSFingerprint(clientHello)
|
|
ptr := clientHello.Context().Value(tlsFingerprintKey{})
|
|
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
|
fpPtr.ja3n.Store(&ja3n)
|
|
fpPtr.ja4.Store(&ja4)
|
|
}
|
|
return getConfigForClient(clientHello)
|
|
}
|
|
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
|
|
return context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
|
|
}
|
|
}
|
|
|
|
type tlsFingerprintKey struct{}
|
|
type TLSFingerprint struct {
|
|
ja3n atomic.Pointer[TLSFingerprintJA3N]
|
|
ja4 atomic.Pointer[TLSFingerprintJA4]
|
|
}
|
|
|
|
type TLSFingerprintJA3N [md5.Size]byte
|
|
|
|
func (f TLSFingerprintJA3N) String() string {
|
|
return hex.EncodeToString(f[:])
|
|
}
|
|
|
|
type TLSFingerprintJA4 struct {
|
|
A [10]byte
|
|
B [6]byte
|
|
C [6]byte
|
|
}
|
|
|
|
func (f TLSFingerprintJA4) String() string {
|
|
return strings.Join([]string{
|
|
string(f.A[:]),
|
|
hex.EncodeToString(f.B[:]),
|
|
hex.EncodeToString(f.C[:]),
|
|
}, "_")
|
|
}
|
|
|
|
func (f *TLSFingerprint) JA3N() *TLSFingerprintJA3N {
|
|
return f.ja3n.Load()
|
|
}
|
|
|
|
func (f *TLSFingerprint) JA4() *TLSFingerprintJA4 {
|
|
return f.ja4.Load()
|
|
}
|
|
|
|
const greaseMask = 0x0F0F
|
|
const greaseValue = 0x0a0a
|
|
|
|
// TLS extension numbers
|
|
const (
|
|
extensionServerName uint16 = 0
|
|
extensionStatusRequest uint16 = 5
|
|
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
|
|
extensionSupportedPoints uint16 = 11
|
|
extensionSignatureAlgorithms uint16 = 13
|
|
extensionALPN uint16 = 16
|
|
extensionSCT uint16 = 18
|
|
extensionExtendedMasterSecret uint16 = 23
|
|
extensionSessionTicket uint16 = 35
|
|
extensionPreSharedKey uint16 = 41
|
|
extensionEarlyData uint16 = 42
|
|
extensionSupportedVersions uint16 = 43
|
|
extensionCookie uint16 = 44
|
|
extensionPSKModes uint16 = 45
|
|
extensionCertificateAuthorities uint16 = 47
|
|
extensionSignatureAlgorithmsCert uint16 = 50
|
|
extensionKeyShare uint16 = 51
|
|
extensionQUICTransportParameters uint16 = 57
|
|
extensionRenegotiationInfo uint16 = 0xff01
|
|
extensionECHOuterExtensions uint16 = 0xfd00
|
|
extensionEncryptedClientHello uint16 = 0xfe0d
|
|
)
|
|
|
|
func tlsFingerprintJA3(hello *tls.ClientHelloInfo, sortExtensions bool) []byte {
|
|
buf := make([]byte, 0, 256)
|
|
|
|
{
|
|
var sslVersion uint16
|
|
var hasGrease bool
|
|
for _, v := range hello.SupportedVersions {
|
|
if v&greaseMask != greaseValue {
|
|
if v > sslVersion {
|
|
sslVersion = v
|
|
}
|
|
} else {
|
|
hasGrease = true
|
|
}
|
|
}
|
|
|
|
// maximum TLS 1.2 as specified on JA3, as TLS 1.3 is put in SupportedVersions
|
|
if slices.Contains(hello.Extensions, extensionSupportedVersions) && hasGrease && sslVersion > tls.VersionTLS12 {
|
|
sslVersion = tls.VersionTLS12
|
|
}
|
|
|
|
buf = strconv.AppendUint(buf, uint64(sslVersion), 10)
|
|
buf = append(buf, ',')
|
|
}
|
|
|
|
n := 0
|
|
for _, cipher := range hello.CipherSuites {
|
|
//if !slices.Contains(greaseValues[:], cipher) {
|
|
if cipher&greaseMask != greaseValue {
|
|
buf = strconv.AppendUint(buf, uint64(cipher), 10)
|
|
buf = append(buf, '-')
|
|
n = 1
|
|
}
|
|
}
|
|
|
|
buf = buf[:len(buf)-n]
|
|
buf = append(buf, ',')
|
|
n = 0
|
|
|
|
extensions := hello.Extensions
|
|
if sortExtensions {
|
|
extensions = slices.Clone(extensions)
|
|
slices.Sort(extensions)
|
|
}
|
|
|
|
for _, extension := range extensions {
|
|
if extension&greaseMask != greaseValue {
|
|
buf = strconv.AppendUint(buf, uint64(extension), 10)
|
|
buf = append(buf, '-')
|
|
n = 1
|
|
}
|
|
}
|
|
|
|
buf = buf[:len(buf)-n]
|
|
buf = append(buf, ',')
|
|
n = 0
|
|
|
|
for _, curve := range hello.SupportedCurves {
|
|
if curve&greaseMask != greaseValue {
|
|
buf = strconv.AppendUint(buf, uint64(curve), 10)
|
|
buf = append(buf, '-')
|
|
n = 1
|
|
}
|
|
}
|
|
|
|
buf = buf[:len(buf)-n]
|
|
buf = append(buf, ',')
|
|
n = 0
|
|
|
|
for _, point := range hello.SupportedPoints {
|
|
buf = strconv.AppendUint(buf, uint64(point), 10)
|
|
buf = append(buf, '-')
|
|
n = 1
|
|
}
|
|
|
|
buf = buf[:len(buf)-n]
|
|
|
|
sum := md5.Sum(buf)
|
|
return sum[:]
|
|
}
|
|
|
|
func tlsFingerprintJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
|
|
buf := make([]byte, 0, 10)
|
|
|
|
// TODO: t = TLS, q = QUIC
|
|
buf = append(buf, 't')
|
|
|
|
{
|
|
var sslVersion uint16
|
|
for _, v := range hello.SupportedVersions {
|
|
if v&greaseMask != greaseValue {
|
|
if v > sslVersion {
|
|
sslVersion = v
|
|
}
|
|
}
|
|
}
|
|
|
|
switch sslVersion {
|
|
case tls.VersionSSL30:
|
|
buf = append(buf, 's', '3')
|
|
case tls.VersionTLS10:
|
|
buf = append(buf, '1', '0')
|
|
case tls.VersionTLS11:
|
|
buf = append(buf, '1', '1')
|
|
case tls.VersionTLS12:
|
|
buf = append(buf, '1', '2')
|
|
case tls.VersionTLS13:
|
|
buf = append(buf, '1', '3')
|
|
default:
|
|
sslVersion -= 0x0201
|
|
buf = strconv.AppendUint(buf, uint64(sslVersion>>8), 10)
|
|
buf = strconv.AppendUint(buf, uint64(sslVersion&0xff), 10)
|
|
}
|
|
|
|
}
|
|
|
|
if slices.Contains(hello.Extensions, extensionServerName) && hello.ServerName != "" {
|
|
buf = append(buf, 'd')
|
|
} else {
|
|
buf = append(buf, 'i')
|
|
}
|
|
|
|
ciphers := make([]uint16, 0, len(hello.CipherSuites))
|
|
for _, cipher := range hello.CipherSuites {
|
|
if cipher&greaseMask != greaseValue {
|
|
ciphers = append(ciphers, cipher)
|
|
}
|
|
}
|
|
|
|
extensionCount := 0
|
|
extensions := make([]uint16, 0, len(hello.Extensions))
|
|
for _, extension := range hello.Extensions {
|
|
if extension&greaseMask != greaseValue {
|
|
extensionCount++
|
|
if extension != extensionALPN && extension != extensionServerName {
|
|
extensions = append(extensions, extension)
|
|
}
|
|
}
|
|
}
|
|
|
|
schemes := make([]tls.SignatureScheme, 0, len(hello.SignatureSchemes))
|
|
|
|
for _, scheme := range hello.SignatureSchemes {
|
|
if scheme&greaseMask != greaseValue {
|
|
schemes = append(schemes, scheme)
|
|
}
|
|
}
|
|
|
|
//TODO: maybe little endian
|
|
slices.Sort(ciphers)
|
|
slices.Sort(extensions)
|
|
//slices.Sort(schemes)
|
|
|
|
if len(ciphers) < 10 {
|
|
buf = append(buf, '0')
|
|
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
|
} else if len(ciphers) > 99 {
|
|
buf = append(buf, '9', '9')
|
|
} else {
|
|
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
|
}
|
|
|
|
if extensionCount < 10 {
|
|
buf = append(buf, '0')
|
|
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
|
} else if extensionCount > 99 {
|
|
buf = append(buf, '9', '9')
|
|
} else {
|
|
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
|
}
|
|
|
|
if len(hello.SupportedProtos) > 0 && len(hello.SupportedProtos[0]) > 1 {
|
|
buf = append(buf, hello.SupportedProtos[0][0], hello.SupportedProtos[0][len(hello.SupportedProtos[0])-1])
|
|
} else {
|
|
buf = append(buf, '0', '0')
|
|
}
|
|
|
|
copy(ja4.A[:], buf)
|
|
|
|
ja4.B = ja4SHA256(uint16SliceToHex(ciphers))
|
|
|
|
extBuf := uint16SliceToHex(extensions)
|
|
|
|
if len(schemes) > 0 {
|
|
extBuf = append(extBuf, '_')
|
|
extBuf = append(extBuf, uint16SliceToHex(schemes)...)
|
|
}
|
|
|
|
ja4.C = ja4SHA256(extBuf)
|
|
|
|
return ja4
|
|
}
|
|
|
|
func uint16SliceToHex[T ~uint16](in []T) (out []byte) {
|
|
if len(in) == 0 {
|
|
return out
|
|
}
|
|
out = slices.Grow(out, hex.EncodedLen(len(in)*2)+len(in))
|
|
|
|
for _, n := range in {
|
|
out = append(out, fmt.Sprintf("%04x", uint16(n))...)
|
|
out = append(out, ',')
|
|
}
|
|
out = out[:len(out)-1]
|
|
|
|
return out
|
|
}
|
|
|
|
func ja4SHA256(buf []byte) [6]byte {
|
|
if len(buf) == 0 {
|
|
return [6]byte{0, 0, 0, 0, 0, 0}
|
|
}
|
|
sum := sha256.Sum256(buf)
|
|
|
|
return [6]byte(sum[:6])
|
|
}
|
|
|
|
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (ja3n TLSFingerprintJA3N, ja4 TLSFingerprintJA4) {
|
|
return TLSFingerprintJA3N(tlsFingerprintJA3(hello, true)), tlsFingerprintJA4(hello)
|
|
}
|
|
|
|
func GetTLSFingerprint(r *http.Request) *TLSFingerprint {
|
|
ptr := r.Context().Value(tlsFingerprintKey{})
|
|
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
|
return fpPtr
|
|
}
|
|
return nil
|
|
}
|