diff --git a/dialer/hysteria2/hysteria2.go b/dialer/hysteria2/hysteria2.go index 2f32be8..22b48b2 100644 --- a/dialer/hysteria2/hysteria2.go +++ b/dialer/hysteria2/hysteria2.go @@ -28,6 +28,8 @@ type Hysteria2 struct { User string Password string Server string + Obfs string + ObfsPass string Insecure bool Sni string PinSHA256 string @@ -81,6 +83,12 @@ func (s *Hysteria2) Dialer(option *dialer.ExtraOption, nextDialer netproxy.Diale } } } + if s.Obfs != "" { + feature1.ObfuscationConfig = client.ObfuscationConfig{ + Obfuscation: s.Obfs, + ObfuscationKey: []byte(s.ObfsPass), + } + } header.Feature1 = feature1 if s.PinSHA256 != "" { @@ -120,7 +128,6 @@ func normalizeCertHash(hash string) string { // ref: https://v2.hysteria.network/zh/docs/developers/URI-Scheme/ func ParseHysteria2URL(link string) (*Hysteria2, error) { - // TODO: support salamander obfuscation u, err := url.Parse(link) if err != nil { return nil, err @@ -148,6 +155,8 @@ func ParseHysteria2URL(link string) (*Hysteria2, error) { Name: u.Fragment, User: u.User.Username(), Server: u.Host, + Obfs: q.Get("obfs"), + ObfsPass: q.Get("obfs-password"), Insecure: insecure, Sni: q.Get("sni"), PinSHA256: q.Get("pinSHA256"), @@ -169,6 +178,12 @@ func (s *Hysteria2) ExportToURL() string { t.User = url.UserPassword(s.User, s.Password) } q := t.Query() + if s.Obfs != "" { + q.Set("obfs", s.Obfs) + } + if s.ObfsPass != "" { + q.Set("obfs-password", s.ObfsPass) + } if s.Insecure { q.Set("insecure", "1") } diff --git a/protocol/hysteria2/client/client.go b/protocol/hysteria2/client/client.go index 3d1dc1d..98e16c7 100644 --- a/protocol/hysteria2/client/client.go +++ b/protocol/hysteria2/client/client.go @@ -63,6 +63,15 @@ func (c *clientImpl) connect(ctx context.Context) (*HandshakeInfo, error) { if err != nil { return nil, err } + + switch c.config.ObfuscationConfig.Obfuscation { + case "salamander": + pktConn = NewSalamanderPacketConn(pktConn, c.config.ObfuscationConfig.ObfuscationKey) + case "", "plain": + default: + return nil, errors.New("unknown obfuscation: " + c.config.ObfuscationConfig.Obfuscation) + } + // Convert config to TLS config & QUIC config tlsConfig := &tls.Config{ ServerName: c.config.TLSConfig.ServerName, diff --git a/protocol/hysteria2/client/config.go b/protocol/hysteria2/client/config.go index 961bc05..71eed1f 100644 --- a/protocol/hysteria2/client/config.go +++ b/protocol/hysteria2/client/config.go @@ -18,14 +18,15 @@ const ( ) type Config struct { - ConnFactory ConnFactory - ServerAddr net.Addr - Auth string - TLSConfig TLSConfig - QUICConfig QUICConfig - BandwidthConfig BandwidthConfig - UDPHopInterval time.Duration - FastOpen bool + ConnFactory ConnFactory + ServerAddr net.Addr + Auth string + TLSConfig TLSConfig + QUICConfig QUICConfig + BandwidthConfig BandwidthConfig + UDPHopInterval time.Duration + ObfuscationConfig ObfuscationConfig + FastOpen bool filled bool // whether the fields have been verified and filled } @@ -114,3 +115,8 @@ type BandwidthConfig struct { MaxTx uint64 MaxRx uint64 } + +type ObfuscationConfig struct { + Obfuscation string + ObfuscationKey []byte +} diff --git a/protocol/hysteria2/client/salamander.go b/protocol/hysteria2/client/salamander.go new file mode 100644 index 0000000..7c252c9 --- /dev/null +++ b/protocol/hysteria2/client/salamander.go @@ -0,0 +1,86 @@ +package client + +import ( + "crypto/rand" + "crypto/subtle" + "net" + "time" + + "golang.org/x/crypto/blake2b" +) + +type SalamanderPacketConn struct { + Connection net.PacketConn + Key []byte +} + +func NewSalamanderPacketConn(conn net.PacketConn, key []byte) SalamanderPacketConn { + return SalamanderPacketConn{ + Connection: conn, + Key: key, + } +} + +func (s SalamanderPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + packet := make([]byte, len(p)+8) + + n, addr, err = s.Connection.ReadFrom(packet) + + if err != nil { + return + } + + if n <= 8 { + return 0, addr, nil + } + + s.Process(p, packet[8:n], packet[:8]) + + n -= 8 + + return +} + +func (s SalamanderPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + packet := make([]byte, len(p)+8) + + rand.Read(packet[:8]) + + s.Process(packet[8:], p, packet[:8]) + + n, err = s.Connection.WriteTo(packet, addr) + + if err == nil { + n -= 8 + } + + return +} + +func (s SalamanderPacketConn) Process(dst, src, salt []byte) { + hash := blake2b.Sum256(append(s.Key, salt...)) + + for i := 0; i < len(src); i += 32 { + subtle.XORBytes(dst[i:], src[i:], hash[:]) + } +} + +func (s SalamanderPacketConn) Close() error { + return s.Connection.Close() +} + +func (s SalamanderPacketConn) LocalAddr() net.Addr { + return s.Connection.LocalAddr() +} + +func (s SalamanderPacketConn) SetDeadline(t time.Time) error { + return s.Connection.SetDeadline(t) +} + +func (s SalamanderPacketConn) SetReadDeadline(t time.Time) error { + return s.Connection.SetReadDeadline(t) +} + +func (s SalamanderPacketConn) SetWriteDeadline(t time.Time) error { + return s.Connection.SetWriteDeadline(t) +} diff --git a/protocol/hysteria2/dialer.go b/protocol/hysteria2/dialer.go index 282efc9..2ba1e7a 100644 --- a/protocol/hysteria2/dialer.go +++ b/protocol/hysteria2/dialer.go @@ -24,8 +24,9 @@ type Dialer struct { } type Feature1 struct { - BandwidthConfig client.BandwidthConfig - UDPHopInterval time.Duration + BandwidthConfig client.BandwidthConfig + UDPHopInterval time.Duration + ObfuscationConfig client.ObfuscationConfig } func NewDialer(nextDialer netproxy.Dialer, header protocol.Header) (netproxy.Dialer, error) { @@ -54,6 +55,7 @@ func NewDialer(nextDialer netproxy.Dialer, header protocol.Header) (netproxy.Dia if feature := header.Feature1; feature != nil { config.BandwidthConfig = feature.(*Feature1).BandwidthConfig config.UDPHopInterval = feature.(*Feature1).UDPHopInterval + config.ObfuscationConfig = feature.(*Feature1).ObfuscationConfig } var err error