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 (
2021-11-18 14:30:22 +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; 'h2mux' - Cloudflare's implementation of HTTP/2, deprecated"
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-01-05 17:58:49 +00:00
autoSelectFlag = "auto"
2020-10-14 13:42:00 +00:00
)
var (
2021-08-17 14:30:02 +00:00
// ProtocolList represents a list of supported protocols for communication with the edge.
2021-10-11 10:31:05 +00:00
ProtocolList = [ ] Protocol { H2mux , HTTP2 , HTTP2Warp , QUIC , QUICWarp }
2020-10-14 13:42:00 +00:00
)
type Protocol int64
const (
2021-08-17 14:30:02 +00:00
// H2mux protocol can be used both with Classic and Named Tunnels. .
2020-10-14 13:42:00 +00:00
H2mux Protocol = iota
2021-08-17 14:30:02 +00:00
// HTTP2 is used only with named tunnels. It's more efficient than H2mux for L4 proxying.
2020-10-14 13:42:00 +00:00
HTTP2
2021-08-17 14:30:02 +00:00
// QUIC is used only with named tunnels.
QUIC
2021-10-11 10:31:05 +00:00
// HTTP2Warp is used only with named tunnels. It's useful for warp-routing where we don't want to fallback to
// H2mux on HTTP2 failure to connect.
HTTP2Warp
//QUICWarp is used only with named tunnels. It's useful for warp-routing where we want to fallback to HTTP2 but
2021-11-12 15:38:06 +00:00
// don't want HTTP2 to fallback to H2mux
2021-10-11 10:31:05 +00:00
QUICWarp
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 H2mux :
return 0 , false
case HTTP2 :
return H2mux , true
2021-10-11 10:31:05 +00:00
case HTTP2Warp :
return 0 , false
2021-08-17 14:30:02 +00:00
case QUIC :
return HTTP2 , true
2021-10-11 10:31:05 +00:00
case QUICWarp :
return HTTP2Warp , true
2020-10-14 13:42:00 +00:00
default :
return 0 , false
}
}
func ( p Protocol ) String ( ) string {
switch p {
case H2mux :
return "h2mux"
2021-10-11 10:31:05 +00:00
case HTTP2 , HTTP2Warp :
2020-10-14 13:42:00 +00:00
return "http2"
2021-10-11 10:31:05 +00:00
case QUIC , QUICWarp :
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 {
case H2mux :
return & TLSSettings {
ServerName : edgeH2muxTLSServerName ,
}
2021-10-11 10:31:05 +00:00
case HTTP2 , HTTP2Warp :
2021-08-17 14:30:02 +00:00
return & TLSSettings {
ServerName : edgeH2TLSServerName ,
}
2021-10-11 10:31:05 +00:00
case QUIC , QUICWarp :
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 )
}
type staticProtocolSelector struct {
current Protocol
}
func ( s * staticProtocolSelector ) Current ( ) Protocol {
return s . current
}
func ( s * staticProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
return 0 , false
}
type autoProtocolSelector 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
fetchFunc PercentageFetcher
refreshAfter time . Time
ttl time . Duration
log * zerolog . Logger
2020-10-14 13:42:00 +00:00
}
func newAutoProtocolSelector (
current Protocol ,
2021-10-11 10:31:05 +00:00
protocolPool [ ] Protocol ,
switchThreshold int32 ,
2020-10-14 13:42:00 +00:00
fetchFunc PercentageFetcher ,
ttl time . Duration ,
2020-11-25 06:55:13 +00:00
log * zerolog . Logger ,
2020-10-14 13:42:00 +00:00
) * autoProtocolSelector {
return & autoProtocolSelector {
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
}
}
func ( s * autoProtocolSelector ) Current ( ) Protocol {
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
}
2021-10-11 10:31:05 +00:00
func getProtocol ( protocolPool [ ] Protocol , fetchFunc PercentageFetcher , switchThreshold int32 ) ( Protocol , error ) {
protocolPercentages , err := fetchFunc ( )
if err != nil {
return 0 , err
}
for _ , protocol := range protocolPool {
protocolPercentage := protocolPercentages . GetPercentage ( protocol . String ( ) )
if protocolPercentage > switchThreshold {
return protocol , nil
}
}
return protocolPool [ len ( protocolPool ) - 1 ] , nil
}
2020-10-14 13:42:00 +00:00
func ( s * autoProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
s . lock . RLock ( )
defer s . lock . RUnlock ( )
return s . current . fallback ( )
}
2021-10-11 10:31:05 +00:00
type PercentageFetcher func ( ) ( edgediscovery . ProtocolPercents , error )
2020-10-14 13:42:00 +00:00
2020-11-25 06:55:13 +00:00
func NewProtocolSelector (
protocolFlag string ,
2021-01-21 15:23:18 +00:00
warpRoutingEnabled bool ,
2020-11-25 06:55:13 +00:00
namedTunnel * NamedTunnelConfig ,
fetchFunc PercentageFetcher ,
ttl time . Duration ,
log * zerolog . Logger ,
) ( ProtocolSelector , error ) {
2021-01-21 15:23:18 +00:00
// Classic tunnel is only supported with h2mux
2020-10-14 13:42:00 +00:00
if namedTunnel == nil {
return & staticProtocolSelector {
current : H2mux ,
} , nil
}
2021-01-21 15:23:18 +00:00
2021-10-11 10:31:05 +00:00
threshold := switchThreshold ( namedTunnel . Credentials . AccountTag )
fetchedProtocol , err := getProtocol ( [ ] Protocol { QUIC , HTTP2 } , fetchFunc , threshold )
if err != nil {
log . Err ( err ) . Msg ( "Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command." )
2021-10-13 18:06:31 +00:00
return & staticProtocolSelector {
current : HTTP2 ,
} , nil
}
2021-10-11 10:31:05 +00:00
if warpRoutingEnabled {
if protocolFlag == H2mux . String ( ) || fetchedProtocol == H2mux {
log . Warn ( ) . Msg ( "Warp routing is not supported in h2mux protocol. Upgrading to http2 to allow it." )
protocolFlag = HTTP2 . String ( )
fetchedProtocol = HTTP2Warp
}
return selectWarpRoutingProtocols ( protocolFlag , fetchFunc , ttl , log , threshold , fetchedProtocol )
}
return selectNamedTunnelProtocols ( protocolFlag , fetchFunc , ttl , log , threshold , fetchedProtocol )
}
2021-01-21 15:23:18 +00:00
2021-10-11 10:31:05 +00:00
func selectNamedTunnelProtocols (
protocolFlag string ,
fetchFunc PercentageFetcher ,
ttl time . Duration ,
log * zerolog . Logger ,
threshold int32 ,
protocol Protocol ,
) ( ProtocolSelector , error ) {
2022-01-05 17:58:49 +00:00
// If the user picks a protocol, then we stick to it no matter what.
switch protocolFlag {
case H2mux . String ( ) :
return & staticProtocolSelector { current : H2mux } , nil
case QUIC . String ( ) :
return & staticProtocolSelector { current : QUIC } , nil
case HTTP2 . String ( ) :
return & staticProtocolSelector { current : HTTP2 } , nil
2020-10-14 13:42:00 +00:00
}
2021-10-11 10:31:05 +00:00
2022-01-05 17:58:49 +00:00
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
// fallback on failures.
if protocolFlag == autoSelectFlag {
return newAutoProtocolSelector ( protocol , [ ] Protocol { QUIC , HTTP2 , H2mux } , threshold , fetchFunc , ttl , 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 )
2021-10-11 10:31:05 +00:00
}
func selectWarpRoutingProtocols (
protocolFlag string ,
fetchFunc PercentageFetcher ,
ttl time . Duration ,
log * zerolog . Logger ,
threshold int32 ,
protocol Protocol ,
) ( ProtocolSelector , error ) {
2022-01-05 17:58:49 +00:00
// If the user picks a protocol, then we stick to it no matter what.
switch protocolFlag {
case QUIC . String ( ) :
return & staticProtocolSelector { current : QUICWarp } , nil
case HTTP2 . String ( ) :
return & staticProtocolSelector { current : HTTP2Warp } , nil
2021-10-11 10:31:05 +00:00
}
2022-01-05 17:58:49 +00:00
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
// fallback on failures.
if protocolFlag == autoSelectFlag {
return newAutoProtocolSelector ( protocol , [ ] Protocol { QUICWarp , HTTP2Warp } , threshold , fetchFunc , ttl , 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 )
}