diff --git a/config/configuration.go b/config/configuration.go index 961de6c6..8b16d4fe 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -175,60 +175,62 @@ func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) { } type UnvalidatedIngressRule struct { - Hostname string - Path string - Service string - OriginRequest OriginRequestConfig `yaml:"originRequest"` + Hostname string `json:"hostname"` + Path string `json:"path"` + Service string `json:"service"` + OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"` } // OriginRequestConfig is a set of optional fields that users may set to // customize how cloudflared sends requests to origin services. It is used to set // up general config that apply to all rules, and also, specific per-rule // config. -// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". +// Note: +// - To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". +// - To specify a time.Duration in json, use int64 of the nanoseconds type OriginRequestConfig struct { // HTTP proxy timeout for establishing a new connection - ConnectTimeout *time.Duration `yaml:"connectTimeout"` + ConnectTimeout *time.Duration `yaml:"connectTimeout" json:"connectTimeout"` // HTTP proxy timeout for completing a TLS handshake - TLSTimeout *time.Duration `yaml:"tlsTimeout"` + TLSTimeout *time.Duration `yaml:"tlsTimeout" json:"tlsTimeout"` // HTTP proxy TCP keepalive duration - TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"` + TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"` // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback - NoHappyEyeballs *bool `yaml:"noHappyEyeballs"` + NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"` // HTTP proxy maximum keepalive connection pool size - KeepAliveConnections *int `yaml:"keepAliveConnections"` + KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections"` // HTTP proxy timeout for closing an idle connection - KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"` + KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"` // Sets the HTTP Host header for the local webserver. - HTTPHostHeader *string `yaml:"httpHostHeader"` + HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader"` // Hostname on the origin server certificate. - OriginServerName *string `yaml:"originServerName"` + OriginServerName *string `yaml:"originServerName" json:"originServerName"` // Path to the CA for the certificate of your origin. // This option should be used only if your certificate is not signed by Cloudflare. - CAPool *string `yaml:"caPool"` + CAPool *string `yaml:"caPool" json:"caPool"` // Disables TLS verification of the certificate presented by your origin. // Will allow any certificate from the origin to be accepted. // Note: The connection from your machine to Cloudflare's Edge is still encrypted. - NoTLSVerify *bool `yaml:"noTLSVerify"` + NoTLSVerify *bool `yaml:"noTLSVerify" json:"noTLSVerify"` // Disables chunked transfer encoding. // Useful if you are running a WSGI server. - DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"` + DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"` // Runs as jump host - BastionMode *bool `yaml:"bastionMode"` + BastionMode *bool `yaml:"bastionMode" json:"bastionMode"` // Listen address for the proxy. - ProxyAddress *string `yaml:"proxyAddress"` + ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress"` // Listen port for the proxy. - ProxyPort *uint `yaml:"proxyPort"` + ProxyPort *uint `yaml:"proxyPort" json:"proxyPort"` // Valid options are 'socks' or empty. - ProxyType *string `yaml:"proxyType"` + ProxyType *string `yaml:"proxyType" json:"proxyType"` // IP rules for the proxy service - IPRules []IngressIPRule `yaml:"ipRules"` + IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules"` } type IngressIPRule struct { - Prefix *string `yaml:"prefix"` - Ports []int `yaml:"ports"` - Allow bool `yaml:"allow"` + Prefix *string `yaml:"prefix" json:"prefix"` + Ports []int `yaml:"ports" json:"ports"` + Allow bool `yaml:"allow" json:"allow"` } type Configuration struct { @@ -240,7 +242,7 @@ type Configuration struct { } type WarpRoutingConfig struct { - Enabled bool `yaml:"enabled"` + Enabled bool `yaml:"enabled" json:"enabled"` } type configFileSettings struct { diff --git a/config/configuration_test.go b/config/configuration_test.go index 58ec9639..11db35db 100644 --- a/config/configuration_test.go +++ b/config/configuration_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "testing" "time" @@ -26,6 +27,18 @@ func TestConfigFileSettings(t *testing.T) { ) rawYAML := ` tunnel: config-file-test +originRequest: + ipRules: + - prefix: "10.0.0.0/8" + ports: + - 80 + - 8080 + allow: false + - prefix: "fc00::/7" + ports: + - 443 + - 4443 + allow: true ingress: - hostname: tunnel1.example.com path: /id @@ -53,6 +66,21 @@ counters: assert.Equal(t, firstIngress, config.Ingress[0]) assert.Equal(t, secondIngress, config.Ingress[1]) assert.Equal(t, warpRouting, config.WarpRouting) + privateV4 := "10.0.0.0/8" + privateV6 := "fc00::/7" + ipRules := []IngressIPRule{ + { + Prefix: &privateV4, + Ports: []int{80, 8080}, + Allow: false, + }, + { + Prefix: &privateV6, + Ports: []int{443, 4443}, + Allow: true, + }, + } + assert.Equal(t, ipRules, config.OriginRequest.IPRules) retries, err := config.Int("retries") assert.NoError(t, err) @@ -81,3 +109,71 @@ counters: assert.Equal(t, 456, counters[1]) } + +func TestUnmarshalOriginRequestConfig(t *testing.T) { + raw := []byte(` +{ + "connectTimeout": 10000000000, + "tlsTimeout": 30000000000, + "tcpKeepAlive": 30000000000, + "noHappyEyeballs": true, + "keepAliveTimeout": 60000000000, + "keepAliveConnections": 10, + "httpHostHeader": "app.tunnel.com", + "originServerName": "app.tunnel.com", + "caPool": "/etc/capool", + "noTLSVerify": true, + "disableChunkedEncoding": true, + "bastionMode": true, + "proxyAddress": "127.0.0.3", + "proxyPort": 9000, + "proxyType": "socks", + "ipRules": [ + { + "prefix": "10.0.0.0/8", + "ports": [80, 8080], + "allow": false + }, + { + "prefix": "fc00::/7", + "ports": [443, 4443], + "allow": true + } + ] +} +`) + var config OriginRequestConfig + assert.NoError(t, json.Unmarshal(raw, &config)) + assert.Equal(t, time.Second*10, *config.ConnectTimeout) + assert.Equal(t, time.Second*30, *config.TLSTimeout) + assert.Equal(t, time.Second*30, *config.TCPKeepAlive) + assert.Equal(t, true, *config.NoHappyEyeballs) + assert.Equal(t, time.Second*60, *config.KeepAliveTimeout) + assert.Equal(t, 10, *config.KeepAliveConnections) + assert.Equal(t, "app.tunnel.com", *config.HTTPHostHeader) + assert.Equal(t, "app.tunnel.com", *config.OriginServerName) + assert.Equal(t, "/etc/capool", *config.CAPool) + assert.Equal(t, true, *config.NoTLSVerify) + assert.Equal(t, true, *config.DisableChunkedEncoding) + assert.Equal(t, true, *config.BastionMode) + assert.Equal(t, "127.0.0.3", *config.ProxyAddress) + assert.Equal(t, true, *config.NoTLSVerify) + assert.Equal(t, uint(9000), *config.ProxyPort) + assert.Equal(t, "socks", *config.ProxyType) + + privateV4 := "10.0.0.0/8" + privateV6 := "fc00::/7" + ipRules := []IngressIPRule{ + { + Prefix: &privateV4, + Ports: []int{80, 8080}, + Allow: false, + }, + { + Prefix: &privateV6, + Ports: []int{443, 4443}, + Allow: true, + }, + } + assert.Equal(t, ipRules, config.IPRules) +} diff --git a/ingress/origin_request_config.go b/ingress/config.go similarity index 79% rename from ingress/origin_request_config.go rename to ingress/config.go index 37e3ebbc..b389bc9b 100644 --- a/ingress/origin_request_config.go +++ b/ingress/config.go @@ -1,6 +1,7 @@ package ingress import ( + "encoding/json" "time" "github.com/urfave/cli/v2" @@ -38,6 +39,34 @@ const ( socksProxy = "socks" ) +// RemoteConfig models ingress settings that can be managed remotely, for example through the dashboard. +type RemoteConfig struct { + Ingress Ingress + WarpRouting config.WarpRoutingConfig +} + +type remoteConfigJSON struct { + GlobalOriginRequest config.OriginRequestConfig `json:"originRequest"` + IngressRules []config.UnvalidatedIngressRule `json:"ingress"` + WarpRouting config.WarpRoutingConfig `json:"warp-routing"` +} + +func (rc *RemoteConfig) UnmarshalJSON(b []byte) error { + var rawConfig remoteConfigJSON + if err := json.Unmarshal(b, &rawConfig); err != nil { + return err + } + ingress, err := validateIngress(rawConfig.IngressRules, originRequestFromConfig(rawConfig.GlobalOriginRequest)) + if err != nil { + return err + } + + rc.Ingress = ingress + rc.WarpRouting = rawConfig.WarpRouting + + return nil +} + func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig { var connectTimeout time.Duration = defaultConnectTimeout var tlsTimeout time.Duration = defaultTLSTimeout @@ -119,7 +148,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig { } } -func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig { +func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig { out := OriginRequestConfig{ ConnectTimeout: defaultConnectTimeout, TLSTimeout: defaultTLSTimeout, @@ -128,50 +157,58 @@ func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig { KeepAliveTimeout: defaultKeepAliveTimeout, ProxyAddress: defaultProxyAddress, } - if y.ConnectTimeout != nil { - out.ConnectTimeout = *y.ConnectTimeout + if c.ConnectTimeout != nil { + out.ConnectTimeout = *c.ConnectTimeout } - if y.TLSTimeout != nil { - out.TLSTimeout = *y.TLSTimeout + if c.TLSTimeout != nil { + out.TLSTimeout = *c.TLSTimeout } - if y.TCPKeepAlive != nil { - out.TCPKeepAlive = *y.TCPKeepAlive + if c.TCPKeepAlive != nil { + out.TCPKeepAlive = *c.TCPKeepAlive } - if y.NoHappyEyeballs != nil { - out.NoHappyEyeballs = *y.NoHappyEyeballs + if c.NoHappyEyeballs != nil { + out.NoHappyEyeballs = *c.NoHappyEyeballs } - if y.KeepAliveConnections != nil { - out.KeepAliveConnections = *y.KeepAliveConnections + if c.KeepAliveConnections != nil { + out.KeepAliveConnections = *c.KeepAliveConnections } - if y.KeepAliveTimeout != nil { - out.KeepAliveTimeout = *y.KeepAliveTimeout + if c.KeepAliveTimeout != nil { + out.KeepAliveTimeout = *c.KeepAliveTimeout } - if y.HTTPHostHeader != nil { - out.HTTPHostHeader = *y.HTTPHostHeader + if c.HTTPHostHeader != nil { + out.HTTPHostHeader = *c.HTTPHostHeader } - if y.OriginServerName != nil { - out.OriginServerName = *y.OriginServerName + if c.OriginServerName != nil { + out.OriginServerName = *c.OriginServerName } - if y.CAPool != nil { - out.CAPool = *y.CAPool + if c.CAPool != nil { + out.CAPool = *c.CAPool } - if y.NoTLSVerify != nil { - out.NoTLSVerify = *y.NoTLSVerify + if c.NoTLSVerify != nil { + out.NoTLSVerify = *c.NoTLSVerify } - if y.DisableChunkedEncoding != nil { - out.DisableChunkedEncoding = *y.DisableChunkedEncoding + if c.DisableChunkedEncoding != nil { + out.DisableChunkedEncoding = *c.DisableChunkedEncoding } - if y.BastionMode != nil { - out.BastionMode = *y.BastionMode + if c.BastionMode != nil { + out.BastionMode = *c.BastionMode } - if y.ProxyAddress != nil { - out.ProxyAddress = *y.ProxyAddress + if c.ProxyAddress != nil { + out.ProxyAddress = *c.ProxyAddress } - if y.ProxyPort != nil { - out.ProxyPort = *y.ProxyPort + if c.ProxyPort != nil { + out.ProxyPort = *c.ProxyPort } - if y.ProxyType != nil { - out.ProxyType = *y.ProxyType + if c.ProxyType != nil { + out.ProxyType = *c.ProxyType + } + if len(c.IPRules) > 0 { + for _, r := range c.IPRules { + rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow) + if err == nil { + out.IPRules = append(out.IPRules, rule) + } + } } return out } @@ -188,10 +225,10 @@ type OriginRequestConfig struct { TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"` // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback NoHappyEyeballs bool `yaml:"noHappyEyeballs"` - // HTTP proxy maximum keepalive connection pool size - KeepAliveConnections int `yaml:"keepAliveConnections"` // HTTP proxy timeout for closing an idle connection KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` + // HTTP proxy maximum keepalive connection pool size + KeepAliveConnections int `yaml:"keepAliveConnections"` // Sets the HTTP Host header for the local webserver. HTTPHostHeader string `yaml:"httpHostHeader"` // Hostname on the origin server certificate. @@ -308,6 +345,19 @@ func (defaults *OriginRequestConfig) setProxyType(overrides config.OriginRequest } } +func (defaults *OriginRequestConfig) setIPRules(overrides config.OriginRequestConfig) { + if val := overrides.IPRules; len(val) > 0 { + ipAccessRule := make([]ipaccess.Rule, len(overrides.IPRules)) + for i, r := range overrides.IPRules { + rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow) + if err == nil { + ipAccessRule[i] = rule + } + } + defaults.IPRules = ipAccessRule + } +} + // SetConfig gets config for the requests that cloudflared sends to origins. // Each field has a setter method which sets a value for the field by trying to find: // 1. The user config for this rule @@ -332,5 +382,6 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi cfg.setProxyPort(overrides) cfg.setProxyAddress(overrides) cfg.setProxyType(overrides) + cfg.setIPRules(overrides) return cfg } diff --git a/ingress/config_test.go b/ingress/config_test.go new file mode 100644 index 00000000..ff0a3c0a --- /dev/null +++ b/ingress/config_test.go @@ -0,0 +1,422 @@ +package ingress + +import ( + "encoding/json" + "flag" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + yaml "gopkg.in/yaml.v2" + + "github.com/cloudflare/cloudflared/config" + "github.com/cloudflare/cloudflared/ipaccess" +) + +// Ensure that the nullable config from `config` package and the +// non-nullable config from `ingress` package have the same number of +// fields. +// This test ensures that programmers didn't add a new field to +// one struct and forget to add it to the other ;) +func TestCorrespondingFields(t *testing.T) { + require.Equal( + t, + CountFields(t, config.OriginRequestConfig{}), + CountFields(t, OriginRequestConfig{}), + ) +} + +func CountFields(t *testing.T, val interface{}) int { + b, err := yaml.Marshal(val) + require.NoError(t, err) + m := make(map[string]interface{}, 0) + err = yaml.Unmarshal(b, &m) + require.NoError(t, err) + return len(m) +} + +func TestUnmarshalRemoteConfigOverridesGlobal(t *testing.T) { + rawConfig := []byte(` +{ + "originRequest": { + "connectTimeout": 90, + "noHappyEyeballs": true + }, + "ingress": [ + { + "hostname": "jira.cfops.com", + "service": "http://192.16.19.1:80", + "originRequest": { + "noTLSVerify": true, + "connectTimeout": 10 + } + }, + { + "service": "http_status:404" + } + ], + "warp-routing": { + "enabled": true + } +} +`) + var remoteConfig RemoteConfig + err := json.Unmarshal(rawConfig, &remoteConfig) + require.NoError(t, err) + require.True(t, remoteConfig.Ingress.Rules[0].Config.NoTLSVerify) + require.True(t, remoteConfig.Ingress.defaults.NoHappyEyeballs) +} + +func TestOriginRequestConfigOverrides(t *testing.T) { + validate := func(ing Ingress) { + // Rule 0 didn't override anything, so it inherits the user-specified + // root-level configuration. + actual0 := ing.Rules[0].Config + expected0 := OriginRequestConfig{ + ConnectTimeout: 1 * time.Minute, + TLSTimeout: 1 * time.Second, + TCPKeepAlive: 1 * time.Second, + NoHappyEyeballs: true, + KeepAliveTimeout: 1 * time.Second, + KeepAliveConnections: 1, + HTTPHostHeader: "abc", + OriginServerName: "a1", + CAPool: "/tmp/path0", + NoTLSVerify: true, + DisableChunkedEncoding: true, + BastionMode: true, + ProxyAddress: "127.1.2.3", + ProxyPort: uint(100), + ProxyType: "socks5", + IPRules: []ipaccess.Rule{ + newIPRule(t, "10.0.0.0/8", []int{80, 8080}, false), + newIPRule(t, "fc00::/7", []int{443, 4443}, true), + }, + } + require.Equal(t, expected0, actual0) + + // Rule 1 overrode all the root-level config. + actual1 := ing.Rules[1].Config + expected1 := OriginRequestConfig{ + ConnectTimeout: 2 * time.Minute, + TLSTimeout: 2 * time.Second, + TCPKeepAlive: 2 * time.Second, + NoHappyEyeballs: false, + KeepAliveTimeout: 2 * time.Second, + KeepAliveConnections: 2, + HTTPHostHeader: "def", + OriginServerName: "b2", + CAPool: "/tmp/path1", + NoTLSVerify: false, + DisableChunkedEncoding: false, + BastionMode: false, + ProxyAddress: "interface", + ProxyPort: uint(200), + ProxyType: "", + IPRules: []ipaccess.Rule{ + newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false), + newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true), + }, + } + require.Equal(t, expected1, actual1) + } + + rulesYAML := ` +originRequest: + connectTimeout: 1m + tlsTimeout: 1s + noHappyEyeballs: true + tcpKeepAlive: 1s + keepAliveConnections: 1 + keepAliveTimeout: 1s + httpHostHeader: abc + originServerName: a1 + caPool: /tmp/path0 + noTLSVerify: true + disableChunkedEncoding: true + bastionMode: True + proxyAddress: 127.1.2.3 + proxyPort: 100 + proxyType: socks5 + ipRules: + - prefix: "10.0.0.0/8" + ports: + - 80 + - 8080 + allow: false + - prefix: "fc00::/7" + ports: + - 443 + - 4443 + allow: true +ingress: +- hostname: tun.example.com + service: https://localhost:8000 +- hostname: "*" + service: https://localhost:8001 + originRequest: + connectTimeout: 2m + tlsTimeout: 2s + noHappyEyeballs: false + tcpKeepAlive: 2s + keepAliveConnections: 2 + keepAliveTimeout: 2s + httpHostHeader: def + originServerName: b2 + caPool: /tmp/path1 + noTLSVerify: false + disableChunkedEncoding: false + bastionMode: false + proxyAddress: interface + proxyPort: 200 + proxyType: "" + ipRules: + - prefix: "10.0.0.0/16" + ports: + - 3000 + - 3030 + allow: false + - prefix: "192.16.0.0/24" + ports: + - 5000 + - 5050 + allow: true +` + + ing, err := ParseIngress(MustReadIngress(rulesYAML)) + require.NoError(t, err) + validate(ing) + + rawConfig := []byte(` +{ + "originRequest": { + "connectTimeout": 60000000000, + "tlsTimeout": 1000000000, + "noHappyEyeballs": true, + "tcpKeepAlive": 1000000000, + "keepAliveConnections": 1, + "keepAliveTimeout": 1000000000, + "httpHostHeader": "abc", + "originServerName": "a1", + "caPool": "/tmp/path0", + "noTLSVerify": true, + "disableChunkedEncoding": true, + "bastionMode": true, + "proxyAddress": "127.1.2.3", + "proxyPort": 100, + "proxyType": "socks5", + "ipRules": [ + { + "prefix": "10.0.0.0/8", + "ports": [80, 8080], + "allow": false + }, + { + "prefix": "fc00::/7", + "ports": [443, 4443], + "allow": true + } + ] + }, + "ingress": [ + { + "hostname": "tun.example.com", + "service": "https://localhost:8000" + }, + { + "hostname": "*", + "service": "https://localhost:8001", + "originRequest": { + "connectTimeout": 120000000000, + "tlsTimeout": 2000000000, + "noHappyEyeballs": false, + "tcpKeepAlive": 2000000000, + "keepAliveConnections": 2, + "keepAliveTimeout": 2000000000, + "httpHostHeader": "def", + "originServerName": "b2", + "caPool": "/tmp/path1", + "noTLSVerify": false, + "disableChunkedEncoding": false, + "bastionMode": false, + "proxyAddress": "interface", + "proxyPort": 200, + "proxyType": "", + "ipRules": [ + { + "prefix": "10.0.0.0/16", + "ports": [3000, 3030], + "allow": false + }, + { + "prefix": "192.16.0.0/24", + "ports": [5000, 5050], + "allow": true + } + ] + } + } + ], + "warp-routing": { + "enabled": true + } +} +`) + var remoteConfig RemoteConfig + err = json.Unmarshal(rawConfig, &remoteConfig) + require.NoError(t, err) + validate(remoteConfig.Ingress) +} + +func TestOriginRequestConfigDefaults(t *testing.T) { + validate := func(ing Ingress) { + // Rule 0 didn't override anything, so it inherits the cloudflared defaults + actual0 := ing.Rules[0].Config + expected0 := OriginRequestConfig{ + ConnectTimeout: defaultConnectTimeout, + TLSTimeout: defaultTLSTimeout, + TCPKeepAlive: defaultTCPKeepAlive, + KeepAliveConnections: defaultKeepAliveConnections, + KeepAliveTimeout: defaultKeepAliveTimeout, + ProxyAddress: defaultProxyAddress, + } + require.Equal(t, expected0, actual0) + + // Rule 1 overrode all defaults. + actual1 := ing.Rules[1].Config + expected1 := OriginRequestConfig{ + ConnectTimeout: 2 * time.Minute, + TLSTimeout: 2 * time.Second, + TCPKeepAlive: 2 * time.Second, + NoHappyEyeballs: false, + KeepAliveTimeout: 2 * time.Second, + KeepAliveConnections: 2, + HTTPHostHeader: "def", + OriginServerName: "b2", + CAPool: "/tmp/path1", + NoTLSVerify: false, + DisableChunkedEncoding: false, + BastionMode: false, + ProxyAddress: "interface", + ProxyPort: uint(200), + ProxyType: "", + IPRules: []ipaccess.Rule{ + newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false), + newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true), + }, + } + require.Equal(t, expected1, actual1) + } + + rulesYAML := ` +ingress: +- hostname: tun.example.com + service: https://localhost:8000 +- hostname: "*" + service: https://localhost:8001 + originRequest: + connectTimeout: 2m + tlsTimeout: 2s + noHappyEyeballs: false + tcpKeepAlive: 2s + keepAliveConnections: 2 + keepAliveTimeout: 2s + httpHostHeader: def + originServerName: b2 + caPool: /tmp/path1 + noTLSVerify: false + disableChunkedEncoding: false + bastionMode: false + proxyAddress: interface + proxyPort: 200 + proxyType: "" + ipRules: + - prefix: "10.0.0.0/16" + ports: + - 3000 + - 3030 + allow: false + - prefix: "192.16.0.0/24" + ports: + - 5000 + - 5050 + allow: true +` + ing, err := ParseIngress(MustReadIngress(rulesYAML)) + if err != nil { + t.Error(err) + } + validate(ing) + + rawConfig := []byte(` +{ + "ingress": [ + { + "hostname": "tun.example.com", + "service": "https://localhost:8000" + }, + { + "hostname": "*", + "service": "https://localhost:8001", + "originRequest": { + "connectTimeout": 120000000000, + "tlsTimeout": 2000000000, + "noHappyEyeballs": false, + "tcpKeepAlive": 2000000000, + "keepAliveConnections": 2, + "keepAliveTimeout": 2000000000, + "httpHostHeader": "def", + "originServerName": "b2", + "caPool": "/tmp/path1", + "noTLSVerify": false, + "disableChunkedEncoding": false, + "bastionMode": false, + "proxyAddress": "interface", + "proxyPort": 200, + "proxyType": "", + "ipRules": [ + { + "prefix": "10.0.0.0/16", + "ports": [3000, 3030], + "allow": false + }, + { + "prefix": "192.16.0.0/24", + "ports": [5000, 5050], + "allow": true + } + ] + } + } + ] +} +`) + + var remoteConfig RemoteConfig + err = json.Unmarshal(rawConfig, &remoteConfig) + require.NoError(t, err) + validate(remoteConfig.Ingress) +} + +func TestDefaultConfigFromCLI(t *testing.T) { + set := flag.NewFlagSet("contrive", 0) + c := cli.NewContext(nil, set, nil) + + expected := OriginRequestConfig{ + ConnectTimeout: defaultConnectTimeout, + TLSTimeout: defaultTLSTimeout, + TCPKeepAlive: defaultTCPKeepAlive, + KeepAliveConnections: defaultKeepAliveConnections, + KeepAliveTimeout: defaultKeepAliveTimeout, + ProxyAddress: defaultProxyAddress, + } + actual := originRequestFromSingeRule(c) + require.Equal(t, expected, actual) +} + +func newIPRule(t *testing.T, prefix string, ports []int, allow bool) ipaccess.Rule { + rule, err := ipaccess.NewRuleByCIDR(&prefix, ports, allow) + require.NoError(t, err) + return rule +} diff --git a/ingress/ingress.go b/ingress/ingress.go index 0529e382..f2ab2791 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -163,7 +163,7 @@ func (ing Ingress) CatchAll() *Rule { return &ing.Rules[len(ing.Rules)-1] } -func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) { +func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) { rules := make([]Rule, len(ingress)) for i, r := range ingress { cfg := setConfig(defaults, r.OriginRequest) @@ -290,7 +290,7 @@ func ParseIngress(conf *config.Configuration) (Ingress, error) { if len(conf.Ingress) == 0 { return Ingress{}, ErrNoIngressRules } - return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest)) + return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest)) } func isHTTPService(url *url.URL) bool { diff --git a/ingress/ingress_test.go b/ingress/ingress_test.go index 7867d2b5..9d09e8f8 100644 --- a/ingress/ingress_test.go +++ b/ingress/ingress_test.go @@ -34,7 +34,7 @@ func Test_parseIngress(t *testing.T) { localhost8000 := MustParseURL(t, "https://localhost:8000") localhost8001 := MustParseURL(t, "https://localhost:8001") fourOhFour := newStatusCode(404) - defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{}) + defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{}) require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections) tr := true type args struct { @@ -324,7 +324,17 @@ ingress: { Hostname: "socks.foo.com", Service: newSocksProxyOverWSService(accessPolicy()), - Config: defaultConfig, + Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{IPRules: []config.IngressIPRule{ + { + Prefix: ipRulePrefix("1.1.1.0/24"), + Ports: []int{80, 443}, + Allow: true, + }, + { + Prefix: ipRulePrefix("0.0.0.0/0"), + Allow: false, + }, + }}), }, { Service: &fourOhFour, @@ -345,7 +355,7 @@ ingress: { Hostname: "bastion.foo.com", Service: newBastionService(), - Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), + Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), }, { Service: &fourOhFour, @@ -365,7 +375,7 @@ ingress: { Hostname: "bastion.foo.com", Service: newBastionService(), - Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), + Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), }, { Service: &fourOhFour, @@ -397,6 +407,10 @@ ingress: } } +func ipRulePrefix(s string) *string { + return &s +} + func TestSingleOriginSetsConfig(t *testing.T) { flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet.Bool("hello-world", true, "") diff --git a/ingress/origin_request_config_test.go b/ingress/origin_request_config_test.go deleted file mode 100644 index d4113d1d..00000000 --- a/ingress/origin_request_config_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package ingress - -import ( - "flag" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - yaml "gopkg.in/yaml.v2" - - "github.com/cloudflare/cloudflared/config" -) - -// Ensure that the nullable config from `config` package and the -// non-nullable config from `ingress` package have the same number of -// fields. -// This test ensures that programmers didn't add a new field to -// one struct and forget to add it to the other ;) -func TestCorrespondingFields(t *testing.T) { - require.Equal( - t, - CountFields(t, config.OriginRequestConfig{}), - CountFields(t, OriginRequestConfig{}), - ) -} - -func CountFields(t *testing.T, val interface{}) int { - b, err := yaml.Marshal(val) - require.NoError(t, err) - m := make(map[string]interface{}, 0) - err = yaml.Unmarshal(b, &m) - require.NoError(t, err) - return len(m) -} - -func TestOriginRequestConfigOverrides(t *testing.T) { - rulesYAML := ` -originRequest: - connectTimeout: 1m - tlsTimeout: 1s - noHappyEyeballs: true - tcpKeepAlive: 1s - keepAliveConnections: 1 - keepAliveTimeout: 1s - httpHostHeader: abc - originServerName: a1 - caPool: /tmp/path0 - noTLSVerify: true - disableChunkedEncoding: true - bastionMode: True - proxyAddress: 127.1.2.3 - proxyPort: 100 - proxyType: socks5 -ingress: -- hostname: tun.example.com - service: https://localhost:8000 -- hostname: "*" - service: https://localhost:8001 - originRequest: - connectTimeout: 2m - tlsTimeout: 2s - noHappyEyeballs: false - tcpKeepAlive: 2s - keepAliveConnections: 2 - keepAliveTimeout: 2s - httpHostHeader: def - originServerName: b2 - caPool: /tmp/path1 - noTLSVerify: false - disableChunkedEncoding: false - bastionMode: false - proxyAddress: interface - proxyPort: 200 - proxyType: "" -` - ing, err := ParseIngress(MustReadIngress(rulesYAML)) - if err != nil { - t.Error(err) - } - - // Rule 0 didn't override anything, so it inherits the user-specified - // root-level configuration. - actual0 := ing.Rules[0].Config - expected0 := OriginRequestConfig{ - ConnectTimeout: 1 * time.Minute, - TLSTimeout: 1 * time.Second, - NoHappyEyeballs: true, - TCPKeepAlive: 1 * time.Second, - KeepAliveConnections: 1, - KeepAliveTimeout: 1 * time.Second, - HTTPHostHeader: "abc", - OriginServerName: "a1", - CAPool: "/tmp/path0", - NoTLSVerify: true, - DisableChunkedEncoding: true, - BastionMode: true, - ProxyAddress: "127.1.2.3", - ProxyPort: uint(100), - ProxyType: "socks5", - } - require.Equal(t, expected0, actual0) - - // Rule 1 overrode all the root-level config. - actual1 := ing.Rules[1].Config - expected1 := OriginRequestConfig{ - ConnectTimeout: 2 * time.Minute, - TLSTimeout: 2 * time.Second, - NoHappyEyeballs: false, - TCPKeepAlive: 2 * time.Second, - KeepAliveConnections: 2, - KeepAliveTimeout: 2 * time.Second, - HTTPHostHeader: "def", - OriginServerName: "b2", - CAPool: "/tmp/path1", - NoTLSVerify: false, - DisableChunkedEncoding: false, - BastionMode: false, - ProxyAddress: "interface", - ProxyPort: uint(200), - ProxyType: "", - } - require.Equal(t, expected1, actual1) -} - -func TestOriginRequestConfigDefaults(t *testing.T) { - rulesYAML := ` -ingress: -- hostname: tun.example.com - service: https://localhost:8000 -- hostname: "*" - service: https://localhost:8001 - originRequest: - connectTimeout: 2m - tlsTimeout: 2s - noHappyEyeballs: false - tcpKeepAlive: 2s - keepAliveConnections: 2 - keepAliveTimeout: 2s - httpHostHeader: def - originServerName: b2 - caPool: /tmp/path1 - noTLSVerify: false - disableChunkedEncoding: false - bastionMode: false - proxyAddress: interface - proxyPort: 200 - proxyType: "" -` - ing, err := ParseIngress(MustReadIngress(rulesYAML)) - if err != nil { - t.Error(err) - } - - // Rule 0 didn't override anything, so it inherits the cloudflared defaults - actual0 := ing.Rules[0].Config - expected0 := OriginRequestConfig{ - ConnectTimeout: defaultConnectTimeout, - TLSTimeout: defaultTLSTimeout, - TCPKeepAlive: defaultTCPKeepAlive, - KeepAliveConnections: defaultKeepAliveConnections, - KeepAliveTimeout: defaultKeepAliveTimeout, - ProxyAddress: defaultProxyAddress, - } - require.Equal(t, expected0, actual0) - - // Rule 1 overrode all defaults. - actual1 := ing.Rules[1].Config - expected1 := OriginRequestConfig{ - ConnectTimeout: 2 * time.Minute, - TLSTimeout: 2 * time.Second, - NoHappyEyeballs: false, - TCPKeepAlive: 2 * time.Second, - KeepAliveConnections: 2, - KeepAliveTimeout: 2 * time.Second, - HTTPHostHeader: "def", - OriginServerName: "b2", - CAPool: "/tmp/path1", - NoTLSVerify: false, - DisableChunkedEncoding: false, - BastionMode: false, - ProxyAddress: "interface", - ProxyPort: uint(200), - ProxyType: "", - } - require.Equal(t, expected1, actual1) -} - -func TestDefaultConfigFromCLI(t *testing.T) { - set := flag.NewFlagSet("contrive", 0) - c := cli.NewContext(nil, set, nil) - - expected := OriginRequestConfig{ - ConnectTimeout: defaultConnectTimeout, - TLSTimeout: defaultTLSTimeout, - TCPKeepAlive: defaultTCPKeepAlive, - KeepAliveConnections: defaultKeepAliveConnections, - KeepAliveTimeout: defaultKeepAliveTimeout, - ProxyAddress: defaultProxyAddress, - } - actual := originRequestFromSingeRule(c) - require.Equal(t, expected, actual) -}