mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
feat(portal): extend DNS settings to allow for DoH providers (#10882)
In order to allow customers to make use of connlib's DoH functionality, we need a configuration UI for it. We take inspiration from the "New Resource" page and implement a 3-choice UI component for configuring how Clients should resolve DNS queries: - System - Secure DNS - Custom The secure and custom DNS options show an additional form when selected for either picking a DoH provider or the addresses of the custom DNS servers. Right now, the "Secure DNS" part is disabled if the `DISABLE_DOH_PROVIDER` env variable is set. We render a "Coming soon" tooltip on hover: <img width="1534" height="1100" alt="image" src="https://github.com/user-attachments/assets/a12a6ba4-806f-4d19-8aea-5c1cd981d609" /> This allows us to test this in staging and still ship to production if needed prior to enabling it. Resolves: #10792 Resolves: #10786 --------- Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -1,29 +1,43 @@
|
||||
defmodule API.Client.Views.Interface do
|
||||
alias Domain.Clients
|
||||
|
||||
@doh_providers %{
|
||||
google: [%{url: "https://dns.google/dns-query"}],
|
||||
quad9: [%{url: "https://dns.quad9.net/dns-query"}],
|
||||
cloudflare: [%{url: "https://cloudflare-dns.com/dns-query"}],
|
||||
opendns: [%{url: "https://doh.opendns.com/dns-query"}]
|
||||
}
|
||||
|
||||
def render(%Clients.Client{} = client) do
|
||||
clients_upstream_dns = Map.get(client.account.config, :clients_upstream_dns, [])
|
||||
dns_config = Map.get(client.account.config, :clients_upstream_dns)
|
||||
|
||||
# TODO: DOH RESOLVERS
|
||||
# Remove this old field once clients are upgraded.
|
||||
# old field - append normalized port
|
||||
upstream_dns =
|
||||
clients_upstream_dns
|
||||
|> Enum.map(fn %{address: address} = dns_config ->
|
||||
ip = if String.contains?(address, ":"), do: "[#{address}]", else: address
|
||||
Map.from_struct(%{dns_config | address: "#{ip}:53"})
|
||||
end)
|
||||
{upstream_do53, upstream_doh, upstream_dns} =
|
||||
case dns_config do
|
||||
%{type: :custom, addresses: addresses} when is_list(addresses) and addresses != [] ->
|
||||
do53 = Enum.map(addresses, fn %{address: address} -> %{ip: address} end)
|
||||
# Legacy field - append normalized port for backwards compatibility
|
||||
legacy_dns =
|
||||
Enum.map(addresses, fn %{address: address} ->
|
||||
ip = if String.contains?(address, ":"), do: "[#{address}]", else: address
|
||||
%{protocol: :ip_port, address: "#{ip}:53"}
|
||||
end)
|
||||
|
||||
# new field - no port
|
||||
upstream_do53 =
|
||||
clients_upstream_dns
|
||||
|> Enum.map(fn %{address: address} -> %{ip: address} end)
|
||||
{do53, [], legacy_dns}
|
||||
|
||||
%{type: :secure, doh_provider: provider}
|
||||
when provider in [:google, :quad9, :cloudflare, :opendns] ->
|
||||
doh = @doh_providers[provider] || []
|
||||
{[], doh, []}
|
||||
|
||||
_ ->
|
||||
# :system or nil or :custom with no addresses - use system resolvers
|
||||
{[], [], []}
|
||||
end
|
||||
|
||||
%{
|
||||
search_domain: client.account.config.search_domain,
|
||||
upstream_do53: upstream_do53,
|
||||
# Populate from DB once present.
|
||||
upstream_doh: [],
|
||||
upstream_doh: upstream_doh,
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6,
|
||||
|
||||
|
||||
@@ -7,11 +7,14 @@ defmodule API.Client.ChannelTest do
|
||||
account =
|
||||
Fixtures.Accounts.create_account(
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1:2:3:4:5:6:7:8"},
|
||||
%{protocol: "ip_port", address: "1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "8.8.8.8"}
|
||||
],
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1:2:3:4:5:6:7:8"},
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "8.8.8.8"}
|
||||
]
|
||||
},
|
||||
search_domain: "example.com"
|
||||
},
|
||||
features: %{
|
||||
|
||||
71
elixir/apps/api/test/api/client/views/interface_test.exs
Normal file
71
elixir/apps/api/test/api/client/views/interface_test.exs
Normal file
@@ -0,0 +1,71 @@
|
||||
defmodule API.Client.Views.InterfaceTest do
|
||||
use API.ChannelCase, async: true
|
||||
alias API.Client.Views.Interface
|
||||
|
||||
describe "render/1" do
|
||||
test "renders interface config with Do53 resolvers" do
|
||||
account =
|
||||
Fixtures.Accounts.create_account(
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "8.8.8.8"}
|
||||
]
|
||||
},
|
||||
search_domain: "example.com"
|
||||
}
|
||||
)
|
||||
|
||||
client = Fixtures.Clients.create_client(account: account) |> Domain.Repo.preload(:account)
|
||||
|
||||
result = Interface.render(client)
|
||||
|
||||
assert result.search_domain == "example.com"
|
||||
assert result.ipv4 == client.ipv4
|
||||
assert result.ipv6 == client.ipv6
|
||||
|
||||
# New Do53 format
|
||||
assert result.upstream_do53 == [
|
||||
%{ip: "1.1.1.1"},
|
||||
%{ip: "8.8.8.8"}
|
||||
]
|
||||
|
||||
# Legacy format
|
||||
assert result.upstream_dns == [
|
||||
%{protocol: :ip_port, address: "1.1.1.1:53"},
|
||||
%{protocol: :ip_port, address: "8.8.8.8:53"}
|
||||
]
|
||||
|
||||
# No DoH provider set
|
||||
assert result.upstream_doh == []
|
||||
end
|
||||
|
||||
test "renders interface config with DoH provider despite addresses if type is :secure" do
|
||||
account =
|
||||
Fixtures.Accounts.create_account(
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :secure,
|
||||
doh_provider: :opendns,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
client = Fixtures.Clients.create_client(account: account) |> Domain.Repo.preload(:account)
|
||||
|
||||
result = Interface.render(client)
|
||||
|
||||
assert result.upstream_doh == [
|
||||
%{url: "https://doh.opendns.com/dns-query"}
|
||||
]
|
||||
|
||||
assert result.upstream_do53 == []
|
||||
assert result.upstream_dns == []
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,11 +5,20 @@ defmodule Domain.Accounts.Config do
|
||||
embedded_schema do
|
||||
field :search_domain, :string
|
||||
|
||||
embeds_many :clients_upstream_dns, ClientsUpstreamDNS,
|
||||
embeds_one :clients_upstream_dns, ClientsUpstreamDns,
|
||||
primary_key: false,
|
||||
on_replace: :delete do
|
||||
field :protocol, Ecto.Enum, values: [:ip_port, :dns_over_tls, :dns_over_http]
|
||||
field :address, :string
|
||||
on_replace: :update do
|
||||
field :type, Ecto.Enum, values: [:system, :secure, :custom], default: :system
|
||||
|
||||
field :doh_provider, Ecto.Enum,
|
||||
values: [:google, :opendns, :cloudflare, :quad9],
|
||||
default: :google
|
||||
|
||||
embeds_many :addresses, Address,
|
||||
primary_key: false,
|
||||
on_replace: :delete do
|
||||
field :address, :string
|
||||
end
|
||||
end
|
||||
|
||||
embeds_one :notifications, Notifications,
|
||||
@@ -22,13 +31,14 @@ defmodule Domain.Accounts.Config do
|
||||
end
|
||||
end
|
||||
|
||||
def supported_dns_protocols, do: ~w[ip_port]a
|
||||
|
||||
@doc """
|
||||
Returns a default config with defaults set
|
||||
"""
|
||||
def default_config do
|
||||
%__MODULE__{
|
||||
clients_upstream_dns: %__MODULE__.ClientsUpstreamDns{
|
||||
type: :system
|
||||
},
|
||||
notifications: %__MODULE__.Notifications{
|
||||
outdated_gateway: %Domain.Accounts.Config.Notifications.Email{enabled: true}
|
||||
}
|
||||
@@ -39,6 +49,7 @@ defmodule Domain.Accounts.Config do
|
||||
Ensures a config has proper defaults
|
||||
"""
|
||||
def ensure_defaults(%__MODULE__{} = config) do
|
||||
# Ensure notifications defaults
|
||||
notifications = config.notifications || %__MODULE__.Notifications{}
|
||||
|
||||
outdated_gateway =
|
||||
|
||||
@@ -3,19 +3,35 @@ defmodule Domain.Accounts.Config.Changeset do
|
||||
alias Domain.Types.IPPort
|
||||
alias Domain.Accounts.Config
|
||||
|
||||
@default_dns_port 53
|
||||
|
||||
def changeset(config \\ %Config{}, attrs) do
|
||||
config
|
||||
|> cast(attrs, [:search_domain])
|
||||
|> cast_embed(:clients_upstream_dns,
|
||||
with: &client_upstream_dns_changeset/2,
|
||||
sort_param: :clients_upstream_dns_sort,
|
||||
drop_param: :clients_upstream_dns_drop
|
||||
)
|
||||
|> cast_embed(:clients_upstream_dns, with: &clients_upstream_dns_changeset/2)
|
||||
|> cast_embed(:notifications, with: ¬ifications_changeset/2)
|
||||
|> validate_search_domain()
|
||||
|> validate_unique_clients_upstream_dns()
|
||||
end
|
||||
|
||||
def clients_upstream_dns_changeset(clients_upstream_dns \\ %Config.ClientsUpstreamDns{}, attrs) do
|
||||
clients_upstream_dns
|
||||
|> cast(attrs, [:type, :doh_provider])
|
||||
|> validate_required([:type])
|
||||
|> cast_embed(:addresses,
|
||||
with: &address_changeset/2,
|
||||
sort_param: :addresses_sort,
|
||||
drop_param: :addresses_drop
|
||||
)
|
||||
|> validate_doh_provider_for_secure()
|
||||
|> validate_addresses_for_type()
|
||||
|> validate_custom_has_addresses()
|
||||
end
|
||||
|
||||
def address_changeset(address \\ %Config.ClientsUpstreamDns.Address{}, attrs) do
|
||||
address
|
||||
|> cast(attrs, [:address])
|
||||
|> validate_required([:address])
|
||||
|> trim_change(:address)
|
||||
|> validate_ip_address()
|
||||
|> validate_reserved_ip_exclusion()
|
||||
end
|
||||
|
||||
defp validate_search_domain(changeset) do
|
||||
@@ -46,48 +62,67 @@ defmodule Domain.Accounts.Config.Changeset do
|
||||
end)
|
||||
end
|
||||
|
||||
defp validate_unique_clients_upstream_dns(changeset) do
|
||||
with false <- has_errors?(changeset, :clients_upstream_dns),
|
||||
{_data_or_changes, client_upstream_dns} <- fetch_field(changeset, :clients_upstream_dns) do
|
||||
addresses =
|
||||
client_upstream_dns
|
||||
|> Enum.map(&normalize_dns_address/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
if addresses -- Enum.uniq(addresses) == [] do
|
||||
changeset
|
||||
defp validate_doh_provider_for_secure(changeset) do
|
||||
with true <- changeset.valid?,
|
||||
{_data_or_changes, type} <- fetch_field(changeset, :type),
|
||||
{_data_or_changes, doh_provider} <- fetch_field(changeset, :doh_provider) do
|
||||
if type == :secure and is_nil(doh_provider) do
|
||||
add_error(changeset, :doh_provider, "must be selected when using secure DNS")
|
||||
else
|
||||
add_error(changeset, :clients_upstream_dns, "all addresses must be unique")
|
||||
changeset
|
||||
end
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
def client_upstream_dns_changeset(client_upstream_dns \\ %Config.ClientsUpstreamDNS{}, attrs) do
|
||||
client_upstream_dns
|
||||
|> cast(attrs, [:protocol, :address])
|
||||
|> validate_required([:protocol, :address])
|
||||
|> trim_change(:address)
|
||||
|> validate_inclusion(:protocol, Config.supported_dns_protocols(),
|
||||
message: "this type of DNS provider is not supported yet"
|
||||
)
|
||||
|> validate_address()
|
||||
|> validate_reserved_ip_exclusion()
|
||||
end
|
||||
defp validate_addresses_for_type(changeset) do
|
||||
with true <- changeset.valid?,
|
||||
{_data_or_changes, type} <- fetch_field(changeset, :type),
|
||||
{_data_or_changes, addresses} <- fetch_field(changeset, :addresses) do
|
||||
case type do
|
||||
:custom ->
|
||||
validate_custom_addresses(changeset, addresses)
|
||||
|
||||
defp validate_address(changeset) do
|
||||
if has_errors?(changeset, :protocol) do
|
||||
changeset
|
||||
else
|
||||
case fetch_field(changeset, :protocol) do
|
||||
{_changes_or_data, :ip_port} -> validate_ip_port(changeset)
|
||||
:error -> changeset
|
||||
_ ->
|
||||
# For system and secure DNS, addresses are ignored but not cleared
|
||||
# This allows users to switch between types without losing their custom addresses
|
||||
changeset
|
||||
end
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_ip_port(changeset) do
|
||||
defp validate_custom_addresses(changeset, addresses) do
|
||||
# Check for unique addresses
|
||||
normalized_addresses =
|
||||
addresses
|
||||
|> Enum.map(&normalize_dns_address/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
if normalized_addresses -- Enum.uniq(normalized_addresses) == [] do
|
||||
changeset
|
||||
else
|
||||
add_error(changeset, :addresses, "all addresses must be unique")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_custom_has_addresses(changeset) do
|
||||
with true <- changeset.valid?,
|
||||
{_data_or_changes, type} <- fetch_field(changeset, :type),
|
||||
{_data_or_changes, addresses} <- fetch_field(changeset, :addresses) do
|
||||
if type == :custom and Enum.empty?(addresses) do
|
||||
add_error(changeset, :addresses, "must have at least one custom resolver")
|
||||
else
|
||||
changeset
|
||||
end
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_ip_address(changeset) do
|
||||
validate_change(changeset, :address, fn :address, address ->
|
||||
case IPPort.cast(address) do
|
||||
{:ok, %IPPort{port: nil}} -> []
|
||||
@@ -115,19 +150,9 @@ defmodule Domain.Accounts.Config.Changeset do
|
||||
|> cast_embed(:idp_sync_error, with: &Config.Notifications.Email.Changeset.changeset/2)
|
||||
end
|
||||
|
||||
defp normalize_dns_address(%Config.ClientsUpstreamDNS{protocol: :ip_port, address: address}) do
|
||||
case IPPort.cast(address) do
|
||||
{:ok, address} ->
|
||||
address
|
||||
|> IPPort.put_default_port(@default_dns_port)
|
||||
|> to_string()
|
||||
|
||||
_ ->
|
||||
address
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_dns_address(%Config.ClientsUpstreamDNS{address: address}) do
|
||||
defp normalize_dns_address(%Config.ClientsUpstreamDns.Address{address: address}) do
|
||||
address
|
||||
end
|
||||
|
||||
defp normalize_dns_address(_), do: nil
|
||||
end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
defmodule Domain.Repo.Migrations.SimplifyAccountClientDNS do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
UPDATE accounts
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{clients_upstream_dns}',
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'address',
|
||||
CASE
|
||||
-- Strip port from IPv4 address (e.g., "1.1.1.1:53" -> "1.1.1.1")
|
||||
WHEN dns->>'address' ~ '^[0-9.]+:[0-9]+$' THEN
|
||||
substring(dns->>'address' from '^([0-9.]+):[0-9]+$')
|
||||
-- Strip port from IPv6 address (e.g., "[::1]:53" -> "::1")
|
||||
WHEN dns->>'address' ~ '^\\\[.*\\\]:[0-9]+$' THEN
|
||||
substring(dns->>'address' from '^\\\[(.+)\\\]:[0-9]+$')
|
||||
-- Address without port, use as-is
|
||||
ELSE
|
||||
dns->>'address'
|
||||
END
|
||||
)
|
||||
)
|
||||
FROM jsonb_array_elements(config->'clients_upstream_dns') AS dns
|
||||
WHERE dns->>'address' IS NOT NULL AND dns->>'address' != ''
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
)
|
||||
WHERE config->'clients_upstream_dns' IS NOT NULL
|
||||
AND jsonb_typeof(config->'clients_upstream_dns') = 'array'
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
defmodule Domain.Repo.Migrations.RefactorClientsUpstreamDns do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
UPDATE accounts
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{clients_upstream_dns}',
|
||||
CASE
|
||||
WHEN jsonb_array_length(config->'clients_upstream_dns') = 0 THEN
|
||||
'{"type": "system", "addresses": []}'::jsonb
|
||||
ELSE
|
||||
jsonb_build_object(
|
||||
'type', 'custom',
|
||||
'addresses', config->'clients_upstream_dns'
|
||||
)
|
||||
END
|
||||
)
|
||||
WHERE config->'clients_upstream_dns' IS NOT NULL
|
||||
AND jsonb_typeof(config->'clients_upstream_dns') = 'array'
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
execute("""
|
||||
UPDATE accounts
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{clients_upstream_dns}',
|
||||
CASE
|
||||
WHEN config->'clients_upstream_dns'->>'type' = 'custom' THEN
|
||||
COALESCE(config->'clients_upstream_dns'->'addresses', '[]'::jsonb)
|
||||
ELSE
|
||||
'[]'::jsonb
|
||||
END
|
||||
)
|
||||
WHERE config->'clients_upstream_dns' IS NOT NULL
|
||||
AND jsonb_typeof(config->'clients_upstream_dns') = 'object'
|
||||
""")
|
||||
end
|
||||
end
|
||||
@@ -360,7 +360,10 @@ defmodule Domain.AccountsTest do
|
||||
monthly_active_users_count: -1
|
||||
},
|
||||
config: %{
|
||||
clients_upstream_dns: [%{protocol: "ip_port", address: "!!!"}]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [%{address: "!!!"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,9 +372,11 @@ defmodule Domain.AccountsTest do
|
||||
assert errors_on(changeset) == %{
|
||||
name: ["should be at most 64 character(s)"],
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{address: ["must be a valid IP address"]}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
addresses: [
|
||||
%{address: ["must be a valid IP address"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -393,10 +398,13 @@ defmodule Domain.AccountsTest do
|
||||
}
|
||||
},
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "8.8.8.8"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "8.8.8.8"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,15 +420,11 @@ defmodule Domain.AccountsTest do
|
||||
|
||||
assert is_nil(account.metadata.stripe.customer_id)
|
||||
|
||||
assert account.config.clients_upstream_dns == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "1.1.1.1"
|
||||
},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "8.8.8.8"
|
||||
}
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
|
||||
assert account.config.clients_upstream_dns.addresses == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "1.1.1.1"},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "8.8.8.8"}
|
||||
]
|
||||
end
|
||||
|
||||
@@ -474,7 +478,10 @@ defmodule Domain.AccountsTest do
|
||||
monthly_active_users_count: -1
|
||||
},
|
||||
config: %{
|
||||
clients_upstream_dns: [%{protocol: "ip_port", address: "!!!"}]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [%{address: "!!!"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,9 +496,11 @@ defmodule Domain.AccountsTest do
|
||||
monthly_active_users_count: ["must be greater than or equal to 0"]
|
||||
},
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{address: ["must be a valid IP address"]}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
addresses: [
|
||||
%{address: ["must be a valid IP address"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -499,34 +508,36 @@ defmodule Domain.AccountsTest do
|
||||
test "trims client upstream dns config address fields", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: " 1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "8.8.8.8 "}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: " 1.1.1.1"},
|
||||
%{address: "8.8.8.8 "}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, account} = update_account_by_id(account.id, attrs)
|
||||
|
||||
assert account.config.clients_upstream_dns == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "1.1.1.1"
|
||||
},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "8.8.8.8"
|
||||
}
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
|
||||
assert account.config.clients_upstream_dns.addresses == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "1.1.1.1"},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "8.8.8.8"}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns error on duplicate upstream dns config addresses", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "1.1.1.1 "}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "1.1.1.1 "}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +545,30 @@ defmodule Domain.AccountsTest do
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
config: %{
|
||||
clients_upstream_dns: ["all addresses must be unique"]
|
||||
clients_upstream_dns: %{
|
||||
addresses: ["all addresses must be unique"]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when custom type has no addresses", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {:error, changeset} = update_account_by_id(account.id, attrs)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
addresses: ["must have at least one custom resolver"]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -542,9 +576,12 @@ defmodule Domain.AccountsTest do
|
||||
test "does not allow ports", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1.1.1.1:53"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1:53"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,9 +589,11 @@ defmodule Domain.AccountsTest do
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{address: ["must not include a port"]}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
addresses: [
|
||||
%{address: ["must not include a port"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -562,9 +601,12 @@ defmodule Domain.AccountsTest do
|
||||
test "returns error on dns config address in IPv4 sentinel range", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "100.64.10.1"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "100.64.10.1"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,9 +614,11 @@ defmodule Domain.AccountsTest do
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{address: ["cannot be in the CIDR 100.64.0.0/10"]}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
addresses: [
|
||||
%{address: ["cannot be in the CIDR 100.64.0.0/10"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -582,9 +626,12 @@ defmodule Domain.AccountsTest do
|
||||
test "returns error on dns config address in IPv6 sentinel range", %{account: account} do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "fd00:2021:1111:10::"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "fd00:2021:1111:10::"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,9 +639,11 @@ defmodule Domain.AccountsTest do
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{address: ["cannot be in the CIDR fd00:2021:1111::/48"]}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
addresses: [
|
||||
%{address: ["cannot be in the CIDR fd00:2021:1111::/48"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -707,6 +756,60 @@ defmodule Domain.AccountsTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "accepts all valid DoH providers", %{account: account} do
|
||||
for provider <- [:google, :quad9, :cloudflare, :opendns] do
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :secure,
|
||||
doh_provider: provider,
|
||||
addresses: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, account} = update_account_by_id(account.id, attrs)
|
||||
assert account.config.clients_upstream_dns.doh_provider == provider
|
||||
assert account.config.clients_upstream_dns.type == :secure
|
||||
end
|
||||
end
|
||||
|
||||
test "retains addresses when switching to secure DNS", %{account: account} do
|
||||
# First set custom DNS
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, account} = update_account_by_id(account.id, attrs)
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
assert length(account.config.clients_upstream_dns.addresses) == 1
|
||||
|
||||
# Now switch to DoH provider
|
||||
attrs = %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :secure,
|
||||
doh_provider: :cloudflare,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, account} = update_account_by_id(account.id, attrs)
|
||||
assert account.config.clients_upstream_dns.type == :secure
|
||||
assert account.config.clients_upstream_dns.doh_provider == :cloudflare
|
||||
assert length(account.config.clients_upstream_dns.addresses) == 1
|
||||
end
|
||||
|
||||
test "updates account and broadcasts a message", %{account: account} do
|
||||
Bypass.open()
|
||||
|> Domain.Mocks.Stripe.mock_update_customer_endpoint(account)
|
||||
@@ -728,10 +831,13 @@ defmodule Domain.AccountsTest do
|
||||
}
|
||||
},
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "8.8.8.8"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "8.8.8.8"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,15 +860,11 @@ defmodule Domain.AccountsTest do
|
||||
assert account.metadata.stripe.billing_email ==
|
||||
attrs.metadata.stripe.billing_email
|
||||
|
||||
assert account.config.clients_upstream_dns == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "1.1.1.1"
|
||||
},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDNS{
|
||||
protocol: :ip_port,
|
||||
address: "8.8.8.8"
|
||||
}
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
|
||||
assert account.config.clients_upstream_dns.addresses == [
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "1.1.1.1"},
|
||||
%Domain.Accounts.Config.ClientsUpstreamDns.Address{address: "8.8.8.8"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -11,11 +11,14 @@ defmodule Domain.Fixtures.Accounts do
|
||||
legal_name: "l-acc-#{unique_num}",
|
||||
slug: "acc_#{unique_num}",
|
||||
config: %{
|
||||
clients_upstream_dns: [
|
||||
%{protocol: "ip_port", address: "1.1.1.1"},
|
||||
%{protocol: "ip_port", address: "2606:4700:4700::1111"},
|
||||
%{protocol: "ip_port", address: "9.9.9.9"}
|
||||
]
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [
|
||||
%{address: "1.1.1.1"},
|
||||
%{address: "2606:4700:4700::1111"},
|
||||
%{address: "9.9.9.9"}
|
||||
]
|
||||
}
|
||||
},
|
||||
features: %{
|
||||
policy_conditions: true,
|
||||
|
||||
@@ -5,16 +5,16 @@ defmodule Web.Settings.DNS do
|
||||
def mount(_params, _session, socket) do
|
||||
with {:ok, account} <-
|
||||
Accounts.fetch_account_by_id(socket.assigns.account.id, socket.assigns.subject) do
|
||||
form =
|
||||
Accounts.change_account(account, %{})
|
||||
|> to_form()
|
||||
# Ensure config has proper defaults
|
||||
account = %{account | config: Domain.Accounts.Config.ensure_defaults(account.config)}
|
||||
|
||||
doh_disabled = System.get_env("DISABLE_DOH_RESOLVERS") == "true"
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
page_title: "DNS",
|
||||
account: account,
|
||||
form: form
|
||||
)
|
||||
socket
|
||||
|> assign(page_title: "DNS")
|
||||
|> assign(doh_disabled: doh_disabled)
|
||||
|> init(account)
|
||||
|
||||
{:ok, socket}
|
||||
else
|
||||
@@ -22,6 +22,12 @@ defmodule Web.Settings.DNS do
|
||||
end
|
||||
end
|
||||
|
||||
defp init(socket, account) do
|
||||
changeset = Accounts.change_account(account)
|
||||
|
||||
assign(socket, form: to_form(changeset))
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumbs account={@account}>
|
||||
@@ -48,109 +54,224 @@ defmodule Web.Settings.DNS do
|
||||
<.form for={@form} phx-submit={:submit} phx-change={:change}>
|
||||
<.flash kind={:success} flash={@flash} phx-click="lv:clear-flash" />
|
||||
|
||||
<.inputs_for :let={config} field={@form[:config]}>
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Search Domain</h2>
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Search Domain</h2>
|
||||
|
||||
<p class="mb-4 text-neutral-500">
|
||||
The search domain, or default DNS suffix, will be appended to all single-label DNS queries made by Client devices
|
||||
while connected to Firezone.
|
||||
</p>
|
||||
<p class="mb-4 text-neutral-500">
|
||||
The search domain, or default DNS suffix, will be appended to all single-label DNS queries made by Client devices
|
||||
while connected to Firezone.
|
||||
</p>
|
||||
|
||||
<div class="mb-8">
|
||||
<.input field={config[:search_domain]} placeholder="E.g. example.com" />
|
||||
<div class="mb-8">
|
||||
<.inputs_for :let={config_form} field={@form[:config]}>
|
||||
<.input field={config_form[:search_domain]} placeholder="E.g. example.com" />
|
||||
<p class="mt-2 text-sm text-neutral-500">
|
||||
Enter a valid FQDN to append to single-label DNS queries. The
|
||||
resulting FQDN will be used to match against DNS Resources in
|
||||
your account, or forwarded to the upstream resolvers if no
|
||||
match is found.
|
||||
</p>
|
||||
</div>
|
||||
</.inputs_for>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Upstream Resolvers</h2>
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Upstream Resolvers</h2>
|
||||
|
||||
<p class="mb-4 text-neutral-500">
|
||||
Queries for Resources will <strong>always</strong> use Firezone's internal DNS.
|
||||
All other queries will use the resolvers configured here or the device's
|
||||
system resolvers if none are configured.
|
||||
</p>
|
||||
<p class="mb-4 text-neutral-500">
|
||||
Queries for Resources will <strong>always</strong> use Firezone's internal DNS.
|
||||
All other queries will use the resolvers configured here.
|
||||
</p>
|
||||
|
||||
<p :if={not upstream_dns_empty?(@account, @form)} class="mb-4 text-neutral-500">
|
||||
Upstream resolvers will be used by Client devices in the order they are listed below.
|
||||
</p>
|
||||
<.inputs_for :let={config_form} field={@form[:config]}>
|
||||
<.inputs_for :let={dns_form} field={config_form[:clients_upstream_dns]}>
|
||||
<div class="mb-6">
|
||||
<ul class="grid w-full gap-6 md:grid-cols-3">
|
||||
<li>
|
||||
<.input
|
||||
id="dns-type--system"
|
||||
type="radio_button_group"
|
||||
field={dns_form[:type]}
|
||||
value="system"
|
||||
checked={"#{dns_form[:type].value}" == "system"}
|
||||
required
|
||||
/>
|
||||
<label for="dns-type--system" class={~w[
|
||||
inline-flex items-center justify-between w-full
|
||||
p-5 text-gray-500 bg-white border border-gray-200
|
||||
rounded cursor-pointer peer-checked:border-accent-500
|
||||
peer-checked:text-accent-500 hover:text-gray-600 hover:bg-gray-100
|
||||
]}>
|
||||
<div class="block">
|
||||
<div class="w-full font-semibold mb-3">
|
||||
<.icon name="hero-computer-desktop" class="w-5 h-5 mr-1" /> System DNS
|
||||
</div>
|
||||
<div class="w-full text-sm">
|
||||
Use the device's default DNS resolvers.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<.input
|
||||
id="dns-type--secure"
|
||||
type="radio_button_group"
|
||||
field={dns_form[:type]}
|
||||
value="secure"
|
||||
checked={"#{dns_form[:type].value}" == "secure"}
|
||||
disabled={@doh_disabled}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
for="dns-type--secure"
|
||||
class={[
|
||||
"inline-flex items-center justify-between w-full",
|
||||
"p-5 text-gray-500 bg-white border border-gray-200",
|
||||
"rounded cursor-pointer peer-checked:border-accent-500",
|
||||
"peer-checked:text-accent-500 hover:text-gray-600 hover:bg-gray-100",
|
||||
@doh_disabled && "opacity-50 cursor-not-allowed",
|
||||
"relative group"
|
||||
]}
|
||||
title={@doh_disabled && "Coming soon"}
|
||||
>
|
||||
<div class="block">
|
||||
<div class="w-full font-semibold mb-3">
|
||||
<.icon name="hero-lock-closed" class="w-5 h-5 mr-1" /> Secure DNS
|
||||
</div>
|
||||
<div class="w-full text-sm">
|
||||
Use DNS-over-HTTPS from trusted providers.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<.input
|
||||
id="dns-type--custom"
|
||||
type="radio_button_group"
|
||||
field={dns_form[:type]}
|
||||
value="custom"
|
||||
checked={"#{dns_form[:type].value}" == "custom"}
|
||||
required
|
||||
/>
|
||||
<label for="dns-type--custom" class={~w[
|
||||
inline-flex items-center justify-between w-full
|
||||
p-5 text-gray-500 bg-white border border-gray-200
|
||||
rounded cursor-pointer peer-checked:border-accent-500
|
||||
peer-checked:text-accent-500 hover:text-gray-600 hover:bg-gray-100
|
||||
]}>
|
||||
<div class="block">
|
||||
<div class="w-full font-semibold mb-3">
|
||||
<.icon name="hero-cog-6-tooth" class="w-5 h-5 mr-1" /> Custom DNS
|
||||
</div>
|
||||
<div class="w-full text-sm">
|
||||
Configure your own DNS server addresses.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p :if={upstream_dns_empty?(@account, @form)} class="text-neutral-500">
|
||||
No upstream resolvers have been configured. Click <strong>New Resolver</strong>
|
||||
to add one.
|
||||
</p>
|
||||
<div :if={"#{dns_form[:type].value}" == "secure"} class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">
|
||||
DNS-over-HTTPS Provider
|
||||
</label>
|
||||
<.input
|
||||
type="select"
|
||||
field={dns_form[:doh_provider]}
|
||||
options={[
|
||||
{"Google Public DNS", :google},
|
||||
{"Cloudflare DNS", :cloudflare},
|
||||
{"Quad9 DNS", :quad9},
|
||||
{"OpenDNS", :opendns}
|
||||
]}
|
||||
/>
|
||||
<p class="mt-4 text-sm text-neutral-500">
|
||||
<strong>Note:</strong>
|
||||
Secure DNS is only supported on very recent Clients. Ensure your users are using the latest version to benefit from Secure DNS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<div>
|
||||
<.inputs_for :let={dns} field={config[:clients_upstream_dns]}>
|
||||
<input
|
||||
type="hidden"
|
||||
name={"#{config.name}[clients_upstream_dns_sort][]"}
|
||||
value={dns.index}
|
||||
/>
|
||||
<div
|
||||
:if={"#{dns_form[:type].value}" == "custom"}
|
||||
class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
:if={not Enum.empty?(dns_form[:addresses].value || [])}
|
||||
class="mb-4 text-neutral-500"
|
||||
>
|
||||
Upstream resolvers will be used by Client devices in the order they are listed below.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4 items-start mb-2">
|
||||
<div class="w-3/12">
|
||||
<.input
|
||||
type="select"
|
||||
label="Protocol"
|
||||
field={dns[:protocol]}
|
||||
placeholder="Protocol"
|
||||
options={dns_options()}
|
||||
value={dns[:protocol].value}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<.input label="Address" field={dns[:address]} placeholder="E.g. 1.1.1.1" />
|
||||
</div>
|
||||
<div class="justify-self-end">
|
||||
<div class="pt-7">
|
||||
<button
|
||||
type="button"
|
||||
name={"#{config.name}[clients_upstream_dns_drop][]"}
|
||||
value={dns.index}
|
||||
phx-click={JS.dispatch("change")}
|
||||
>
|
||||
<.icon
|
||||
name="hero-trash"
|
||||
class="-ml-1 text-red-500 w-5 h-5 relative top-2"
|
||||
/>
|
||||
</button>
|
||||
<p
|
||||
:if={Enum.empty?(dns_form[:addresses].value || [])}
|
||||
class="mb-4 text-neutral-500"
|
||||
>
|
||||
No upstream resolvers have been configured. Click <strong>New Resolver</strong>
|
||||
to add one.
|
||||
</p>
|
||||
|
||||
<.inputs_for :let={address_form} field={dns_form[:addresses]}>
|
||||
<input
|
||||
type="hidden"
|
||||
name="account[config][clients_upstream_dns][addresses_sort][]"
|
||||
value={address_form.index}
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-start mb-2">
|
||||
<div class="flex-grow">
|
||||
<.input
|
||||
label="IP Address"
|
||||
field={address_form[:address]}
|
||||
placeholder="E.g. 1.1.1.1"
|
||||
/>
|
||||
</div>
|
||||
<div class="justify-self-end">
|
||||
<div class="pt-7">
|
||||
<button
|
||||
type="button"
|
||||
name="account[config][clients_upstream_dns][addresses_drop][]"
|
||||
value={address_form.index}
|
||||
phx-click={JS.dispatch("change")}
|
||||
>
|
||||
<.icon
|
||||
name="hero-trash"
|
||||
class="-ml-1 text-red-500 w-5 h-5 relative top-2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.inputs_for>
|
||||
</.inputs_for>
|
||||
|
||||
<input type="hidden" name={"#{config.name}[clients_upstream_dns_drop][]"} />
|
||||
<.button
|
||||
class="mt-6 w-full"
|
||||
type="button"
|
||||
style="info"
|
||||
name={"#{config.name}[clients_upstream_dns_sort][]"}
|
||||
value="new"
|
||||
phx-click={JS.dispatch("change")}
|
||||
>
|
||||
New Resolver
|
||||
</.button>
|
||||
<.error
|
||||
:for={error <- dns_config_errors(@form.source.changes)}
|
||||
data-validation-error-for="clients_upstream_dns"
|
||||
>
|
||||
{error}
|
||||
</.error>
|
||||
<.error :for={{msg, _opts} <- dns_form[:addresses].errors}>
|
||||
{msg}
|
||||
</.error>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="account[config][clients_upstream_dns][addresses_drop][]"
|
||||
/>
|
||||
<.button
|
||||
class="mt-6 w-full"
|
||||
type="button"
|
||||
style="info"
|
||||
name="account[config][clients_upstream_dns][addresses_sort][]"
|
||||
value="new"
|
||||
phx-click={JS.dispatch("change")}
|
||||
>
|
||||
New Resolver
|
||||
</.button>
|
||||
|
||||
<p class="mt-4 text-sm text-neutral-500">
|
||||
<strong>Note:</strong>
|
||||
It is highly recommended to specify <strong>both</strong>
|
||||
IPv4 and IPv6 addresses when adding upstream resolvers. Otherwise, Clients without IPv4
|
||||
or IPv6 connectivity may not be able to resolve DNS queries.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-neutral-500">
|
||||
<strong>Note:</strong>
|
||||
It is highly recommended to specify <strong>both</strong>
|
||||
IPv4 and IPv6 addresses when adding upstream resolvers. Otherwise, Clients without IPv4
|
||||
or IPv6 connectivity may not be able to resolve DNS queries.
|
||||
</p>
|
||||
</.inputs_for>
|
||||
</.inputs_for>
|
||||
|
||||
<div class="mt-16">
|
||||
<.submit_button>
|
||||
Save DNS Settings
|
||||
@@ -163,66 +284,27 @@ defmodule Web.Settings.DNS do
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("change", %{"account" => attrs}, socket) do
|
||||
def handle_event("change", %{"account" => params}, socket) do
|
||||
form =
|
||||
Accounts.change_account(socket.assigns.account, attrs)
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form()
|
||||
socket.assigns.form.data
|
||||
|> Accounts.change_account(params)
|
||||
|> to_form(action: :validate)
|
||||
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"account" => attrs}, socket) do
|
||||
with {:ok, account} <-
|
||||
Accounts.update_account(socket.assigns.account, attrs, socket.assigns.subject) do
|
||||
form =
|
||||
Accounts.change_account(account, %{})
|
||||
|> to_form()
|
||||
def handle_event("submit", %{"account" => params}, socket) do
|
||||
case Accounts.update_account(socket.assigns.form.data, params, socket.assigns.subject) do
|
||||
{:ok, account} ->
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:success, "DNS settings saved successfully")
|
||||
|> init(account)
|
||||
|
||||
socket = put_flash(socket, :success, "Save successful!")
|
||||
{:noreply, socket}
|
||||
|
||||
{:noreply, assign(socket, account: account, form: form)}
|
||||
else
|
||||
{:error, changeset} ->
|
||||
form =
|
||||
changeset
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form()
|
||||
|
||||
{:noreply, assign(socket, form: form)}
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp upstream_dns_empty?(account, form) do
|
||||
upstream_dns_changes =
|
||||
Map.get(form.source.changes, :config, %{})
|
||||
|> Map.get(:changes, %{})
|
||||
|> Map.get(:clients_upstream_dns, %{})
|
||||
|
||||
Enum.empty?(account.config.clients_upstream_dns) and Enum.empty?(upstream_dns_changes)
|
||||
end
|
||||
|
||||
defp dns_options do
|
||||
supported_dns_protocols = Enum.map(Accounts.Config.supported_dns_protocols(), &to_string/1)
|
||||
|
||||
[
|
||||
[key: "IP", value: "ip_port"],
|
||||
[key: "DNS over TLS", value: "dns_over_tls"],
|
||||
[key: "DNS over HTTPS", value: "dns_over_https"]
|
||||
]
|
||||
|> Enum.map(fn option ->
|
||||
case option[:value] in supported_dns_protocols do
|
||||
true -> option
|
||||
false -> option ++ [disabled: true]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp dns_config_errors(changes) when changes == %{} do
|
||||
[]
|
||||
end
|
||||
|
||||
defp dns_config_errors(changes) do
|
||||
translate_errors(changes.config.errors, :clients_upstream_dns)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,64 +40,68 @@ defmodule Web.Live.Settings.DNSTest do
|
||||
assert breadcrumbs =~ "DNS Settings"
|
||||
end
|
||||
|
||||
test "renders form with no input fields", %{
|
||||
test "renders form with DNS type options", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :system, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
{:ok, _lv, html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
form = lv |> form("form")
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"account[config][_persistent_id]",
|
||||
"account[config][clients_upstream_dns_drop][]",
|
||||
"account[config][search_domain]"
|
||||
]
|
||||
assert html =~ "System DNS"
|
||||
assert html =~ "Secure DNS"
|
||||
assert html =~ "Custom DNS"
|
||||
end
|
||||
|
||||
test "renders input field on button click", %{
|
||||
test "shows DoH provider dropdown when secure DNS selected", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :secure, doh_provider: :google, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
{:ok, _lv, html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
attrs = %{
|
||||
"_target" => ["account", "config", "clients_upstream_dns_sort"],
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"_persistent_id" => "0",
|
||||
"clients_upstream_dns_drop" => [""],
|
||||
"clients_upstream_dns_sort" => ["new"]
|
||||
assert html =~ "DNS-over-HTTPS Provider"
|
||||
assert html =~ "Google Public DNS"
|
||||
assert html =~ "Cloudflare DNS"
|
||||
assert html =~ "Quad9 DNS"
|
||||
assert html =~ "OpenDNS"
|
||||
end
|
||||
|
||||
test "shows custom DNS fields when custom type selected", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [%{address: "8.8.8.8"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
lv
|
||||
|> render_click(:change, attrs)
|
||||
{:ok, _lv, html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
form = lv |> form("form")
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"account[config][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][address]",
|
||||
"account[config][clients_upstream_dns][0][protocol]",
|
||||
"account[config][clients_upstream_dns_drop][]",
|
||||
"account[config][clients_upstream_dns_sort][]",
|
||||
"account[config][search_domain]"
|
||||
]
|
||||
assert html =~ "IP Address"
|
||||
assert html =~ "8.8.8.8"
|
||||
assert html =~ "New Resolver"
|
||||
end
|
||||
|
||||
test "saves search domain", %{
|
||||
@@ -105,7 +109,10 @@ defmodule Web.Live.Settings.DNSTest do
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
account = Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
|
||||
account =
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :system, addresses: []}}
|
||||
})
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
@@ -124,16 +131,7 @@ defmodule Web.Live.Settings.DNSTest do
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert lv
|
||||
|> form("form")
|
||||
|> find_inputs() == [
|
||||
"account[config][_persistent_id]",
|
||||
"account[config][clients_upstream_dns_drop][]",
|
||||
"account[config][search_domain]"
|
||||
]
|
||||
|
||||
account = Domain.Accounts.fetch_account_by_id!(account.id)
|
||||
|
||||
assert account.config.search_domain == "example.com"
|
||||
end
|
||||
|
||||
@@ -142,7 +140,10 @@ defmodule Web.Live.Settings.DNSTest do
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
account = Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
|
||||
account =
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :system, addresses: []}}
|
||||
})
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
@@ -167,173 +168,375 @@ defmodule Web.Live.Settings.DNSTest do
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
|
||||
# Start with custom type and one address already configured
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [%{address: "1.1.1.1"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_change(%{
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"clients_upstream_dns_drop" => [""],
|
||||
"clients_upstream_dns_sort" => ["new"]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert lv
|
||||
|> form("form")
|
||||
|> find_inputs() == [
|
||||
"account[config][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][address]",
|
||||
"account[config][clients_upstream_dns][0][protocol]",
|
||||
"account[config][clients_upstream_dns_drop][]",
|
||||
"account[config][clients_upstream_dns_sort][]",
|
||||
"account[config][search_domain]"
|
||||
]
|
||||
end
|
||||
|
||||
test "removes blank entries upon save", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
"0" => %{address: ""}
|
||||
type: "custom",
|
||||
addresses: %{"0" => %{"address" => "8.8.8.8"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert lv
|
||||
|> form("form")
|
||||
|> find_inputs() == [
|
||||
"account[config][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][0][address]",
|
||||
"account[config][clients_upstream_dns][0][protocol]",
|
||||
"account[config][clients_upstream_dns][1][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][1][address]",
|
||||
"account[config][clients_upstream_dns][1][protocol]",
|
||||
"account[config][clients_upstream_dns][2][_persistent_id]",
|
||||
"account[config][clients_upstream_dns][2][address]",
|
||||
"account[config][clients_upstream_dns][2][protocol]",
|
||||
"account[config][clients_upstream_dns_drop][]",
|
||||
"account[config][clients_upstream_dns_sort][]",
|
||||
"account[config][search_domain]"
|
||||
]
|
||||
account = Domain.Accounts.fetch_account_by_id!(account.id)
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
assert length(account.config.clients_upstream_dns.addresses) == 1
|
||||
assert hd(account.config.clients_upstream_dns.addresses).address == "8.8.8.8"
|
||||
end
|
||||
|
||||
test "warns when duplicate IPv4 addresses found", %{
|
||||
test "returns error when custom type has no addresses", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
addr1 = %{address: "8.8.8.8"}
|
||||
addr1_dup = %{address: "8.8.8.8"}
|
||||
addr2 = %{address: "1.1.1.1"}
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{"0" => addr1}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Start with custom type but no addresses
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :custom, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
# Try to submit without any addresses
|
||||
html =
|
||||
lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{clients_upstream_dns: %{"1" => addr1}}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "all addresses must be unique"
|
||||
|
||||
refute lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{clients_upstream_dns: %{"1" => addr2}}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "all addresses must be unique"
|
||||
|
||||
assert lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{clients_upstream_dns: %{"1" => addr1_dup}}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "all addresses must be unique"
|
||||
assert html =~ "must have at least one custom resolver"
|
||||
end
|
||||
|
||||
test "displays 'cannot be empty' error message", %{
|
||||
test "validates duplicate addresses", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{"0" => %{address: ""}}
|
||||
clients_upstream_dns: %{
|
||||
type: "custom",
|
||||
addresses: %{
|
||||
"0" => %{"address" => "8.8.8.8"},
|
||||
"1" => %{"address" => "8.8.8.8"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "can't be blank"
|
||||
|> render_change() =~ "all addresses must be unique"
|
||||
end
|
||||
|
||||
test "validates IP addresses", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
assert lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "custom",
|
||||
addresses: %{"0" => %{"address" => "invalid"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "must be a valid IP address"
|
||||
|
||||
refute lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "custom",
|
||||
addresses: %{"0" => %{"address" => "8.8.8.8"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_change() =~ "must be a valid IP address"
|
||||
end
|
||||
|
||||
test "saves secure DNS with DoH provider", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
# First change just the type to make doh_provider field appear
|
||||
lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "secure"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_change()
|
||||
|
||||
# Now submit with both type and doh_provider
|
||||
lv
|
||||
|> form("form", %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "secure",
|
||||
doh_provider: "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
account = Repo.reload!(account)
|
||||
assert account.config.clients_upstream_dns.type == :secure
|
||||
assert account.config.clients_upstream_dns.doh_provider == :google
|
||||
end
|
||||
|
||||
test "saves system resolver selection", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
# Start with secure DNS set
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :secure, doh_provider: :cloudflare, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "system"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
account = Domain.Accounts.fetch_account_by_id!(account.id)
|
||||
assert account.config.clients_upstream_dns.type == :system
|
||||
assert Enum.empty?(account.config.clients_upstream_dns.addresses)
|
||||
end
|
||||
|
||||
test "retains DoH provider when switching from secure to system and back", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
# Start with secure DNS
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :secure, doh_provider: :quad9, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
# Switch to system
|
||||
render_change(lv, :change, %{
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"clients_upstream_dns" => %{
|
||||
"type" => "system"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Switch back to secure - DoH provider should still be there
|
||||
html =
|
||||
render_change(lv, :change, %{
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"clients_upstream_dns" => %{
|
||||
"type" => "secure",
|
||||
"doh_provider" => "quad9"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert html =~ "Quad9 DNS"
|
||||
end
|
||||
|
||||
test "retains custom addresses when switching from custom to system and back", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
# Start with custom addresses
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: :custom,
|
||||
addresses: [%{address: "8.8.8.8"}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
# Switch to system
|
||||
render_change(lv, :change, %{
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"clients_upstream_dns" => %{
|
||||
"type" => "system"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Switch back to custom - addresses should still be there
|
||||
html =
|
||||
render_change(lv, :change, %{
|
||||
"account" => %{
|
||||
"config" => %{
|
||||
"clients_upstream_dns" => %{
|
||||
"type" => "custom",
|
||||
"addresses" => %{"0" => %{"address" => "8.8.8.8"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert html =~ "8.8.8.8"
|
||||
end
|
||||
|
||||
test "can add multiple custom DNS addresses", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "custom",
|
||||
addresses: %{
|
||||
"0" => %{"address" => "8.8.8.8"},
|
||||
"1" => %{"address" => "1.1.1.1"},
|
||||
"2" => %{"address" => "2001:4860:4860::8888"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
account = Domain.Accounts.fetch_account_by_id!(account.id)
|
||||
assert account.config.clients_upstream_dns.type == :custom
|
||||
assert length(account.config.clients_upstream_dns.addresses) == 3
|
||||
|
||||
addresses = Enum.map(account.config.clients_upstream_dns.addresses, & &1.address)
|
||||
assert "8.8.8.8" in addresses
|
||||
assert "1.1.1.1" in addresses
|
||||
assert "2001:4860:4860::8888" in addresses
|
||||
end
|
||||
|
||||
test "can change DoH provider", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
# Start with Google
|
||||
Fixtures.Accounts.update_account(account, %{
|
||||
config: %{clients_upstream_dns: %{type: :secure, doh_provider: :google, addresses: []}}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/dns")
|
||||
|
||||
# Change to Cloudflare
|
||||
attrs = %{
|
||||
account: %{
|
||||
config: %{
|
||||
clients_upstream_dns: %{
|
||||
type: "secure",
|
||||
doh_provider: "cloudflare"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv
|
||||
|> form("form", attrs)
|
||||
|> render_submit()
|
||||
|
||||
account = Domain.Accounts.fetch_account_by_id!(account.id)
|
||||
assert account.config.clients_upstream_dns.type == :secure
|
||||
assert account.config.clients_upstream_dns.doh_provider == :cloudflare
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,38 +102,39 @@ feedback on
|
||||
|
||||
</Alert>
|
||||
|
||||
## Configuring Client DNS upstream resolvers
|
||||
## Configuring Client DNS
|
||||
|
||||
Go to `Settings -> DNS` to configure how Firezone Clients should resolve DNS.
|
||||
|
||||
### Default system resolvers
|
||||
|
||||
By default, Firezone Clients will use the system resolvers, typically set by the
|
||||
DHCP server of their local network.
|
||||
|
||||
### Secure DNS
|
||||
|
||||
<Alert color="info">
|
||||
Secure DNS was added in macOS 1.5.10, iOS 1.5.10, Android 1.5.7, Windows
|
||||
1.5.9, and Linux 1.5.9.
|
||||
</Alert>
|
||||
|
||||
Firezone Clients can use the DNS-over-HTTPS protocol for all non-Firezone
|
||||
resources. Secure DNS encrypts all DNS traffic, preventing middleboxes
|
||||
(including ISPs) from seeing or manipulating DNS traffic. This is especially
|
||||
useful on insecure or untrusted networks.
|
||||
|
||||
### Custom resolvers
|
||||
|
||||
Upstream DNS in all Clients can be configured with the servers of your choosing
|
||||
so that all queries on Client devices will be forwarded to the servers you
|
||||
specify for all non-Firezone resources.
|
||||
|
||||
Go to `Settings -> DNS` and enter IPv4 and/or IPv6 servers to use as fallback
|
||||
resolvers. Firezone Clients will use these servers in the order they are defined
|
||||
for any query that doesn't match a Resource the user has access to.
|
||||
|
||||
<Alert color="warning">
|
||||
When setting custom upstream resolvers, it is **highly** recommended to
|
||||
configure **both** an IPv4 and IPv6 option. Otherwise, a Client that has only
|
||||
IPv4 or IPv6 connectivity may not be able to resolve DNS queries.
|
||||
</Alert>
|
||||
|
||||
<Alert color="warning">
|
||||
Firezone Clients support only DNS over UDP/53 at this time. DNS-over-TLS and
|
||||
DNS-over-HTTPS upstream servers are not yet supported.
|
||||
</Alert>
|
||||
|
||||
If no custom resolvers are configured, Firezone Clients will fall back to the
|
||||
default system resolvers, typically set by the DHCP server of their local
|
||||
network.
|
||||
|
||||
<Alert color="info">
|
||||
Custom resolvers such as
|
||||
[Cloudflare](https://developers.cloudflare.com/1.1.1.1/setup/#1111-for-families)
|
||||
or [NextDNS](https://nextdns.io) can be used to block malware, ads, adult
|
||||
material and other content for all users in your Firezone account.
|
||||
</Alert>
|
||||
|
||||
## Configuring Gateway resolvers
|
||||
|
||||
Firezone makes no assumptions about the DNS environment in which the Gateway
|
||||
|
||||
Reference in New Issue
Block a user