mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
feat(portal): Wildcard dns with backwards compatibility (#6214)
If a new resource is created that will use format not supported by previous client versions we temporarily show a warning: <img width="683" alt="Screenshot 2024-08-07 at 2 28 57 PM" src="https://github.com/user-attachments/assets/bbfdfc96-0c4b-4226-93c5-bc2b5fdb9d30"> It will also be excluded from `resources` list for older clients (below 1.2). --------- Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
@@ -91,6 +91,9 @@ defmodule API.Client.Channel do
|
||||
:ok = Enum.each(relays, &Relays.subscribe_to_relay_presence/1)
|
||||
:ok = maybe_subscribe_for_relays_presence(relays, socket)
|
||||
|
||||
resources =
|
||||
map_and_filter_compatible_resources(resources, socket.assigns.client.last_seen_version)
|
||||
|
||||
:ok =
|
||||
push(socket, "init", %{
|
||||
resources: Views.Resource.render_many(resources),
|
||||
@@ -223,7 +226,20 @@ defmodule API.Client.Channel do
|
||||
preload: [:gateway_groups]
|
||||
) do
|
||||
{:ok, resource} ->
|
||||
push(socket, "resource_created_or_updated", Views.Resource.render(resource))
|
||||
case map_and_filter_compatible_resource(
|
||||
resource,
|
||||
socket.assigns.client.last_seen_version
|
||||
) do
|
||||
{:cont, resource} ->
|
||||
push(
|
||||
socket,
|
||||
"resource_created_or_updated",
|
||||
Views.Resource.render(resource)
|
||||
)
|
||||
|
||||
:drop ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
@@ -269,7 +285,20 @@ defmodule API.Client.Channel do
|
||||
preload: [:gateway_groups]
|
||||
) do
|
||||
{:ok, resource} ->
|
||||
push(socket, "resource_created_or_updated", Views.Resource.render(resource))
|
||||
case map_and_filter_compatible_resource(
|
||||
resource,
|
||||
socket.assigns.client.last_seen_version
|
||||
) do
|
||||
{:cont, resource} ->
|
||||
push(
|
||||
socket,
|
||||
"resource_created_or_updated",
|
||||
Views.Resource.render(resource)
|
||||
)
|
||||
|
||||
:drop ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
@@ -302,7 +331,20 @@ defmodule API.Client.Channel do
|
||||
preload: [:gateway_groups]
|
||||
) do
|
||||
{:ok, resource} ->
|
||||
push(socket, "resource_created_or_updated", Views.Resource.render(resource))
|
||||
case map_and_filter_compatible_resource(
|
||||
resource,
|
||||
socket.assigns.client.last_seen_version
|
||||
) do
|
||||
{:cont, resource} ->
|
||||
push(
|
||||
socket,
|
||||
"resource_created_or_updated",
|
||||
Views.Resource.render(resource)
|
||||
)
|
||||
|
||||
:drop ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
@@ -699,4 +741,27 @@ defmodule API.Client.Channel do
|
||||
gateways -> {:ok, gateways}
|
||||
end
|
||||
end
|
||||
|
||||
defp map_and_filter_compatible_resources(resources, client_version) do
|
||||
Enum.flat_map(resources, fn resource ->
|
||||
case map_and_filter_compatible_resource(resource, client_version) do
|
||||
{:cont, resource} -> [resource]
|
||||
:drop -> []
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_and_filter_compatible_resource(resource, client_version) do
|
||||
if Version.match?(client_version, ">= 1.2.0") do
|
||||
{:cont, resource}
|
||||
else
|
||||
resource.address
|
||||
|> String.codepoints()
|
||||
|> Resources.map_resource_address()
|
||||
|> case do
|
||||
{:cont, address} -> {:cont, %{resource | address: address}}
|
||||
:drop -> :drop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,7 @@ defmodule API.Client.Views.Resource do
|
||||
id: resource.id,
|
||||
type: :cidr,
|
||||
address: address,
|
||||
# TODO: This is a workaround due to clients expecting address_description not
|
||||
# to be null. Remove this to send null address_description on or after 8/13/24
|
||||
# once we can reasonably expect clients to have upgraded.
|
||||
address_description: resource.address_description || address,
|
||||
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)
|
||||
@@ -30,10 +27,7 @@ defmodule API.Client.Views.Resource do
|
||||
id: resource.id,
|
||||
type: resource.type,
|
||||
address: resource.address,
|
||||
# TODO: This is a workaround due to clients expecting address_description not
|
||||
# to be null. Remove this to send null address_description on or after 8/13/24
|
||||
# once we can reasonably expect clients to have upgraded.
|
||||
address_description: resource.address_description || resource.address,
|
||||
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)
|
||||
|
||||
@@ -289,6 +289,92 @@ defmodule API.Client.ChannelTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "sends backwards compatible list of resources if client version is below 1.2", %{
|
||||
account: account,
|
||||
subject: subject,
|
||||
client: client,
|
||||
gateway_group: gateway_group,
|
||||
actor_group: actor_group
|
||||
} do
|
||||
assert_push "init", %{}
|
||||
|
||||
star_mapped_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
address: "**.glob-example.com",
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
question_mark_mapped_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
address: "*.question-example.com",
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
mid_question_mark_mapped_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
address: "foo.*.example.com",
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
mid_star_mapped_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
address: "foo.**.glob-example.com",
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
mid_single_char_mapped_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
address: "us-east?-d.glob-example.com",
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
for resource <- [
|
||||
star_mapped_resource,
|
||||
question_mark_mapped_resource,
|
||||
mid_question_mark_mapped_resource,
|
||||
mid_star_mapped_resource,
|
||||
mid_single_char_mapped_resource
|
||||
] do
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource
|
||||
)
|
||||
end
|
||||
|
||||
API.Client.Socket
|
||||
|> socket("client:#{client.id}", %{
|
||||
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
|
||||
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
|
||||
client: client,
|
||||
subject: subject
|
||||
})
|
||||
|> subscribe_and_join(API.Client.Channel, "client")
|
||||
|
||||
assert_push "init", %{
|
||||
resources: resources
|
||||
}
|
||||
|
||||
resource_addresses = Enum.map(resources, & &1.address)
|
||||
|
||||
assert "*.glob-example.com" in resource_addresses
|
||||
assert "?.question-example.com" in resource_addresses
|
||||
|
||||
assert "foo.*.example.com" not in resource_addresses
|
||||
assert "foo.?.example.com" not in resource_addresses
|
||||
|
||||
assert "foo.**.glob-example.com" not in resource_addresses
|
||||
assert "foo.*.glob-example.com" not in resource_addresses
|
||||
|
||||
assert "us-east?-d.glob-example.com" not in resource_addresses
|
||||
assert "us-east*-d.glob-example.com" not in resource_addresses
|
||||
end
|
||||
|
||||
test "subscribes for client events", %{
|
||||
client: client
|
||||
} do
|
||||
|
||||
@@ -283,4 +283,30 @@ defmodule Domain.Resources do
|
||||
|> account_topic()
|
||||
|> PubSub.broadcast(payload)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# This is the code that will be removed in future version of Firezone (in 1.3-1.4)
|
||||
# and is reused to prevent breaking changes
|
||||
def map_resource_address(address, acc \\ "")
|
||||
|
||||
def map_resource_address(["*", "*" | rest], ""),
|
||||
do: map_resource_address(rest, "*")
|
||||
|
||||
def map_resource_address(["*", "*" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address(["*" | rest], ""),
|
||||
do: map_resource_address(rest, "?")
|
||||
|
||||
def map_resource_address(["*" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address(["?" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address([char | rest], acc),
|
||||
do: map_resource_address(rest, acc <> char)
|
||||
|
||||
def map_resource_address([], acc),
|
||||
do: {:cont, acc}
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule Domain.Resources.Resource.Changeset do
|
||||
|> validate_does_not_end_with(:address, "localhost",
|
||||
message: "localhost cannot be used, please add a DNS alias to /etc/hosts instead"
|
||||
)
|
||||
|> validate_format(:address, ~r/^([*?]\.)?[\p{L}0-9-]{1,63}(\.[\p{L}0-9-]{1,63})*$/iu)
|
||||
|> validate_format(:address, ~r/^[\p{L}\*\?0-9-]{1,63}(\.[\p{L}\*\?0-9-]{1,63})*$/iu)
|
||||
end
|
||||
|
||||
defp validate_cidr_address(changeset) do
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
defmodule Domain.Repo.Migrations.MigrateDnsResourcePatterns do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
execute("""
|
||||
UPDATE resources
|
||||
SET address = replace(replace(address, '*', '**'), '?', '*')
|
||||
WHERE type = 'dns'
|
||||
""")
|
||||
end
|
||||
end
|
||||
@@ -640,8 +640,8 @@ IO.puts("")
|
||||
Resources.create_resource(
|
||||
%{
|
||||
type: :dns,
|
||||
name: "*.firez.one",
|
||||
address: "*.firez.one",
|
||||
name: "**.firez.one",
|
||||
address: "**.firez.one",
|
||||
address_description: "https://firez.one/",
|
||||
connections: [%{gateway_group_id: gateway_group.id}],
|
||||
filters: []
|
||||
@@ -653,8 +653,8 @@ IO.puts("")
|
||||
Resources.create_resource(
|
||||
%{
|
||||
type: :dns,
|
||||
name: "?.firezone.dev",
|
||||
address: "?.firezone.dev",
|
||||
name: "*.firezone.dev",
|
||||
address: "*.firezone.dev",
|
||||
address_description: "https://firezone.dev/",
|
||||
connections: [%{gateway_group_id: gateway_group.id}],
|
||||
filters: []
|
||||
@@ -751,8 +751,8 @@ IO.puts("")
|
||||
Resources.create_resource(
|
||||
%{
|
||||
type: :dns,
|
||||
name: "?.httpbin",
|
||||
address: "?.httpbin",
|
||||
name: "*.httpbin",
|
||||
address: "*.httpbin",
|
||||
address_description: "http://httpbin/",
|
||||
connections: [%{gateway_group_id: gateway_group.id}],
|
||||
filters: [
|
||||
|
||||
@@ -2752,7 +2752,7 @@ defmodule Domain.ActorsTest do
|
||||
assert {:ok, _actor} = disable_actor(actor, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "returns error when trying to disable the last admin actor" do
|
||||
@@ -3003,7 +3003,7 @@ defmodule Domain.ActorsTest do
|
||||
assert {:ok, _actor} = delete_actor(actor, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "returns error when trying to delete the last admin actor", %{
|
||||
|
||||
@@ -933,7 +933,7 @@ defmodule Domain.AuthTest do
|
||||
assert {:ok, _provider} = disable_provider(provider, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "returns error when trying to disable the last provider", %{
|
||||
@@ -1153,7 +1153,7 @@ defmodule Domain.AuthTest do
|
||||
assert {:ok, _provider} = delete_provider(provider, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "returns error when trying to delete the last provider", %{
|
||||
@@ -2876,7 +2876,7 @@ defmodule Domain.AuthTest do
|
||||
assert delete_identities_for(actor, subject) == :ok
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "updates dynamic group memberships", %{
|
||||
|
||||
@@ -930,7 +930,7 @@ defmodule Domain.FlowsTest do
|
||||
actor_group: actor_group
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(actor_group)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -939,7 +939,7 @@ defmodule Domain.FlowsTest do
|
||||
identity: identity
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(identity)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
end
|
||||
@@ -974,7 +974,7 @@ defmodule Domain.FlowsTest do
|
||||
policy: policy
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(actor.id, policy.actor_group_id)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -984,7 +984,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(actor, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -994,7 +994,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(policy, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -1004,7 +1004,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(resource, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -1014,7 +1014,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(actor_group, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -1024,7 +1024,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(identity, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
@@ -1034,7 +1034,7 @@ defmodule Domain.FlowsTest do
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, [expired_flow]} = expire_flows_for(provider, subject)
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expired_flow.expires_at, DateTime.utc_now()) <= 1
|
||||
assert expired_flow.id == flow.id
|
||||
end
|
||||
|
||||
|
||||
@@ -511,7 +511,7 @@ defmodule Domain.PoliciesTest do
|
||||
assert {:ok, _policy} = disable_policy(policy, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "broadcasts an account message when policy is disabled", %{
|
||||
@@ -888,7 +888,7 @@ defmodule Domain.PoliciesTest do
|
||||
assert {:ok, [_deleted_policy]} = delete_policies_for(resource, subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "broadcasts an account message when policy is deleted", %{
|
||||
|
||||
@@ -917,7 +917,7 @@ defmodule Domain.ResourcesTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "validates dns address", %{subject: subject} do
|
||||
test "validates dns address", %{account: account, subject: subject} do
|
||||
attrs = %{"address" => String.duplicate("a", 256), "type" => "dns"}
|
||||
assert {:error, changeset} = create_resource(attrs, subject)
|
||||
assert "should be at most 253 character(s)" in errors_on(changeset).address
|
||||
@@ -925,6 +925,50 @@ defmodule Domain.ResourcesTest do
|
||||
attrs = %{"address" => "a", "type" => "dns"}
|
||||
assert {:error, changeset} = create_resource(attrs, subject)
|
||||
refute Map.has_key?(errors_on(changeset), :address)
|
||||
|
||||
for dns <- [
|
||||
"**.example.com",
|
||||
"example.com",
|
||||
"app.**.example.com",
|
||||
"app.bar.foo.example.com",
|
||||
"**.example.com",
|
||||
"foo.example.com",
|
||||
"**.example.com",
|
||||
"foo.bar.example.com",
|
||||
"*.example.com",
|
||||
"foo.example.com",
|
||||
"*.example.com",
|
||||
"example.com",
|
||||
"foo.*.example.com",
|
||||
"foo.bar.example.com",
|
||||
"app.*.*.example.com",
|
||||
"app.foo.bar.example.com",
|
||||
"app.f??.example.com",
|
||||
"app.foo.example.com",
|
||||
"app.example.com",
|
||||
"app.example.com",
|
||||
"*?*.example.com",
|
||||
"app.example.com",
|
||||
"app.**.web.**.example.com",
|
||||
"app.web.example.com",
|
||||
"app.*.example.com",
|
||||
"app.*com",
|
||||
"app?com",
|
||||
"google.com",
|
||||
"myhost"
|
||||
] do
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
attrs =
|
||||
Fixtures.Resources.resource_attrs(
|
||||
address: dns,
|
||||
connections: [
|
||||
%{gateway_group_id: gateway.group_id}
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, _resource} = create_resource(attrs, subject)
|
||||
end
|
||||
end
|
||||
|
||||
test "validates cidr address", %{subject: subject} do
|
||||
|
||||
@@ -524,7 +524,7 @@ defmodule Domain.TokensTest do
|
||||
assert {:ok, _token} = delete_token_for(subject)
|
||||
|
||||
expires_at = Repo.one(Domain.Flows.Flow).expires_at
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) < 1
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) <= 1
|
||||
end
|
||||
|
||||
test "does not delete tokens for other actors", %{account: account, subject: subject} do
|
||||
|
||||
@@ -134,12 +134,32 @@ defmodule Web.Resources.New do
|
||||
disabled={is_nil(@form[:type].value)}
|
||||
required
|
||||
/>
|
||||
<p
|
||||
:if={
|
||||
@form[:type].value == :dns and
|
||||
is_binary(@form[:address].value) and
|
||||
@form[:address].value
|
||||
|> String.codepoints()
|
||||
|> Resources.map_resource_address() == :drop
|
||||
}
|
||||
class="flex items-center gap-2 text-sm leading-6 text-accent-600 mt-2 w-full"
|
||||
>
|
||||
<.icon name="hero-exclamation-triangle" class="w-4 h-4" />
|
||||
This is an advanced address format. This Resource will be available to Clients v1.2.0 and higher only.
|
||||
</p>
|
||||
<p :if={@form[:type].value == :dns} class="mt-2 text-xs text-neutral-500">
|
||||
Wildcards are supported:<br />
|
||||
<code class="ml-2 px-0.5 font-semibold">**.c.com</code>
|
||||
matches any level of subdomains (e.g., <code class="px-0.5 font-semibold">foo.c.com</code>,
|
||||
<code class="px-0.5 font-semibold">bar.foo.c.com</code>
|
||||
and <code class="px-0.5 font-semibold">c.com</code>).<br />
|
||||
<code class="ml-2 px-0.5 font-semibold">*.c.com</code>
|
||||
will match recursively (<code class="px-0.5 font-semibold">b.c.com</code> and <code class="px-0.5 font-semibold">a.b.c.com</code>).<br />
|
||||
<code class="ml-2 px-0.5 font-semibold">?.c.com</code>
|
||||
will match top-level subdomains only (<code class="px-0.5 font-semibold">b.c.com</code>).
|
||||
matches a zero and single level subdomains (e.g.,
|
||||
<code class="px-0.5 font-semibold">foo.c.com</code>
|
||||
and <code class="px-0.5 font-semibold">c.com</code>
|
||||
but not <code class="px-0.5 font-semibold">bar.foo.c.com</code>). <br />
|
||||
<code class="ml-2 px-0.5 font-semibold">us-east?.c.com</code>
|
||||
matches a single character (e.g., <code class="px-0.5 font-semibold">us-east1.c.com</code>).
|
||||
</p>
|
||||
<p :if={@form[:type].value == :ip} class="mt-2 text-xs text-neutral-500">
|
||||
IPv4 and IPv6 addresses are supported.
|
||||
|
||||
@@ -30,6 +30,19 @@ From there, you can select the type of Resource you want to create:
|
||||
- To non-recursively match all subdomains, use a question mark, such as
|
||||
`?.example.com`. This will match `example.com`, `sub1.example.com`, and
|
||||
`sub2.example.com` **but not** `sub1.sub2.example.com`.
|
||||
{/* - By default, the pattern will only match the exact name you enter. */}
|
||||
{/* - To match all subdomains recursively, use a double-wildcard, such as */}
|
||||
{/* `**.example.com`. This will match `example.com`, `sub.example.com`, and */}
|
||||
{/* `sub.sub.example.com`. */}
|
||||
{/* - To match all subdomains non-recursively, use a single wildcard, such as */}
|
||||
{/* `*.example.com`. This will match `sub.example.com` but not */}
|
||||
{/* `sub.sub.example.com`. */}
|
||||
{/* - To match a single character, use a question mark, such as */}
|
||||
{/* `us-east?.example.com`. This will match `us-east1.example.com` but not */}
|
||||
{/* `us-eastXY.example.com`. */}
|
||||
{/* - Wildcards can be placed between domain components, e.g., `foo.*.example.com` */}
|
||||
{/* will match `foo.bar.example.com` or `foo.**.example.com` will match */}
|
||||
{/* `foo.bar.baz.example.com`. */}
|
||||
- **IP**: A single IPv4 or IPv6 address
|
||||
- **CIDR**: A range of IPv4 or IPv6 addresses in CIDR notation, such as
|
||||
`10.1.2.0/24` or `2001:db8::/48`
|
||||
|
||||
Reference in New Issue
Block a user