feat(portal): configurable ip stack for DNS resources (#9303)

Some poorly-behaved applications (e.g. mongo) will fail to connect if
they see both IPv4 and IPv6 addresses for a DNS resource, because they
will try to connect to both of them and fail the whole connection setup
if either one is not routable.

To fix this, we need to introduce a knob to allow admins to restrict DNS
resources to only A or AAAA records.


<img width="750" alt="Screenshot 2025-06-02 at 10 48 39 AM"
src="https://github.com/user-attachments/assets/4dbcb6ae-685f-43ee-b9e8-1502b365a294"
/>

<img width="1174" alt="Screenshot 2025-06-02 at 11 05 53 AM"
src="https://github.com/user-attachments/assets/02d0a4b3-e6e8-4b6d-89fa-d3d999b5811e"
/>

---

Related:
https://firezonehq.slack.com/archives/C08KPQKJZKM/p1746720923535349
Related: #9300
Fixes: #9042
This commit is contained in:
Jamil
2025-06-02 19:24:41 -07:00
committed by GitHub
parent 0ca31307f4
commit 6fc7d2e4e0
22 changed files with 574 additions and 22 deletions

View File

@@ -11,7 +11,8 @@ defmodule API.Client.Views.Resource do
id: resource.id,
type: :internet,
gateway_groups: Views.GatewayGroup.render_many(resource.gateway_groups),
can_be_disabled: true
can_be_disabled: true,
ip_stack: resource.ip_stack
}
end
@@ -27,7 +28,8 @@ defmodule API.Client.Views.Resource do
address_description: resource.address_description,
name: resource.name,
gateway_groups: Views.GatewayGroup.render_many(resource.gateway_groups),
filters: Enum.flat_map(resource.filters, &render_filter/1)
filters: Enum.flat_map(resource.filters, &render_filter/1),
ip_stack: resource.ip_stack
}
end
@@ -39,7 +41,8 @@ defmodule API.Client.Views.Resource do
address_description: resource.address_description,
name: resource.name,
gateway_groups: Views.GatewayGroup.render_many(resource.gateway_groups),
filters: Enum.flat_map(resource.filters, &render_filter/1)
filters: Enum.flat_map(resource.filters, &render_filter/1),
ip_stack: resource.ip_stack
}
end

View File

@@ -25,7 +25,8 @@ defmodule API.ResourceJSON do
name: resource.name,
address: resource.address,
address_description: resource.address_description,
type: resource.type
type: resource.type,
ip_stack: resource.ip_stack
}
end
end

View File

@@ -14,7 +14,12 @@ defmodule API.Schemas.Resource do
name: %Schema{type: :string, description: "Resource name"},
address: %Schema{type: :string, description: "Resource address"},
address_description: %Schema{type: :string, description: "Resource address description"},
type: %Schema{type: :string, description: "Resource type"}
type: %Schema{type: :string, description: "Resource type"},
ip_stack: %Schema{
type: :string,
description: "IP stack type. Only supported for DNS resources.",
enum: ["ipv4_only", "ipv6_only", "dual"]
}
},
required: [:name, :type],
example: %{
@@ -22,7 +27,8 @@ defmodule API.Schemas.Resource do
"name" => "Prod DB",
"address" => "10.0.0.10",
"address_description" => "Production Database",
"type" => "ip"
"type" => "ip",
"ip_stack" => "ipv4_only"
}
})
end
@@ -90,7 +96,8 @@ defmodule API.Schemas.Resource do
"name" => "Prod DB",
"address" => "10.0.0.10",
"address_description" => "Production Database",
"type" => "ip"
"type" => "ip",
"ip_stack" => "ipv4_only"
}
}
})

View File

@@ -40,6 +40,7 @@ defmodule API.Client.ChannelTest do
dns_resource =
Fixtures.Resources.create_resource(
account: account,
ip_stack: :ipv4_only,
connections: [%{gateway_group_id: gateway_group.id}]
)
@@ -287,6 +288,7 @@ defmodule API.Client.ChannelTest do
assert %{
id: dns_resource.id,
type: :dns,
ip_stack: :ipv4_only,
name: dns_resource.name,
address: dns_resource.address,
address_description: dns_resource.address_description,
@@ -307,6 +309,7 @@ defmodule API.Client.ChannelTest do
assert %{
id: cidr_resource.id,
type: :cidr,
ip_stack: nil,
name: cidr_resource.name,
address: cidr_resource.address,
address_description: cidr_resource.address_description,
@@ -327,6 +330,7 @@ defmodule API.Client.ChannelTest do
assert %{
id: ip_resource.id,
type: :cidr,
ip_stack: nil,
name: ip_resource.name,
address: "#{ip_resource.address}/32",
address_description: ip_resource.address_description,
@@ -347,6 +351,7 @@ defmodule API.Client.ChannelTest do
assert %{
id: internet_resource.id,
type: :internet,
ip_stack: nil,
gateway_groups: [
%{
id: internet_gateway_group.id,
@@ -830,6 +835,7 @@ defmodule API.Client.ChannelTest do
assert payload == %{
id: resource.id,
type: :dns,
ip_stack: :ipv4_only,
name: resource.name,
address: resource.address,
address_description: resource.address_description,
@@ -867,6 +873,7 @@ defmodule API.Client.ChannelTest do
assert payload == %{
id: resource.id,
type: :dns,
ip_stack: :ipv4_only,
name: resource.name,
address: resource.address,
address_description: resource.address_description,
@@ -959,6 +966,7 @@ defmodule API.Client.ChannelTest do
assert payload == %{
id: resource.id,
type: :dns,
ip_stack: :ipv4_only,
name: resource.name,
address: resource.address,
address_description: resource.address_description,
@@ -1031,6 +1039,7 @@ defmodule API.Client.ChannelTest do
assert payload == %{
id: resource.id,
type: :dns,
ip_stack: :ipv4_only,
name: resource.name,
address: resource.address,
address_description: resource.address_description,

View File

@@ -91,7 +91,7 @@ defmodule API.ResourceControllerTest do
end
test "returns a single resource", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
resource = Fixtures.Resources.create_resource(%{account: account, ip_stack: :ipv4_only})
conn =
conn
@@ -105,7 +105,8 @@ defmodule API.ResourceControllerTest do
"address_description" => resource.address_description,
"id" => resource.id,
"name" => resource.name,
"type" => Atom.to_string(resource.type)
"type" => Atom.to_string(resource.type),
"ip_stack" => "ipv4_only"
}
}
end
@@ -159,6 +160,7 @@ defmodule API.ResourceControllerTest do
"address" => "google.com",
"name" => "Google",
"type" => "dns",
"ip_stack" => "ipv6_only",
"connections" => [
%{"gateway_group_id" => gateway_group.id}
]
@@ -176,6 +178,7 @@ defmodule API.ResourceControllerTest do
assert resp["data"]["address_description"] == nil
assert resp["data"]["name"] == attrs["name"]
assert resp["data"]["type"] == attrs["type"]
assert resp["data"]["ip_stack"] == attrs["ip_stack"]
end
end
@@ -223,7 +226,7 @@ defmodule API.ResourceControllerTest do
test "updates a resource", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
attrs = %{"name" => "Google"}
attrs = %{"name" => "Google", "ip_stack" => "ipv6_only"}
conn =
conn
@@ -236,6 +239,7 @@ defmodule API.ResourceControllerTest do
assert resp["data"]["address"] == resource.address
assert resp["data"]["address_description"] == resource.address_description
assert resp["data"]["name"] == attrs["name"]
assert resp["data"]["ip_stack"] == attrs["ip_stack"]
end
end
@@ -261,7 +265,8 @@ defmodule API.ResourceControllerTest do
"address_description" => resource.address_description,
"id" => resource.id,
"name" => resource.name,
"type" => Atom.to_string(resource.type)
"type" => Atom.to_string(resource.type),
"ip_stack" => Atom.to_string(resource.ip_stack)
}
}