diff --git a/elixir/apps/api/lib/api/client/channel.ex b/elixir/apps/api/lib/api/client/channel.ex index 1b9819135..5fd148251 100644 --- a/elixir/apps/api/lib/api/client/channel.ex +++ b/elixir/apps/api/lib/api/client/channel.ex @@ -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 diff --git a/elixir/apps/api/lib/api/gateway/channel.ex b/elixir/apps/api/lib/api/gateway/channel.ex index 749debbca..ee73c2b71 100644 --- a/elixir/apps/api/lib/api/gateway/channel.ex +++ b/elixir/apps/api/lib/api/gateway/channel.ex @@ -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 diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index 14b8b8040..4930fa8a1 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -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, diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index 93288d754..00c37f1a6 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -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: [ diff --git a/elixir/apps/web/lib/web/live/resources/new.ex b/elixir/apps/web/lib/web/live/resources/new.ex index 6cd95ca75..f85441f59 100644 --- a/elixir/apps/web/lib/web/live/resources/new.ex +++ b/elixir/apps/web/lib/web/live/resources/new.ex @@ -134,6 +134,7 @@ defmodule Web.Resources.New do disabled={is_nil(@form[:type].value)} required /> +

<.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.

-

- Wildcards are supported:
- **.c.com - matches any level of subdomains (e.g., foo.c.com, - bar.foo.c.com - and c.com).
- *.c.com - matches a zero and single level subdomains (e.g., - foo.c.com - and c.com - but not bar.foo.c.com).
- us-east?.c.com - matches a single character (e.g., us-east1.c.com). -

-

+

+
+ <.badge type="info" class="p-0 mr-2">NEW + Wildcard matching is supported: +
+
+ **.c.com + matches any level of subdomains (e.g., foo.c.com, + bar.foo.c.com + and c.com).
+ *.c.com + matches a zero and single level subdomains (e.g., + foo.c.com + and c.com + but not bar.foo.c.com).
+ us-east?.c.com + matches a single character (e.g., us-east1.c.com). +
+
+
IPv4 and IPv6 addresses are supported. -

-

+

+
IPv4 and IPv6 CIDR ranges are supported. -

+