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"
2020-10-14 13:42:00 +00:00
)
const (
AvailableProtocolFlagMessage = "Available protocols: http2 - Go's implementation, h2mux - Cloudflare's implementation of HTTP/2, and auto - automatically select between http2 and h2mux"
// 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"
2020-10-14 13:42:00 +00:00
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
explicitHTTP2FallbackThreshold = - 1
autoSelectFlag = "auto"
)
var (
2021-08-17 14:30:02 +00:00
// ProtocolList represents a list of supported protocols for communication with the edge.
ProtocolList = [ ] Protocol { H2mux , HTTP2 , QUIC }
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
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-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 {
case H2mux :
return "h2mux"
case HTTP2 :
return "http2"
2021-08-17 14:30:02 +00:00
case QUIC :
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 ,
}
case HTTP2 :
return & TLSSettings {
ServerName : edgeH2TLSServerName ,
}
case QUIC :
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 {
lock sync . RWMutex
current Protocol
switchThrehold int32
fetchFunc PercentageFetcher
refreshAfter time . Time
ttl time . Duration
2020-11-25 06:55:13 +00:00
log * zerolog . Logger
2020-10-14 13:42:00 +00:00
}
func newAutoProtocolSelector (
current Protocol ,
switchThrehold int32 ,
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 {
current : current ,
switchThrehold : switchThrehold ,
fetchFunc : fetchFunc ,
refreshAfter : time . Now ( ) . Add ( ttl ) ,
ttl : ttl ,
2020-11-25 06:55:13 +00:00
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
}
percentage , err := s . fetchFunc ( )
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
}
if s . switchThrehold < percentage {
s . current = HTTP2
} else {
s . current = H2mux
}
s . refreshAfter = time . Now ( ) . Add ( s . ttl )
return s . current
}
func ( s * autoProtocolSelector ) Fallback ( ) ( Protocol , bool ) {
s . lock . RLock ( )
defer s . lock . RUnlock ( )
return s . current . fallback ( )
}
type PercentageFetcher func ( ) ( int32 , error )
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-08-26 09:20:10 +00:00
// warp routing cannot be served over h2mux connections
2021-01-21 15:23:18 +00:00
if warpRoutingEnabled {
if protocolFlag == H2mux . String ( ) {
2021-08-26 09:20:10 +00:00
log . Warn ( ) . Msg ( "Warp routing is not supported in h2mux protocol. Upgrading to http2 to allow it." )
}
if protocolFlag == QUIC . String ( ) {
return & staticProtocolSelector {
current : QUIC ,
} , nil
2021-01-21 15:23:18 +00:00
}
return & staticProtocolSelector {
current : HTTP2 ,
} , nil
}
2020-10-14 13:42:00 +00:00
if protocolFlag == H2mux . String ( ) {
return & staticProtocolSelector {
current : H2mux ,
} , nil
}
2021-08-26 09:20:10 +00:00
if protocolFlag == QUIC . String ( ) {
return newAutoProtocolSelector ( QUIC , explicitHTTP2FallbackThreshold , fetchFunc , ttl , log ) , nil
}
2020-10-14 13:42:00 +00:00
http2Percentage , err := fetchFunc ( )
if err != nil {
2021-07-28 22:31:22 +00:00
log . Err ( err ) . Msg ( "Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command." )
return & staticProtocolSelector {
current : HTTP2 ,
} , nil
2020-10-14 13:42:00 +00:00
}
if protocolFlag == HTTP2 . String ( ) {
if http2Percentage < 0 {
2020-11-25 06:55:13 +00:00
return newAutoProtocolSelector ( H2mux , explicitHTTP2FallbackThreshold , fetchFunc , ttl , log ) , nil
2020-10-14 13:42:00 +00:00
}
2020-11-25 06:55:13 +00:00
return newAutoProtocolSelector ( HTTP2 , explicitHTTP2FallbackThreshold , fetchFunc , ttl , log ) , nil
2020-10-14 13:42:00 +00:00
}
if protocolFlag != autoSelectFlag {
return nil , fmt . Errorf ( "Unknown protocol %s, %s" , protocolFlag , AvailableProtocolFlagMessage )
}
2020-11-23 21:36:16 +00:00
threshold := switchThreshold ( namedTunnel . Credentials . AccountTag )
2020-10-14 13:42:00 +00:00
if threshold < http2Percentage {
2020-11-25 06:55:13 +00:00
return newAutoProtocolSelector ( HTTP2 , threshold , fetchFunc , ttl , log ) , nil
2020-10-14 13:42:00 +00:00
}
2020-11-25 06:55:13 +00:00
return newAutoProtocolSelector ( H2mux , threshold , fetchFunc , ttl , log ) , nil
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 )
}