diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 79bda46..d40e93e 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -177,23 +177,26 @@ type WebRTCDialer struct { webrtcConfig *webrtc.Configuration max int - eventLogger event.SnowflakeEventReceiver - proxy *url.URL + eventLogger event.SnowflakeEventReceiver + proxy *url.URL + dtlsRandomize bool + dtlsMimic bool } // Deprecated: Use NewWebRTCDialerWithEventsAndProxy instead func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer { - return NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, nil, nil) + return NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, nil, nil, false, false) } // Deprecated: Use NewWebRTCDialerWithEventsAndProxy instead func NewWebRTCDialerWithEvents(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer { - return NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, eventLogger, nil) + return NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, eventLogger, nil, false, false) } // NewWebRTCDialerWithEventsAndProxy constructs a new WebRTCDialer. func NewWebRTCDialerWithEventsAndProxy(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver, proxy *url.URL, + dtlsRandomize bool, dtlsMimic bool, ) *WebRTCDialer { config := webrtc.Configuration{ ICEServers: iceServers, @@ -204,8 +207,10 @@ func NewWebRTCDialerWithEventsAndProxy(broker *BrokerChannel, iceServers []webrt webrtcConfig: &config, max: max, - eventLogger: eventLogger, - proxy: proxy, + eventLogger: eventLogger, + proxy: proxy, + dtlsRandomize: dtlsRandomize, + dtlsMimic: dtlsMimic, } } @@ -213,7 +218,7 @@ func NewWebRTCDialerWithEventsAndProxy(broker *BrokerChannel, iceServers []webrt func (w WebRTCDialer) Catch() (*WebRTCPeer, error) { // TODO: [#25591] Fetch ICE server information from Broker. // TODO: [#25596] Consider TURN servers here too. - return NewWebRTCPeerWithEventsAndProxy(w.webrtcConfig, w.BrokerChannel, w.eventLogger, w.proxy) + return NewWebRTCPeerWithEventsAndProxy(w.webrtcConfig, w.BrokerChannel, w.eventLogger, w.proxy, w.dtlsRandomize, w.dtlsMimic) } // GetMax returns the maximum number of snowflakes to collect. diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 29251ba..8982e22 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -118,6 +118,8 @@ type ClientConfig struct { BridgeFingerprint string // CommunicationProxy is the proxy address for network communication CommunicationProxy *url.URL + DTLSRandomize bool + DTLSMimic bool } // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple @@ -161,7 +163,7 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) { max = config.Max } eventsLogger := event.NewSnowflakeEventDispatcher() - transport := &Transport{dialer: NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, eventsLogger, config.CommunicationProxy), eventDispatcher: eventsLogger} + transport := &Transport{dialer: NewWebRTCDialerWithEventsAndProxy(broker, iceServers, max, eventsLogger, config.CommunicationProxy, config.DTLSRandomize, config.DTLSMimic), eventDispatcher: eventsLogger} return transport, nil } diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index b648f59..fdbbb31 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -11,10 +11,13 @@ import ( "sync" "time" + "github.com/pion/dtls/v3" "github.com/pion/ice/v4" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" "github.com/pion/webrtc/v4" + "github.com/theodorsm/covert-dtls/pkg/mimicry" + "github.com/theodorsm/covert-dtls/pkg/randomize" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/event" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/proxy" @@ -49,7 +52,7 @@ type WebRTCPeer struct { func NewWebRTCPeer( config *webrtc.Configuration, broker *BrokerChannel, ) (*WebRTCPeer, error) { - return NewWebRTCPeerWithEventsAndProxy(config, broker, nil, nil) + return NewWebRTCPeerWithEventsAndProxy(config, broker, nil, nil, false, false) } // Deprecated: Use NewWebRTCPeerWithEventsAndProxy Instead. @@ -57,7 +60,7 @@ func NewWebRTCPeerWithEvents( config *webrtc.Configuration, broker *BrokerChannel, eventsLogger event.SnowflakeEventReceiver, ) (*WebRTCPeer, error) { - return NewWebRTCPeerWithEventsAndProxy(config, broker, eventsLogger, nil) + return NewWebRTCPeerWithEventsAndProxy(config, broker, eventsLogger, nil, false, false) } // NewWebRTCPeerWithEventsAndProxy constructs a WebRTC PeerConnection to a snowflake proxy. @@ -68,6 +71,7 @@ func NewWebRTCPeerWithEvents( func NewWebRTCPeerWithEventsAndProxy( config *webrtc.Configuration, broker *BrokerChannel, eventsLogger event.SnowflakeEventReceiver, proxy *url.URL, + dtlsRandomize bool, dtlsMimic bool, ) (*WebRTCPeer, error) { if eventsLogger == nil { eventsLogger = event.NewSnowflakeEventDispatcher() @@ -92,7 +96,7 @@ func NewWebRTCPeerWithEventsAndProxy( connection.eventsLogger = eventsLogger connection.proxy = proxy - err := connection.connect(config, broker) + err := connection.connect(config, broker, dtlsRandomize, dtlsMimic) if err != nil { connection.Close() return nil, err @@ -166,10 +170,9 @@ func (c *WebRTCPeer) checkForStaleness(timeout time.Duration) { // connect does the bulk of the work: gather ICE candidates, send the SDP offer to broker, // receive an answer from broker, and wait for data channel to open -func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel) error { +func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel, dtlsRandomize bool, dtlsMimic bool) error { log.Println(c.id, " connecting...") - - err := c.preparePeerConnection(config, broker.keepLocalAddresses) + err := c.preparePeerConnection(config, broker.keepLocalAddresses, dtlsRandomize, dtlsMimic) localDescription := c.pc.LocalDescription() c.eventsLogger.OnNewSnowflakeEvent(event.EventOnOfferCreated{ WebRTCLocalDescription: localDescription, @@ -213,6 +216,8 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel func (c *WebRTCPeer) preparePeerConnection( config *webrtc.Configuration, keepLocalAddresses bool, + dtlsRandomize bool, + dtlsMimic bool, ) error { var err error s := webrtc.SettingEngine{} @@ -245,6 +250,24 @@ func (c *WebRTCPeer) preparePeerConnection( } s.SetNet(vnet) + + if dtlsRandomize { + rand := randomize.RandomizedMessageClientHello{RandomALPN: true} + s.SetDTLSClientHelloMessageHook(rand.Hook) + } else if dtlsMimic { + mimic := &mimicry.MimickedClientHello{} + profiles := []dtls.SRTPProtectionProfile{ + dtls.SRTP_AES128_CM_HMAC_SHA1_80, + dtls.SRTP_AES128_CM_HMAC_SHA1_32, + dtls.SRTP_AEAD_AES_128_GCM, + dtls.SRTP_AEAD_AES_256_GCM, + dtls.SRTP_AES256_CM_SHA1_32, + dtls.SRTP_AES256_CM_SHA1_80, + } + s.SetSRTPProtectionProfiles(profiles...) + s.SetDTLSClientHelloMessageHook(mimic.Hook) + } + api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) c.pc, err = api.NewPeerConnection(*config) if err != nil { diff --git a/client/snowflake.go b/client/snowflake.go index 648481f..5474a47 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -174,6 +174,8 @@ func main() { max := flag.Int("max", DefaultSnowflakeCapacity, "capacity for number of multiplexed WebRTC peers") versionFlag := flag.Bool("version", false, "display version info to stderr and quit") + dtlsRandomize := flag.Bool("dtls-randomize", false, "randomize DTLS client hello") + dtlsMimic := flag.Bool("dtls-mimic", false, "mimic DTLS client hello of Chrome and Firefox") // Deprecated oldLogToStateDir := flag.Bool("logToStateDir", false, "use -log-to-state-dir instead") @@ -186,6 +188,10 @@ func main() { os.Exit(0) } + if *dtlsMimic && *dtlsRandomize { + log.Fatal("Cannot both Randomize and Mimic DTLS client hello") + } + log.SetFlags(log.LstdFlags | log.LUTC) // Don't write to stderr; versions of tor earlier than about 0.3.5.6 do @@ -240,6 +246,8 @@ func main() { ICEAddresses: iceAddresses, KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses, Max: *max, + DTLSRandomize: *dtlsRandomize, + DTLSMimic: *dtlsMimic, } // Begin goptlib client process. diff --git a/go.mod b/go.mod index 820ab8b..b683070 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 -go 1.21 +go 1.22 require ( github.com/aws/aws-sdk-go-v2 v1.32.6 @@ -10,6 +10,7 @@ require ( github.com/golang/mock v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/miekg/dns v1.1.62 + github.com/pion/dtls/v3 v3.0.4 github.com/pion/ice/v4 v4.0.3 github.com/pion/sdp/v3 v3.0.9 github.com/pion/stun/v3 v3.0.0 @@ -20,6 +21,7 @@ require ( github.com/refraction-networking/utls v1.6.7 github.com/smartystreets/goconvey v1.8.1 github.com/stretchr/testify v1.10.0 + github.com/theodorsm/covert-dtls v0.0.2-0.20241201194039-050c26fb1e5f github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 github.com/xtaci/kcp-go/v5 v5.6.8 github.com/xtaci/smux v1.5.31 @@ -56,7 +58,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pion/datachannel v1.5.9 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/interceptor v0.1.37 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index e8e6fe5..56031fe 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40= github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= +github.com/theodorsm/covert-dtls v0.0.2-0.20241201194039-050c26fb1e5f h1:+x3jtBX9WWEXSkdcoyw1Ryztrc0SJbfLD8r7ELR7NwU= +github.com/theodorsm/covert-dtls v0.0.2-0.20241201194039-050c26fb1e5f/go.mod h1:U3A87xJnEsomZcftqJ0QpM1MRiLIxNveypK4VGFp1jk= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= diff --git a/proxy/README.md b/proxy/README.md index e98d7c9..032c28b 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -48,6 +48,10 @@ Usage of ./proxy: maximum concurrent clients (default is to accept an unlimited number of clients) -disable-stats-logger disable the exposing mechanism for stats using logs + -dtls-mimic + mimic DTLS client hello of Chrome and Firefox + -dtls-randomize + randomize DTLS client hello -ephemeral-ports-range range Set the range of ports used for client connections (format:":"). If omitted, the ports will be chosen automatically. diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go index fd93f68..1303376 100644 --- a/proxy/lib/snowflake.go +++ b/proxy/lib/snowflake.go @@ -42,8 +42,11 @@ import ( "github.com/pion/ice/v4" "github.com/gorilla/websocket" + "github.com/pion/dtls/v3" "github.com/pion/transport/v3/stdnet" "github.com/pion/webrtc/v4" + "github.com/theodorsm/covert-dtls/pkg/mimicry" + "github.com/theodorsm/covert-dtls/pkg/randomize" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/event" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages" @@ -166,6 +169,9 @@ type SnowflakeProxy struct { periodicProxyStats *periodicProxyStats bytesLogger bytesLogger + + DTLSRandomize bool + DTLSMimic bool } // Checks whether an IP address is a remote address for the client @@ -424,6 +430,23 @@ func (sf *SnowflakeProxy) makeWebRTCAPI() *webrtc.API { settingsEngine.SetDTLSInsecureSkipHelloVerify(true) + if sf.DTLSRandomize { + rand := randomize.RandomizedMessageClientHello{RandomALPN: true} + settingsEngine.SetDTLSClientHelloMessageHook(rand.Hook) + } else if sf.DTLSMimic { + mimic := &mimicry.MimickedClientHello{} + profiles := []dtls.SRTPProtectionProfile{ + dtls.SRTP_AES128_CM_HMAC_SHA1_80, + dtls.SRTP_AES128_CM_HMAC_SHA1_32, + dtls.SRTP_AEAD_AES_128_GCM, + dtls.SRTP_AEAD_AES_256_GCM, + dtls.SRTP_AES256_CM_SHA1_32, + dtls.SRTP_AES256_CM_SHA1_80, + } + settingsEngine.SetSRTPProtectionProfiles(profiles...) + settingsEngine.SetDTLSClientHelloMessageHook(mimic.Hook) + } + return webrtc.NewAPI(webrtc.WithSettingEngine(settingsEngine)) } diff --git a/proxy/main.go b/proxy/main.go index 12093da..8ff8981 100644 --- a/proxy/main.go +++ b/proxy/main.go @@ -46,6 +46,8 @@ func main() { verboseLogging := flag.Bool("verbose", false, "increase log verbosity") ephemeralPortsRangeFlag := flag.String("ephemeral-ports-range", "", "Set the `range` of ports used for client connections (format:\":\").\nIf omitted, the ports will be chosen automatically.") versionFlag := flag.Bool("version", false, "display version info to stderr and quit") + dtlsRandomize := flag.Bool("dtls-randomize", false, "randomize DTLS client hello") + dtlsMimic := flag.Bool("dtls-mimic", false, "mimic DTLS client hello of Chrome and Firefox") var ephemeralPortsRange []uint16 = []uint16{0, 0} @@ -64,6 +66,10 @@ func main() { log.Fatal("Cannot keep local address candidates when outbound address is specified") } + if *dtlsMimic && *dtlsRandomize { + log.Fatal("Cannot both Randomize and Mimic DTLS client hello") + } + eventLogger := event.NewSnowflakeEventDispatcher() if *ephemeralPortsRangeFlag != "" { @@ -112,6 +118,8 @@ func main() { AllowNonTLSRelay: *allowNonTLSRelay, SummaryInterval: *summaryInterval, + DTLSRandomize: *dtlsRandomize, + DTLSMimic: *dtlsMimic, } var logOutput = io.Discard