2020-10-14 13:42:00 +00:00
package connection
import (
"fmt"
"hash/fnv"
"sync"
"time"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2021-10-11 10:31:05 +00:00
"github.com/cloudflare/cloudflared/edgediscovery"
2020-10-14 13:42:00 +00:00
)
const (
2023-02-06 19:06:02 +00:00
AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge"
2020-10-14 13:42:00 +00:00
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
edgeH2muxTLSServerName = "cftunnel.com"
// edgeH2TLSServerName is the server name to establish http2 connection with edge
edgeH2TLSServerName = "h2.cftunnel.com"
2021-08-17 14:30:02 +00:00
// edgeQUICServerName is the server name to establish quic connection with edge.
edgeQUICServerName = "quic.cftunnel.com"
2022-04-05 22:07:10 +00:00
AutoSelectFlag = "auto"
2023-02-06 19:06:02 +00:00
// SRV and TXT record resolution TTL
ResolveTTL = time . Hour
2020-10-14 13:42:00 +00:00
)
var (
2023-02-07 04:05:48 +00:00
// ProtocolList represents a list of supported protocols for communication with the edge
// in order of precedence for remote percentage fetcher.
ProtocolList = [ ] Protocol { QUIC , HTTP2 }
2020-10-14 13:42:00 +00:00
)
type Protocol int64
const (
2023-02-06 19:06:02 +00:00
// HTTP2 using golang HTTP2 library for edge connections.
HTTP2 Protocol = iota
// QUIC using quic-go for edge connections.
2021-08-17 14:30:02 +00:00
QUIC
2020-10-14 13:42:00 +00:00
)
// Fallback returns the fallback protocol and whether the protocol has a fallback
func ( p Protocol ) fallback ( ) ( Protocol , bool ) {
switch p {
case HTTP2 :
2021-10-11 10:31:05 +00:00
return 0 , false
2021-08-17 14:30:02 +00:00
case QUIC :
return HTTP2 , true
2020-10-14 13:42:00 +00:00
default :
return 0 , false
}
}
func ( p Protocol ) String ( ) string {
switch p {
2023-02-06 19:06:02 +00:00
case HTTP2 :
2020-10-14 13:42:00 +00:00
return "http2"
2023-02-06 19:06:02 +00:00
case QUIC :
2021-08-17 14:30:02 +00:00
return "quic"
2020-10-14 13:42:00 +00:00
default :
return fmt . Sprintf ( "unknown protocol" )
}
}
2021-08-17 14:30:02 +00:00
func ( p Protocol ) TLSSettings ( ) * TLSSettings {
switch p {
2023-02-06 19:06:02 +00:00
case HTTP2 :
2021-08-17 14:30:02 +00:00
return & TLSSettings {
ServerName : edgeH2TLSServerName ,
}
2023-02-06 19:06:02 +00:00
case QUIC :
2021-08-17 14:30:02 +00:00
return & TLSSettings {
ServerName : edgeQUICServerName ,
NextProtos : [ ] string { "argotunnel" } ,
}
default :
return nil
}
}
type TLSSettings struct {
ServerName string
NextProtos [ ] string
}
2020-10-14 13:42:00 +00:00
type ProtocolSelector interface {
Current ( ) Protocol
Fallback ( ) ( Protocol , bool )
}
2023-02-06 19:06:02 +00:00
// staticProtocolSelector will not provide a different protocol for Fallback
2020-10-14 13:42:00 +00:00
type staticProtocolSelector struct {
current Protocol
}
func ( s * staticProtocolSelector ) Current ( ) Protocol {
return s . current
}
func ( s * staticProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
2023-02-06 19:06:02 +00:00
return s . current , false
2020-10-14 13:42:00 +00:00
}
2023-02-06 19:06:02 +00:00
// remoteProtocolSelector will fetch a list of remote protocols to provide for edge discovery
type remoteProtocolSelector struct {
2021-10-11 10:31:05 +00:00
lock sync . RWMutex
current Protocol
// protocolPool is desired protocols in the order of priority they should be picked in.
protocolPool [ ] Protocol
switchThreshold int32
2023-02-06 19:06:02 +00:00
fetchFunc edgediscovery . PercentageFetcher
2021-10-11 10:31:05 +00:00
refreshAfter time . Time
ttl time . Duration
log * zerolog . Logger
2020-10-14 13:42:00 +00:00
}
2023-02-06 19:06:02 +00:00
func newRemoteProtocolSelector (
2020-10-14 13:42:00 +00:00
current Protocol ,
2021-10-11 10:31:05 +00:00
protocolPool [ ] Protocol ,
switchThreshold int32 ,
2023-02-06 19:06:02 +00:00
fetchFunc edgediscovery . PercentageFetcher ,
2020-10-14 13:42:00 +00:00
ttl time . Duration ,
2020-11-25 06:55:13 +00:00
log * zerolog . Logger ,
2023-02-06 19:06:02 +00:00
) * remoteProtocolSelector {
return & remoteProtocolSelector {
2021-10-11 10:31:05 +00:00
current : current ,
protocolPool : protocolPool ,
switchThreshold : switchThreshold ,
fetchFunc : fetchFunc ,
refreshAfter : time . Now ( ) . Add ( ttl ) ,
ttl : ttl ,
log : log ,
2020-10-14 13:42:00 +00:00
}
}
2023-02-06 19:06:02 +00:00
func ( s * remoteProtocolSelector ) Current ( ) Protocol {
2020-10-14 13:42:00 +00:00
s . lock . Lock ( )
defer s . lock . Unlock ( )
if time . Now ( ) . Before ( s . refreshAfter ) {
return s . current
}
2021-10-11 10:31:05 +00:00
protocol , err := getProtocol ( s . protocolPool , s . fetchFunc , s . switchThreshold )
2020-10-14 13:42:00 +00:00
if err != nil {
2020-12-28 18:10:01 +00:00
s . log . Err ( err ) . Msg ( "Failed to refresh protocol" )
2020-10-14 13:42:00 +00:00
return s . current
}
2021-10-11 10:31:05 +00:00
s . current = protocol
2020-10-14 13:42:00 +00:00
s . refreshAfter = time . Now ( ) . Add ( s . ttl )
return s . current
}
2023-02-06 19:06:02 +00:00
func ( s * remoteProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
s . lock . RLock ( )
defer s . lock . RUnlock ( )
return s . current . fallback ( )
}
func getProtocol ( protocolPool [ ] Protocol , fetchFunc edgediscovery . PercentageFetcher , switchThreshold int32 ) ( Protocol , error ) {
2021-10-11 10:31:05 +00:00
protocolPercentages , err := fetchFunc ( )
if err != nil {
return 0 , err
}
for _ , protocol := range protocolPool {
protocolPercentage := protocolPercentages . GetPercentage ( protocol . String ( ) )
if protocolPercentage > switchThreshold {
return protocol , nil
}
}
2023-02-07 04:05:48 +00:00
// Default to first index in protocolPool list
return protocolPool [ 0 ] , nil
2021-10-11 10:31:05 +00:00
}
2023-02-06 19:06:02 +00:00
// defaultProtocolSelector will allow for a protocol to have a fallback
type defaultProtocolSelector struct {
lock sync . RWMutex
current Protocol
}
func newDefaultProtocolSelector (
current Protocol ,
) * defaultProtocolSelector {
return & defaultProtocolSelector {
current : current ,
}
}
func ( s * defaultProtocolSelector ) Current ( ) Protocol {
s . lock . Lock ( )
defer s . lock . Unlock ( )
return s . current
}
func ( s * defaultProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
2020-10-14 13:42:00 +00:00
s . lock . RLock ( )
defer s . lock . RUnlock ( )
return s . current . fallback ( )
}
2020-11-25 06:55:13 +00:00
func NewProtocolSelector (
protocolFlag string ,
2023-02-06 19:06:02 +00:00
accountTag string ,
tunnelTokenProvided bool ,
2022-08-24 12:33:10 +00:00
needPQ bool ,
2023-02-06 19:06:02 +00:00
protocolFetcher edgediscovery . PercentageFetcher ,
resolveTTL time . Duration ,
log * zerolog . Logger ,
2020-11-25 06:55:13 +00:00
) ( ProtocolSelector , error ) {
2023-02-06 19:06:02 +00:00
// With --post-quantum, we force quic
if needPQ {
2020-10-14 13:42:00 +00:00
return & staticProtocolSelector {
2023-02-06 19:06:02 +00:00
current : QUIC ,
2020-10-14 13:42:00 +00:00
} , nil
}
2021-01-21 15:23:18 +00:00
2023-02-06 19:06:02 +00:00
// When a --token is provided, we want to start with QUIC but have fallback to HTTP2
if tunnelTokenProvided {
return newDefaultProtocolSelector ( QUIC ) , nil
2021-10-11 10:31:05 +00:00
}
2023-02-06 19:06:02 +00:00
threshold := switchThreshold ( accountTag )
fetchedProtocol , err := getProtocol ( ProtocolList , protocolFetcher , threshold )
2023-02-07 04:05:48 +00:00
log . Debug ( ) . Msgf ( "Fetched protocol: %s" , fetchedProtocol )
2023-02-06 19:06:02 +00:00
if err != nil {
log . Warn ( ) . Msg ( "Unable to lookup protocol percentage." )
// Falling through here since 'auto' is handled in the switch and failing
// to do the protocol lookup isn't a failure since it can be triggered again
// after the TTL.
}
2021-01-21 15:23:18 +00:00
2022-01-05 17:58:49 +00:00
// If the user picks a protocol, then we stick to it no matter what.
switch protocolFlag {
2023-02-06 19:06:02 +00:00
case "h2mux" :
// Any users still requesting h2mux will be upgraded to http2 instead
log . Warn ( ) . Msg ( "h2mux is no longer a supported protocol: upgrading edge connection to http2. Please remove '--protocol h2mux' from runtime arguments to remove this warning." )
return & staticProtocolSelector { current : HTTP2 } , nil
2022-01-05 17:58:49 +00:00
case QUIC . String ( ) :
return & staticProtocolSelector { current : QUIC } , nil
case HTTP2 . String ( ) :
return & staticProtocolSelector { current : HTTP2 } , nil
2023-02-06 19:06:02 +00:00
case AutoSelectFlag :
return newRemoteProtocolSelector ( fetchedProtocol , ProtocolList , threshold , protocolFetcher , resolveTTL , log ) , nil
2021-10-11 10:31:05 +00:00
}
2022-01-05 17:58:49 +00:00
return nil , fmt . Errorf ( "Unknown protocol %s, %s" , protocolFlag , AvailableProtocolFlagMessage )
2020-10-14 13:42:00 +00:00
}
func switchThreshold ( accountTag string ) int32 {
h := fnv . New32a ( )
2020-11-25 06:55:13 +00:00
_ , _ = h . Write ( [ ] byte ( accountTag ) )
2020-10-14 13:42:00 +00:00
return int32 ( h . Sum32 ( ) % 100 )
}