Skip to content

Commit 69fd502

Browse files
committed
TUN-3581: Tunnels can be run by name using only --credentials-file, no
origin cert necessary.
1 parent fcc393e commit 69fd502

File tree

11 files changed

+338
-90
lines changed

11 files changed

+338
-90
lines changed

cmd/cloudflared/linux_service.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
1414
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
15+
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
1516
"github.com/cloudflare/cloudflared/logger"
1617
)
1718

@@ -250,7 +251,7 @@ func installLinuxService(c *cli.Context) error {
250251
val, err := src.String(s)
251252
return err == nil && val != ""
252253
}
253-
if src.TunnelID == "" || !configPresent("credentials-file") {
254+
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
254255
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
255256
tunnel: TUNNEL-UUID
256257
credentials-file: CREDENTIALS-FILE
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package tunnel
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
8+
"github.com/cloudflare/cloudflared/logger"
9+
"github.com/google/uuid"
10+
"github.com/urfave/cli/v2"
11+
)
12+
13+
// CredFinder can find the tunnel credentials file.
14+
type CredFinder interface {
15+
Path() (string, error)
16+
}
17+
18+
// Implements CredFinder and looks for the credentials file at the given
19+
// filepath.
20+
type staticPath struct {
21+
filePath string
22+
fs fileSystem
23+
}
24+
25+
func newStaticPath(filePath string, fs fileSystem) CredFinder {
26+
return staticPath{
27+
filePath: filePath,
28+
fs: fs,
29+
}
30+
}
31+
32+
func (a staticPath) Path() (string, error) {
33+
if a.filePath != "" && a.fs.validFilePath(a.filePath) {
34+
return a.filePath, nil
35+
}
36+
return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath)
37+
}
38+
39+
// Implements CredFinder and looks for the credentials file in several directories
40+
// searching for a file named <id>.json
41+
type searchByID struct {
42+
id uuid.UUID
43+
c *cli.Context
44+
logger logger.Service
45+
fs fileSystem
46+
}
47+
48+
func newSearchByID(id uuid.UUID, c *cli.Context, logger logger.Service, fs fileSystem) CredFinder {
49+
return searchByID{
50+
id: id,
51+
c: c,
52+
logger: logger,
53+
fs: fs,
54+
}
55+
}
56+
57+
func (s searchByID) Path() (string, error) {
58+
59+
// Fallback to look for tunnel credentials in the origin cert directory
60+
if originCertPath, err := findOriginCert(s.c, s.logger); err == nil {
61+
originCertDir := filepath.Dir(originCertPath)
62+
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
63+
if s.fs.validFilePath(filePath) {
64+
return filePath, nil
65+
}
66+
}
67+
}
68+
69+
// Last resort look under default config directories
70+
for _, configDir := range config.DefaultConfigSearchDirectories() {
71+
if filePath, err := tunnelFilePath(s.id, configDir); err == nil {
72+
if s.fs.validFilePath(filePath) {
73+
return filePath, nil
74+
}
75+
}
76+
}
77+
return "", fmt.Errorf("Tunnel credentials file not found")
78+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package tunnel
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
)
7+
8+
// Abstract away details of reading files, so that SubcommandContext can read
9+
// from either the real filesystem, or a mock (when running unit tests).
10+
type fileSystem interface {
11+
readFile(filePath string) ([]byte, error)
12+
validFilePath(path string) bool
13+
}
14+
15+
type realFileSystem struct{}
16+
17+
func (fs realFileSystem) validFilePath(path string) bool {
18+
fileStat, err := os.Stat(path)
19+
if err != nil {
20+
return false
21+
}
22+
return !fileStat.IsDir()
23+
}
24+
25+
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
26+
return ioutil.ReadFile(filePath)
27+
}

cmd/cloudflared/tunnel/subcommand_context.go

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,16 @@ package tunnel
33
import (
44
"encoding/json"
55
"fmt"
6-
"io/ioutil"
76
"os"
8-
"path/filepath"
97
"strings"
108

119
"github.com/google/uuid"
1210
"github.com/pkg/errors"
1311
"github.com/urfave/cli/v2"
1412

1513
"github.com/cloudflare/cloudflared/certutil"
16-
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
1714
"github.com/cloudflare/cloudflared/connection"
1815
"github.com/cloudflare/cloudflared/logger"
19-
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
2016
"github.com/cloudflare/cloudflared/tunnelstore"
2117
)
2218

@@ -32,14 +28,14 @@ func (e errInvalidJSONCredential) Error() string {
3228
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
3329
// pass between subcommands, and make sure they are only initialized once
3430
type subcommandContext struct {
35-
c *cli.Context
36-
logger logger.Service
31+
c *cli.Context
32+
logger logger.Service
33+
isUIEnabled bool
34+
fs fileSystem
3735

3836
// These fields should be accessed using their respective Getter
3937
tunnelstoreClient tunnelstore.Client
4038
userCredential *userCredential
41-
42-
isUIEnabled bool
4339
}
4440

4541
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
@@ -55,9 +51,18 @@ func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
5551
c: c,
5652
logger: logger,
5753
isUIEnabled: isUIEnabled,
54+
fs: realFileSystem{},
5855
}, nil
5956
}
6057

58+
// Returns something that can find the given tunnel's credentials file.
59+
func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
60+
if path := sc.c.String(CredFileFlag); path != "" {
61+
return newStaticPath(path, sc.fs)
62+
}
63+
return newSearchByID(tunnelID, sc.c, sc.logger, sc.fs)
64+
}
65+
6166
type userCredential struct {
6267
cert *certutil.OriginCert
6368
certPath string
@@ -108,56 +113,27 @@ func (sc *subcommandContext) credential() (*userCredential, error) {
108113
return sc.userCredential, nil
109114
}
110115

111-
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
112-
filePath, err := sc.tunnelCredentialsPath(tunnelID)
116+
func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) {
117+
filePath, err := credFinder.Path()
113118
if err != nil {
114-
return nil, err
119+
return connection.Credentials{}, err
115120
}
116-
body, err := ioutil.ReadFile(filePath)
121+
body, err := sc.fs.readFile(filePath)
117122
if err != nil {
118-
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
123+
return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
119124
}
120125

121-
var auth pogs.TunnelAuth
122-
if err = json.Unmarshal(body, &auth); err != nil {
126+
var credentials connection.Credentials
127+
if err = json.Unmarshal(body, &credentials); err != nil {
123128
if strings.HasSuffix(filePath, ".pem") {
124-
return nil, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. "+
125-
"The tunnel credentials file was originally created by `cloudflared tunnel create` and named %s.json."+
126-
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel "+
127-
"login`.", tunnelID)
129+
return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " +
130+
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
131+
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
132+
"login`.")
128133
}
129-
return nil, errInvalidJSONCredential{path: filePath, err: err}
134+
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err}
130135
}
131-
return &auth, nil
132-
}
133-
134-
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
135-
if filePath := sc.c.String("credentials-file"); filePath != "" {
136-
if validFilePath(filePath) {
137-
return filePath, nil
138-
}
139-
return "", fmt.Errorf("Tunnel credentials file %s doesn't exist or is not a file", filePath)
140-
}
141-
142-
// Fallback to look for tunnel credentials in the origin cert directory
143-
if originCertPath, err := findOriginCert(sc.c, sc.logger); err == nil {
144-
originCertDir := filepath.Dir(originCertPath)
145-
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
146-
if validFilePath(filePath) {
147-
return filePath, nil
148-
}
149-
}
150-
}
151-
152-
// Last resort look under default config directories
153-
for _, configDir := range config.DefaultConfigSearchDirectories() {
154-
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
155-
if validFilePath(filePath) {
156-
return filePath, nil
157-
}
158-
}
159-
}
160-
return "", fmt.Errorf("Tunnel credentials file not found")
136+
return credentials, nil
161137
}
162138

163139
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
@@ -180,7 +156,14 @@ func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
180156
if err != nil {
181157
return nil, err
182158
}
183-
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
159+
tunnelCredentials := connection.Credentials{
160+
AccountTag: credential.cert.AccountID,
161+
TunnelSecret: tunnelSecret,
162+
TunnelID: tunnel.ID,
163+
TunnelName: name,
164+
}
165+
filePath, writeFileErr := writeTunnelCredentials(credential.certPath, &tunnelCredentials)
166+
if err != nil {
184167
var errorLines []string
185168
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
186169
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
@@ -193,6 +176,7 @@ func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
193176
errorMsg := strings.Join(errorLines, "\n")
194177
return nil, errors.New(errorMsg)
195178
}
179+
sc.logger.Infof("Tunnel credentials written to %v. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.", filePath)
196180

197181
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
198182
return nil, renderOutput(outputFormat, &tunnel)
@@ -243,7 +227,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
243227
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
244228
}
245229

246-
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
230+
credFinder := sc.credentialFinder(id)
231+
tunnelCredentialsPath, err := credFinder.Path()
247232
if err != nil {
248233
sc.logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
249234
return nil
@@ -256,22 +241,34 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
256241
return nil
257242
}
258243

244+
// findCredentials will choose the right way to find the credentials file, find it,
245+
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
246+
// field to credentials files)
247+
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
248+
credFinder := sc.credentialFinder(tunnelID)
249+
credentials, err := sc.readTunnelCredentials(credFinder)
250+
// This line ensures backwards compatibility with credentials files generated before
251+
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
252+
// with the ID, which we have already resolved from the user input.
253+
credentials.TunnelID = tunnelID
254+
return credentials, err
255+
}
256+
259257
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
260-
credentials, err := sc.readTunnelCredentials(tunnelID)
258+
credentials, err := sc.findCredentials(tunnelID)
261259
if err != nil {
262260
if e, ok := err.(errInvalidJSONCredential); ok {
263261
sc.logger.Errorf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
264262
sc.logger.Errorf("Invalid JSON when parsing credentials file: %s", e.err.Error())
265263
}
266264
return err
267265
}
268-
269266
return StartServer(
270267
sc.c,
271268
version,
272269
shutdownC,
273270
graceShutdownC,
274-
&connection.NamedTunnelConfig{Auth: *credentials, ID: tunnelID},
271+
&connection.NamedTunnelConfig{Credentials: credentials},
275272
sc.logger,
276273
sc.isUIEnabled,
277274
)
@@ -300,6 +297,7 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
300297
return client.RouteTunnel(tunnelID, r)
301298
}
302299

300+
// Query Tunnelstore to find the active tunnel with the given name.
303301
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
304302
filter := tunnelstore.NewFilter()
305303
filter.NoDeleted()
@@ -322,6 +320,15 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
322320
return u, nil
323321
}
324322

323+
// Look up name in the credentials file.
324+
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
325+
if credentials, err := sc.readTunnelCredentials(credFinder); err == nil {
326+
if credentials.TunnelID != uuid.Nil && input == credentials.TunnelName {
327+
return credentials.TunnelID, nil
328+
}
329+
}
330+
331+
// Fall back to querying Tunnelstore.
325332
if tunnel, found, err := sc.tunnelActive(input); err != nil {
326333
return uuid.Nil, err
327334
} else if found {

0 commit comments

Comments
 (0)