diff --git a/firewall_rules.go b/firewall_rules.go index c917549f7..537def251 100644 --- a/firewall_rules.go +++ b/firewall_rules.go @@ -5,7 +5,13 @@ import ( "encoding/json" ) -// NetworkProtocol enum type +// NetworkProtocol is used for firewall rule protocol fields. +// +// This can also represent arbitrary protocols, for example an IP protocol number +// like `NetworkProtocol("50")`. +// +// NOTE: ALL and numeric protocols may not yet +// be available to all users. type NetworkProtocol string // NetworkProtocol enum values @@ -14,6 +20,8 @@ const ( UDP NetworkProtocol = "UDP" ICMP NetworkProtocol = "ICMP" IPENCAP NetworkProtocol = "IPENCAP" + + AllNetworkProtocols NetworkProtocol = "ALL" ) // NetworkAddresses are arrays of ipv4 and v6 addresses diff --git a/test/integration/firewall_rules_test.go b/test/integration/firewall_rules_test.go index a8cb96b00..1a7462d79 100644 --- a/test/integration/firewall_rules_test.go +++ b/test/integration/firewall_rules_test.go @@ -2,10 +2,13 @@ package integration import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/linode/linodego/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -77,6 +80,76 @@ func TestFirewallRules_Get_smoke(t *testing.T) { } } +func TestFirewallRules_ExtendedProtocols(t *testing.T) { + label := fmt.Sprintf("fw-ext-%s", getUniqueText()) + + rules := linodego.FirewallRulesCreateOptions{ + Inbound: []linodego.FirewallRuleInbound{ + { + Label: "linodego-fwrule-all", + Action: "ACCEPT", + Protocol: linodego.AllNetworkProtocols, + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + { + Label: "linodego-fwrule-numeric", + Action: "ACCEPT", + Protocol: linodego.NetworkProtocol("50"), + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + { + Label: "linodego-fwrule-tcp-numeric", + Action: "ACCEPT", + Ports: "443", + Protocol: linodego.NetworkProtocol("6"), + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + }, + InboundPolicy: "DROP", + OutboundPolicy: "ACCEPT", + } + + client, firewall, teardown, err := setupFirewall(t, []firewallModifier{ + func(createOpts *linodego.FirewallCreateOptions) { + createOpts.Label = label + createOpts.Rules = rules + }, + }, "fixtures/TestFirewallRules_ExtendedProtocols") + t.Cleanup(teardown) + require.NoError(t, err) + + require.Len(t, firewall.Rules.Inbound, 3) + + assert.Equal(t, linodego.AllNetworkProtocols, firewall.Rules.Inbound[0].Protocol) + assert.Empty(t, firewall.Rules.Inbound[0].Ports) + + assert.Equal(t, linodego.NetworkProtocol("50"), firewall.Rules.Inbound[1].Protocol) + assert.Empty(t, firewall.Rules.Inbound[1].Ports) + + assert.Equal(t, linodego.NetworkProtocol("6"), firewall.Rules.Inbound[2].Protocol) + assert.Equal(t, "443", firewall.Rules.Inbound[2].Ports) + + result, err := client.GetFirewall(context.Background(), firewall.ID) + require.NoErrorf(t, err, "failed to get firewall %d", firewall.ID) + + require.Len(t, result.Rules.Inbound, 3) + + assert.Equal(t, linodego.AllNetworkProtocols, result.Rules.Inbound[0].Protocol) + assert.Empty(t, result.Rules.Inbound[0].Ports) + + assert.Equal(t, linodego.NetworkProtocol("50"), result.Rules.Inbound[1].Protocol) + assert.Empty(t, result.Rules.Inbound[1].Ports) + + assert.Equal(t, linodego.NetworkProtocol("6"), result.Rules.Inbound[2].Protocol) + assert.Equal(t, "443", result.Rules.Inbound[2].Ports) +} + func TestFirewallRules_Update(t *testing.T) { client, firewall, teardown, err := setupFirewall(t, []firewallModifier{}, "fixtures/TestFirewallRules_Update") if err != nil { diff --git a/test/integration/fixtures/TestFirewallRules_ExtendedProtocols.yaml b/test/integration/fixtures/TestFirewallRules_ExtendedProtocols.yaml new file mode 100644 index 000000000..c3b3e1b0e --- /dev/null +++ b/test/integration/fixtures/TestFirewallRules_ExtendedProtocols.yaml @@ -0,0 +1,198 @@ +--- +version: 1 +interactions: +- request: + body: '{"label":"fw-ext-1782999745568178000","rules":{"inbound":[{"action":"ACCEPT","label":"linodego-fwrule-all","protocol":"ALL","addresses":{"ipv4":["0.0.0.0/0"]}},{"action":"ACCEPT","label":"linodego-fwrule-numeric","protocol":"50","addresses":{"ipv4":["0.0.0.0/0"]}},{"action":"ACCEPT","label":"linodego-fwrule-tcp-numeric","ports":"443","protocol":"6","addresses":{"ipv4":["0.0.0.0/0"]}}],"inbound_policy":"DROP","outbound":null,"outbound_policy":"ACCEPT"},"tags":["testing"]}' + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/networking/firewalls + method: POST + response: + body: '{"id": 1645933, "label": "fw-ext-1782999745568178000", "created": "2018-01-02T03:04:05", + "updated": "2018-01-02T03:04:05", "status": "enabled", "rules": {"inbound": + [{"protocol": "ALL", "addresses": {"ipv4": ["0.0.0.0/0"]}, "action": "ACCEPT", + "label": "linodego-fwrule-all"}, {"protocol": "50", "addresses": {"ipv4": ["0.0.0.0/0"]}, + "action": "ACCEPT", "label": "linodego-fwrule-numeric"}, {"protocol": "6", "addresses": + {"ipv4": ["0.0.0.0/0"]}, "action": "ACCEPT", "label": "linodego-fwrule-tcp-numeric", + "ports": "443"}], "inbound_policy": "DROP", "outbound": null, "outbound_policy": + "ACCEPT", "version": 1, "fingerprint": "1e4bedfd"}, "tags": ["testing"], "entities": + []}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - keep-alive + Content-Length: + - "676" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - firewall:read_write + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "400" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/networking/firewalls/1645933 + method: GET + response: + body: '{"id": 1645933, "label": "fw-ext-1782999745568178000", "created": "2018-01-02T03:04:05", + "updated": "2018-01-02T03:04:05", "status": "enabled", "rules": {"inbound": + [{"protocol": "ALL", "addresses": {"ipv4": ["0.0.0.0/0"]}, "action": "ACCEPT", + "label": "linodego-fwrule-all"}, {"protocol": "50", "addresses": {"ipv4": ["0.0.0.0/0"]}, + "action": "ACCEPT", "label": "linodego-fwrule-numeric"}, {"protocol": "6", "addresses": + {"ipv4": ["0.0.0.0/0"]}, "action": "ACCEPT", "label": "linodego-fwrule-tcp-numeric", + "ports": "443"}], "inbound_policy": "DROP", "outbound": null, "outbound_policy": + "ACCEPT", "version": 1, "fingerprint": "1e4bedfd"}, "tags": ["testing"], "entities": + []}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - private, max-age=0, s-maxage=0, no-cache, no-store + - private, max-age=60, s-maxage=60 + Connection: + - keep-alive + Content-Length: + - "676" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - firewall:read_only + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "400" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/networking/firewalls/1645933 + method: DELETE + response: + body: '{}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - keep-alive + Content-Length: + - "2" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - firewall:read_write + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "400" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/unit/firewall_rules_test.go b/test/unit/firewall_rules_test.go index d9f61ee55..970d60e8f 100644 --- a/test/unit/firewall_rules_test.go +++ b/test/unit/firewall_rules_test.go @@ -7,6 +7,7 @@ import ( "github.com/linode/linodego/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFirewallRule_Get(t *testing.T) { @@ -70,6 +71,81 @@ func TestFirewallRule_MarshalJSON(t *testing.T) { }`, string(data)) } +func TestFirewallRule_MarshalExtendedProtocols(t *testing.T) { + tests := []struct { + name string + rule any + label string + protocol string + ports string + expectPorts bool + }{ + { + name: "all without ports", + rule: linodego.FirewallRuleInbound{ + Action: "ACCEPT", + Label: "allow-all", + Protocol: linodego.AllNetworkProtocols, + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + label: "allow-all", + protocol: "ALL", + }, + { + name: "numeric without ports", + rule: linodego.FirewallRuleOutbound{ + Action: "ACCEPT", + Label: "allow-esp", + Protocol: linodego.NetworkProtocol("50"), + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + label: "allow-esp", + protocol: "50", + }, + { + name: "numeric tcp with ports", + rule: linodego.FirewallRuleSetRuleCreateOptions{ + Action: "ACCEPT", + Label: "allow-https", + Ports: "443", + Protocol: linodego.NetworkProtocol("6"), + Addresses: linodego.NetworkAddresses{ + IPv4: []string{"0.0.0.0/0"}, + }, + }, + label: "allow-https", + protocol: "6", + ports: "443", + expectPorts: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.rule) + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, json.Unmarshal(data, &payload)) + + assert.Equal(t, "ACCEPT", payload["action"]) + assert.Equal(t, tt.label, payload["label"]) + assert.Equal(t, tt.protocol, payload["protocol"]) + assert.Equal(t, map[string]any{"ipv4": []any{"0.0.0.0/0"}}, payload["addresses"]) + + if tt.expectPorts { + assert.Equal(t, tt.ports, payload["ports"]) + } else { + assert.NotContains(t, payload, "ports") + } + }) + } +} + func TestFirewallRule_Update(t *testing.T) { fixtureData, err := fixtures.GetFixture("firewall_rule_update") assert.NoError(t, err)