cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2026.5.2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

carrier/carrier.go

186lines · modecode

1// Package carrier provides a WebSocket proxy to carry or proxy a connection
2// from the local client to the edge. See it as a wrapper around any protocol
3// that it packages up in a WebSocket connection to the edge.
4package carrier
5
6import (
7 "crypto/tls"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "net/url"
13 "os"
14 "strings"
15
16 "github.com/pkg/errors"
17 "github.com/rs/zerolog"
18
19 "github.com/cloudflare/cloudflared/token"
20)
21
22const (
23 LogFieldOriginURL = "originURL"
24 CFAccessTokenHeader = "Cf-Access-Token"
25 cfJumpDestinationHeader = "Cf-Access-Jump-Destination"
26)
27
28type StartOptions struct {
29 AppInfo *token.AppInfo
30 OriginURL string
31 Headers http.Header
32 Host string
33 TLSClientConfig *tls.Config
34 AutoCloseInterstitial bool
35 IsFedramp bool
36}
37
38// Connection wraps up all the needed functions to forward over the tunnel
39type Connection interface {
40 // ServeStream is used to forward data from the client to the edge
41 ServeStream(*StartOptions, io.ReadWriter) error
42}
43
44// StdinoutStream is empty struct for wrapping stdin/stdout
45// into a single ReadWriter
46type StdinoutStream struct{}
47
48// Read will read from Stdin
49func (c *StdinoutStream) Read(p []byte) (int, error) {
50 return os.Stdin.Read(p)
51}
52
53// Write will write to Stdout
54func (c *StdinoutStream) Write(p []byte) (int, error) {
55 return os.Stdout.Write(p)
56}
57
58// Helper to allow deferring the response close with a check that the resp is not nil
59func closeRespBody(resp *http.Response) {
60 if resp != nil {
61 _ = resp.Body.Close()
62 }
63}
64
65// StartForwarder will setup a listener on a specified address/port and then
66// forward connections to the origin by calling `Serve()`.
67func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error {
68 listener, err := net.Listen("tcp", address)
69 if err != nil {
70 return errors.Wrap(err, "failed to start forwarding server")
71 }
72 return Serve(conn, listener, shutdownC, options)
73}
74
75// StartClient will copy the data from stdin/stdout over a WebSocket connection
76// to the edge (originURL)
77func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
78 return conn.ServeStream(options, stream)
79}
80
81// Serve accepts incoming connections on the specified net.Listener.
82// Each connection is handled in a new goroutine: its data is copied over a
83// WebSocket connection to the edge (originURL).
84// `Serve` always closes `listener`.
85func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
86 defer listener.Close()
87 errChan := make(chan error)
88
89 go func() {
90 for {
91 conn, err := listener.Accept()
92 if err != nil {
93 // don't block if parent goroutine quit early
94 select {
95 case errChan <- err:
96 default:
97 }
98 return
99 }
100 go serveConnection(remoteConn, conn, options)
101 }
102 }()
103
104 select {
105 case <-shutdownC:
106 return nil
107 case err := <-errChan:
108 return err
109 }
110}
111
112// serveConnection handles connections for the Serve() call
113func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
114 defer c.Close()
115 _ = remoteConn.ServeStream(options, c)
116}
117
118// IsAccessResponse checks the http Response to see if the url location
119// contains the Access structure.
120func IsAccessResponse(resp *http.Response) bool {
121 if resp == nil || resp.StatusCode != http.StatusFound {
122 return false
123 }
124
125 location, err := resp.Location()
126 if err != nil || location == nil {
127 return false
128 }
129 if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) {
130 return true
131 }
132
133 return false
134}
135
136// BuildAccessRequest builds an HTTP request with the Access token set
137func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Request, error) {
138 req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
139 if err != nil {
140 return nil, err
141 }
142
143 token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, log)
144 if err != nil {
145 return nil, err
146 }
147
148 // We need to create a new request as FetchToken will modify req (boo mutable)
149 // as it has to follow redirect on the API and such, so here we init a new one
150 originRequest, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
151 if err != nil {
152 return nil, err
153 }
154 originRequest.Header.Set(CFAccessTokenHeader, token)
155
156 for k, v := range options.Headers {
157 if len(v) >= 1 {
158 originRequest.Header.Set(k, v[0])
159 }
160 }
161
162 return originRequest, nil
163}
164
165func SetBastionDest(header http.Header, destination string) {
166 if destination != "" {
167 header.Set(cfJumpDestinationHeader, destination)
168 }
169}
170
171func ResolveBastionDest(r *http.Request) (string, error) {
172 jumpDestination := r.Header.Get(cfJumpDestinationHeader)
173 if jumpDestination == "" {
174 return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side")
175 }
176 // Strip scheme and path set by client. Without a scheme
177 // Parsing a hostname and path without scheme might not return an error due to parsing ambiguities
178 if jumpURL, err := url.Parse(jumpDestination); err == nil && jumpURL.Host != "" {
179 return removePath(jumpURL.Host), nil
180 }
181 return removePath(jumpDestination), nil
182}
183
184func removePath(dest string) string {
185 return strings.SplitN(dest, "/", 2)[0]
186}
187