mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-29 22:56:35 +00:00

The current supervisor serves the quic connection by performing all of the following in one method: 1. Dial QUIC edge connection 2. Initialize datagram muxer for UDP sessions and ICMP 3. Wrap all together in a single struct to serve the process loops In an effort to better support modularity, each of these steps were broken out into their own separate methods that the supervisor will compose together to create the TunnelConnection and run its `Serve` method. This also provides us with the capability to better interchange the functionality supported by the datagram session manager in the future with a new mechanism. Closes TUN-8661
296 lines
7.6 KiB
Go
296 lines
7.6 KiB
Go
package connection
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/cloudflare/cloudflared/tracing"
|
|
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
|
"github.com/cloudflare/cloudflared/websocket"
|
|
)
|
|
|
|
const (
|
|
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
|
LogFieldConnIndex = "connIndex"
|
|
MaxGracePeriod = time.Minute * 3
|
|
MaxConcurrentStreams = math.MaxUint32
|
|
|
|
contentTypeHeader = "content-type"
|
|
sseContentType = "text/event-stream"
|
|
grpcContentType = "application/grpc"
|
|
)
|
|
|
|
var (
|
|
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
|
flushableContentTypes = []string{sseContentType, grpcContentType}
|
|
)
|
|
|
|
// TunnelConnection represents the connection to the edge.
|
|
// The Serve method is provided to allow clients to handle any errors from the connection encountered during
|
|
// processing of the connection. Cancelling of the context provided to Serve will close the connection.
|
|
type TunnelConnection interface {
|
|
Serve(ctx context.Context) error
|
|
}
|
|
|
|
type Orchestrator interface {
|
|
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
|
GetConfigJSON() ([]byte, error)
|
|
GetOriginProxy() (OriginProxy, error)
|
|
}
|
|
|
|
type TunnelProperties struct {
|
|
Credentials Credentials
|
|
Client pogs.ClientInfo
|
|
QuickTunnelUrl string
|
|
}
|
|
|
|
// Credentials are stored in the credentials file and contain all info needed to run a tunnel.
|
|
type Credentials struct {
|
|
AccountTag string
|
|
TunnelSecret []byte
|
|
TunnelID uuid.UUID
|
|
}
|
|
|
|
func (c *Credentials) Auth() pogs.TunnelAuth {
|
|
return pogs.TunnelAuth{
|
|
AccountTag: c.AccountTag,
|
|
TunnelSecret: c.TunnelSecret,
|
|
}
|
|
}
|
|
|
|
// TunnelToken are Credentials but encoded with custom fields namings.
|
|
type TunnelToken struct {
|
|
AccountTag string `json:"a"`
|
|
TunnelSecret []byte `json:"s"`
|
|
TunnelID uuid.UUID `json:"t"`
|
|
}
|
|
|
|
func (t TunnelToken) Credentials() Credentials {
|
|
return Credentials{
|
|
AccountTag: t.AccountTag,
|
|
TunnelSecret: t.TunnelSecret,
|
|
TunnelID: t.TunnelID,
|
|
}
|
|
}
|
|
|
|
func (t TunnelToken) Encode() (string, error) {
|
|
val, err := json.Marshal(t)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "could not JSON encode token")
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(val), nil
|
|
}
|
|
|
|
type ClassicTunnelProperties struct {
|
|
Hostname string
|
|
OriginCert []byte
|
|
// feature-flag to use new edge reconnect tokens
|
|
UseReconnectToken bool
|
|
}
|
|
|
|
// Type indicates the connection type of the connection.
|
|
type Type int
|
|
|
|
const (
|
|
TypeWebsocket Type = iota
|
|
TypeTCP
|
|
TypeControlStream
|
|
TypeHTTP
|
|
TypeConfiguration
|
|
)
|
|
|
|
// ShouldFlush returns whether this kind of connection should actively flush data
|
|
func (t Type) shouldFlush() bool {
|
|
switch t {
|
|
case TypeWebsocket, TypeTCP, TypeControlStream:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (t Type) String() string {
|
|
switch t {
|
|
case TypeWebsocket:
|
|
return "websocket"
|
|
case TypeTCP:
|
|
return "tcp"
|
|
case TypeControlStream:
|
|
return "control stream"
|
|
case TypeHTTP:
|
|
return "http"
|
|
default:
|
|
return fmt.Sprintf("Unknown Type %d", t)
|
|
}
|
|
}
|
|
|
|
// OriginProxy is how data flows from cloudflared to the origin services running behind it.
|
|
type OriginProxy interface {
|
|
ProxyHTTP(w ResponseWriter, tr *tracing.TracedHTTPRequest, isWebsocket bool) error
|
|
ProxyTCP(ctx context.Context, rwa ReadWriteAcker, req *TCPRequest) error
|
|
}
|
|
|
|
// TCPRequest defines the input format needed to perform a TCP proxy.
|
|
type TCPRequest struct {
|
|
Dest string
|
|
CFRay string
|
|
LBProbe bool
|
|
FlowID string
|
|
CfTraceID string
|
|
ConnIndex uint8
|
|
}
|
|
|
|
// ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has
|
|
// accepted the connection.
|
|
type ReadWriteAcker interface {
|
|
io.ReadWriter
|
|
AckConnection(tracePropagation string) error
|
|
}
|
|
|
|
// HTTPResponseReadWriteAcker is an HTTP implementation of ReadWriteAcker.
|
|
type HTTPResponseReadWriteAcker struct {
|
|
r io.Reader
|
|
w ResponseWriter
|
|
f http.Flusher
|
|
req *http.Request
|
|
}
|
|
|
|
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
|
func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker {
|
|
return &HTTPResponseReadWriteAcker{
|
|
r: req.Body,
|
|
w: w,
|
|
f: flusher,
|
|
req: req,
|
|
}
|
|
}
|
|
|
|
func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
|
return h.r.Read(p)
|
|
}
|
|
|
|
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
|
n, err := h.w.Write(p)
|
|
if n > 0 {
|
|
h.f.Flush()
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
|
// upgrade to streams.
|
|
func (h *HTTPResponseReadWriteAcker) AckConnection(tracePropagation string) error {
|
|
resp := &http.Response{
|
|
Status: switchingProtocolText,
|
|
StatusCode: http.StatusSwitchingProtocols,
|
|
ContentLength: -1,
|
|
Header: http.Header{},
|
|
}
|
|
|
|
if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" {
|
|
resp.Header = websocket.NewResponseHeader(h.req)
|
|
}
|
|
|
|
if tracePropagation != "" {
|
|
resp.Header.Add(tracing.CanonicalCloudflaredTracingHeader, tracePropagation)
|
|
}
|
|
|
|
return h.w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
|
}
|
|
|
|
// localProxyConnection emulates an incoming connection to cloudflared as a net.Conn.
|
|
// Used when handling a "hijacked" connection from connection.ResponseWriter
|
|
type localProxyConnection struct {
|
|
io.ReadWriteCloser
|
|
}
|
|
|
|
func (c *localProxyConnection) Read(b []byte) (int, error) {
|
|
return c.ReadWriteCloser.Read(b)
|
|
}
|
|
|
|
func (c *localProxyConnection) Write(b []byte) (int, error) {
|
|
return c.ReadWriteCloser.Write(b)
|
|
}
|
|
|
|
func (c *localProxyConnection) Close() error {
|
|
return c.ReadWriteCloser.Close()
|
|
}
|
|
|
|
func (c *localProxyConnection) LocalAddr() net.Addr {
|
|
// Unused LocalAddr
|
|
return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""}
|
|
}
|
|
|
|
func (c *localProxyConnection) RemoteAddr() net.Addr {
|
|
// Unused RemoteAddr
|
|
return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""}
|
|
}
|
|
|
|
func (c *localProxyConnection) SetDeadline(t time.Time) error {
|
|
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
|
return nil
|
|
}
|
|
|
|
func (c *localProxyConnection) SetReadDeadline(t time.Time) error {
|
|
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
|
return nil
|
|
}
|
|
|
|
func (c *localProxyConnection) SetWriteDeadline(t time.Time) error {
|
|
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
|
return nil
|
|
}
|
|
|
|
// ResponseWriter is the response path for a request back through cloudflared's tunnel.
|
|
type ResponseWriter interface {
|
|
WriteRespHeaders(status int, header http.Header) error
|
|
AddTrailer(trailerName, trailerValue string)
|
|
http.ResponseWriter
|
|
http.Hijacker
|
|
io.Writer
|
|
}
|
|
|
|
type ConnectedFuse interface {
|
|
Connected()
|
|
IsConnected() bool
|
|
}
|
|
|
|
// Helper method to let the caller know what content-types should require a flush on every
|
|
// write to a ResponseWriter.
|
|
func shouldFlush(headers http.Header) bool {
|
|
if contentType := headers.Get(contentTypeHeader); contentType != "" {
|
|
contentType = strings.ToLower(contentType)
|
|
for _, c := range flushableContentTypes {
|
|
if strings.HasPrefix(contentType, c) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func uint8ToString(input uint8) string {
|
|
return strconv.FormatUint(uint64(input), 10)
|
|
}
|
|
|
|
func FindCfRayHeader(req *http.Request) string {
|
|
return req.Header.Get("Cf-Ray")
|
|
}
|
|
|
|
func IsLBProbeRequest(req *http.Request) bool {
|
|
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
|
}
|