mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
fix(portal): Fix DNS wildcard support for Gateways (#6270)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user