mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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:
@@ -53,6 +53,34 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an inline code tag with formatting.
|
||||
|
||||
## Examples
|
||||
|
||||
<.code>def foo: do :bar</.code>
|
||||
"""
|
||||
attr :id, :string, default: nil
|
||||
attr :class, :string, default: ""
|
||||
slot :inner_block, required: true
|
||||
attr :rest, :global
|
||||
|
||||
def code(assigns) do
|
||||
assigns =
|
||||
assign(
|
||||
assigns,
|
||||
:class,
|
||||
"#{assigns.class} font-semibold p-[0.15rem] bg-neutral-100 rounded"
|
||||
)
|
||||
|
||||
# Important: leave the </code> on the same line as the render_slot call, otherwise there will be
|
||||
# an undesired trailing space in the output.
|
||||
~H"""
|
||||
<code id={@id} class={@class} {@rest} phx-no-format>
|
||||
{render_slot(@inner_block)}</code>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a monospace code block suitable for copying and pasting content.
|
||||
|
||||
|
||||
@@ -330,9 +330,9 @@ defmodule Web.NavigationComponents do
|
||||
|
||||
## Examples
|
||||
|
||||
<.website_link href="/pricing>Pricing</.website_link>
|
||||
<.website_link href="/kb/deploy/gateways" class="text-neutral-900">Deploy Gateway(s)</.website_link>
|
||||
<.website_link href={~p"/contact/sales"}>Contact Sales</.website_link>
|
||||
<.website_link path="/pricing>Pricing</.website_link>
|
||||
<.website_link path="/kb/deploy/gateways" class="text-neutral-900">Deploy Gateway(s)</.website_link>
|
||||
<.website_link path="/contact/sales">Contact Sales</.website_link>
|
||||
"""
|
||||
attr :path, :string, required: true
|
||||
attr :fragment, :string, required: false, default: ""
|
||||
@@ -357,9 +357,7 @@ defmodule Web.NavigationComponents do
|
||||
|
||||
## Examples
|
||||
|
||||
<.website_link href="/pricing>Pricing</.website_link>
|
||||
<.website_link href="/kb/deploy/gateways" class="text-neutral-900">Deploy Gateway(s)</.website_link>
|
||||
<.website_link href={~p"/contact/sales"}>Contact Sales</.website_link>
|
||||
<.docs_action path="/kb/deploy/gateways" class="text-neutral-900">Deploy Gateway(s)</.docs_action>
|
||||
"""
|
||||
attr :path, :string, required: true
|
||||
attr :fragment, :string, required: false, default: ""
|
||||
|
||||
@@ -222,6 +222,77 @@ defmodule Web.Resources.Components do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :form, :any, required: true
|
||||
|
||||
def ip_stack_form(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<legend class="text-xl mb-4">IP Stack</legend>
|
||||
<p class="text-sm text-neutral-500 mb-4">
|
||||
Determines what
|
||||
<.website_link path="/kb/deploy/resources" fragment="ip-stack">record types</.website_link>
|
||||
are generated by the stub resolver. If unsure, leave this unchanged.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<.input
|
||||
id="resource-ip-stack--dual"
|
||||
type="radio"
|
||||
field={@form[:ip_stack]}
|
||||
value="dual"
|
||||
checked={"#{@form[:ip_stack].value}" == "" or "#{@form[:ip_stack].value}" == "dual"}
|
||||
>
|
||||
<label>
|
||||
<span class="font-medium">Dual-stack:</span>
|
||||
<.code class="text-xs">A</.code>
|
||||
and
|
||||
<.code class="text-xs">AAAA</.code>
|
||||
records
|
||||
<span :if={ip_stack_recommendation(@form) == "dual"}>
|
||||
<.badge type="info">Recommended for this Resource</.badge>
|
||||
</span>
|
||||
</label>
|
||||
</.input>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<.input
|
||||
id="resource-ip-stack--ipv4-only"
|
||||
type="radio"
|
||||
field={@form[:ip_stack]}
|
||||
value="ipv4_only"
|
||||
checked={"#{@form[:ip_stack].value}" == "ipv4_only"}
|
||||
>
|
||||
<label>
|
||||
<span class="font-medium">IPv4:</span>
|
||||
<.code class="text-xs">A</.code>
|
||||
records only
|
||||
<span :if={ip_stack_recommendation(@form) == "ipv4_only"}>
|
||||
<.badge type="info">Recommended for this Resource</.badge>
|
||||
</span>
|
||||
</label>
|
||||
</.input>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<.input
|
||||
id="resource-ip-stack--ipv6-only"
|
||||
type="radio"
|
||||
field={@form[:ip_stack]}
|
||||
value="ipv6_only"
|
||||
checked={"#{@form[:ip_stack].value}" == "ipv6_only"}
|
||||
>
|
||||
<label>
|
||||
<span class="font-medium">IPv6:</span>
|
||||
<.code class="text-xs">AAAA</.code>
|
||||
records only
|
||||
<span :if={ip_stack_recommendation(@form) == "ipv6_only"}>
|
||||
<.badge type="info">Recommended for this Resource</.badge>
|
||||
</span>
|
||||
</label>
|
||||
</.input>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :filter, :any, required: true
|
||||
|
||||
def filter_description(assigns) do
|
||||
@@ -354,4 +425,17 @@ defmodule Web.Resources.Components do
|
||||
[id]
|
||||
end)
|
||||
end
|
||||
|
||||
@known_recommendations %{
|
||||
"mongodb.net" => "ipv4_only"
|
||||
}
|
||||
|
||||
defp ip_stack_recommendation(form) do
|
||||
if address = form[:address].value do
|
||||
@known_recommendations
|
||||
|> Enum.find_value(fn {key, value} ->
|
||||
if String.ends_with?(String.trim(address), key), do: value
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,6 +203,8 @@ defmodule Web.Resources.Edit do
|
||||
required
|
||||
/>
|
||||
|
||||
<.ip_stack_form :if={"#{@form[:type].value}" == "dns"} form={@form} />
|
||||
|
||||
<.filters_form
|
||||
:if={@resource.type != :internet}
|
||||
account={@account}
|
||||
|
||||
@@ -187,6 +187,8 @@ defmodule Web.Resources.New do
|
||||
required
|
||||
/>
|
||||
|
||||
<.ip_stack_form :if={"#{@form[:type].value}" == "dns"} form={@form} />
|
||||
|
||||
<.filters_form account={@account} form={@form[:filters]} />
|
||||
|
||||
<.connections_form
|
||||
|
||||
@@ -170,6 +170,16 @@ defmodule Web.Resources.Show do
|
||||
</span>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row :if={@resource.ip_stack}>
|
||||
<:label>
|
||||
IP Stack
|
||||
</:label>
|
||||
<:value>
|
||||
<span>
|
||||
{format_ip_stack(@resource.ip_stack)}
|
||||
</span>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>
|
||||
Connected Sites
|
||||
@@ -453,4 +463,8 @@ defmodule Web.Resources.Show do
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp format_ip_stack(:dual), do: "Dual-stack (IPv4 and IPv6)"
|
||||
defp format_ip_stack(:ipv4_only), do: "IPv4 only"
|
||||
defp format_ip_stack(:ipv6_only), do: "IPv6 only"
|
||||
end
|
||||
|
||||
@@ -110,6 +110,7 @@ defmodule Web.Live.Resources.EditTest do
|
||||
"resource[filters][udp][enabled]",
|
||||
"resource[filters][udp][ports]",
|
||||
"resource[filters][udp][protocol]",
|
||||
"resource[ip_stack]",
|
||||
"resource[name]",
|
||||
"resource[type]"
|
||||
])
|
||||
@@ -143,6 +144,7 @@ defmodule Web.Live.Resources.EditTest do
|
||||
"resource[filters][udp][enabled]",
|
||||
"resource[filters][udp][ports]",
|
||||
"resource[filters][udp][protocol]",
|
||||
"resource[ip_stack]",
|
||||
"resource[name]",
|
||||
"resource[type]"
|
||||
]
|
||||
@@ -256,6 +258,62 @@ defmodule Web.Live.Resources.EditTest do
|
||||
assert updated_filters.udp == attrs.filters.udp
|
||||
end
|
||||
|
||||
test "updates ip_stack when resource type is dns", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
resource: resource,
|
||||
conn: conn
|
||||
} do
|
||||
conn = authorize_conn(conn, identity)
|
||||
|
||||
attrs = %{
|
||||
name: "foobar.com",
|
||||
type: "dns",
|
||||
address: "foobar.com",
|
||||
ip_stack: "ipv4_only"
|
||||
}
|
||||
|
||||
{:ok, lv, html} =
|
||||
conn
|
||||
|> live(~p"/#{account}/resources/#{resource}/edit")
|
||||
|
||||
refute html =~ "Recommended for this Resource"
|
||||
|
||||
{:ok, _lv, html} =
|
||||
lv
|
||||
|> form("form", resource: attrs)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/#{account}/resources")
|
||||
|
||||
assert updated_resource = Repo.get_by(Domain.Resources.Resource, id: resource.id)
|
||||
assert updated_resource.name == attrs.name
|
||||
assert updated_resource.type == :dns
|
||||
assert updated_resource.address == attrs.address
|
||||
assert updated_resource.ip_stack == :ipv4_only
|
||||
assert html =~ "Resource #{updated_resource.name} updated successfully"
|
||||
end
|
||||
|
||||
test "renders ip stack recommendation", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
conn = authorize_conn(conn, identity)
|
||||
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
type: :dns,
|
||||
address: "**.mongodb.net"
|
||||
)
|
||||
|
||||
{:ok, _lv, html} =
|
||||
conn
|
||||
|> live(~p"/#{account}/resources/#{resource}/edit")
|
||||
|
||||
assert html =~ "Recommended for this Resource"
|
||||
end
|
||||
|
||||
test "redirects to a site when site_id query param is set", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
@@ -314,6 +372,7 @@ defmodule Web.Live.Resources.EditTest do
|
||||
"resource[filters][udp][enabled]",
|
||||
"resource[filters][udp][ports]",
|
||||
"resource[filters][udp][protocol]",
|
||||
"resource[ip_stack]",
|
||||
"resource[name]",
|
||||
"resource[type]"
|
||||
]
|
||||
|
||||
@@ -490,6 +490,61 @@ defmodule Web.Live.Resources.NewTest do
|
||||
assert html =~ "UPGRADE TO UNLOCK"
|
||||
end
|
||||
|
||||
test "sets ip_stack when resource type is dns", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
attrs = %{
|
||||
name: "foobar.com",
|
||||
address: "foobar.com",
|
||||
ip_stack: "ipv4_only"
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/resources/new?site_id=#{group}")
|
||||
|
||||
lv
|
||||
|> form("form")
|
||||
|> render_change(resource: %{type: :dns})
|
||||
|
||||
lv
|
||||
|> form("form", resource: attrs)
|
||||
|> render_submit()
|
||||
|
||||
resource = Repo.get_by(Domain.Resources.Resource, %{name: attrs.name, address: attrs.address})
|
||||
assert resource.ip_stack == :ipv4_only
|
||||
end
|
||||
|
||||
test "renders ip stack recommendation", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
attrs = %{
|
||||
name: "Mongo DB",
|
||||
address: "**.mongodb.net",
|
||||
ip_stack: :ipv6_only,
|
||||
type: :dns
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/resources/new?site_id=#{group}")
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("form")
|
||||
|> render_change(resource: attrs)
|
||||
|
||||
assert html =~ "Recommended for this Resource"
|
||||
end
|
||||
|
||||
test "creates a resource on valid attrs when traffic filter form disabled", %{
|
||||
account: account,
|
||||
group: group,
|
||||
|
||||
@@ -15,6 +15,7 @@ defmodule Web.Live.Resources.ShowTest do
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
subject: subject,
|
||||
ip_stack: :ipv4_only,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
@@ -140,6 +141,7 @@ defmodule Web.Live.Resources.ShowTest do
|
||||
assert table["address"] =~ resource.address
|
||||
assert table["created"] =~ actor.name
|
||||
assert table["address description"] =~ resource.address_description
|
||||
assert table["ip stack"] =~ "IPv4 only"
|
||||
|
||||
for filter <- resource.filters do
|
||||
assert String.downcase(table["traffic restriction"]) =~ Atom.to_string(filter.protocol)
|
||||
|
||||
Reference in New Issue
Block a user