fix(portal): Fix DNS wildcard support for Gateways (#6270)

This commit is contained in:
Andrew Dryga
2024-08-12 12:54:20 -06:00
committed by GitHub
parent 7fed443b74
commit c922ea29e9
5 changed files with 210 additions and 64 deletions

View File

@@ -226,7 +226,7 @@ defmodule API.Client.Channel do
preload: [:gateway_groups]
) do
{:ok, resource} ->
case map_and_filter_compatible_resource(
case map_or_drop_compatible_resource(
resource,
socket.assigns.client.last_seen_version
) do
@@ -285,7 +285,7 @@ defmodule API.Client.Channel do
preload: [:gateway_groups]
) do
{:ok, resource} ->
case map_and_filter_compatible_resource(
case map_or_drop_compatible_resource(
resource,
socket.assigns.client.last_seen_version
) do
@@ -331,7 +331,7 @@ defmodule API.Client.Channel do
preload: [:gateway_groups]
) do
{:ok, resource} ->
case map_and_filter_compatible_resource(
case map_or_drop_compatible_resource(
resource,
socket.assigns.client.last_seen_version
) do
@@ -483,8 +483,12 @@ defmodule API.Client.Channel do
Gateways.all_connected_gateways_for_resource(resource, socket.assigns.subject,
preload: :group
),
{:ok, gateways} <-
filter_compatible_gateways(gateways, socket.assigns.gateway_version_requirement) do
gateway_version_requirement =
maybe_update_gateway_version_requirement(
resource,
socket.assigns.gateway_version_requirement
),
{:ok, gateways} <- filter_compatible_gateways(gateways, gateway_version_requirement) do
location = {
socket.assigns.client.last_seen_remote_ip_location_lat,
socket.assigns.client.last_seen_remote_ip_location_lon
@@ -731,6 +735,16 @@ defmodule API.Client.Channel do
end
end
defp maybe_update_gateway_version_requirement(resource, gateway_version_requirement) do
case map_or_drop_compatible_resource(resource, "1.0.0") do
{:cont, _resource} ->
gateway_version_requirement
:drop ->
">= 1.2.0"
end
end
defp filter_compatible_gateways(gateways, gateway_version_requirement) do
gateways
|> Enum.filter(fn gateway ->
@@ -744,15 +758,15 @@ defmodule API.Client.Channel do
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
case map_or_drop_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
def map_or_drop_compatible_resource(resource, client_or_gateway_version) do
if Version.match?(client_or_gateway_version, ">= 1.2.0") do
{:cont, resource}
else
resource.address

View File

@@ -132,26 +132,41 @@ defmodule API.Gateway.Channel do
:ok = Flows.subscribe_to_flow_expiration_events(flow_id)
resource = Resources.fetch_resource_by_id!(resource_id)
:ok = Resources.unsubscribe_from_events_for_resource(resource_id)
:ok = Resources.subscribe_to_events_for_resource(resource_id)
opentelemetry_headers = :otel_propagator_text_map.inject([])
ref = encode_ref(socket, channel_pid, socket_ref, resource_id, opentelemetry_headers)
case API.Client.Channel.map_or_drop_compatible_resource(
resource,
socket.assigns.gateway.last_seen_version
) do
{:cont, resource} ->
:ok = Resources.unsubscribe_from_events_for_resource(resource_id)
:ok = Resources.subscribe_to_events_for_resource(resource_id)
push(socket, "allow_access", %{
ref: ref,
client_id: client_id,
flow_id: flow_id,
resource: Views.Resource.render(resource),
expires_at: DateTime.to_unix(authorization_expires_at, :second),
payload: payload
})
opentelemetry_headers = :otel_propagator_text_map.inject([])
ref = encode_ref(socket, channel_pid, socket_ref, resource_id, opentelemetry_headers)
Logger.debug("Awaiting gateway connection_ready message",
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
push(socket, "allow_access", %{
ref: ref,
client_id: client_id,
flow_id: flow_id,
resource: Views.Resource.render(resource),
expires_at: DateTime.to_unix(authorization_expires_at, :second),
payload: payload
})
Logger.debug("Awaiting gateway connection_ready message",
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
:drop ->
Logger.debug("Resource is not compatible with the gateway version",
gateway_id: socket.assigns.gateway.id,
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
end
{:noreply, socket}
end
@@ -165,7 +180,21 @@ defmodule API.Gateway.Channel do
OpenTelemetry.Tracer.with_span "gateway.resource_updated",
attributes: %{resource_id: resource_id} do
resource = Resources.fetch_resource_by_id!(resource_id)
push(socket, "resource_updated", Views.Resource.render(resource))
case API.Client.Channel.map_or_drop_compatible_resource(
resource,
socket.assigns.gateway.last_seen_version
) do
{:cont, resource} ->
push(socket, "resource_updated", Views.Resource.render(resource))
:drop ->
Logger.debug("Resource is not compatible with the gateway version",
gateway_id: socket.assigns.gateway.id,
resource_id: resource_id
)
end
{:noreply, socket}
end
end
@@ -229,26 +258,41 @@ defmodule API.Gateway.Channel do
client = Clients.fetch_client_by_id!(client_id, preload: [:actor])
resource = Resources.fetch_resource_by_id!(resource_id)
:ok = Resources.unsubscribe_from_events_for_resource(resource_id)
:ok = Resources.subscribe_to_events_for_resource(resource_id)
opentelemetry_headers = :otel_propagator_text_map.inject([])
ref = encode_ref(socket, channel_pid, socket_ref, resource_id, opentelemetry_headers)
case API.Client.Channel.map_or_drop_compatible_resource(
resource,
socket.assigns.gateway.last_seen_version
) do
{:cont, resource} ->
:ok = Resources.unsubscribe_from_events_for_resource(resource_id)
:ok = Resources.subscribe_to_events_for_resource(resource_id)
push(socket, "request_connection", %{
ref: ref,
flow_id: flow_id,
actor: Views.Actor.render(client.actor),
resource: Views.Resource.render(resource),
client: Views.Client.render(client, payload, preshared_key),
expires_at: DateTime.to_unix(authorization_expires_at, :second)
})
opentelemetry_headers = :otel_propagator_text_map.inject([])
ref = encode_ref(socket, channel_pid, socket_ref, resource_id, opentelemetry_headers)
Logger.debug("Awaiting gateway connection_ready message",
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
push(socket, "request_connection", %{
ref: ref,
flow_id: flow_id,
actor: Views.Actor.render(client.actor),
resource: Views.Resource.render(resource),
client: Views.Client.render(client, payload, preshared_key),
expires_at: DateTime.to_unix(authorization_expires_at, :second)
})
Logger.debug("Awaiting gateway connection_ready message",
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
:drop ->
Logger.debug("Resource is not compatible with the gateway version",
gateway_id: socket.assigns.gateway.id,
client_id: client_id,
resource_id: resource_id,
flow_id: flow_id
)
end
{:noreply, socket}
end

View File

@@ -1004,6 +1004,88 @@ defmodule API.Client.ChannelTest do
assert gateway_last_seen_remote_ip == gateway.last_seen_remote_ip
end
test "returns gateways that support the resource version", %{
account: account,
dns_resource: resource,
socket: socket
} do
gateway = Fixtures.Gateways.create_gateway(account: account)
:ok = Domain.Gateways.connect_gateway(gateway)
ref = push(socket, "prepare_connection", %{"resource_id" => resource.id})
assert_reply ref, :error, %{reason: :offline}
end
test "returns gateway that support the DNS resource address syntax", %{
account: account,
actor_group: actor_group,
socket: socket
} do
global_relay_group = Fixtures.Relays.create_global_group()
global_relay = Fixtures.Relays.create_relay(group: global_relay_group)
stamp_secret = Ecto.UUID.generate()
:ok = Domain.Relays.connect_relay(global_relay, stamp_secret)
Fixtures.Relays.update_relay(global_relay,
last_seen_at: DateTime.utc_now() |> DateTime.add(-10, :second)
)
gateway_group = Fixtures.Gateways.create_group(account: account)
gateway =
Fixtures.Gateways.create_gateway(
account: account,
group: gateway_group,
last_seen_version: "1.1.0"
)
resource =
Fixtures.Resources.create_resource(
address: "foo.*.example.com",
account: account,
connections: [%{gateway_group_id: gateway_group.id}]
)
Fixtures.Policies.create_policy(
account: account,
actor_group: actor_group,
resource: resource
)
:ok = Domain.Gateways.connect_gateway(gateway)
ref = push(socket, "prepare_connection", %{"resource_id" => resource.id})
resource_id = resource.id
assert_reply ref, :error, %{reason: :not_found}
gateway =
Fixtures.Gateways.create_gateway(
account: account,
group: gateway_group,
context: %{
user_agent: "iOS/12.5 (iPhone) connlib/1.2.0"
}
)
Fixtures.Relays.update_relay(global_relay,
last_seen_at: DateTime.utc_now() |> DateTime.add(-10, :second)
)
:ok = Domain.Gateways.connect_gateway(gateway)
ref = push(socket, "prepare_connection", %{"resource_id" => resource.id})
assert_reply ref, :ok, %{
gateway_id: gateway_id,
gateway_remote_ip: gateway_last_seen_remote_ip,
resource_id: ^resource_id
}
assert gateway_id == gateway.id
assert gateway_last_seen_remote_ip == gateway.last_seen_remote_ip
end
test "works with service accounts", %{
account: account,
dns_resource: resource,

View File

@@ -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: [

View File

@@ -134,6 +134,7 @@ defmodule Web.Resources.New do
disabled={is_nil(@form[:type].value)}
required
/>
<p
:if={
@form[:type].value == :dns and
@@ -145,28 +146,33 @@ defmodule Web.Resources.New do
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.
This is an advanced address format. This Resource will be available to Clients and Gateways 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>
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">
<div :if={@form[:type].value == :dns}>
<div class="mt-2 text-xs text-neutral-500">
<.badge type="info" class="p-0 mr-2">NEW</.badge>
Wildcard matching is supported:
</div>
<div class="mt-2 text-xs text-neutral-500">
<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>
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>).
</div>
</div>
<div :if={@form[:type].value == :ip} class="mt-2 text-xs text-neutral-500">
IPv4 and IPv6 addresses are supported.
</p>
<p :if={@form[:type].value == :cidr} class="mt-2 text-xs text-neutral-500">
</div>
<div :if={@form[:type].value == :cidr} class="mt-2 text-xs text-neutral-500">
IPv4 and IPv6 CIDR ranges are supported.
</p>
</div>
</div>
<div>