Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Support ingress rule matching for bastion mode #1244

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ ifneq ($(TARGET_ARM), )
ARM_COMMAND := GOARM=$(TARGET_ARM)
endif

ifeq ($(TARGET_ARM), 7)
ifeq ($(TARGET_ARM), 7)
PACKAGE_ARCH := armhf
else
PACKAGE_ARCH := $(TARGET_ARCH)
Expand Down
13 changes: 9 additions & 4 deletions carrier/carrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog"

"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/token"
)

const (
LogFieldOriginURL = "originURL"
CFAccessTokenHeader = "Cf-Access-Token"
cfJumpDestinationHeader = "Cf-Access-Jump-Destination"
CFJumpDestinationHeader = "Cf-Access-Jump-Destination"
)

type StartOptions struct {
Expand Down Expand Up @@ -163,12 +164,16 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque

func SetBastionDest(header http.Header, destination string) {
if destination != "" {
header.Set(cfJumpDestinationHeader, destination)
header.Set(CFJumpDestinationHeader, destination)
}
}

func ResolveBastionDest(r *http.Request) (string, error) {
jumpDestination := r.Header.Get(cfJumpDestinationHeader)
func ResolveBastionDest(req *http.Request, bastionMode bool, service string) (string, error) {
jumpDestination := req.Header.Get(CFJumpDestinationHeader)
if bastionMode && service != config.BastionFlag {
jumpDestination = service
}

if jumpDestination == "" {
return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side")
}
Expand Down
51 changes: 40 additions & 11 deletions carrier/carrier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,77 +163,106 @@ func TestBastionDestination(t *testing.T) {
header http.Header
expectedDest string
wantErr bool
bastionMode bool
service string
}{
{
name: "hostname destination",
header: http.Header{
cfJumpDestinationHeader: []string{"localhost"},
CFJumpDestinationHeader: []string{"localhost"},
},
expectedDest: "localhost",
},
{
name: "hostname destination with port",
header: http.Header{
cfJumpDestinationHeader: []string{"localhost:9000"},
CFJumpDestinationHeader: []string{"localhost:9000"},
},
expectedDest: "localhost:9000",
},
{
name: "hostname destination with scheme and port",
header: http.Header{
cfJumpDestinationHeader: []string{"ssh://localhost:9000"},
CFJumpDestinationHeader: []string{"ssh://localhost:9000"},
},
expectedDest: "localhost:9000",
},
{
name: "full hostname url",
header: http.Header{
cfJumpDestinationHeader: []string{"ssh://localhost:9000/metrics"},
CFJumpDestinationHeader: []string{"ssh://localhost:9000/metrics"},
},
expectedDest: "localhost:9000",
},
{
name: "hostname destination with port and path",
header: http.Header{
cfJumpDestinationHeader: []string{"localhost:9000/metrics"},
CFJumpDestinationHeader: []string{"localhost:9000/metrics"},
},
expectedDest: "localhost:9000",
},
{
name: "ip destination",
header: http.Header{
cfJumpDestinationHeader: []string{"127.0.0.1"},
CFJumpDestinationHeader: []string{"127.0.0.1"},
},
expectedDest: "127.0.0.1",
},
{
name: "ip destination with port",
header: http.Header{
cfJumpDestinationHeader: []string{"127.0.0.1:9000"},
CFJumpDestinationHeader: []string{"127.0.0.1:9000"},
},
expectedDest: "127.0.0.1:9000",
},
{
name: "ip destination with port and path",
header: http.Header{
cfJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
CFJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
},
expectedDest: "127.0.0.1:9000",
},
{
name: "ip destination with schem and port",
header: http.Header{
cfJumpDestinationHeader: []string{"tcp://127.0.0.1:9000"},
CFJumpDestinationHeader: []string{"tcp://127.0.0.1:9000"},
},
expectedDest: "127.0.0.1:9000",
},
{
name: "full ip url",
header: http.Header{
cfJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"},
CFJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"},
},
expectedDest: "127.0.0.1:9000",
},
{
name: "full ip url with bastion mode",
header: http.Header{
CFJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"},
},
bastionMode: true,
service: "ssh://127.0.0.1:9002/metrics",
expectedDest: "127.0.0.1:9002",
},
{
name: "ip destination with port and path with bastion mode",
header: http.Header{
CFJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
},
bastionMode: true,
service: "127.0.0.1:9002/metrics",
expectedDest: "127.0.0.1:9002",
},
{
name: "ip destination with port and path without bastion mode",
header: http.Header{
CFJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
},
bastionMode: false,
service: "127.0.0.1:9002/metrics",
expectedDest: "127.0.0.1:9000",
},
{
name: "no destination",
wantErr: true,
Expand All @@ -243,7 +272,7 @@ func TestBastionDestination(t *testing.T) {
r := &http.Request{
Header: test.header,
}
dest, err := ResolveBastionDest(r)
dest, err := ResolveBastionDest(r, test.bastionMode, test.service)
if test.wantErr {
assert.Error(t, err, "Test %s expects error", test.name)
} else {
Expand Down
2 changes: 1 addition & 1 deletion cmd/cloudflared/tunnel/ingress_subcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func testURLCommand(c *cli.Context) error {
return errors.Wrap(err, "Validation failed")
}

_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path, "")
fmt.Printf("Matched rule #%d\n", i)
fmt.Println(ing.Rules[i].MultiLineString())
return nil
Expand Down
32 changes: 27 additions & 5 deletions ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ var (
)

const (
ServiceBastion = "bastion"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidate the use via config.BastionFlag and also to avoid import cycle not allowed in test which got raised when I was referencing ingress from carrier

ServiceSocksProxy = "socks-proxy"
ServiceWarpRouting = "warp-routing"
)
Expand All @@ -38,12 +37,13 @@ const (
// which is the case if the rules were instantiated via the ingress#Validate method.
//
// Negative index rule signifies local cloudflared rules (not-user defined).
func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
func (ing Ingress) FindMatchingRule(hostname, path string, cfJumpDestinationHeader string) (*Rule, int) {
// The hostname might contain port. We only want to compare the host part with the rule
host, _, err := net.SplitHostPort(hostname)
if err == nil {
hostname = host
}
derivedHostName := hostname
for i, rule := range ing.InternalRules {
if rule.Matches(hostname, path) {
// Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
Expand All @@ -52,7 +52,15 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
}
}
for i, rule := range ing.Rules {
if rule.Matches(hostname, path) {
// If bastion mode is turned on and request is made as bastion, attempt
// to match a rule where jump destination header matches the hostname
if matchBastionDest(rule, cfJumpDestinationHeader) {
jumpDestinationUri, err := url.Parse(cfJumpDestinationHeader)
if err == nil {
derivedHostName = jumpDestinationUri.Hostname()
}
}
if rule.Matches(derivedHostName, path) {
return &rule, i
}
}
Expand All @@ -61,6 +69,10 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
return &ing.Rules[i], i
}

func matchBastionDest(rule Rule, cfJumpDestinationHeader string) bool {
return rule.Config.BastionMode && len(cfJumpDestinationHeader) > 0 && rule.Service != nil && rule.Service.String() != config.BastionFlag
}

func matchHost(ruleHost, reqHost string) bool {
if ruleHost == reqHost {
return true
Expand Down Expand Up @@ -265,6 +277,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
}
srv := newStatusCode(statusCode)
service = &srv

} else if r.Service == HelloWorldFlag || r.Service == HelloWorldService {
service = new(helloWorld)
} else if r.Service == ServiceSocksProxy {
Expand All @@ -284,12 +297,21 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
}

service = newSocksProxyOverWSService(accessPolicy)
} else if r.Service == ServiceBastion || cfg.BastionMode {
} else if r.Service == config.BastionFlag || cfg.BastionMode {
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg.BastionMode = true
service = newBastionService()

if cfg.BastionMode && r.Service != config.BastionFlag {
u, err := url.Parse(r.Service)
if err != nil {
return Ingress{}, err
}
service = newBastionServiceWithDest(u)
} else {
service = newBastionService()
}
} else {
// Validate URL services
u, err := url.Parse(r.Service)
Expand Down
Loading