feat(juicity): support certificate pinning (#256)

This commit is contained in:
mzz
2023-08-04 18:19:28 +08:00
committed by GitHub
parent 0c8cda4b28
commit 4d615205c0
4 changed files with 77 additions and 50 deletions

View File

@ -8,6 +8,7 @@ package common
import ( import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
@ -495,3 +496,16 @@ func StringSet(list []string) map[string]struct{} {
} }
return m return m
} }
func GenerateCertChainHash(rawCerts [][]byte) (chainHash []byte) {
for _, cert := range rawCerts {
certHash := sha256.Sum256(cert)
if chainHash == nil {
chainHash = certHash[:]
} else {
newHash := sha256.Sum256(append(chainHash, certHash[:]...))
chainHash = newHash[:]
}
}
return chainHash
}

View File

@ -1,7 +1,10 @@
package juicity package juicity
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@ -14,46 +17,61 @@ import (
) )
func init() { func init() {
dialer.FromLinkRegister("juicity", NewJuice) dialer.FromLinkRegister("juicity", NewJuicity)
} }
type Juice struct { type Juicity struct {
Name string Name string
Server string Server string
Port int Port int
User string User string
Password string Password string
Sni string Sni string
AllowInsecure bool AllowInsecure bool
CongestionControl string CongestionControl string
Protocol string PinnedCertchainSha256 string
Protocol string
} }
func NewJuice(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) { func NewJuicity(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseJuiceURL(link) s, err := ParseJuicityURL(link)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return s.Dialer(option, nextDialer) return s.Dialer(option, nextDialer)
} }
func (s *Juice) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) { func (s *Juicity) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
d := nextDialer d := nextDialer
var err error var err error
var flags protocol.Flags var flags protocol.Flags
tlsConfig := &tls.Config{
NextProtos: []string{"h3"},
MinVersion: tls.VersionTLS13,
ServerName: s.Sni,
InsecureSkipVerify: s.AllowInsecure || option.AllowInsecure,
}
if s.PinnedCertchainSha256 != "" {
pinnedHash, err := base64.StdEncoding.DecodeString(s.PinnedCertchainSha256)
if err != nil {
return nil, nil, fmt.Errorf("decode pin_certchain_sha256: %w", err)
}
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if !bytes.Equal(common.GenerateCertChainHash(rawCerts), pinnedHash) {
return fmt.Errorf("pinned hash of cert chain does not match")
}
return nil
}
}
if d, err = protocol.NewDialer("juicity", d, protocol.Header{ if d, err = protocol.NewDialer("juicity", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)), ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Feature1: s.CongestionControl, Feature1: s.CongestionControl,
TlsConfig: &tls.Config{ TlsConfig: tlsConfig,
NextProtos: []string{"h3"}, User: s.User,
MinVersion: tls.VersionTLS13, Password: s.Password,
ServerName: s.Sni, IsClient: true,
InsecureSkipVerify: s.AllowInsecure || option.AllowInsecure, Flags: flags,
},
User: s.User,
Password: s.Password,
IsClient: true,
Flags: flags,
}); err != nil { }); err != nil {
return nil, nil, err return nil, nil, err
} }
@ -65,7 +83,7 @@ func (s *Juice) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer)
}, nil }, nil
} }
func ParseJuiceURL(u string) (data *Juice, err error) { func ParseJuicityURL(u string) (data *Juicity, err error) {
//trojan://password@server:port#escape(remarks) //trojan://password@server:port#escape(remarks)
t, err := url.Parse(u) t, err := url.Parse(u)
if err != nil { if err != nil {
@ -89,31 +107,27 @@ func ParseJuiceURL(u string) (data *Juice, err error) {
if sni == "" { if sni == "" {
sni = t.Hostname() sni = t.Hostname()
} }
disableSni, _ := strconv.ParseBool(t.Query().Get("disable_sni"))
if disableSni {
sni = ""
allowInsecure = true
}
port, err := strconv.Atoi(t.Port()) port, err := strconv.Atoi(t.Port())
if err != nil { if err != nil {
return nil, dialer.InvalidParameterErr return nil, dialer.InvalidParameterErr
} }
password, _ := t.User.Password() password, _ := t.User.Password()
data = &Juice{ data = &Juicity{
Name: t.Fragment, Name: t.Fragment,
Server: t.Hostname(), Server: t.Hostname(),
Port: port, Port: port,
User: t.User.Username(), User: t.User.Username(),
Password: password, Password: password,
Sni: sni, Sni: sni,
AllowInsecure: allowInsecure, AllowInsecure: allowInsecure,
CongestionControl: t.Query().Get("congestion_control"), CongestionControl: t.Query().Get("congestion_control"),
Protocol: "juicity", PinnedCertchainSha256: t.Query().Get("pinned_certchain_sha256"),
Protocol: "juicity",
} }
return data, nil return data, nil
} }
func (t *Juice) ExportToURL() string { func (t *Juicity) ExportToURL() string {
u := &url.URL{ u := &url.URL{
Scheme: "juicity", Scheme: "juicity",
User: url.UserPassword(t.User, t.Password), User: url.UserPassword(t.User, t.Password),
@ -125,9 +139,8 @@ func (t *Juice) ExportToURL() string {
q.Set("allow_insecure", "1") q.Set("allow_insecure", "1")
} }
common.SetValue(&q, "sni", t.Sni) common.SetValue(&q, "sni", t.Sni)
if t.CongestionControl != "" { common.SetValue(&q, "congestion_control", t.CongestionControl)
common.SetValue(&q, "congestion_control", t.CongestionControl) common.SetValue(&q, "pinned_certchain_sha256", t.PinnedCertchainSha256)
}
u.RawQuery = q.Encode() u.RawQuery = q.Encode()
return u.String() return u.String()
} }

4
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/miekg/dns v1.1.55 github.com/miekg/dns v1.1.55
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/mzz2017/softwind v0.0.0-20230730190454-1b4f59056283 github.com/mzz2017/softwind v0.0.0-20230803152605-5f1f6bc06934
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
github.com/safchain/ethtool v0.3.0 github.com/safchain/ethtool v0.3.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
@ -34,7 +34,7 @@ require (
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/compress v1.16.7 // indirect
github.com/mzz2017/quic-go v0.0.0-20230730161358-da6d530ddb18 // indirect github.com/mzz2017/quic-go v0.0.0-20230731153658-2aabc7c97220 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.0 // indirect
golang.org/x/mod v0.12.0 // indirect golang.org/x/mod v0.12.0 // indirect

8
go.sum
View File

@ -89,10 +89,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mzz2017/disk-bloom v1.0.1 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M= github.com/mzz2017/disk-bloom v1.0.1 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M=
github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI= github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI=
github.com/mzz2017/quic-go v0.0.0-20230730161358-da6d530ddb18 h1:SzaoU9jIglF82Qp2rmPhsXhdtgOYiTwmxbd85qa5+Gs= github.com/mzz2017/quic-go v0.0.0-20230731153658-2aabc7c97220 h1:StI6iNucfDVrm54yhrHL0Dr1vKC1nBqkok2Itkypvdk=
github.com/mzz2017/quic-go v0.0.0-20230730161358-da6d530ddb18/go.mod h1:+8o4sdYYZCkNfMGiP3oKstpdHeSDFJy84A68evIzhw4= github.com/mzz2017/quic-go v0.0.0-20230731153658-2aabc7c97220/go.mod h1:+8o4sdYYZCkNfMGiP3oKstpdHeSDFJy84A68evIzhw4=
github.com/mzz2017/softwind v0.0.0-20230730190454-1b4f59056283 h1:nmDedxylxjWN8CW4xxa4krkZknijTA/qHgRk849o8zs= github.com/mzz2017/softwind v0.0.0-20230803152605-5f1f6bc06934 h1:IAaVbSfNiYP/TuJ4n686SeAwcs+PMvKdzJM5R4Y1rhM=
github.com/mzz2017/softwind v0.0.0-20230730190454-1b4f59056283/go.mod h1:H5th8OX4bXcksqNjwKhyisZS4lmO1ZezAbcoIL3P1dU= github.com/mzz2017/softwind v0.0.0-20230803152605-5f1f6bc06934/go.mod h1:TopiWHnL2ty4V4AkNM1nNVdbLlASWdQqploagf4tcKc=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=