Skip to content

Commit fed60ae

Browse files
authored
GH-352: Add Tunnel CLI option "edge-bind-address" (#870)
* Add Tunnel CLI option "edge-bind-address"
1 parent b979794 commit fed60ae

File tree

9 files changed

+129
-16
lines changed

9 files changed

+129
-16
lines changed

cmd/cloudflared/tunnel/cmd.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ const (
8484
LogFieldTmpTraceFilename = "tmpTraceFilename"
8585
LogFieldTraceOutputFilepath = "traceOutputFilepath"
8686

87-
tunnelCmdErrorMessage = `You did not specify any valid additional argument to the cloudflared tunnel command.
87+
tunnelCmdErrorMessage = `You did not specify any valid additional argument to the cloudflared tunnel command.
8888
89-
If you are trying to run a Quick Tunnel then you need to explicitly pass the --url flag.
90-
Eg. cloudflared tunnel --url localhost:8080/.
89+
If you are trying to run a Quick Tunnel then you need to explicitly pass the --url flag.
90+
Eg. cloudflared tunnel --url localhost:8080/.
9191
92-
Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
92+
Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
9393
For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)
9494
`
9595
)
@@ -551,11 +551,17 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
551551
}),
552552
altsrc.NewStringFlag(&cli.StringFlag{
553553
Name: "edge-ip-version",
554-
Usage: "Cloudflare Edge ip address version to connect with. {4, 6, auto}",
554+
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
555555
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
556556
Value: "4",
557557
Hidden: false,
558558
}),
559+
altsrc.NewStringFlag(&cli.StringFlag{
560+
Name: "edge-bind-address",
561+
Usage: "Bind to IP address for outgoing connections to Cloudflare Edge.",
562+
EnvVars: []string{"TUNNEL_EDGE_BIND_ADDRESS"},
563+
Hidden: false,
564+
}),
559565
altsrc.NewStringFlag(&cli.StringFlag{
560566
Name: tlsconfig.CaCertFlag,
561567
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",

cmd/cloudflared/tunnel/configuration.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ var (
4444
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
4545
defaultFeatures = []string{supervisor.FeatureAllowRemoteConfig, supervisor.FeatureSerializedHeaders, supervisor.FeatureDatagramV2, supervisor.FeatureQUICSupportEOF}
4646

47-
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version"}
47+
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version", "edge-bind-address"}
4848
)
4949

5050
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
@@ -284,6 +284,18 @@ func prepareTunnelConfig(
284284
if err != nil {
285285
return nil, nil, err
286286
}
287+
edgeBindAddr, err := parseConfigBindAddress(c.String("edge-bind-address"))
288+
if err != nil {
289+
return nil, nil, err
290+
}
291+
if err := testIPBindable(edgeBindAddr); err != nil {
292+
return nil, nil, fmt.Errorf("invalid edge-bind-address %s: %v", edgeBindAddr, err)
293+
}
294+
edgeIPVersion, err = adjustIPVersionByBindAddress(edgeIPVersion, edgeBindAddr)
295+
if err != nil {
296+
// This is not a fatal error, we just overrode edgeIPVersion
297+
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
298+
}
287299

288300
var pqKexIdx int
289301
if needPQ {
@@ -302,6 +314,7 @@ func prepareTunnelConfig(
302314
EdgeAddrs: c.StringSlice("edge"),
303315
Region: c.String("region"),
304316
EdgeIPVersion: edgeIPVersion,
317+
EdgeBindAddr: edgeBindAddr,
305318
HAConnections: c.Int("ha-connections"),
306319
IncidentLookup: supervisor.NewIncidentLookup(),
307320
IsAutoupdated: c.Bool("is-autoupdated"),
@@ -394,6 +407,51 @@ func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err err
394407
return
395408
}
396409

410+
func parseConfigBindAddress(ipstr string) (net.IP, error) {
411+
// Unspecified - it's fine
412+
if ipstr == "" {
413+
return nil, nil
414+
}
415+
ip := net.ParseIP(ipstr)
416+
if ip == nil {
417+
return nil, fmt.Errorf("invalid value for edge-bind-address: %s", ipstr)
418+
}
419+
return ip, nil
420+
}
421+
422+
func testIPBindable(ip net.IP) error {
423+
// "Unspecified" = let OS choose, so always bindable
424+
if ip == nil {
425+
return nil
426+
}
427+
428+
addr := &net.UDPAddr{IP: ip, Port: 0}
429+
listener, err := net.ListenUDP("udp", addr)
430+
if err != nil {
431+
return err
432+
}
433+
listener.Close()
434+
return nil
435+
}
436+
437+
func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.IP) (allregions.ConfigIPVersion, error) {
438+
if ip == nil {
439+
return ipVersion, nil
440+
}
441+
// https://pkg.go.dev/net#IP.To4: "If ip is not an IPv4 address, To4 returns nil."
442+
if ip.To4() != nil {
443+
if ipVersion == allregions.IPv6Only {
444+
return allregions.IPv4Only, fmt.Errorf("IPv4 bind address is specified, but edge-ip-version is IPv6")
445+
}
446+
return allregions.IPv4Only, nil
447+
} else {
448+
if ipVersion == allregions.IPv4Only {
449+
return allregions.IPv6Only, fmt.Errorf("IPv6 bind address is specified, but edge-ip-version is IPv4")
450+
}
451+
return allregions.IPv6Only, nil
452+
}
453+
}
454+
397455
func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRouterConfig, error) {
398456
ipv4Src, err := determineICMPv4Src(c.String("icmpv4-src"), logger)
399457
if err != nil {

cmd/cloudflared/tunnel/configuration_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"crypto/x509"
1010
"crypto/x509/pkix"
1111
"encoding/asn1"
12+
"net"
1213
"os"
1314
"testing"
1415

@@ -214,3 +215,23 @@ func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
214215
func isUnrecoverableError(err error) bool {
215216
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
216217
}
218+
219+
func TestTestIPBindable(t *testing.T) {
220+
assert.Nil(t, testIPBindable(nil))
221+
222+
// Public services - if one of these IPs is on the machine, the test environment is too weird
223+
assert.NotNil(t, testIPBindable(net.ParseIP("8.8.8.8")))
224+
assert.NotNil(t, testIPBindable(net.ParseIP("1.1.1.1")))
225+
226+
addrs, err := net.InterfaceAddrs()
227+
if err != nil {
228+
t.Fatal(err)
229+
}
230+
for i, addr := range addrs {
231+
if i >= 3 {
232+
break
233+
}
234+
ip := addr.(*net.IPNet).IP
235+
assert.Nil(t, testIPBindable(ip))
236+
}
237+
}

connection/quic.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type QUICConnection struct {
6767
func NewQUICConnection(
6868
quicConfig *quic.Config,
6969
edgeAddr net.Addr,
70+
localAddr net.IP,
7071
connIndex uint8,
7172
tlsConfig *tls.Config,
7273
orchestrator Orchestrator,
@@ -75,7 +76,7 @@ func NewQUICConnection(
7576
logger *zerolog.Logger,
7677
packetRouterConfig *ingress.GlobalRouterConfig,
7778
) (*QUICConnection, error) {
78-
udpConn, err := createUDPConnForConnIndex(connIndex, logger)
79+
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
7980
if err != nil {
8081
return nil, err
8182
}
@@ -563,13 +564,17 @@ func (rp *muxerWrapper) Close() error {
563564
return nil
564565
}
565566

566-
func createUDPConnForConnIndex(connIndex uint8, logger *zerolog.Logger) (*net.UDPConn, error) {
567+
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.Logger) (*net.UDPConn, error) {
567568
portMapMutex.Lock()
568569
defer portMapMutex.Unlock()
569570

571+
if localIP == nil {
572+
localIP = net.IPv4zero
573+
}
574+
570575
// if port was not set yet, it will be zero, so bind will randomly allocate one.
571576
if port, ok := portForConnIndex[connIndex]; ok {
572-
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: port})
577+
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: port})
573578
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
574579
if err == nil {
575580
return udpConn, nil
@@ -579,7 +584,7 @@ func createUDPConnForConnIndex(connIndex uint8, logger *zerolog.Logger) (*net.UD
579584
}
580585

581586
// if we reached here, then there was an error or port as not been allocated it.
582-
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
587+
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: 0})
583588
if err == nil {
584589
udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
585590
if !ok {

connection/quic_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ func TestNopCloserReadWriterCloseAfterEOF(t *testing.T) {
572572

573573
func TestCreateUDPConnReuseSourcePort(t *testing.T) {
574574
logger := zerolog.Nop()
575-
conn, err := createUDPConnForConnIndex(0, &logger)
575+
conn, err := createUDPConnForConnIndex(0, nil, &logger)
576576
require.NoError(t, err)
577577

578578
getPortFunc := func(conn *net.UDPConn) int {
@@ -586,17 +586,17 @@ func TestCreateUDPConnReuseSourcePort(t *testing.T) {
586586
conn.Close()
587587

588588
// should get the same port as before.
589-
conn, err = createUDPConnForConnIndex(0, &logger)
589+
conn, err = createUDPConnForConnIndex(0, nil, &logger)
590590
require.NoError(t, err)
591591
require.Equal(t, initialPort, getPortFunc(conn))
592592

593593
// new index, should get a different port
594-
conn1, err := createUDPConnForConnIndex(1, &logger)
594+
conn1, err := createUDPConnForConnIndex(1, nil, &logger)
595595
require.NoError(t, err)
596596
require.NotEqual(t, initialPort, getPortFunc(conn1))
597597

598598
// not closing the conn and trying to obtain a new conn for same index should give a different random port
599-
conn, err = createUDPConnForConnIndex(0, &logger)
599+
conn, err = createUDPConnForConnIndex(0, nil, &logger)
600600
require.NoError(t, err)
601601
require.NotEqual(t, initialPort, getPortFunc(conn))
602602
}
@@ -716,6 +716,7 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QU
716716
qc, err := NewQUICConnection(
717717
testQUICConfig,
718718
udpListenerAddr,
719+
nil,
719720
index,
720721
tlsClientConfig,
721722
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},

edgediscovery/allregions/discovery.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ const (
4141
IPv6Only ConfigIPVersion = 6
4242
)
4343

44+
func (c ConfigIPVersion) String() string {
45+
switch c {
46+
case Auto:
47+
return "auto"
48+
case IPv4Only:
49+
return "4"
50+
case IPv6Only:
51+
return "6"
52+
default:
53+
return ""
54+
}
55+
}
56+
4457
// IPVersion is the IP version of an EdgeAddr
4558
type EdgeIPVersion int8
4659

edgediscovery/dial.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ func DialEdge(
1515
timeout time.Duration,
1616
tlsConfig *tls.Config,
1717
edgeTCPAddr *net.TCPAddr,
18+
localIP net.IP,
1819
) (net.Conn, error) {
1920
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
2021
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
2122
defer dialCancel()
2223

2324
dialer := net.Dialer{}
25+
if localIP != nil {
26+
dialer.LocalAddr = &net.TCPAddr{IP: localIP, Port: 0}
27+
}
2428
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String())
2529
if err != nil {
2630
return nil, newDialError(err, "DialContext error")

supervisor/supervisor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,15 @@ func NewSupervisor(config *TunnelConfig, orchestrator *orchestration.Orchestrato
8282
log := NewConnAwareLogger(config.Log, tracker, config.Observer)
8383

8484
edgeAddrHandler := NewIPAddrFallback(config.MaxEdgeAddrRetries)
85+
edgeBindAddr := config.EdgeBindAddr
8586

8687
edgeTunnelServer := EdgeTunnelServer{
8788
config: config,
8889
orchestrator: orchestrator,
8990
credentialManager: reconnectCredentialManager,
9091
edgeAddrs: edgeIPs,
9192
edgeAddrHandler: edgeAddrHandler,
93+
edgeBindAddr: edgeBindAddr,
9294
tracker: tracker,
9395
reconnectCh: reconnectCh,
9496
gracefulShutdownC: gracefulShutdownC,

supervisor/tunnel.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type TunnelConfig struct {
4949
EdgeAddrs []string
5050
Region string
5151
EdgeIPVersion allregions.ConfigIPVersion
52+
EdgeBindAddr net.IP
5253
HAConnections int
5354
IncidentLookup IncidentLookup
5455
IsAutoupdated bool
@@ -207,6 +208,7 @@ type EdgeTunnelServer struct {
207208
credentialManager *reconnectCredentialManager
208209
edgeAddrHandler EdgeAddrHandler
209210
edgeAddrs *edgediscovery.Edge
211+
edgeBindAddr net.IP
210212
reconnectCh chan ReconnectSignal
211213
gracefulShutdownC <-chan struct{}
212214
tracker *tunnelstate.ConnTracker
@@ -497,7 +499,7 @@ func (e *EdgeTunnelServer) serveConnection(
497499
connIndex)
498500

499501
case connection.HTTP2:
500-
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP)
502+
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP, e.edgeBindAddr)
501503
if err != nil {
502504
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge")
503505
return err, true
@@ -516,7 +518,7 @@ func (e *EdgeTunnelServer) serveConnection(
516518
}
517519

518520
default:
519-
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP)
521+
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP, e.edgeBindAddr)
520522
if err != nil {
521523
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge")
522524
return err, true
@@ -672,6 +674,7 @@ func (e *EdgeTunnelServer) serveQUIC(
672674
quicConn, err := connection.NewQUICConnection(
673675
quicConfig,
674676
edgeAddr,
677+
e.edgeBindAddr,
675678
connIndex,
676679
tlsConfig,
677680
e.orchestrator,

0 commit comments

Comments
 (0)