From 8e34457340a34a7e887aa6d5393e31bc7c2f0f4e Mon Sep 17 00:00:00 2001 From: Gabi Date: Fri, 8 Dec 2023 00:16:42 -0500 Subject: [PATCH] Add support for DNS sudomains (#2735) This PR changes the protocol and adds support for DNS subdomains, now when a DNS resource is added all its subdomains are automatically tunneled too. Later we will add support for `*.domain` or `?.domain` but currently there is an Apple split tunnel implementation limitation which is too labor-intensive to fix right away. Fixes #2661 Co-authored-by: Andrew Dryga --- docker-compose.yml | 11 +- elixir/README.md | 2 +- elixir/apps/api/lib/api/client/channel.ex | 19 +- .../apps/api/lib/api/client/views/resource.ex | 24 +- elixir/apps/api/lib/api/gateway/channel.ex | 44 +- .../apps/api/lib/api/gateway/views/client.ex | 4 +- .../api/lib/api/gateway/views/resource.ex | 16 +- .../apps/api/test/api/client/channel_test.exs | 87 ++- .../api/test/api/gateway/channel_test.exs | 73 ++- .../domain/auth/adapters/openid_connect.ex | 3 +- elixir/apps/domain/lib/domain/flows.ex | 3 +- elixir/apps/domain/lib/domain/gateways.ex | 3 +- elixir/apps/domain/lib/domain/network.ex | 4 +- elixir/apps/domain/lib/domain/policies.ex | 9 +- elixir/apps/domain/lib/domain/relays.ex | 3 +- elixir/apps/domain/lib/domain/resources.ex | 47 +- .../domain/lib/domain/resources/resource.ex | 5 +- .../domain/resources/resource/changeset.ex | 99 +-- elixir/apps/domain/lib/domain/types/cidr.ex | 6 +- elixir/apps/domain/lib/domain/types/ip.ex | 3 +- elixir/apps/domain/lib/domain/validator.ex | 40 +- ...22195751_remove_resources_ipvx_address.exs | 25 + elixir/apps/domain/priv/repo/seeds.exs | 127 +++- elixir/apps/domain/test/domain/auth_test.exs | 30 +- elixir/apps/domain/test/domain/flows_test.exs | 19 +- .../apps/domain/test/domain/policies_test.exs | 3 +- .../resources/resource/changeset_test.exs | 85 +++ .../domain/test/domain/resources_test.exs | 79 +-- .../domain/test/support/fixtures/actors.ex | 6 +- .../apps/domain/test/support/fixtures/auth.ex | 3 +- .../mocks/google_workspace_directory.ex | 590 +++++++++--------- .../lib/web/controllers/auth_controller.ex | 3 +- .../lib/web/controllers/home_controller.ex | 6 +- elixir/apps/web/lib/web/live/resources/new.ex | 72 ++- elixir/apps/web/test/web/auth_test.exs | 9 +- .../web/test/web/live/resources/new_test.exs | 64 +- .../identity_providers/index_test.exs | 3 +- .../settings/identity_providers/new_test.exs | 3 +- .../openid_connect/edit_test.exs | 3 +- .../openid_connect/show_test.exs | 3 +- rust/Cargo.lock | 282 +++++---- rust/Cargo.toml | 1 + rust/connlib/clients/shared/src/control.rs | 29 +- rust/connlib/clients/shared/src/messages.rs | 29 +- rust/connlib/shared/Cargo.toml | 1 + rust/connlib/shared/src/error.rs | 3 + rust/connlib/shared/src/lib.rs | 2 + rust/connlib/shared/src/messages.rs | 77 ++- rust/connlib/tunnel/Cargo.toml | 5 +- rust/connlib/tunnel/src/client.rs | 368 ++++++++--- rust/connlib/tunnel/src/control_protocol.rs | 22 +- .../tunnel/src/control_protocol/client.rs | 163 ++++- .../tunnel/src/control_protocol/gateway.rs | 159 +++-- .../src/device_channel/device_channel_unix.rs | 4 +- .../src/device_channel/device_channel_win.rs | 7 +- .../src/device_channel/tun/tun_android.rs | 8 +- .../src/device_channel/tun/tun_darwin.rs | 3 + .../src/device_channel/tun/tun_linux.rs | 3 + rust/connlib/tunnel/src/dns.rs | 220 +++++-- rust/connlib/tunnel/src/gateway.rs | 57 +- rust/connlib/tunnel/src/ip_packet.rs | 14 + rust/connlib/tunnel/src/lib.rs | 179 +++--- rust/connlib/tunnel/src/peer.rs | 360 +++++------ rust/connlib/tunnel/src/peer_handler.rs | 12 +- rust/connlib/tunnel/src/resource_table.rs | 188 ------ rust/gateway/Cargo.toml | 1 + rust/gateway/src/eventloop.rs | 32 +- rust/gateway/src/messages.rs | 30 +- .../src-tauri/src/debug_commands.rs | 2 +- rust/windows-client/src-tauri/src/main.rs | 2 +- 70 files changed, 2333 insertions(+), 1568 deletions(-) create mode 100644 elixir/apps/domain/priv/repo/migrations/20231122195751_remove_resources_ipvx_address.exs create mode 100644 elixir/apps/domain/test/domain/resources/resource/changeset_test.exs delete mode 100644 rust/connlib/tunnel/src/resource_table.rs diff --git a/docker-compose.yml b/docker-compose.yml index 9846f3b90..bcd964824 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,7 @@ services: client: environment: FIREZONE_TOKEN: "SFMyNTY.g2gDaAN3CGlkZW50aXR5bQAAACQ3ZGE3ZDFjZC0xMTFjLTQ0YTctYjVhYy00MDI3YjlkMjMwZTVtAAAAIBn8Xu1jtFlxZxp4ZvAz0f0QEN2PZThA-7awHMPxn_tHbgYAbLRvQokBYgHhM38.pM-prhb7uvvCVKf51-tAUMEtMzLPZk1n3nLsY44dGFA" - RUST_LOG: firezone_linux_client=trace,connlib_client_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn + RUST_LOG: firezone_linux_client=trace,wire=trace,connlib_client_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn FIREZONE_API_URL: ws://api:8081 FIREZONE_ID: D0455FDE-8F65-4960-A778-B934E4E85A5F build: @@ -128,7 +128,6 @@ services: image: us-east1-docker.pkg.dev/firezone-staging/firezone/client:${VERSION:-main} dns: - 100.100.111.1 - - 8.8.8.8 cap_add: - NET_ADMIN sysctls: @@ -151,7 +150,7 @@ services: test: ["CMD-SHELL", "ip link | grep tun-firezone"] environment: FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAEC0b0KJAWIAAVGA.9Oirn9t8rvQpfOhW7hwGBFVzeMm9di0xYGTlwf9cFFk" - RUST_LOG: firezone_gateway=trace,connlib_gateway_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn + RUST_LOG: firezone_gateway=trace,wire=trace,connlib_gateway_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn FIREZONE_ENABLE_MASQUERADE: 1 FIREZONE_API_URL: ws://api:8081 FIREZONE_ID: 4694E56C-7643-4A15-9DF3-638E5B05F570 @@ -371,15 +370,17 @@ services: networks: resources: - enable_ipv6: false + enable_ipv6: true ipam: config: - subnet: 172.20.0.0/16 + - subnet: 2001:db8:1::/64 app: - enable_ipv6: false + enable_ipv6: true ipam: config: - subnet: 172.28.0.0/16 + - subnet: 2001:db8:2::/64 volumes: postgres-data: diff --git a/elixir/README.md b/elixir/README.md index 21bb44bcd..2ef390af4 100644 --- a/elixir/README.md +++ b/elixir/README.md @@ -125,7 +125,7 @@ Now you can verify that it's working by connecting to a websocket: {"ref":"unique_prepare_connection_ref","topic":"client","event":"phx_reply","payload":{"status":"ok","response":{"relays":[{"type":"stun","uri":"stun:172.28.0.101:3478"},{"type":"turn","username":"1719090081:UVxHhieTJWaD8_Sg","password":"Ml65XDZyYpuBiEIvk/q0Zy6EEJ1ZwGa4pWztXFP+tOo","uri":"turn:172.28.0.101:3478","expires_at":1719090081}],"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b"}}} # Initiate connection to a resource -❯ {"event":"request_connection","topic":"client","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b","client_rtc_session_description":"RTC_SD","client_preshared_key":"+HapiGI5UdeRjKuKTwk4ZPPYpCnlXHvvqebcIevL+2A="},"ref":"unique_request_connection_ref"} +❯ {"event":"request_connection","topic":"client","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b","client_payload":"RTC_SD","client_preshared_key":"+HapiGI5UdeRjKuKTwk4ZPPYpCnlXHvvqebcIevL+2A="},"ref":"unique_request_connection_ref"} ``` diff --git a/elixir/apps/api/lib/api/client/channel.ex b/elixir/apps/api/lib/api/client/channel.ex index a575ac248..27ff8d380 100644 --- a/elixir/apps/api/lib/api/client/channel.ex +++ b/elixir/apps/api/lib/api/client/channel.ex @@ -99,7 +99,7 @@ defmodule API.Client.Channel do # This message is sent by the gateway when it is ready # to accept the connection from the client def handle_info( - {:connect, socket_ref, resource_id, gateway_public_key, rtc_session_description, + {:connect, socket_ref, resource_id, gateway_public_key, payload, {opentelemetry_ctx, opentelemetry_span_ctx}}, socket ) do @@ -114,7 +114,7 @@ defmodule API.Client.Channel do resource_id: resource_id, persistent_keepalive: 25, gateway_public_key: gateway_public_key, - gateway_rtc_session_description: rtc_session_description + gateway_payload: payload }} ) @@ -186,8 +186,7 @@ defmodule API.Client.Channel do {:ok, [_ | _] = gateways} <- Gateways.list_connected_gateways_for_resource(resource, preload: :group), gateway_groups = Enum.map(gateways, & &1.group), - {relay_hosting_type, relay_connection_type} = - Gateways.relay_strategy(gateway_groups), + {relay_hosting_type, relay_connection_type} = Gateways.relay_strategy(gateway_groups), {:ok, [_ | _] = relays} <- Relays.list_connected_relays_for_resource(resource, relay_hosting_type) do location = { @@ -236,7 +235,8 @@ defmodule API.Client.Channel do "reuse_connection", %{ "gateway_id" => gateway_id, - "resource_id" => resource_id + "resource_id" => resource_id, + "payload" => payload } = attrs, socket ) do @@ -259,12 +259,13 @@ defmodule API.Client.Channel do :ok = API.Gateway.Channel.broadcast( gateway, - {:allow_access, + {:allow_access, {self(), socket_ref(socket)}, %{ client_id: socket.assigns.client.id, resource_id: resource.id, flow_id: flow.id, - authorization_expires_at: socket.assigns.subject.expires_at + authorization_expires_at: socket.assigns.subject.expires_at, + client_payload: payload }, {opentelemetry_ctx, opentelemetry_span_ctx}} ) @@ -287,7 +288,7 @@ defmodule API.Client.Channel do %{ "gateway_id" => gateway_id, "resource_id" => resource_id, - "client_rtc_session_description" => client_rtc_session_description, + "client_payload" => client_payload, "client_preshared_key" => preshared_key }, socket @@ -318,7 +319,7 @@ defmodule API.Client.Channel do resource_id: resource.id, flow_id: flow.id, authorization_expires_at: socket.assigns.subject.expires_at, - client_rtc_session_description: client_rtc_session_description, + client_payload: client_payload, client_preshared_key: preshared_key }, {opentelemetry_ctx, opentelemetry_span_ctx}} ) diff --git a/elixir/apps/api/lib/api/client/views/resource.ex b/elixir/apps/api/lib/api/client/views/resource.ex index c368b8a50..4eb2febad 100644 --- a/elixir/apps/api/lib/api/client/views/resource.ex +++ b/elixir/apps/api/lib/api/client/views/resource.ex @@ -5,21 +5,23 @@ defmodule API.Client.Views.Resource do Enum.map(resources, &render/1) end - def render(%Resources.Resource{type: :dns} = resource) do - %{ - id: resource.id, - type: :dns, - address: resource.address, - name: resource.name, - ipv4: resource.ipv4, - ipv6: resource.ipv6 - } - end + def render(%Resources.Resource{type: :ip} = resource) do + {:ok, inet} = Domain.Types.IP.cast(resource.address) + netmask = Domain.Types.CIDR.max_netmask(inet) + address = to_string(%{inet | netmask: netmask}) - def render(%Resources.Resource{type: :cidr} = resource) do %{ id: resource.id, type: :cidr, + address: address, + name: resource.name + } + end + + def render(%Resources.Resource{} = resource) do + %{ + id: resource.id, + type: resource.type, address: resource.address, name: resource.name } diff --git a/elixir/apps/api/lib/api/gateway/channel.ex b/elixir/apps/api/lib/api/gateway/channel.ex index 47f77d809..85bc779be 100644 --- a/elixir/apps/api/lib/api/gateway/channel.ex +++ b/elixir/apps/api/lib/api/gateway/channel.ex @@ -76,7 +76,11 @@ defmodule API.Gateway.Channel do end end - def handle_info({:allow_access, attrs, {opentelemetry_ctx, opentelemetry_span_ctx}}, socket) do + def handle_info( + {:allow_access, {channel_pid, socket_ref}, attrs, + {opentelemetry_ctx, opentelemetry_span_ctx}}, + socket + ) do OpenTelemetry.Ctx.attach(opentelemetry_ctx) OpenTelemetry.Tracer.set_current_span(opentelemetry_span_ctx) @@ -85,18 +89,39 @@ defmodule API.Gateway.Channel do client_id: client_id, resource_id: resource_id, flow_id: flow_id, - authorization_expires_at: authorization_expires_at + authorization_expires_at: authorization_expires_at, + client_payload: payload } = attrs resource = Resources.fetch_resource_by_id!(resource_id) + ref = Ecto.UUID.generate() + 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) + 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, + ref: ref + ) + + refs = + Map.put( + socket.assigns.refs, + ref, + {channel_pid, socket_ref, resource_id, {opentelemetry_ctx, opentelemetry_span_ctx}} + ) + + socket = assign(socket, :refs, refs) + {:noreply, socket} end end @@ -136,7 +161,7 @@ defmodule API.Gateway.Channel do resource_id: resource_id, flow_id: flow_id, authorization_expires_at: authorization_expires_at, - client_rtc_session_description: rtc_session_description, + client_payload: payload, client_preshared_key: preshared_key } = attrs @@ -151,8 +176,7 @@ defmodule API.Gateway.Channel do {relay_hosting_type, relay_connection_type} = Gateways.relay_strategy([socket.assigns.gateway_group]) - {:ok, relays} = - Relays.list_connected_relays_for_resource(resource, relay_hosting_type) + {:ok, relays} = Relays.list_connected_relays_for_resource(resource, relay_hosting_type) ref = Ecto.UUID.generate() @@ -162,7 +186,7 @@ defmodule API.Gateway.Channel do actor: Views.Actor.render(client.actor), relays: Views.Relay.render_many(relays, authorization_expires_at, relay_connection_type), resource: Views.Resource.render(resource), - client: Views.Client.render(client, rtc_session_description, preshared_key), + client: Views.Client.render(client, payload, preshared_key), expires_at: DateTime.to_unix(authorization_expires_at, :second) }) @@ -191,7 +215,7 @@ defmodule API.Gateway.Channel do "connection_ready", %{ "ref" => ref, - "gateway_rtc_session_description" => rtc_session_description + "gateway_payload" => payload }, socket ) do @@ -209,8 +233,8 @@ defmodule API.Gateway.Channel do send( channel_pid, - {:connect, socket_ref, resource_id, socket.assigns.gateway.public_key, - rtc_session_description, {opentelemetry_ctx, opentelemetry_span_ctx}} + {:connect, socket_ref, resource_id, socket.assigns.gateway.public_key, payload, + {opentelemetry_ctx, opentelemetry_span_ctx}} ) Logger.debug("Gateway replied to the Client with :connect message", diff --git a/elixir/apps/api/lib/api/gateway/views/client.ex b/elixir/apps/api/lib/api/gateway/views/client.ex index 34cf2bd97..051fc2d22 100644 --- a/elixir/apps/api/lib/api/gateway/views/client.ex +++ b/elixir/apps/api/lib/api/gateway/views/client.ex @@ -1,10 +1,10 @@ defmodule API.Gateway.Views.Client do alias Domain.Clients - def render(%Clients.Client{} = client, client_rtc_session_description, preshared_key) do + def render(%Clients.Client{} = client, client_payload, preshared_key) do %{ id: client.id, - rtc_session_description: client_rtc_session_description, + payload: client_payload, peer: %{ persistent_keepalive: 25, public_key: client.public_key, diff --git a/elixir/apps/api/lib/api/gateway/views/resource.ex b/elixir/apps/api/lib/api/gateway/views/resource.ex index 6522933cc..d983bed44 100644 --- a/elixir/apps/api/lib/api/gateway/views/resource.ex +++ b/elixir/apps/api/lib/api/gateway/views/resource.ex @@ -7,8 +7,6 @@ defmodule API.Gateway.Views.Resource do type: :dns, address: resource.address, name: resource.name, - ipv4: resource.ipv4, - ipv6: resource.ipv6, filters: Enum.flat_map(resource.filters, &render_filter/1) } end @@ -23,6 +21,20 @@ defmodule API.Gateway.Views.Resource do } end + def render(%Resources.Resource{type: :ip} = resource) do + {:ok, inet} = Domain.Types.IP.cast(resource.address) + netmask = Domain.Types.CIDR.max_netmask(inet) + address = to_string(%{inet | netmask: netmask}) + + %{ + id: resource.id, + type: :cidr, + address: address, + name: resource.name, + filters: Enum.flat_map(resource.filters, &render_filter/1) + } + end + def render_filter(%Resources.Resource.Filter{} = filter) do Enum.map(filter.ports, fn port -> case String.split(port, "-") do diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index 7b0b5fd88..1e294c5ca 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -38,6 +38,14 @@ defmodule API.Client.ChannelTest do connections: [%{gateway_group_id: gateway_group.id}] ) + ip_resource = + Fixtures.Resources.create_resource( + type: :ip, + address: "192.168.100.1", + account: account, + connections: [%{gateway_group_id: gateway_group.id}] + ) + unauthorized_resource = Fixtures.Resources.create_resource( account: account, @@ -56,6 +64,12 @@ defmodule API.Client.ChannelTest do resource: cidr_resource ) + Fixtures.Policies.create_policy( + account: account, + actor_group: actor_group, + resource: ip_resource + ) + expires_at = DateTime.utc_now() |> DateTime.add(30, :second) subject = %{subject | expires_at: expires_at} @@ -80,6 +94,7 @@ defmodule API.Client.ChannelTest do gateway: gateway, dns_resource: dns_resource, cidr_resource: cidr_resource, + ip_resource: ip_resource, unauthorized_resource: unauthorized_resource, socket: socket } @@ -119,18 +134,17 @@ defmodule API.Client.ChannelTest do test "sends list of resources after join", %{ client: client, dns_resource: dns_resource, - cidr_resource: cidr_resource + cidr_resource: cidr_resource, + ip_resource: ip_resource } do assert_push "init", %{resources: resources, interface: interface} - assert length(resources) == 2 + assert length(resources) == 3 assert %{ id: dns_resource.id, type: :dns, name: dns_resource.name, - address: dns_resource.address, - ipv4: dns_resource.ipv4, - ipv6: dns_resource.ipv6 + address: dns_resource.address } in resources assert %{ @@ -140,6 +154,13 @@ defmodule API.Client.ChannelTest do address: cidr_resource.address } in resources + assert %{ + id: ip_resource.id, + type: :cidr, + name: ip_resource.name, + address: "#{ip_resource.address}/32" + } in resources + assert interface == %{ ipv4: client.ipv4, ipv6: client.ipv6, @@ -487,7 +508,8 @@ defmodule API.Client.ChannelTest do test "returns error when resource is not found", %{gateway: gateway, socket: socket} do attrs = %{ "resource_id" => Ecto.UUID.generate(), - "gateway_id" => gateway.id + "gateway_id" => gateway.id, + "payload" => "DNS_Q" } ref = push(socket, "reuse_connection", attrs) @@ -497,7 +519,8 @@ defmodule API.Client.ChannelTest do test "returns error when gateway is not found", %{dns_resource: resource, socket: socket} do attrs = %{ "resource_id" => resource.id, - "gateway_id" => Ecto.UUID.generate() + "gateway_id" => Ecto.UUID.generate(), + "payload" => "DNS_Q" } ref = push(socket, "reuse_connection", attrs) @@ -514,7 +537,8 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, - "gateway_id" => gateway.id + "gateway_id" => gateway.id, + "payload" => "DNS_Q" } ref = push(socket, "reuse_connection", attrs) @@ -532,7 +556,8 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, - "gateway_id" => gateway.id + "gateway_id" => gateway.id, + "payload" => "DNS_Q" } ref = push(socket, "reuse_connection", attrs) @@ -546,7 +571,8 @@ defmodule API.Client.ChannelTest do } do attrs = %{ "resource_id" => resource.id, - "gateway_id" => gateway.id + "gateway_id" => gateway.id, + "payload" => "DNS_Q" } ref = push(socket, "reuse_connection", attrs) @@ -559,6 +585,7 @@ defmodule API.Client.ChannelTest do client: client, socket: socket } do + public_key = gateway.public_key resource_id = resource.id client_id = client.id @@ -567,20 +594,36 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, - "gateway_id" => gateway.id + "gateway_id" => gateway.id, + "payload" => "DNS_Q" } - push(socket, "reuse_connection", attrs) + ref = push(socket, "reuse_connection", attrs) - assert_receive {:allow_access, payload, _opentelemetry_ctx} + assert_receive {:allow_access, {channel_pid, socket_ref}, payload, _opentelemetry_ctx} assert %{ resource_id: ^resource_id, client_id: ^client_id, - authorization_expires_at: authorization_expires_at + authorization_expires_at: authorization_expires_at, + client_payload: "DNS_Q" } = payload assert authorization_expires_at == socket.assigns.subject.expires_at + + otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} + + send( + channel_pid, + {:connect, socket_ref, resource.id, gateway.public_key, "DNS_RPL", otel_ctx} + ) + + assert_reply ref, :ok, %{ + resource_id: ^resource_id, + persistent_keepalive: 25, + gateway_public_key: ^public_key, + gateway_payload: "DNS_RPL" + } end end @@ -589,7 +632,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => Ecto.UUID.generate(), "gateway_id" => gateway.id, - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -601,7 +644,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, "gateway_id" => Ecto.UUID.generate(), - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -620,7 +663,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, "gateway_id" => gateway.id, - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -640,7 +683,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, "gateway_id" => gateway.id, - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -656,7 +699,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, "gateway_id" => gateway.id, - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -680,7 +723,7 @@ defmodule API.Client.ChannelTest do attrs = %{ "resource_id" => resource.id, "gateway_id" => gateway.id, - "client_rtc_session_description" => "RTC_SD", + "client_payload" => "RTC_SD", "client_preshared_key" => "PSK" } @@ -692,7 +735,7 @@ defmodule API.Client.ChannelTest do resource_id: ^resource_id, client_id: ^client_id, client_preshared_key: "PSK", - client_rtc_session_description: "RTC_SD", + client_payload: "RTC_SD", authorization_expires_at: authorization_expires_at } = payload @@ -709,7 +752,7 @@ defmodule API.Client.ChannelTest do resource_id: ^resource_id, persistent_keepalive: 25, gateway_public_key: ^public_key, - gateway_rtc_session_description: "FULL_RTC_SD" + gateway_payload: "FULL_RTC_SD" } end end diff --git a/elixir/apps/api/test/api/gateway/channel_test.exs b/elixir/apps/api/test/api/gateway/channel_test.exs index 73c7c8b72..279d613b6 100644 --- a/elixir/apps/api/test/api/gateway/channel_test.exs +++ b/elixir/apps/api/test/api/gateway/channel_test.exs @@ -75,21 +75,25 @@ defmodule API.Gateway.ChannelTest do relay: relay, socket: socket } do + channel_pid = self() + socket_ref = make_ref() expires_at = DateTime.utc_now() |> DateTime.add(30, :second) otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} flow_id = Ecto.UUID.generate() + client_payload = "RTC_SD_or_DNS_Q" stamp_secret = Ecto.UUID.generate() :ok = Domain.Relays.connect_relay(relay, stamp_secret) send( socket.channel_pid, - {:allow_access, + {:allow_access, {channel_pid, socket_ref}, %{ client_id: client.id, resource_id: resource.id, flow_id: flow_id, - authorization_expires_at: expires_at + authorization_expires_at: expires_at, + client_payload: client_payload }, otel_ctx} ) @@ -100,8 +104,6 @@ defmodule API.Gateway.ChannelTest do id: resource.id, name: resource.name, type: :dns, - ipv4: resource.ipv4, - ipv6: resource.ipv6, filters: [ %{protocol: :tcp, port_range_end: 80, port_range_start: 80}, %{protocol: :tcp, port_range_end: 433, port_range_start: 433}, @@ -109,6 +111,7 @@ defmodule API.Gateway.ChannelTest do ] } + assert payload.ref assert payload.flow_id == flow_id assert payload.client_id == client.id assert DateTime.from_unix!(payload.expires_at) == DateTime.truncate(expires_at, :second) @@ -149,7 +152,7 @@ defmodule API.Gateway.ChannelTest do socket_ref = make_ref() expires_at = DateTime.utc_now() |> DateTime.add(30, :second) preshared_key = "PSK" - rtc_session_description = "RTC_SD" + client_payload = "RTC_SD" flow_id = Ecto.UUID.generate() otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} @@ -165,7 +168,7 @@ defmodule API.Gateway.ChannelTest do resource_id: resource.id, flow_id: flow_id, authorization_expires_at: expires_at, - client_rtc_session_description: rtc_session_description, + client_payload: client_payload, client_preshared_key: preshared_key }, otel_ctx} ) @@ -208,8 +211,6 @@ defmodule API.Gateway.ChannelTest do id: resource.id, name: resource.name, type: :dns, - ipv4: resource.ipv4, - ipv6: resource.ipv6, filters: [ %{protocol: :tcp, port_range_end: 80, port_range_start: 80}, %{protocol: :tcp, port_range_end: 433, port_range_start: 433}, @@ -226,10 +227,11 @@ defmodule API.Gateway.ChannelTest do preshared_key: preshared_key, public_key: client.public_key }, - rtc_session_description: rtc_session_description + payload: client_payload } - assert DateTime.from_unix!(payload.expires_at) == DateTime.truncate(expires_at, :second) + assert DateTime.from_unix!(payload.expires_at) == + DateTime.truncate(expires_at, :second) end test "pushes request_connection message with self-hosted relays", %{ @@ -260,7 +262,7 @@ defmodule API.Gateway.ChannelTest do socket_ref = make_ref() expires_at = DateTime.utc_now() |> DateTime.add(30, :second) preshared_key = "PSK" - rtc_session_description = "RTC_SD" + client_payload = "RTC_SD" flow_id = Ecto.UUID.generate() otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} @@ -276,7 +278,7 @@ defmodule API.Gateway.ChannelTest do resource_id: resource.id, flow_id: flow_id, authorization_expires_at: expires_at, - client_rtc_session_description: rtc_session_description, + client_payload: client_payload, client_preshared_key: preshared_key }, otel_ctx} ) @@ -319,8 +321,6 @@ defmodule API.Gateway.ChannelTest do id: resource.id, name: resource.name, type: :dns, - ipv4: resource.ipv4, - ipv6: resource.ipv6, filters: [ %{protocol: :tcp, port_range_end: 80, port_range_start: 80}, %{protocol: :tcp, port_range_end: 433, port_range_start: 433}, @@ -337,7 +337,7 @@ defmodule API.Gateway.ChannelTest do preshared_key: preshared_key, public_key: client.public_key }, - rtc_session_description: rtc_session_description + payload: client_payload } assert DateTime.from_unix!(payload.expires_at) == DateTime.truncate(expires_at, :second) @@ -371,7 +371,7 @@ defmodule API.Gateway.ChannelTest do socket_ref = make_ref() expires_at = DateTime.utc_now() |> DateTime.add(30, :second) preshared_key = "PSK" - rtc_session_description = "RTC_SD" + client_payload = "RTC_SD" flow_id = Ecto.UUID.generate() otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} @@ -387,7 +387,7 @@ defmodule API.Gateway.ChannelTest do resource_id: resource.id, flow_id: flow_id, authorization_expires_at: expires_at, - client_rtc_session_description: rtc_session_description, + client_payload: client_payload, client_preshared_key: preshared_key }, otel_ctx} ) @@ -417,8 +417,6 @@ defmodule API.Gateway.ChannelTest do id: resource.id, name: resource.name, type: :dns, - ipv4: resource.ipv4, - ipv6: resource.ipv6, filters: [ %{protocol: :tcp, port_range_end: 80, port_range_start: 80}, %{protocol: :tcp, port_range_end: 433, port_range_start: 433}, @@ -435,7 +433,7 @@ defmodule API.Gateway.ChannelTest do preshared_key: preshared_key, public_key: client.public_key }, - rtc_session_description: rtc_session_description + payload: client_payload } assert DateTime.from_unix!(payload.expires_at) == DateTime.truncate(expires_at, :second) @@ -455,7 +453,7 @@ defmodule API.Gateway.ChannelTest do expires_at = DateTime.utc_now() |> DateTime.add(30, :second) preshared_key = "PSK" gateway_public_key = gateway.public_key - rtc_session_description = "RTC_SD" + payload = "RTC_SD" flow_id = Ecto.UUID.generate() otel_ctx = {OpenTelemetry.Ctx.new(), OpenTelemetry.Tracer.start_span("connect")} @@ -471,7 +469,7 @@ defmodule API.Gateway.ChannelTest do resource_id: resource.id, authorization_expires_at: expires_at, flow_id: flow_id, - client_rtc_session_description: rtc_session_description, + client_payload: payload, client_preshared_key: preshared_key }, otel_ctx} ) @@ -481,13 +479,13 @@ defmodule API.Gateway.ChannelTest do push_ref = push(socket, "connection_ready", %{ "ref" => ref, - "gateway_rtc_session_description" => rtc_session_description + "gateway_payload" => payload }) assert_reply push_ref, :ok - assert_receive {:connect, ^socket_ref, resource_id, ^gateway_public_key, - ^rtc_session_description, _opentelemetry_ctx} + assert_receive {:connect, ^socket_ref, resource_id, ^gateway_public_key, ^payload, + _opentelemetry_ctx} assert resource_id == resource.id end @@ -553,19 +551,18 @@ defmodule API.Gateway.ChannelTest do {:ok, destination} = Domain.Types.IPPort.cast("127.0.0.1:80") - attrs = - %{ - "started_at" => DateTime.to_unix(one_minute_ago), - "ended_at" => DateTime.to_unix(now), - "metrics" => [ - %{ - "flow_id" => flow.id, - "destination" => destination, - "rx_bytes" => 100, - "tx_bytes" => 200 - } - ] - } + attrs = %{ + "started_at" => DateTime.to_unix(one_minute_ago), + "ended_at" => DateTime.to_unix(now), + "metrics" => [ + %{ + "flow_id" => flow.id, + "destination" => destination, + "rx_bytes" => 100, + "tx_bytes" => 200 + } + ] + } push_ref = push(socket, "metrics", attrs) assert_reply push_ref, :ok diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex index 812c7e92a..5b8fcb16b 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex @@ -166,8 +166,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do adapter_state_updates = Map.take(adapter_state, ["expires_at", "access_token", "userinfo", "claims"]) - adapter_state = - Map.merge(provider.adapter_state, adapter_state_updates) + adapter_state = Map.merge(provider.adapter_state, adapter_state_updates) Provider.Changeset.update(provider, %{adapter_state: adapter_state}) end diff --git a/elixir/apps/domain/lib/domain/flows.ex b/elixir/apps/domain/lib/domain/flows.ex index 12c5bca03..cb513091b 100644 --- a/elixir/apps/domain/lib/domain/flows.ex +++ b/elixir/apps/domain/lib/domain/flows.ex @@ -110,8 +110,7 @@ defmodule Domain.Flows do end def upsert_activities(activities) do - {num, _} = - Repo.insert_all(Activity, activities, on_conflict: :nothing) + {num, _} = Repo.insert_all(Activity, activities, on_conflict: :nothing) {:ok, num} end diff --git a/elixir/apps/domain/lib/domain/gateways.ex b/elixir/apps/domain/lib/domain/gateways.ex index 6132a8e1f..b91eaf740 100644 --- a/elixir/apps/domain/lib/domain/gateways.ex +++ b/elixir/apps/domain/lib/domain/gateways.ex @@ -474,8 +474,7 @@ defmodule Domain.Gateways do online_at: System.system_time(:second) }) - {:ok, _} = - Presence.track(self(), "gateway_groups:#{gateway.group_id}", gateway.id, %{}) + {:ok, _} = Presence.track(self(), "gateway_groups:#{gateway.group_id}", gateway.id, %{}) :ok end diff --git a/elixir/apps/domain/lib/domain/network.ex b/elixir/apps/domain/lib/domain/network.ex index fa775d6a8..7717dfad0 100644 --- a/elixir/apps/domain/lib/domain/network.ex +++ b/elixir/apps/domain/lib/domain/network.ex @@ -4,8 +4,8 @@ defmodule Domain.Network do @cidrs %{ # Notice: those are also part of "resources_account_id_cidr_address_index" DB constraint - ipv4: %Postgrex.INET{address: {100, 64, 0, 0}, netmask: 10}, - ipv6: %Postgrex.INET{address: {64_768, 8_225, 4_369, 0, 0, 0, 0, 0}, netmask: 106} + ipv4: %Postgrex.INET{address: {100, 64, 0, 0}, netmask: 11}, + ipv6: %Postgrex.INET{address: {64_768, 8_225, 4_369, 0, 0, 0, 0, 0}, netmask: 107} } def cidrs, do: @cidrs diff --git a/elixir/apps/domain/lib/domain/policies.ex b/elixir/apps/domain/lib/domain/policies.ex index 161b7ae4c..955603e05 100644 --- a/elixir/apps/domain/lib/domain/policies.ex +++ b/elixir/apps/domain/lib/domain/policies.ex @@ -50,8 +50,7 @@ defmodule Domain.Policies do end def create_policy(attrs, %Auth.Subject{} = subject) do - required_permissions = - {:one_of, [Authorizer.manage_policies_permission()]} + required_permissions = {:one_of, [Authorizer.manage_policies_permission()]} with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do Policy.Changeset.create(attrs, subject) @@ -60,8 +59,7 @@ defmodule Domain.Policies do end def update_policy(%Policy{} = policy, attrs, %Auth.Subject{} = subject) do - required_permissions = - {:one_of, [Authorizer.manage_policies_permission()]} + required_permissions = {:one_of, [Authorizer.manage_policies_permission()]} with :ok <- Auth.ensure_has_permissions(subject, required_permissions), :ok <- ensure_has_access_to(subject, policy) do @@ -87,8 +85,7 @@ defmodule Domain.Policies do end def delete_policy(%Policy{} = policy, %Auth.Subject{} = subject) do - required_permissions = - {:one_of, [Authorizer.manage_policies_permission()]} + required_permissions = {:one_of, [Authorizer.manage_policies_permission()]} with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do Policy.Query.by_id(policy.id) diff --git a/elixir/apps/domain/lib/domain/relays.ex b/elixir/apps/domain/lib/domain/relays.ex index 486f733ab..04fa1cba7 100644 --- a/elixir/apps/domain/lib/domain/relays.ex +++ b/elixir/apps/domain/lib/domain/relays.ex @@ -387,8 +387,7 @@ defmodule Domain.Relays do secret: secret }) - {:ok, _} = - Presence.track(self(), "relay_groups:#{relay.group_id}", relay.id, %{}) + {:ok, _} = Presence.track(self(), "relay_groups:#{relay.group_id}", relay.id, %{}) :ok end diff --git a/elixir/apps/domain/lib/domain/resources.ex b/elixir/apps/domain/lib/domain/resources.ex index ab9e7ce81..5b01b0712 100644 --- a/elixir/apps/domain/lib/domain/resources.ex +++ b/elixir/apps/domain/lib/domain/resources.ex @@ -143,48 +143,21 @@ defmodule Domain.Resources do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do changeset = Resource.Changeset.create(subject.account, attrs, subject) - Ecto.Multi.new() - |> Ecto.Multi.insert(:resource, changeset, returning: true) - |> resolve_address_multi(:ipv4) - |> resolve_address_multi(:ipv6) - |> Ecto.Multi.update(:resource_with_address, fn - %{resource: %Resource{} = resource, ipv4: ipv4, ipv6: ipv6} -> - Resource.Changeset.finalize_create(resource, ipv4, ipv6) - end) - |> Repo.transaction() - |> case do - {:ok, %{resource_with_address: resource}} -> - # TODO: Add optimistic lock to resource.updated_at to serialize the resource updates - # TODO: Broadcast only to actors that have access to the resource - # {:ok, actors} = list_authorized_actors(resource) - # Phoenix.PubSub.broadcast( - # Domain.PubSub, - # "actor_client:#{subject.actor.id}", - # {:resource_added, resource.id} - # ) + with {:ok, resource} <- Repo.insert(changeset) do + # TODO: Add optimistic lock to resource.updated_at to serialize the resource updates + # TODO: Broadcast only to actors that have access to the resource + # {:ok, actors} = list_authorized_actors(resource) + # Phoenix.PubSub.broadcast( + # Domain.PubSub, + # "actor_client:#{subject.actor.id}", + # {:resource_added, resource.id} + # ) - {:ok, resource} - - {:error, :resource, changeset, _effects_so_far} -> - {:error, changeset} + {:ok, resource} end end end - defp resolve_address_multi(multi, type) do - Ecto.Multi.run(multi, type, fn - _repo, %{resource: %Resource{type: :cidr}} -> - {:ok, nil} - - _repo, %{resource: %Resource{type: :dns} = resource} -> - if address = Map.get(resource, type) do - {:ok, address} - else - {:ok, Domain.Network.fetch_next_available_address!(resource.account_id, type)} - end - end) - end - def change_resource(%Resource{} = resource, attrs \\ %{}, %Auth.Subject{} = subject) do Resource.Changeset.update(resource, attrs, subject) end diff --git a/elixir/apps/domain/lib/domain/resources/resource.ex b/elixir/apps/domain/lib/domain/resources/resource.ex index 047e0ed92..81fcdfb9d 100644 --- a/elixir/apps/domain/lib/domain/resources/resource.ex +++ b/elixir/apps/domain/lib/domain/resources/resource.ex @@ -5,16 +5,13 @@ defmodule Domain.Resources.Resource do field :address, :string field :name, :string - field :type, Ecto.Enum, values: [:cidr, :dns] + field :type, Ecto.Enum, values: [:cidr, :ip, :dns] embeds_many :filters, Filter, on_replace: :delete, primary_key: false do field :protocol, Ecto.Enum, values: [tcp: 6, udp: 17, icmp: 1, all: -1] field :ports, {:array, Domain.Types.Int4Range}, default: [] end - field :ipv4, Domain.Types.IP - field :ipv6, Domain.Types.IP - belongs_to :account, Domain.Accounts.Account has_many :connections, Domain.Resources.Connection, on_replace: :delete # TODO: where doesn't work on join tables so soft-deleted records will be preloaded, diff --git a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex index 8dc102b60..7d360a767 100644 --- a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex +++ b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex @@ -33,15 +33,6 @@ defmodule Domain.Resources.Resource.Changeset do ) end - def finalize_create(%Resource{} = resource, ipv4, ipv6) do - resource - |> change() - |> put_change(:ipv4, ipv4) - |> put_change(:ipv6, ipv6) - |> unique_constraint(:ipv4, name: :resources_account_id_ipv4_index) - |> unique_constraint(:ipv6, name: :resources_account_id_ipv6_index) - end - defp validate_address(changeset) do if has_errors?(changeset, :type) do changeset @@ -53,6 +44,9 @@ defmodule Domain.Resources.Resource.Changeset do {_data_or_changes, :cidr} -> validate_cidr_address(changeset) + {_data_or_changes, :ip} -> + validate_ip_address(changeset) + _other -> changeset end @@ -60,12 +54,71 @@ defmodule Domain.Resources.Resource.Changeset do end defp validate_dns_address(changeset) do - validate_length(changeset, :address, min: 1, max: 253) + changeset + |> validate_length(:address, min: 1, max: 253) + |> validate_does_not_end_with(:address, "localhost", + message: "localhost can not 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) end defp validate_cidr_address(changeset) do - changeset = validate_and_normalize_cidr(changeset, :address) + changeset + |> validate_and_normalize_cidr(:address) + |> validate_not_in_cidr(:address, %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 32}, + message: "can not contain all IPv4 addresses" + ) + |> validate_not_in_cidr(:address, %Postgrex.INET{address: {127, 0, 0, 0}, netmask: 8}, + message: "can not contain loopback addresses" + ) + |> validate_not_in_cidr( + :address, + %Postgrex.INET{ + address: {0, 0, 0, 0, 0, 0, 0, 0}, + netmask: 128 + }, + message: "can not contain all IPv6 addresses" + ) + |> validate_not_in_cidr( + :address, + %Postgrex.INET{ + address: {0, 0, 0, 0, 0, 0, 0, 1}, + netmask: 128 + }, + message: "can not contain loopback addresses" + ) + |> validate_address_is_not_in_private_range() + end + defp validate_ip_address(changeset) do + changeset + |> validate_and_normalize_ip(:address) + |> validate_not_in_cidr(:address, %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 32}, + message: "can not contain all IPv4 addresses" + ) + |> validate_not_in_cidr(:address, %Postgrex.INET{address: {127, 0, 0, 0}, netmask: 8}, + message: "can not contain loopback addresses" + ) + |> validate_not_in_cidr( + :address, + %Postgrex.INET{ + address: {0, 0, 0, 0, 0, 0, 0, 0}, + netmask: 128 + }, + message: "can not contain all IPv6 addresses" + ) + |> validate_not_in_cidr( + :address, + %Postgrex.INET{ + address: {0, 0, 0, 0, 0, 0, 0, 1}, + netmask: 128 + }, + message: "can not contain loopback addresses" + ) + |> validate_address_is_not_in_private_range() + end + + defp validate_address_is_not_in_private_range(changeset) do cond do has_errors?(changeset, :address) -> changeset @@ -84,28 +137,6 @@ defmodule Domain.Resources.Resource.Changeset do end end - def put_resource_type(changeset) do - put_default_value(changeset, :type, fn _cs -> - address = get_field(changeset, :address) - - if address_contains_cidr?(address) do - :cidr - else - :dns - end - end) - end - - defp address_contains_cidr?(address) do - case Domain.Types.INET.cast(address) do - {:ok, _} -> - true - - _ -> - false - end - end - def update(%Resource{} = resource, attrs, %Auth.Subject{} = subject) do resource |> cast(attrs, @update_fields) @@ -119,9 +150,7 @@ defmodule Domain.Resources.Resource.Changeset do defp changeset(changeset) do changeset - |> put_default_value(:name, from: :address) |> validate_length(:name, min: 1, max: 255) - |> put_resource_type() |> cast_embed(:filters, with: &cast_filter/2) |> unique_constraint(:ipv4, name: :resources_account_id_ipv4_index) |> unique_constraint(:ipv6, name: :resources_account_id_ipv6_index) diff --git a/elixir/apps/domain/lib/domain/types/cidr.ex b/elixir/apps/domain/lib/domain/types/cidr.ex index f47e379a5..adc2e4bb4 100644 --- a/elixir/apps/domain/lib/domain/types/cidr.ex +++ b/elixir/apps/domain/lib/domain/types/cidr.ex @@ -24,7 +24,7 @@ defmodule Domain.Types.CIDR do def range(%Postgrex.INET{address: address, netmask: netmask} = cidr) do tuple_size = tuple_size(address) - shift = max_netmask(cidr) - netmask + shift = max_netmask(cidr) - (netmask || 32) address_as_number = address2number(address) first_address_number = reset_right_bits(address_as_number, shift) @@ -34,8 +34,8 @@ defmodule Domain.Types.CIDR do number2address(tuple_size, last_address_number)} end - defp max_netmask(%Postgrex.INET{address: address}) when tuple_size(address) == 4, do: 32 - defp max_netmask(%Postgrex.INET{address: address}) when tuple_size(address) == 8, do: 128 + def max_netmask(%Postgrex.INET{address: address}) when tuple_size(address) == 4, do: 32 + def max_netmask(%Postgrex.INET{address: address}) when tuple_size(address) == 8, do: 128 defp address2number({a, b, c, d}) do a <<< 24 ||| b <<< 16 ||| c <<< 8 ||| d diff --git a/elixir/apps/domain/lib/domain/types/ip.ex b/elixir/apps/domain/lib/domain/types/ip.ex index 053557c1d..e7d15f816 100644 --- a/elixir/apps/domain/lib/domain/types/ip.ex +++ b/elixir/apps/domain/lib/domain/types/ip.ex @@ -19,7 +19,8 @@ defmodule Domain.Types.IP do with {:ok, address} <- Domain.Types.IPPort.cast_address(binary) do {:ok, %Postgrex.INET{address: address, netmask: nil}} else - {:error, _reason} -> {:error, message: "is invalid"} + {:error, _reason} -> + {:error, message: "is invalid"} end end diff --git a/elixir/apps/domain/lib/domain/validator.ex b/elixir/apps/domain/lib/domain/validator.ex index d2887fa80..abd1c801a 100644 --- a/elixir/apps/domain/lib/domain/validator.ex +++ b/elixir/apps/domain/lib/domain/validator.ex @@ -26,6 +26,28 @@ defmodule Domain.Validator do |> validate_length(field, max: 160) end + def validate_does_not_contain(changeset, field, substring, opts \\ []) do + validate_change(changeset, field, fn _current_field, value -> + if String.contains?(value, substring) do + message = Keyword.get(opts, :message, "can not contain #{inspect(substring)}") + [{field, message}] + else + [] + end + end) + end + + def validate_does_not_end_with(changeset, field, suffix, opts \\ []) do + validate_change(changeset, field, fn _current_field, value -> + if String.ends_with?(value, suffix) do + message = Keyword.get(opts, :message, "can not end with #{inspect(suffix)}") + [{field, message}] + else + [] + end + end) + end + def validate_uri(changeset, field, opts \\ []) when is_atom(field) do valid_schemes = Keyword.get(opts, :schemes, ~w[http https]) require_trailing_slash? = Keyword.get(opts, :require_trailing_slash, false) @@ -185,13 +207,14 @@ defmodule Domain.Validator do end) end - def validate_not_in_cidr(changeset, ip_or_cidr_field, cidr) do + def validate_not_in_cidr(changeset, ip_or_cidr_field, cidr, opts \\ []) do validate_change(changeset, ip_or_cidr_field, fn _ip_or_cidr_field, ip_or_cidr -> case Domain.Types.INET.cast(ip_or_cidr) do {:ok, ip_or_cidr} -> if Domain.Types.CIDR.contains?(cidr, ip_or_cidr) or Domain.Types.CIDR.contains?(ip_or_cidr, cidr) do - [{ip_or_cidr_field, "can not be in the CIDR #{cidr}"}] + message = Keyword.get(opts, :message, "can not be in the CIDR #{cidr}") + [{ip_or_cidr_field, message}] else [] end @@ -217,6 +240,19 @@ defmodule Domain.Validator do end end + def validate_and_normalize_ip(changeset, field, _opts \\ []) do + with {_data_or_changes, value} <- fetch_change(changeset, field), + {:ok, ip} <- Domain.Types.IP.cast(value) do + put_change(changeset, field, to_string(ip)) + else + :error -> + changeset + + {:error, _reason} -> + add_error(changeset, field, "is not a valid IP address") + end + end + def validate_base64(changeset, field) do validate_change(changeset, field, fn _cur, value -> case Base.decode64(value) do diff --git a/elixir/apps/domain/priv/repo/migrations/20231122195751_remove_resources_ipvx_address.exs b/elixir/apps/domain/priv/repo/migrations/20231122195751_remove_resources_ipvx_address.exs new file mode 100644 index 000000000..640796b8f --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20231122195751_remove_resources_ipvx_address.exs @@ -0,0 +1,25 @@ +defmodule Domain.Repo.Migrations.RemoveResourcesIpvxAddress do + use Ecto.Migration + + def change do + alter table(:resources) do + remove( + :ipv4, + references(:network_addresses, + column: :address, + type: :inet, + with: [account_id: :account_id] + ) + ) + + remove( + :ipv6, + references(:network_addresses, + column: :address, + type: :inet, + with: [account_id: :account_id] + ) + ) + end + end +end diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index 1fd9acc42..8d5e6015c 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -457,8 +457,46 @@ IO.puts("") Resources.create_resource( %{ type: :dns, + name: "google.com", address: "google.com", - connections: [%{gateway_group_id: gateway_group.id}] + connections: [%{gateway_group_id: gateway_group.id}], + filters: [%{protocol: :all}] + }, + admin_subject + ) + +{:ok, t_firez_one} = + Resources.create_resource( + %{ + type: :dns, + name: "t.firez.one", + address: "t.firez.one", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [%{protocol: :all}] + }, + admin_subject + ) + +{:ok, ping_firez_one} = + Resources.create_resource( + %{ + type: :dns, + name: "ping.firez.one", + address: "ping.firez.one", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [%{protocol: :all}] + }, + admin_subject + ) + +{:ok, ip6only} = + Resources.create_resource( + %{ + type: :dns, + name: "ip6only", + address: "ip6only.me", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [%{protocol: :all}] }, admin_subject ) @@ -467,6 +505,7 @@ IO.puts("") Resources.create_resource( %{ type: :dns, + name: "gitlab.mycorp.com", address: "gitlab.mycorp.com", connections: [%{gateway_group_id: gateway_group.id}], filters: [ @@ -482,6 +521,7 @@ IO.puts("") Resources.create_resource( %{ type: :cidr, + name: "MyCorp Network", address: "172.20.0.1/16", connections: [%{gateway_group_id: gateway_group.id}], filters: [%{protocol: :all}] @@ -490,35 +530,70 @@ IO.puts("") ) IO.puts("Created resources:") - -IO.puts( - " #{dns_google_resource.address} - DNS - #{dns_google_resource.ipv4} - gateways: #{gateway_name}" -) - -IO.puts( - " #{dns_gitlab_resource.address} - DNS - #{dns_gitlab_resource.ipv4} - gateways: #{gateway_name}" -) - +IO.puts(" #{dns_google_resource.address} - DNS - gateways: #{gateway_name}") +IO.puts(" #{dns_gitlab_resource.address} - DNS - gateways: #{gateway_name}") IO.puts(" #{cidr_resource.address} - CIDR - gateways: #{gateway_name}") IO.puts("") -Policies.create_policy( - %{ - name: "Eng Access To Gitlab", - actor_group_id: eng_group.id, - resource_id: dns_gitlab_resource.id - }, - admin_subject -) +{:ok, _} = + Policies.create_policy( + %{ + name: "All Access To Google", + actor_group_id: all_group.id, + resource_id: dns_google_resource.id + }, + admin_subject + ) -Policies.create_policy( - %{ - name: "All Access To Network", - actor_group_id: all_group.id, - resource_id: cidr_resource.id - }, - admin_subject -) +{:ok, _} = + Policies.create_policy( + %{ + name: "All Access To t.firez.one", + actor_group_id: all_group.id, + resource_id: t_firez_one.id + }, + admin_subject + ) + +{:ok, _} = + Policies.create_policy( + %{ + name: "All Access To ping.firez.one", + actor_group_id: all_group.id, + resource_id: ping_firez_one.id + }, + admin_subject + ) + +{:ok, _} = + Policies.create_policy( + %{ + name: "All Access To ip6only.me", + actor_group_id: all_group.id, + resource_id: ip6only.id + }, + admin_subject + ) + +{:ok, _} = + Policies.create_policy( + %{ + name: "Eng Access To Gitlab", + actor_group_id: eng_group.id, + resource_id: dns_gitlab_resource.id + }, + admin_subject + ) + +{:ok, _} = + Policies.create_policy( + %{ + name: "All Access To Network", + actor_group_id: all_group.id, + resource_id: cidr_resource.id + }, + admin_subject + ) IO.puts("Policies Created") IO.puts("") diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index 4ba34a03b..527d3c97a 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -269,8 +269,7 @@ defmodule Domain.AuthTest do end test "ignores disabled providers" do - {provider, _bypass} = - Fixtures.Auth.start_and_create_google_workspace_provider() + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() Domain.Fixture.update!(provider, %{ disabled_at: DateTime.utc_now(), @@ -283,8 +282,7 @@ defmodule Domain.AuthTest do end test "ignores non-custom provisioners" do - {provider, _bypass} = - Fixtures.Auth.start_and_create_google_workspace_provider() + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() Domain.Fixture.update!(provider, %{ provisioner: :manual, @@ -297,8 +295,7 @@ defmodule Domain.AuthTest do end test "returns providers with tokens that will expire in ~1 hour" do - {provider, _bypass} = - Fixtures.Auth.start_and_create_google_workspace_provider() + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() Domain.Fixture.update!(provider, %{ adapter_state: %{ @@ -328,8 +325,7 @@ defmodule Domain.AuthTest do end test "ignores disabled providers" do - {provider, _bypass} = - Fixtures.Auth.start_and_create_google_workspace_provider() + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() Domain.Fixture.update!(provider, %{ disabled_at: DateTime.utc_now(), @@ -342,8 +338,7 @@ defmodule Domain.AuthTest do end test "ignores non-custom provisioners" do - {provider, _bypass} = - Fixtures.Auth.start_and_create_google_workspace_provider() + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() Domain.Fixture.update!(provider, %{ provisioner: :manual, @@ -362,8 +357,7 @@ defmodule Domain.AuthTest do eleven_minutes_ago = DateTime.utc_now() |> DateTime.add(-11, :minute) Domain.Fixture.update!(provider2, %{last_synced_at: eleven_minutes_ago}) - assert {:ok, providers} = - list_providers_pending_sync_by_adapter(:google_workspace) + assert {:ok, providers} = list_providers_pending_sync_by_adapter(:google_workspace) assert Enum.map(providers, & &1.id) |> Enum.sort() == Enum.sort([provider1.id, provider2.id]) @@ -2158,8 +2152,7 @@ defmodule Domain.AuthTest do redirect_uri = "https://example.com/" payload = {redirect_uri, code_verifier, "MyFakeCode"} - assert {:ok, %Auth.Subject{} = subject} = - sign_in(provider, payload, user_agent, remote_ip) + assert {:ok, %Auth.Subject{} = subject} = sign_in(provider, payload, user_agent, remote_ip) assert subject.account.id == account.id assert subject.actor.id == identity.actor_id @@ -2192,8 +2185,7 @@ defmodule Domain.AuthTest do redirect_uri = "https://example.com/" payload = {redirect_uri, code_verifier, "MyFakeCode"} - assert {:ok, %Auth.Subject{} = subject} = - sign_in(provider, payload, user_agent, remote_ip) + assert {:ok, %Auth.Subject{} = subject} = sign_in(provider, payload, user_agent, remote_ip) one_week = 7 * 24 * 60 * 60 assert_datetime_diff(subject.expires_at, DateTime.utc_now(), one_week) @@ -2223,8 +2215,7 @@ defmodule Domain.AuthTest do redirect_uri = "https://example.com/" payload = {redirect_uri, code_verifier, "MyFakeCode"} - assert {:ok, %Auth.Subject{} = subject} = - sign_in(provider, payload, user_agent, remote_ip) + assert {:ok, %Auth.Subject{} = subject} = sign_in(provider, payload, user_agent, remote_ip) three_hours = 3 * 60 * 60 assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours) @@ -2369,8 +2360,7 @@ defmodule Domain.AuthTest do redirect_uri = "https://example.com/" payload = {redirect_uri, code_verifier, "MyFakeCode"} - assert {:ok, _subject} = - sign_in(provider, payload, user_agent, remote_ip) + assert {:ok, _subject} = sign_in(provider, payload, user_agent, remote_ip) assert updated_identity = Repo.one(Auth.Identity) assert updated_identity.last_seen_at != identity.last_seen_at diff --git a/elixir/apps/domain/test/domain/flows_test.exs b/elixir/apps/domain/test/domain/flows_test.exs index 809416b95..cbe1f8fb1 100644 --- a/elixir/apps/domain/test/domain/flows_test.exs +++ b/elixir/apps/domain/test/domain/flows_test.exs @@ -394,16 +394,15 @@ defmodule Domain.FlowsTest do {:ok, destination} = Domain.Types.IPPort.cast("127.0.0.1:80") - activity = - %{ - window_started_at: DateTime.add(now, -1, :minute), - window_ended_at: now, - destination: destination, - rx_bytes: 100, - tx_bytes: 200, - flow_id: flow.id, - account_id: account.id - } + activity = %{ + window_started_at: DateTime.add(now, -1, :minute), + window_ended_at: now, + destination: destination, + rx_bytes: 100, + tx_bytes: 200, + flow_id: flow.id, + account_id: account.id + } assert upsert_activities([activity]) == {:ok, 1} diff --git a/elixir/apps/domain/test/domain/policies_test.exs b/elixir/apps/domain/test/domain/policies_test.exs index 66dfff8b5..1c1f10516 100644 --- a/elixir/apps/domain/test/domain/policies_test.exs +++ b/elixir/apps/domain/test/domain/policies_test.exs @@ -248,8 +248,7 @@ defmodule Domain.PoliciesTest do resource_id: other_resource.id } - assert {:error, changeset} = - create_policy(attrs, subject) + assert {:error, changeset} = create_policy(attrs, subject) assert errors_on(changeset) == %{resource: ["does not exist"]} end diff --git a/elixir/apps/domain/test/domain/resources/resource/changeset_test.exs b/elixir/apps/domain/test/domain/resources/resource/changeset_test.exs new file mode 100644 index 000000000..3c1f76ed1 --- /dev/null +++ b/elixir/apps/domain/test/domain/resources/resource/changeset_test.exs @@ -0,0 +1,85 @@ +defmodule Domain.Resources.Resource.ChangesetTest do + use Domain.DataCase, async: true + import Domain.Resources.Resource.Changeset + + describe "create/2" do + test "validates and normalizes CIDR ranges" do + for {string, cidr} <- [ + {"192.168.1.1/24", "192.168.1.0/24"}, + {"101.100.100.0/28", "101.100.100.0/28"}, + {"192.168.1.255/28", "192.168.1.240/28"}, + {"192.168.1.255/32", "192.168.1.255/32"}, + {"2607:f8b0:4012:0::200e/128", "2607:f8b0:4012::200e/128"} + ] do + changeset = create(%{type: :cidr, address: string}) + assert changeset.changes[:address] == cidr + assert changeset.valid? + end + + refute create(%{type: :cidr, address: "192.168.1.256/28"}).valid? + refute create(%{type: :cidr, address: "100.64.0.0/8"}).valid? + refute create(%{type: :cidr, address: "fd00:2021:1111::/102"}).valid? + refute create(%{type: :cidr, address: "0.0.0.0/32"}).valid? + refute create(%{type: :cidr, address: "0.0.0.0/16"}).valid? + refute create(%{type: :cidr, address: "0.0.0.0/0"}).valid? + refute create(%{type: :cidr, address: "127.0.0.1/32"}).valid? + refute create(%{type: :cidr, address: "::0/32"}).valid? + refute create(%{type: :cidr, address: "::1/128"}).valid? + refute create(%{type: :cidr, address: "::8/8"}).valid? + refute create(%{type: :cidr, address: "2607:f8b0:4012:0::200e/128:80"}).valid? + end + + test "validates and normalizes IP addresses" do + for {string, ip} <- [ + {"192.168.1.1", "192.168.1.1"}, + {"101.100.100.0", "101.100.100.0"}, + {"192.168.1.255", "192.168.1.255"}, + {"2607:f8b0:4012:0::200e", "2607:f8b0:4012::200e"} + ] do + changeset = create(%{type: :ip, address: string}) + assert changeset.changes[:address] == ip + assert changeset.valid? + end + + refute create(%{type: :ip, address: "192.168.1.256"}).valid? + refute create(%{type: :ip, address: "100.64.0.0"}).valid? + refute create(%{type: :ip, address: "fd00:2021:1111::"}).valid? + refute create(%{type: :ip, address: "0.0.0.0"}).valid? + refute create(%{type: :ip, address: "::0"}).valid? + refute create(%{type: :ip, address: "127.0.0.1"}).valid? + refute create(%{type: :ip, address: "::1"}).valid? + refute create(%{type: :ip, address: "[2607:f8b0:4012:0::200e]:80"}).valid? + end + + test "accepts valid DNS addresses" do + for valid_address <- [ + "*.google", + "?.google", + "google", + "example.com", + "example.weird", + "1234567890.com", + "#{String.duplicate("a", 63)}.com", + "такі.справи", + "subdomain.subdomain2.example.space" + ] do + changeset = create(%{type: :dns, address: valid_address}) + assert changeset.valid? + end + + refute create(%{type: :dns, address: "exa&mple.com"}).valid? + refute create(%{type: :dns, address: ""}).valid? + refute create(%{type: :dns, address: "http://example.com/"}).valid? + refute create(%{type: :dns, address: "//example.com/"}).valid? + refute create(%{type: :dns, address: "example.com/"}).valid? + refute create(%{type: :dns, address: ".example.com"}).valid? + refute create(%{type: :dns, address: "example."}).valid? + refute create(%{type: :dns, address: "example.com:80"}).valid? + end + end + + def create(attrs) do + Fixtures.Accounts.create_account() + |> create(attrs) + end +end diff --git a/elixir/apps/domain/test/domain/resources_test.exs b/elixir/apps/domain/test/domain/resources_test.exs index 9bc6fd58d..dc1a742ac 100644 --- a/elixir/apps/domain/test/domain/resources_test.exs +++ b/elixir/apps/domain/test/domain/resources_test.exs @@ -789,6 +789,7 @@ defmodule Domain.ResourcesTest do assert errors_on(changeset) == %{ address: ["can't be blank"], + type: ["can't be blank"], connections: ["can't be blank"] } end @@ -800,6 +801,7 @@ defmodule Domain.ResourcesTest do assert errors_on(changeset) == %{ address: ["can't be blank"], name: ["should be at most 255 character(s)"], + type: ["can't be blank"], filters: ["is invalid"], connections: ["is invalid"] } @@ -820,25 +822,27 @@ defmodule Domain.ResourcesTest do assert {:error, changeset} = create_resource(attrs, subject) assert "is not a valid CIDR range" in errors_on(changeset).address - attrs = %{"address" => "192.168.1.1", "type" => "cidr"} + attrs = %{"address" => "192.168.1.1", "type" => "ip"} assert {:error, changeset} = create_resource(attrs, subject) - assert "is not a valid CIDR range" in errors_on(changeset).address + refute Map.has_key?(errors_on(changeset), :address) attrs = %{"address" => "100.64.0.0/8", "type" => "cidr"} assert {:error, changeset} = create_resource(attrs, subject) - assert "can not be in the CIDR 100.64.0.0/10" in errors_on(changeset).address + assert "can not be in the CIDR 100.64.0.0/11" in errors_on(changeset).address attrs = %{"address" => "fd00:2021:1111::/102", "type" => "cidr"} assert {:error, changeset} = create_resource(attrs, subject) - assert "can not be in the CIDR fd00:2021:1111::/106" in errors_on(changeset).address + assert "can not be in the CIDR fd00:2021:1111::/107" in errors_on(changeset).address attrs = %{"address" => "::/0", "type" => "cidr"} assert {:error, changeset} = create_resource(attrs, subject) - refute Map.has_key?(errors_on(changeset), :address) + assert "can not contain loopback addresses" in errors_on(changeset).address + assert "can not contain all IPv6 addresses" in errors_on(changeset).address attrs = %{"address" => "0.0.0.0/0", "type" => "cidr"} assert {:error, changeset} = create_resource(attrs, subject) - refute Map.has_key?(errors_on(changeset), :address) + assert "can not contain loopback addresses" in errors_on(changeset).address + assert "can not contain all IPv4 addresses" in errors_on(changeset).address end # We allow names to be duplicate because Resources are split into Sites @@ -875,9 +879,6 @@ defmodule Domain.ResourcesTest do assert resource.name == attrs.address assert resource.account_id == account.id - refute is_nil(resource.ipv4) - refute is_nil(resource.ipv6) - assert resource.created_by == :identity assert resource.created_by_identity_id == subject.identity.id @@ -904,19 +905,16 @@ defmodule Domain.ResourcesTest do %{gateway_group_id: gateway.group_id} ], type: :cidr, - name: nil, + name: "mycidr", address: "192.168.1.1/28" ) assert {:ok, resource} = create_resource(attrs, subject) assert resource.address == "192.168.1.0/28" - assert resource.name == attrs.address + assert resource.name == attrs.name assert resource.account_id == account.id - assert is_nil(resource.ipv4) - assert is_nil(resource.ipv6) - assert [ %Domain.Resources.Connection{ resource_id: resource_id, @@ -937,59 +935,6 @@ defmodule Domain.ResourcesTest do assert Repo.aggregate(Domain.Network.Address, :count) == address_count end - test "does not allow to reuse IP addresses within an account", %{ - account: account, - subject: subject - } do - gateway = Fixtures.Gateways.create_gateway(account: account) - - attrs = - Fixtures.Resources.resource_attrs( - connections: [ - %{gateway_group_id: gateway.group_id} - ] - ) - - assert {:ok, resource} = create_resource(attrs, subject) - - addresses = - Domain.Network.Address - |> Repo.all() - |> Enum.map(fn %Domain.Network.Address{address: address, type: type} -> - %{address: address, type: type} - end) - - assert %{address: resource.ipv4, type: :ipv4} in addresses - assert %{address: resource.ipv6, type: :ipv6} in addresses - - assert_raise Ecto.ConstraintError, fn -> - Fixtures.Network.create_address(address: resource.ipv4, account: account) - end - - assert_raise Ecto.ConstraintError, fn -> - Fixtures.Network.create_address(address: resource.ipv6, account: account) - end - end - - test "ip addresses are unique per account", %{ - account: account, - subject: subject - } do - gateway = Fixtures.Gateways.create_gateway(account: account) - - attrs = - Fixtures.Resources.resource_attrs( - connections: [ - %{gateway_group_id: gateway.group_id} - ] - ) - - assert {:ok, resource} = create_resource(attrs, subject) - - assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: resource.ipv4) - assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: resource.ipv6) - end - test "returns error when subject has no permission to create resources", %{ subject: subject } do diff --git a/elixir/apps/domain/test/support/fixtures/actors.ex b/elixir/apps/domain/test/support/fixtures/actors.ex index f132b7354..76f5ad054 100644 --- a/elixir/apps/domain/test/support/fixtures/actors.ex +++ b/elixir/apps/domain/test/support/fixtures/actors.ex @@ -16,8 +16,7 @@ defmodule Domain.Fixtures.Actors do Fixtures.Accounts.create_account(assoc_attrs) end) - {provider, attrs} = - Map.pop(attrs, :provider) + {provider, attrs} = Map.pop(attrs, :provider) {provider_identifier, attrs} = Map.pop_lazy(attrs, :provider_identifier, fn -> @@ -98,8 +97,7 @@ defmodule Domain.Fixtures.Actors do Fixtures.Accounts.create_account(assoc_attrs) end) - {provider, attrs} = - Map.pop(attrs, :provider) + {provider, attrs} = Map.pop(attrs, :provider) {group_id, attrs} = pop_assoc_fixture_id(attrs, :group, fn assoc_attrs -> diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index 295f12e01..157ac65d0 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -227,8 +227,7 @@ defmodule Domain.Fixtures.Auth do random_provider_identifier(provider) end) - {provider_state, attrs} = - Map.pop(attrs, :provider_state) + {provider_state, attrs} = Map.pop(attrs, :provider_state) {actor, attrs} = pop_assoc_fixture(attrs, :actor, fn assoc_attrs -> diff --git a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex index 878d5d5a9..1ab2903fe 100644 --- a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex +++ b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex @@ -10,181 +10,180 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do def mock_users_list_endpoint(bypass, users \\ nil) do users_list_endpoint_path = "/admin/directory/v1/users" - resp = - %{ - "kind" => "admin#directory#users", - "users" => - users || - [ - %{ - "agreedToTerms" => true, - "archived" => false, - "changePasswordAtNextLogin" => false, - "creationTime" => "2023-06-10T17:32:06.000Z", - "customerId" => "CustomerID1", - "emails" => [ - %{"address" => "b@firez.xxx", "primary" => true}, - %{"address" => "b@ext.firez.xxx"} - ], - "etag" => "\"ET-61Bnx4\"", - "id" => "USER_ID1", - "includeInGlobalAddressList" => true, - "ipWhitelisted" => false, - "isAdmin" => false, - "isDelegatedAdmin" => false, - "isEnforcedIn2Sv" => false, - "isEnrolledIn2Sv" => false, - "isMailboxSetup" => true, - "kind" => "admin#directory#user", - "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], - "lastLoginTime" => "2023-06-26T13:53:30.000Z", - "name" => %{ - "familyName" => "Manifold", - "fullName" => "Brian Manifold", - "givenName" => "Brian" - }, - "nonEditableAliases" => ["b@ext.firez.xxx"], - "orgUnitPath" => "/Engineering", - "organizations" => [ - %{ - "customType" => "", - "department" => "Engineering", - "location" => "", - "name" => "Firezone, Inc.", - "primary" => true, - "title" => "Senior Fullstack Engineer", - "type" => "work" - } - ], - "phones" => [%{"type" => "mobile", "value" => "(567) 111-2233"}], - "primaryEmail" => "b@firez.xxx", - "recoveryEmail" => "xxx@xxx.com", - "suspended" => false, - "thumbnailPhotoEtag" => "\"ET\"", - "thumbnailPhotoUrl" => - "https://lh3.google.com/ao/AP2z2aWvm9JM99oCFZ1TVOJgQZlmZdMMYNr7w9G0jZApdTuLHfAueGFb_XzgTvCNRhGw=s96-c" + resp = %{ + "kind" => "admin#directory#users", + "users" => + users || + [ + %{ + "agreedToTerms" => true, + "archived" => false, + "changePasswordAtNextLogin" => false, + "creationTime" => "2023-06-10T17:32:06.000Z", + "customerId" => "CustomerID1", + "emails" => [ + %{"address" => "b@firez.xxx", "primary" => true}, + %{"address" => "b@ext.firez.xxx"} + ], + "etag" => "\"ET-61Bnx4\"", + "id" => "USER_ID1", + "includeInGlobalAddressList" => true, + "ipWhitelisted" => false, + "isAdmin" => false, + "isDelegatedAdmin" => false, + "isEnforcedIn2Sv" => false, + "isEnrolledIn2Sv" => false, + "isMailboxSetup" => true, + "kind" => "admin#directory#user", + "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], + "lastLoginTime" => "2023-06-26T13:53:30.000Z", + "name" => %{ + "familyName" => "Manifold", + "fullName" => "Brian Manifold", + "givenName" => "Brian" }, - %{ - "agreedToTerms" => true, - "archived" => false, - "changePasswordAtNextLogin" => false, - "creationTime" => "2023-05-18T19:10:28.000Z", - "customerId" => "CustomerID1", - "emails" => [ - %{"address" => "f@firez.xxx", "primary" => true}, - %{"address" => "f@ext.firez.xxx"} - ], - "etag" => "\"ET-c\"", - "id" => "USER_ID2", - "includeInGlobalAddressList" => true, - "ipWhitelisted" => false, - "isAdmin" => false, - "isDelegatedAdmin" => false, - "isEnforcedIn2Sv" => false, - "isEnrolledIn2Sv" => false, - "isMailboxSetup" => true, - "kind" => "admin#directory#user", - "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], - "lastLoginTime" => "2023-06-27T23:12:16.000Z", - "name" => %{ - "familyName" => "Lovebloom", - "fullName" => "Francesca Lovebloom", - "givenName" => "Francesca" - }, - "nonEditableAliases" => ["f@ext.firez.xxx"], - "orgUnitPath" => "/Engineering", - "organizations" => [ - %{ - "customType" => "", - "department" => "Engineering", - "location" => "", - "name" => "Firezone, Inc.", - "primary" => true, - "title" => "Senior Systems Engineer", - "type" => "work" - } - ], - "phones" => [%{"type" => "mobile", "value" => "(567) 111-2233"}], - "primaryEmail" => "f@firez.xxx", - "recoveryEmail" => "xxx.xxx", - "recoveryPhone" => "+15671112323", - "suspended" => false + "nonEditableAliases" => ["b@ext.firez.xxx"], + "orgUnitPath" => "/Engineering", + "organizations" => [ + %{ + "customType" => "", + "department" => "Engineering", + "location" => "", + "name" => "Firezone, Inc.", + "primary" => true, + "title" => "Senior Fullstack Engineer", + "type" => "work" + } + ], + "phones" => [%{"type" => "mobile", "value" => "(567) 111-2233"}], + "primaryEmail" => "b@firez.xxx", + "recoveryEmail" => "xxx@xxx.com", + "suspended" => false, + "thumbnailPhotoEtag" => "\"ET\"", + "thumbnailPhotoUrl" => + "https://lh3.google.com/ao/AP2z2aWvm9JM99oCFZ1TVOJgQZlmZdMMYNr7w9G0jZApdTuLHfAueGFb_XzgTvCNRhGw=s96-c" + }, + %{ + "agreedToTerms" => true, + "archived" => false, + "changePasswordAtNextLogin" => false, + "creationTime" => "2023-05-18T19:10:28.000Z", + "customerId" => "CustomerID1", + "emails" => [ + %{"address" => "f@firez.xxx", "primary" => true}, + %{"address" => "f@ext.firez.xxx"} + ], + "etag" => "\"ET-c\"", + "id" => "USER_ID2", + "includeInGlobalAddressList" => true, + "ipWhitelisted" => false, + "isAdmin" => false, + "isDelegatedAdmin" => false, + "isEnforcedIn2Sv" => false, + "isEnrolledIn2Sv" => false, + "isMailboxSetup" => true, + "kind" => "admin#directory#user", + "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], + "lastLoginTime" => "2023-06-27T23:12:16.000Z", + "name" => %{ + "familyName" => "Lovebloom", + "fullName" => "Francesca Lovebloom", + "givenName" => "Francesca" }, - %{ - "agreedToTerms" => true, - "archived" => false, - "changePasswordAtNextLogin" => false, - "creationTime" => "2022-05-31T19:17:41.000Z", - "customerId" => "CustomerID1", - "emails" => [ - %{"address" => "g@firez.xxx", "primary" => true}, - %{"address" => "gabi@firez.xxx"} - ], - "etag" => "\"ET\"", - "id" => "USER_ID3", - "includeInGlobalAddressList" => true, - "ipWhitelisted" => false, - "isAdmin" => false, - "isDelegatedAdmin" => false, - "isEnforcedIn2Sv" => false, - "isEnrolledIn2Sv" => true, - "isMailboxSetup" => true, - "kind" => "admin#directory#user", - "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], - "lastLoginTime" => "2023-07-03T17:47:37.000Z", - "name" => %{ - "familyName" => "Steinberg", - "fullName" => "Gabriel Steinberg", - "givenName" => "Gabriel" - }, - "nonEditableAliases" => ["g@ext.firez.xxx"], - "orgUnitPath" => "/Engineering", - "primaryEmail" => "g@firez.xxx", - "suspended" => false + "nonEditableAliases" => ["f@ext.firez.xxx"], + "orgUnitPath" => "/Engineering", + "organizations" => [ + %{ + "customType" => "", + "department" => "Engineering", + "location" => "", + "name" => "Firezone, Inc.", + "primary" => true, + "title" => "Senior Systems Engineer", + "type" => "work" + } + ], + "phones" => [%{"type" => "mobile", "value" => "(567) 111-2233"}], + "primaryEmail" => "f@firez.xxx", + "recoveryEmail" => "xxx.xxx", + "recoveryPhone" => "+15671112323", + "suspended" => false + }, + %{ + "agreedToTerms" => true, + "archived" => false, + "changePasswordAtNextLogin" => false, + "creationTime" => "2022-05-31T19:17:41.000Z", + "customerId" => "CustomerID1", + "emails" => [ + %{"address" => "g@firez.xxx", "primary" => true}, + %{"address" => "gabi@firez.xxx"} + ], + "etag" => "\"ET\"", + "id" => "USER_ID3", + "includeInGlobalAddressList" => true, + "ipWhitelisted" => false, + "isAdmin" => false, + "isDelegatedAdmin" => false, + "isEnforcedIn2Sv" => false, + "isEnrolledIn2Sv" => true, + "isMailboxSetup" => true, + "kind" => "admin#directory#user", + "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], + "lastLoginTime" => "2023-07-03T17:47:37.000Z", + "name" => %{ + "familyName" => "Steinberg", + "fullName" => "Gabriel Steinberg", + "givenName" => "Gabriel" }, - %{ - "agreedToTerms" => true, - "aliases" => ["j@firez.xxx"], - "archived" => false, - "changePasswordAtNextLogin" => false, - "creationTime" => "2022-04-19T21:54:21.000Z", - "customerId" => "CustomerID1", - "emails" => [ - %{"address" => "j@gmail.com", "type" => "home"}, - %{"address" => "j@firez.xxx", "primary" => true}, - %{"address" => "j@firez.xxx"}, - %{"address" => "j@ext.firez.xxx"} - ], - "etag" => "\"ET-4Z0R5TBJvppLL8\"", - "id" => "USER_ID4", - "includeInGlobalAddressList" => true, - "ipWhitelisted" => false, - "isAdmin" => true, - "isDelegatedAdmin" => false, - "isEnforcedIn2Sv" => false, - "isEnrolledIn2Sv" => true, - "isMailboxSetup" => true, - "kind" => "admin#directory#user", - "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], - "lastLoginTime" => "2023-07-04T15:08:45.000Z", - "name" => %{ - "familyName" => "Bou Kheir", - "fullName" => "Jamil Bou Kheir", - "givenName" => "Jamil" - }, - "nonEditableAliases" => ["jamil@ext.firez.xxx"], - "orgUnitPath" => "/", - "phones" => [], - "primaryEmail" => "jamil@firez.xxx", - "recoveryEmail" => "xxx.xxx", - "recoveryPhone" => "+15671112323", - "suspended" => false, - "thumbnailPhotoEtag" => "\"ETX\"", - "thumbnailPhotoUrl" => - "https://lh3.google.com/ao/AP2z2aWvm9JM99oCFZ1TVOJgQZlmZdMMYNr7w9G0jZApdTuLHfAueGFb_XzgTvCNRhGw=s96-c" - } - ] - } + "nonEditableAliases" => ["g@ext.firez.xxx"], + "orgUnitPath" => "/Engineering", + "primaryEmail" => "g@firez.xxx", + "suspended" => false + }, + %{ + "agreedToTerms" => true, + "aliases" => ["j@firez.xxx"], + "archived" => false, + "changePasswordAtNextLogin" => false, + "creationTime" => "2022-04-19T21:54:21.000Z", + "customerId" => "CustomerID1", + "emails" => [ + %{"address" => "j@gmail.com", "type" => "home"}, + %{"address" => "j@firez.xxx", "primary" => true}, + %{"address" => "j@firez.xxx"}, + %{"address" => "j@ext.firez.xxx"} + ], + "etag" => "\"ET-4Z0R5TBJvppLL8\"", + "id" => "USER_ID4", + "includeInGlobalAddressList" => true, + "ipWhitelisted" => false, + "isAdmin" => true, + "isDelegatedAdmin" => false, + "isEnforcedIn2Sv" => false, + "isEnrolledIn2Sv" => true, + "isMailboxSetup" => true, + "kind" => "admin#directory#user", + "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], + "lastLoginTime" => "2023-07-04T15:08:45.000Z", + "name" => %{ + "familyName" => "Bou Kheir", + "fullName" => "Jamil Bou Kheir", + "givenName" => "Jamil" + }, + "nonEditableAliases" => ["jamil@ext.firez.xxx"], + "orgUnitPath" => "/", + "phones" => [], + "primaryEmail" => "jamil@firez.xxx", + "recoveryEmail" => "xxx.xxx", + "recoveryPhone" => "+15671112323", + "suspended" => false, + "thumbnailPhotoEtag" => "\"ETX\"", + "thumbnailPhotoUrl" => + "https://lh3.google.com/ao/AP2z2aWvm9JM99oCFZ1TVOJgQZlmZdMMYNr7w9G0jZApdTuLHfAueGFb_XzgTvCNRhGw=s96-c" + } + ] + } test_pid = self() @@ -202,26 +201,25 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do def mock_organization_units_list_endpoint(bypass, org_units \\ nil) do org_units_list_endpoint_path = "/admin/directory/v1/customer/my_customer/orgunits" - resp = - %{ - "kind" => "admin#directory#org_units", - "etag" => "\"FwDC5ZsOozt9qI9yuJfiMqwYO1K-EEG4flsXSov57CY/Y3F7O3B5N0h0C_3Pd3OMifRNUVc\"", - "organizationUnits" => - org_units || - [ - %{ - "kind" => "admin#directory#orgUnit", - "name" => "Engineering", - "description" => "Engineering team", - "etag" => "\"ET\"", - "blockInheritance" => false, - "orgUnitId" => "OU_ID1", - "orgUnitPath" => "/Engineering", - "parentOrgUnitId" => "OU_ID0", - "parentOrgUnitPath" => "/" - } - ] - } + resp = %{ + "kind" => "admin#directory#org_units", + "etag" => "\"FwDC5ZsOozt9qI9yuJfiMqwYO1K-EEG4flsXSov57CY/Y3F7O3B5N0h0C_3Pd3OMifRNUVc\"", + "organizationUnits" => + org_units || + [ + %{ + "kind" => "admin#directory#orgUnit", + "name" => "Engineering", + "description" => "Engineering team", + "etag" => "\"ET\"", + "blockInheritance" => false, + "orgUnitId" => "OU_ID1", + "orgUnitPath" => "/Engineering", + "parentOrgUnitId" => "OU_ID0", + "parentOrgUnitPath" => "/" + } + ] + } test_pid = self() @@ -239,57 +237,56 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do def mock_groups_list_endpoint(bypass, groups \\ nil) do groups_list_endpoint_path = "/admin/directory/v1/groups" - resp = - %{ - "kind" => "admin#directory#groups", - "etag" => "\"FwDC5ZsOozt9qI9yuJfiMqwYO1K-EEG4flsXSov57CY/Y3F7O3B5N0h0C_3Pd3OMifRNUVc\"", - "groups" => - groups || - [ - %{ - "kind" => "admin#directory#group", - "id" => "GROUP_ID1", - "etag" => "\"ET\"", - "email" => "i@fiez.xxx", - "name" => "Infrastructure", - "directMembersCount" => "5", - "description" => "Group to handle infrastructure alerts and management", - "adminCreated" => true, - "aliases" => [ - "pnr@firez.one" - ], - "nonEditableAliases" => [ - "i@ext.fiez.xxx" - ] - }, - %{ - "kind" => "admin#directory#group", - "id" => "GROUP_ID2", - "etag" => "\"ET\"", - "email" => "eng@fiez.xxx", - "name" => "Engineering", - "directMembersCount" => "1", - "description" => "Firezone Engineering team", - "adminCreated" => true, - "nonEditableAliases" => [ - "eng@ext.fiez.xxx" - ] - }, - %{ - "kind" => "admin#directory#group", - "id" => "GROUP_ID9c6y382yitz1j", - "etag" => "\"ET\"", - "email" => "sec@fiez.xxx", - "name" => "Security", - "directMembersCount" => "5", - "description" => "Security Notifications", - "adminCreated" => false, - "nonEditableAliases" => [ - "sec@ext.fiez.xxx" - ] - } - ] - } + resp = %{ + "kind" => "admin#directory#groups", + "etag" => "\"FwDC5ZsOozt9qI9yuJfiMqwYO1K-EEG4flsXSov57CY/Y3F7O3B5N0h0C_3Pd3OMifRNUVc\"", + "groups" => + groups || + [ + %{ + "kind" => "admin#directory#group", + "id" => "GROUP_ID1", + "etag" => "\"ET\"", + "email" => "i@fiez.xxx", + "name" => "Infrastructure", + "directMembersCount" => "5", + "description" => "Group to handle infrastructure alerts and management", + "adminCreated" => true, + "aliases" => [ + "pnr@firez.one" + ], + "nonEditableAliases" => [ + "i@ext.fiez.xxx" + ] + }, + %{ + "kind" => "admin#directory#group", + "id" => "GROUP_ID2", + "etag" => "\"ET\"", + "email" => "eng@fiez.xxx", + "name" => "Engineering", + "directMembersCount" => "1", + "description" => "Firezone Engineering team", + "adminCreated" => true, + "nonEditableAliases" => [ + "eng@ext.fiez.xxx" + ] + }, + %{ + "kind" => "admin#directory#group", + "id" => "GROUP_ID9c6y382yitz1j", + "etag" => "\"ET\"", + "email" => "sec@fiez.xxx", + "name" => "Security", + "directMembersCount" => "5", + "description" => "Security Notifications", + "adminCreated" => false, + "nonEditableAliases" => [ + "sec@ext.fiez.xxx" + ] + } + ] + } test_pid = self() @@ -307,60 +304,59 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do def mock_group_members_list_endpoint(bypass, group_id, members \\ nil) do group_members_list_endpoint_path = "/admin/directory/v1/groups/#{group_id}/members" - resp = - %{ - "kind" => "admin#directory#members", - "etag" => "\"XXX\"", - "members" => - members || - [ - %{ - "kind" => "admin#directory#member", - "etag" => "\"ET\"", - "id" => "USER_ID1", - "email" => "b@firez.xxx", - "role" => "MEMBER", - "type" => "USER", - "status" => "ACTIVE" - }, - %{ - "kind" => "admin#directory#member", - "etag" => "\"ET\"", - "id" => "USER_ID4", - "email" => "j@firez.xxx", - "role" => "MEMBER", - "type" => "USER", - "status" => "ACTIVE" - }, - %{ - "kind" => "admin#directory#member", - "etag" => "\"ET\"", - "id" => "USER_ID3", - "email" => "g@firez.xxx", - "role" => "MEMBER", - "type" => "USER", - "status" => "INACTIVE" - }, - %{ - "kind" => "admin#directory#member", - "etag" => "\"ET\"", - "id" => "GROUP_ID1", - "email" => "eng@firez.xxx", - "role" => "MEMBER", - "type" => "GROUP", - "status" => "ACTIVE" - }, - %{ - "kind" => "admin#directory#member", - "etag" => "\"ET\"", - "id" => "GROUP_ID2", - "email" => "sec@firez.xxx", - "role" => "MEMBER", - "type" => "GROUP", - "status" => "ACTIVE" - } - ] - } + resp = %{ + "kind" => "admin#directory#members", + "etag" => "\"XXX\"", + "members" => + members || + [ + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "USER_ID1", + "email" => "b@firez.xxx", + "role" => "MEMBER", + "type" => "USER", + "status" => "ACTIVE" + }, + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "USER_ID4", + "email" => "j@firez.xxx", + "role" => "MEMBER", + "type" => "USER", + "status" => "ACTIVE" + }, + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "USER_ID3", + "email" => "g@firez.xxx", + "role" => "MEMBER", + "type" => "USER", + "status" => "INACTIVE" + }, + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "GROUP_ID1", + "email" => "eng@firez.xxx", + "role" => "MEMBER", + "type" => "GROUP", + "status" => "ACTIVE" + }, + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "GROUP_ID2", + "email" => "sec@firez.xxx", + "role" => "MEMBER", + "type" => "GROUP", + "status" => "ACTIVE" + } + ] + } test_pid = self() diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex index 04cc9e921..b5865811f 100644 --- a/elixir/apps/web/lib/web/controllers/auth_controller.ex +++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex @@ -30,8 +30,7 @@ defmodule Web.AuthController do } } = params ) do - redirect_params = - take_non_empty_params(params, ["client_platform", "client_csrf_token"]) + redirect_params = take_non_empty_params(params, ["client_platform", "client_csrf_token"]) with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), {:ok, subject} <- diff --git a/elixir/apps/web/lib/web/controllers/home_controller.ex b/elixir/apps/web/lib/web/controllers/home_controller.ex index f69dcdb96..590de982c 100644 --- a/elixir/apps/web/lib/web/controllers/home_controller.ex +++ b/elixir/apps/web/lib/web/controllers/home_controller.ex @@ -16,8 +16,7 @@ defmodule Web.HomeController do _other -> {[], conn} end - redirect_params = - take_non_empty_params(params, ["client_platform", "client_csrf_token"]) + redirect_params = take_non_empty_params(params, ["client_platform", "client_csrf_token"]) conn |> put_layout(html: {Web.Layouts, :public}) @@ -25,8 +24,7 @@ defmodule Web.HomeController do end def redirect_to_sign_in(conn, %{"account_id_or_slug" => account_id_or_slug} = params) do - redirect_params = - take_non_empty_params(params, ["client_platform", "client_csrf_token"]) + redirect_params = take_non_empty_params(params, ["client_platform", "client_csrf_token"]) redirect(conn, to: ~p"/#{account_id_or_slug}?#{redirect_params}") end diff --git a/elixir/apps/web/lib/web/live/resources/new.ex b/elixir/apps/web/lib/web/live/resources/new.ex index b677926b4..4ea077c33 100644 --- a/elixir/apps/web/lib/web/live/resources/new.ex +++ b/elixir/apps/web/lib/web/live/resources/new.ex @@ -11,6 +11,7 @@ defmodule Web.Resources.New do assign( socket, gateway_groups: gateway_groups, + name_changed?: false, form: to_form(changeset), params: Map.take(params, ["site_id"]), traffic_filters_enabled?: Config.traffic_filters_enabled?() @@ -43,17 +44,64 @@ defmodule Web.Resources.New do label="Name" placeholder="Name this resource" required - phx-debounce="300" /> +
+ +
+
+ <.input + id="resource-type--dns" + type="radio" + field={@form[:type]} + value="dns" + label="DNS" + checked={@form[:type].value == :dns} + required + /> +
+
+ <.input + id="resource-type--ip" + type="radio" + field={@form[:type]} + value="ip" + label="IP" + checked={@form[:type].value == :ip} + required + /> +
+
+ <.input + id="resource-type--cidr" + type="radio" + field={@form[:type]} + value="cidr" + label="CIDR" + checked={@form[:type].value == :cidr} + required + /> +
+
+
+ <.input field={@form[:address]} autocomplete="off" type="text" label="Address" - placeholder="Enter IP address, CIDR, or DNS name" + placeholder={ + cond do + @form[:type].value == :dns -> "app.namespace.cluster.local" + @form[:type].value == :cidr -> "192.168.1.1/28" + @form[:type].value == :ip -> "1.1.1.1" + true -> "Please select a Type from the options first" + end + } + disabled={is_nil(@form[:type].value)} required - phx-debounce="300" /> <.filters_form :if={@traffic_filters_enabled?} form={@form[:filters]} /> @@ -75,9 +123,12 @@ defmodule Web.Resources.New do """ end - def handle_event("change", %{"resource" => attrs}, socket) do + def handle_event("change", %{"resource" => attrs} = payload, socket) do + name_changed? = socket.assigns.name_changed? || payload["_target"] == ["resource", "name"] + attrs = attrs + |> maybe_put_default_name(name_changed?) |> map_filters_form_attrs() |> map_connections_form_attrs() |> maybe_put_connections(socket.assigns.params) @@ -86,12 +137,13 @@ defmodule Web.Resources.New do Resources.new_resource(socket.assigns.account, attrs) |> Map.put(:action, :validate) - {:noreply, assign(socket, form: to_form(changeset))} + {:noreply, assign(socket, form: to_form(changeset), name_changed?: name_changed?)} end def handle_event("submit", %{"resource" => attrs}, socket) do attrs = attrs + |> maybe_put_default_name() |> map_filters_form_attrs() |> map_connections_form_attrs() |> maybe_put_connections(socket.assigns.params) @@ -109,6 +161,16 @@ defmodule Web.Resources.New do end end + defp maybe_put_default_name(attrs, name_changed? \\ true) + + defp maybe_put_default_name(attrs, true) do + attrs + end + + defp maybe_put_default_name(attrs, false) do + Map.put(attrs, "name", attrs["address"]) + end + defp maybe_put_connections(attrs, params) do if site_id = params["site_id"] do Map.put(attrs, "connections", %{ diff --git a/elixir/apps/web/test/web/auth_test.exs b/elixir/apps/web/test/web/auth_test.exs index 482e5ef68..e764be4aa 100644 --- a/elixir/apps/web/test/web/auth_test.exs +++ b/elixir/apps/web/test/web/auth_test.exs @@ -519,8 +519,7 @@ defmodule Web.AuthTest do session = conn |> put_session(:session_token, session_token) |> get_session() params = %{"account_id_or_slug" => subject.account.id} - assert {:cont, updated_socket} = - on_mount(:ensure_authenticated, params, session, socket) + assert {:cont, updated_socket} = on_mount(:ensure_authenticated, params, session, socket) assert updated_socket.assigns.subject.identity.id == subject.identity.id assert is_nil(updated_socket.redirected) @@ -535,8 +534,7 @@ defmodule Web.AuthTest do session = conn |> put_session(:session_token, session_token) |> get_session() params = %{"account_id_or_slug" => subject.account.slug} - assert {:halt, updated_socket} = - on_mount(:ensure_authenticated, params, session, socket) + assert {:halt, updated_socket} = on_mount(:ensure_authenticated, params, session, socket) assert is_nil(updated_socket.assigns.subject) @@ -551,8 +549,7 @@ defmodule Web.AuthTest do session = conn |> get_session() params = %{"account_id_or_slug" => subject.account.slug} - assert {:halt, updated_socket} = - on_mount(:ensure_authenticated, params, session, socket) + assert {:halt, updated_socket} = on_mount(:ensure_authenticated, params, session, socket) assert is_nil(updated_socket.assigns.subject) diff --git a/elixir/apps/web/test/web/live/resources/new_test.exs b/elixir/apps/web/test/web/live/resources/new_test.exs index 028b51f2f..2767ce11d 100644 --- a/elixir/apps/web/test/web/live/resources/new_test.exs +++ b/elixir/apps/web/test/web/live/resources/new_test.exs @@ -59,11 +59,10 @@ defmodule Web.Live.Resources.NewTest do form = form(lv, "form") - connection_inputs = - [ - "resource[connections][#{group.id}][enabled]", - "resource[connections][#{group.id}][gateway_group_id]" - ] + connection_inputs = [ + "resource[connections][#{group.id}][enabled]", + "resource[connections][#{group.id}][gateway_group_id]" + ] expected_inputs = (connection_inputs ++ @@ -79,7 +78,8 @@ defmodule Web.Live.Resources.NewTest do "resource[filters][udp][enabled]", "resource[filters][udp][ports]", "resource[filters][udp][protocol]", - "resource[name]" + "resource[name]", + "resource[type]" ]) |> Enum.sort() @@ -111,7 +111,8 @@ defmodule Web.Live.Resources.NewTest do "resource[filters][udp][enabled]", "resource[filters][udp][ports]", "resource[filters][udp][protocol]", - "resource[name]" + "resource[name]", + "resource[type]" ] end @@ -121,11 +122,11 @@ defmodule Web.Live.Resources.NewTest do conn: conn } do attrs = %{ - name: "foobar.com", + name: "my website", address: "foobar.com", filters: %{ tcp: %{ports: "80, 443", enabled: true}, - udp: %{ports: "100", enabled: true} + udp: %{ports: "100,102-105", enabled: true} } } @@ -134,13 +135,12 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + lv |> form("form", resource: attrs) - |> validate_change(%{resource: %{name: String.duplicate("a", 256)}}, fn form, _html -> - assert form_validation_errors(form) == %{ - "resource[name]" => ["should be at most 255 character(s)"] - } - end) |> validate_change(%{resource: %{filters: %{tcp: %{ports: "a"}}}}, fn form, _html -> assert form_validation_errors(form) == %{ "resource[filters][tcp][ports]" => ["is invalid"] @@ -172,6 +172,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + assert lv |> form("form", resource: attrs) |> render_submit() @@ -203,6 +207,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + assert lv |> form("form", resource: attrs) |> render_submit() @@ -220,12 +228,7 @@ defmodule Web.Live.Resources.NewTest do [connection | _] = resource.connections attrs = %{ - name: "foobar.com", address: "foobar.com", - filters: %{ - tcp: %{ports: "80, 443", enabled: true}, - udp: %{ports: "100", enabled: true} - }, connections: %{connection.gateway_group_id => %{enabled: false}} } @@ -234,6 +237,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + assert lv |> form("form", resource: attrs) |> render_submit() @@ -251,6 +258,7 @@ defmodule Web.Live.Resources.NewTest do attrs = %{ name: "foobar.com", + type: "dns", address: "foobar.com", filters: %{ icmp: %{enabled: true}, @@ -265,6 +273,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + lv |> form("form", resource: attrs) |> render_submit() @@ -295,6 +307,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new?site_id=#{group}") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + lv |> form("form", resource: attrs) |> render_submit() @@ -319,7 +335,11 @@ defmodule Web.Live.Resources.NewTest do form = form(lv, "form") - assert find_inputs(form) == ["resource[address]", "resource[name]"] + assert find_inputs(form) == [ + "resource[address]", + "resource[name]", + "resource[type]" + ] end test "creates a resource on valid attrs when traffic filter form disabled", %{ @@ -338,6 +358,10 @@ defmodule Web.Live.Resources.NewTest do |> authorize_conn(identity) |> live(~p"/#{account}/resources/new?site_id=#{group}") + lv + |> form("form") + |> render_change(resource: %{type: :dns}) + lv |> form("form", resource: attrs) |> render_submit() diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/index_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/index_test.exs index 7e9b1eace..1b4593e38 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/index_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/index_test.exs @@ -7,8 +7,7 @@ defmodule Web.Live.Settings.IdentityProviders.IndexTest do account = Fixtures.Accounts.create_account() actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) - {provider, bypass} = - Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + {provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account) identity = Fixtures.Auth.create_identity(account: account, actor: actor, provider: provider) subject = Fixtures.Auth.create_subject(identity: identity) diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs index 63b030eb0..720d553bf 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs @@ -7,8 +7,7 @@ defmodule Web.Live.Settings.IdentityProviders.NewTest do account = Fixtures.Accounts.create_account() actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) - {provider, bypass} = - Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + {provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account) identity = Fixtures.Auth.create_identity(account: account, actor: actor, provider: provider) diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs index 43bb494db..a4fc2ac35 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs @@ -6,8 +6,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.EditTest do account = Fixtures.Accounts.create_account() - {provider, bypass} = - Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + {provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account) actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, actor: actor) diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs index 867cb2319..68fe6d410 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs @@ -7,8 +7,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.ShowTest do account = Fixtures.Accounts.create_account() actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) - {provider, bypass} = - Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + {provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account) identity = Fixtures.Auth.create_identity(account: account, actor: actor, provider: provider) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2e27805b4..88d37e08d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -141,30 +141,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -260,11 +260,11 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock 3.1.2", + "async-lock 3.2.0", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite 2.0.1", + "futures-lite 2.1.0", "slab", ] @@ -306,14 +306,14 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" dependencies = [ - "async-lock 3.1.2", + "async-lock 3.2.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.0.1", + "futures-lite 2.1.0", "parking", "polling 3.3.1", - "rustix 0.38.25", + "rustix 0.38.26", "slab", "tracing", "windows-sys 0.52.0", @@ -330,9 +330,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" dependencies = [ "event-listener 4.0.0", "event-listener-strategy", @@ -352,7 +352,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.25", + "rustix 0.38.26", "windows-sys 0.48.0", ] @@ -379,7 +379,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.25", + "rustix 0.38.26", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -537,6 +537,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bincode" version = "1.3.3" @@ -651,11 +657,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", - "async-lock 3.1.2", + "async-lock 3.2.0", "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite 2.0.1", + "futures-lite 2.1.0", "piper", "tracing", ] @@ -677,7 +683,7 @@ dependencies = [ "nix 0.25.1", "parking_lot", "rand_core 0.6.4", - "ring 0.17.6", + "ring 0.17.7", "tracing", "untrusted 0.9.0", "x25519-dalek", @@ -975,9 +981,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.10" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -985,9 +991,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -1022,7 +1028,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics", "foreign-types", "libc", @@ -1037,7 +1043,7 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics-types", "libc", "objc", @@ -1071,9 +1077,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -1151,6 +1157,7 @@ dependencies = [ "base64 0.21.5", "boringtun", "chrono", + "domain", "futures", "futures-util", "hickory-resolver", @@ -1160,7 +1167,7 @@ dependencies = [ "parking_lot", "rand 0.8.5", "rand_core 0.6.4", - "ring 0.17.6", + "ring 0.17.7", "rtnetlink", "secrecy", "serde", @@ -1209,11 +1216,11 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys 0.8.6", "libc", ] @@ -1225,9 +1232,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core-graphics" @@ -1236,7 +1243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -1244,12 +1251,12 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "libc", ] @@ -1381,12 +1388,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.26" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -1503,9 +1510,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", "serde", @@ -1620,6 +1627,7 @@ checksum = "7af83e443e4bfe8602af356e5ca10b9676634e53d178875017f2ff729898a388" dependencies = [ "octseq", "rand 0.8.5", + "serde", "time", ] @@ -1865,14 +1873,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall", + "windows-sys 0.52.0", ] [[package]] @@ -1899,6 +1907,7 @@ dependencies = [ "chrono", "clap", "connlib-shared", + "domain", "firezone-cli-utils", "firezone-tunnel", "futures", @@ -1976,6 +1985,7 @@ version = "1.20231001.0" dependencies = [ "arc-swap", "async-trait", + "bimap", "boringtun", "bytes", "chrono", @@ -2162,14 +2172,13 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ "fastrand 2.0.1", "futures-core", "futures-io", - "memchr", "parking", "pin-project-lite", ] @@ -2933,7 +2942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", - "core-foundation-sys 0.8.4", + "core-foundation-sys 0.8.6", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", @@ -3038,9 +3047,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" dependencies = [ "cfb", ] @@ -3166,7 +3175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.25", + "rustix 0.38.26", "windows-sys 0.48.0", ] @@ -3303,9 +3312,9 @@ dependencies = [ [[package]] name = "keyring" -version = "2.0.5" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492" +checksum = "ec6488afbd1d8202dbd6e2dd38c0753d8c0adba9ac9985fc6f732a0d551f75e1" dependencies = [ "byteorder", "lazy_static", @@ -3416,7 +3425,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", - "redox_syscall 0.4.1", + "redox_syscall", ] [[package]] @@ -3452,9 +3461,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -3648,9 +3657,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -3955,9 +3964,9 @@ dependencies = [ [[package]] name = "objc-sys" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e1d07c6eab1ce8b6382b8e3c7246fe117ff3f8b34be065f5ebace6749fe845" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" [[package]] name = "objc2" @@ -4007,6 +4016,9 @@ name = "octseq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd09a3d18c298e5d9b66cf65db82e2e1694b94fbc032f83e304a5cbc87bcc3bb" +dependencies = [ + "serde", +] [[package]] name = "oid-registry" @@ -4019,9 +4031,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -4248,7 +4260,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -4336,9 +4348,17 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_macros 0.10.0", "phf_shared 0.10.0", - "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", ] [[package]] @@ -4381,6 +4401,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + [[package]] name = "phf_macros" version = "0.8.0" @@ -4397,16 +4427,15 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", + "phf_generator 0.11.2", + "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -4427,6 +4456,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "phoenix-channel" version = "1.20231001.0" @@ -4603,7 +4641,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.25", + "rustix 0.38.26", "tracing", "windows-sys 0.52.0", ] @@ -4902,15 +4940,6 @@ dependencies = [ "url", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -5078,9 +5107,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom 0.2.11", @@ -5176,15 +5205,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.11", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] @@ -5194,7 +5223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring 0.17.6", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -5226,7 +5255,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -5296,7 +5325,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -5362,8 +5391,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.4", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.6", "libc", "security-framework-sys", ] @@ -5374,7 +5403,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys 0.8.6", "libc", ] @@ -5795,7 +5824,7 @@ dependencies = [ "lazy_static", "md-5", "rand 0.8.5", - "ring 0.17.6", + "ring 0.17.7", "subtle", "thiserror", "tokio", @@ -5925,7 +5954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5935,7 +5964,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys 0.8.6", "libc", ] @@ -5975,7 +6004,7 @@ dependencies = [ "cairo-rs", "cc", "cocoa", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics", "crossbeam-channel", "dirs-next", @@ -6044,9 +6073,9 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tauri" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9" +checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2" dependencies = [ "anyhow", "cocoa", @@ -6137,9 +6166,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af" +checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -6188,9 +6217,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895" +checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206" dependencies = [ "cocoa", "gtk", @@ -6208,9 +6237,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46" +checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986" dependencies = [ "brotli", "ctor", @@ -6223,7 +6252,7 @@ dependencies = [ "kuchikiki", "log", "memchr", - "phf 0.10.1", + "phf 0.11.2", "proc-macro2", "quote", "semver", @@ -6233,7 +6262,7 @@ dependencies = [ "thiserror", "url", "walkdir", - "windows 0.39.0", + "windows-version", ] [[package]] @@ -6254,8 +6283,8 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.4.1", - "rustix 0.38.25", + "redox_syscall", + "rustix 0.38.26", "windows-sys 0.48.0", ] @@ -6808,9 +6837,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -6843,7 +6872,7 @@ dependencies = [ "log", "md-5", "rand 0.8.5", - "ring 0.17.6", + "ring 0.17.7", "stun", "thiserror", "tokio", @@ -6874,9 +6903,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -7230,7 +7259,7 @@ dependencies = [ "rand 0.8.5", "rcgen", "regex", - "ring 0.17.6", + "ring 0.17.7", "rtcp", "rtp", "rustls", @@ -7290,7 +7319,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rcgen", - "ring 0.17.6", + "ring 0.17.7", "rustls", "sec1", "serde", @@ -7456,7 +7485,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.25", + "rustix 0.38.26", ] [[package]] @@ -7689,6 +7718,15 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7877,9 +7915,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" dependencies = [ "memchr", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 411009415..31cee7be9 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -27,6 +27,7 @@ secrecy = "0.8" hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } webrtc = "0.9" futures-bounded = "0.2.1" +domain = { version = "0.9", features = ["serde"] } connlib-client-android = { path = "connlib/clients/android"} connlib-client-apple = { path = "connlib/clients/apple"} diff --git a/rust/connlib/clients/shared/src/control.rs b/rust/connlib/clients/shared/src/control.rs index 0a2c03009..a3c3e25ba 100644 --- a/rust/connlib/clients/shared/src/control.rs +++ b/rust/connlib/clients/shared/src/control.rs @@ -1,5 +1,5 @@ use async_compression::tokio::bufread::GzipEncoder; -use connlib_shared::messages::{DnsServer, IpDnsServer}; +use connlib_shared::messages::{DnsServer, GatewayResponse, IpDnsServer}; use std::path::PathBuf; use std::{io, sync::Arc}; @@ -110,18 +110,31 @@ impl ControlPlane { pub fn connect( &mut self, Connect { - gateway_rtc_session_description, + gateway_payload, resource_id, gateway_public_key, .. }: Connect, ) { - if let Err(e) = self.tunnel.received_offer_response( - resource_id, - gateway_rtc_session_description, - gateway_public_key.0.into(), - ) { - let _ = self.tunnel.callbacks().on_error(&e); + match gateway_payload { + GatewayResponse::ConnectionAccepted(gateway_payload) => { + if let Err(e) = self.tunnel.received_offer_response( + resource_id, + gateway_payload.ice_parameters, + gateway_payload.domain_response, + gateway_public_key.0.into(), + ) { + let _ = self.tunnel.callbacks().on_error(&e); + } + } + GatewayResponse::ResourceAccepted(gateway_payload) => { + if let Err(e) = self + .tunnel + .received_domain_parameters(resource_id, gateway_payload.domain_response) + { + let _ = self.tunnel.callbacks().on_error(&e); + } + } } } diff --git a/rust/connlib/clients/shared/src/messages.rs b/rust/connlib/clients/shared/src/messages.rs index 7a60e6831..2bd23ba0e 100644 --- a/rust/connlib/clients/shared/src/messages.rs +++ b/rust/connlib/clients/shared/src/messages.rs @@ -3,11 +3,11 @@ use std::{collections::HashSet, net::IpAddr}; use serde::{Deserialize, Serialize}; use connlib_shared::messages::{ - GatewayId, Interface, Key, Relay, RequestConnection, ResourceDescription, ResourceId, - ReuseConnection, + GatewayId, GatewayResponse, Interface, Key, Relay, RequestConnection, ResourceDescription, + ResourceId, ReuseConnection, }; use url::Url; -use webrtc::ice_transport::{ice_candidate::RTCIceCandidate, ice_parameters::RTCIceParameters}; +use webrtc::ice_transport::ice_candidate::RTCIceCandidate; #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub struct InitClient { @@ -31,7 +31,7 @@ pub struct ConnectionDetails { #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Connect { - pub gateway_rtc_session_description: RTCIceParameters, + pub gateway_payload: GatewayResponse, pub resource_id: ResourceId, pub gateway_public_key: Key, pub persistent_keepalive: u64, @@ -180,10 +180,21 @@ mod test { "response": { "resource_id": "ea6570d1-47c7-49d2-9dc3-efff1c0c9e0b", "gateway_public_key": "dvy0IwyxAi+txSbAdT7WKgf7K4TekhKzrnYwt5WfbSM=", - "gateway_rtc_session_description": { - "ice_lite":false, - "password": "xEwoXEzHuSyrcgOCSRnwOXQVnbnbeGeF", - "username_fragment": "PvCPFevCOgkvVCtH" + "gateway_payload": { + "ConnectionAccepted":{ + "domain_response":{ + "address":[ + "2607:f8b0:4008:804::200e", + "142.250.64.206" + ], + "domain":"google.com" + }, + "ice_parameters":{ + "ice_lite":false, + "password":"pMAxxTgHHSdpqHRzHGNvuNsZinLrMxwe", + "username_fragment":"tGeqOjtGuPzPpuOx" + } + } }, "persistent_keepalive": 25 } @@ -211,8 +222,6 @@ mod test { ResourceDescription::Dns(ResourceDescriptionDns { id: "03000143-e25e-45c7-aafb-144990e57dcd".parse().unwrap(), address: "gitlab.mycorp.com".to_string(), - ipv4: "100.126.44.50".parse().unwrap(), - ipv6: "fd00:2021:1111::e:7758".parse().unwrap(), name: "gitlab.mycorp.com".to_string(), }), ], diff --git a/rust/connlib/shared/Cargo.toml b/rust/connlib/shared/Cargo.toml index ceb2cd460..33c3d916b 100644 --- a/rust/connlib/shared/Cargo.toml +++ b/rust/connlib/shared/Cargo.toml @@ -33,6 +33,7 @@ uuid = { version = "1.5", default-features = false, features = ["std", "v4", "se webrtc = { workspace = true } ring = "0.17" hickory-resolver = { workspace = true } +domain = { workspace = true } # Needed for Android logging until tracing is working log = "0.4" diff --git a/rust/connlib/shared/src/error.rs b/rust/connlib/shared/src/error.rs index 9c77d5a72..6ded21f6d 100644 --- a/rust/connlib/shared/src/error.rs +++ b/rust/connlib/shared/src/error.rs @@ -125,6 +125,9 @@ pub enum ConnlibError { /// Invalid source address for peer #[error("Invalid source address")] InvalidSource, + /// Invalid destination for packet + #[error("Invalid dest address")] + InvalidDst, /// Any parse error #[error("parse error")] ParseError, diff --git a/rust/connlib/shared/src/lib.rs b/rust/connlib/shared/src/lib.rs index dc039b8c9..83641073f 100644 --- a/rust/connlib/shared/src/lib.rs +++ b/rust/connlib/shared/src/lib.rs @@ -25,6 +25,8 @@ use url::Url; pub const DNS_SENTINEL: Ipv4Addr = Ipv4Addr::new(100, 100, 111, 1); +pub type Dname = domain::base::Dname>; + const VERSION: &str = env!("CARGO_PKG_VERSION"); const LIB_NAME: &str = "connlib"; diff --git a/rust/connlib/shared/src/messages.rs b/rust/connlib/shared/src/messages.rs index 549174322..becc1cb5f 100644 --- a/rust/connlib/shared/src/messages.rs +++ b/rust/connlib/shared/src/messages.rs @@ -12,6 +12,8 @@ mod key; pub use key::{Key, SecretKey}; +use crate::Dname; + #[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] pub struct GatewayId(Uuid); #[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] @@ -92,7 +94,13 @@ pub struct RequestConnection { /// The preshared key the client generated for the connection that it is trying to establish. pub client_preshared_key: SecretKey, /// Client's local RTC Session Description that the client will use for this connection. - pub client_rtc_session_description: RTCIceParameters, + pub client_payload: ClientPayload, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ClientPayload { + pub ice_parameters: RTCIceParameters, + pub domain: Option, } /// Represent a request to reuse an existing gateway connection from a client to a given resource. @@ -105,6 +113,8 @@ pub struct ReuseConnection { pub resource_id: ResourceId, /// Id of the gateway we want to reuse pub gateway_id: GatewayId, + /// Payload that the gateway will receive + pub payload: Option, } // Custom implementation of partial eq to ignore client_rtc_sdp @@ -123,23 +133,36 @@ pub enum ResourceDescription { Cidr(ResourceDescriptionCidr), } +#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq)] +pub struct DomainResponse { + pub domain: Dname, + pub address: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ConnectionAccepted { + pub ice_parameters: RTCIceParameters, + pub domain_response: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ResourceAccepted { + pub domain_response: DomainResponse, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub enum GatewayResponse { + ConnectionAccepted(ConnectionAccepted), + ResourceAccepted(ResourceAccepted), +} + /// Description of a resource that maps to a DNS record. -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)] pub struct ResourceDescriptionDns { /// Resource's id. pub id: ResourceId, /// Internal resource's domain name. pub address: String, - /// Resource's ipv4 mapping. - /// - /// Note that this is not the actual ipv4 for the resource not even wireguard's ipv4 for the resource. - /// This is just the mapping we use internally between a resource and its ip for intercepting packets. - pub ipv4: Ipv4Addr, - /// Resource's ipv6 mapping. - /// - /// Note that this is not the actual ipv6 for the resource not even wireguard's ipv6 for the resource. - /// This is just the mapping we use internally between a resource and its ip for intercepting packets. - pub ipv6: Ipv6Addr, /// Name of the resource. /// /// Used only for display. @@ -149,28 +172,7 @@ pub struct ResourceDescriptionDns { impl ResourceDescription { pub fn dns_name(&self) -> Option<&str> { match self { - ResourceDescription::Dns(r) => Some(&r.name), - ResourceDescription::Cidr(_) => None, - } - } - - pub fn ips(&self) -> Vec { - match self { - ResourceDescription::Dns(r) => vec![r.ipv4.into(), r.ipv6.into()], - ResourceDescription::Cidr(r) => vec![r.address], - } - } - - pub fn ipv4(&self) -> Option { - match self { - ResourceDescription::Dns(r) => Some(r.ipv4), - ResourceDescription::Cidr(_) => None, - } - } - - pub fn ipv6(&self) -> Option { - match self { - ResourceDescription::Dns(r) => Some(r.ipv6), + ResourceDescription::Dns(r) => Some(&r.address), ResourceDescription::Cidr(_) => None, } } @@ -181,13 +183,6 @@ impl ResourceDescription { ResourceDescription::Cidr(r) => r.id, } } - - pub fn contains(&self, ip: IpAddr) -> bool { - match self { - ResourceDescription::Dns(r) => r.ipv4 == ip || r.ipv6 == ip, - ResourceDescription::Cidr(r) => r.address.contains(ip), - } - } } /// Description of a resource that maps to a CIDR. diff --git a/rust/connlib/tunnel/Cargo.toml b/rust/connlib/tunnel/Cargo.toml index 1fdfb4f97..c23964d39 100644 --- a/rust/connlib/tunnel/Cargo.toml +++ b/rust/connlib/tunnel/Cargo.toml @@ -21,13 +21,14 @@ connlib-shared = { workspace = true } libc = { version = "0.2", default-features = false, features = ["std", "const-extern-fn", "extra_traits"] } ip_network = { version = "0.4", default-features = false } ip_network_table = { version = "0.2", default-features = false } -domain = "0.9" +domain = { workspace = true } boringtun = { workspace = true } chrono = { workspace = true } pnet_packet = { version = "0.34" } futures-bounded = { workspace = true } hickory-resolver = { workspace = true } arc-swap = "1.6.0" +bimap = "0.6" # TODO: research replacing for https://github.com/algesten/str0m webrtc = { workspace = true } @@ -39,7 +40,7 @@ log = "0.4" [target.'cfg(target_os = "linux")'.dependencies] netlink-packet-route = { version = "0.17", default-features = false } netlink-packet-core = { version = "0.7", default-features = false } -rtnetlink = { version = "0.13", default-features = false, features = ["tokio_socket"] } +rtnetlink = { version = "0.13" } # Android tunnel dependencies [target.'cfg(target_os = "android")'.dependencies] diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index c262ffe76..8d07bdeea 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -1,34 +1,52 @@ use crate::bounded_queue::BoundedQueue; use crate::device_channel::{create_iface, Packet}; use crate::ip_packet::{IpPacket, MutableIpPacket}; -use crate::resource_table::ResourceTable; +use crate::peer::PacketTransformClient; use crate::{ - dns, ConnectedPeer, DnsQuery, Event, PeerConfig, RoleState, Tunnel, DNS_QUERIES_QUEUE_SIZE, - ICE_GATHERING_TIMEOUT_SECONDS, MAX_CONCURRENT_ICE_GATHERING, + dns, ConnectedPeer, DnsFallbackStrategy, DnsQuery, Event, PeerConfig, RoleState, Tunnel, + DNS_QUERIES_QUEUE_SIZE, ICE_GATHERING_TIMEOUT_SECONDS, MAX_CONCURRENT_ICE_GATHERING, }; use boringtun::x25519::{PublicKey, StaticSecret}; use connlib_shared::error::{ConnlibError as Error, ConnlibError}; use connlib_shared::messages::{ - GatewayId, Interface as InterfaceConfig, Key, ResourceDescription, ResourceId, ReuseConnection, - SecretKey, + GatewayId, Interface as InterfaceConfig, Key, ResourceDescription, ResourceDescriptionCidr, + ResourceDescriptionDns, ResourceId, ReuseConnection, SecretKey, }; -use connlib_shared::{Callbacks, DNS_SENTINEL}; +use connlib_shared::{Callbacks, Dname, DNS_SENTINEL}; +use domain::base::Rtype; use futures::channel::mpsc::Receiver; use futures::stream; use futures_bounded::{PushError, StreamMap}; use hickory_resolver::lookup::Lookup; -use ip_network::IpNetwork; +use ip_network::{IpNetwork, Ipv4Network, Ipv6Network}; use ip_network_table::IpNetworkTable; +use itertools::Itertools; + use rand_core::OsRng; use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; -use std::net::IpAddr; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; use tokio::time::Instant; use webrtc::ice_transport::ice_candidate::RTCIceCandidate; +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct DnsResource { + pub id: ResourceId, + pub address: Dname, +} + +impl DnsResource { + pub fn from_description(description: &ResourceDescriptionDns, address: Dname) -> DnsResource { + DnsResource { + id: description.id, + address, + } + } +} + impl Tunnel where CB: Callbacks + 'static, @@ -42,28 +60,29 @@ where &self, resource_description: ResourceDescription, ) -> connlib_shared::Result<()> { - let mut any_valid_route = false; - { - for ip in resource_description.ips() { - if let Err(e) = self.add_route(ip).await { - tracing::warn!(route = %ip, error = ?e, "add_route"); - let _ = self.callbacks().on_error(&e); - } else { - any_valid_route = true; - } + match &resource_description { + ResourceDescription::Dns(dns) => { + self.role_state + .lock() + .dns_resources + .insert(dns.address.clone(), dns.clone()); + } + ResourceDescription::Cidr(cidr) => { + self.add_route(cidr.address).await?; + + self.role_state + .lock() + .cidr_resources + .insert(cidr.address, cidr.clone()); } } - if !any_valid_route { - return Err(Error::InvalidResource); - } - let resource_list = { - let mut role_state = self.role_state.lock(); - role_state.resources.insert(resource_description); - role_state.resources.resource_list() - }; - - self.callbacks.on_update_resources(resource_list)?; + let mut role_state = self.role_state.lock(); + role_state + .resource_ids + .insert(resource_description.id(), resource_description); + self.callbacks + .on_update_resources(role_state.resource_ids.values().cloned().collect())?; Ok(()) } @@ -88,7 +107,8 @@ where /// Sets the interface configuration and starts background tasks. #[tracing::instrument(level = "trace", skip(self))] pub async fn set_interface(&self, config: &InterfaceConfig) -> connlib_shared::Result<()> { - let device = Arc::new(create_iface(config, self.callbacks()).await?); + let dns_strategy = self.role_state.lock().dns_strategy; + let device = Arc::new(create_iface(config, self.callbacks(), dns_strategy).await?); self.device.store(Some(device.clone())); self.no_device_waker.wake(); @@ -97,6 +117,10 @@ where self.callbacks.on_tunnel_ready()?; + if !config.upstream_dns.is_empty() { + self.role_state.lock().dns_strategy = DnsFallbackStrategy::UpstreamResolver; + } + tracing::debug!("background_loop_started"); Ok(()) @@ -110,7 +134,7 @@ where } #[tracing::instrument(level = "trace", skip(self))] - async fn add_route(&self, route: IpNetwork) -> connlib_shared::Result<()> { + pub async fn add_route(&self, route: IpNetwork) -> connlib_shared::Result<()> { let maybe_new_device = self .device .load() @@ -142,14 +166,27 @@ pub struct ClientState { pub gateway_public_keys: HashMap, pub gateway_preshared_keys: HashMap, resources_gateways: HashMap, - resources: ResourceTable, - dns_queries: BoundedQueue>, + + pub dns_resources_internal_ips: HashMap>, + dns_resources: HashMap, + cidr_resources: IpNetworkTable, + pub resource_ids: HashMap, + pub deferred_dns_queries: HashMap<(DnsResource, Rtype), IpPacket<'static>>, + + #[allow(clippy::type_complexity)] + pub peers_by_ip: IpNetworkTable>, + + pub dns_strategy: DnsFallbackStrategy, + forwarded_dns_queries: BoundedQueue>, + + pub ip_provider: IpProvider, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct AwaitingConnectionDetails { total_attemps: usize, response_received: bool, + domain: Option, gateways: HashSet, } @@ -161,34 +198,59 @@ impl ClientState { pub(crate) fn handle_dns<'a>( &mut self, packet: MutableIpPacket<'a>, + resolve_strategy: DnsFallbackStrategy, ) -> Result>, MutableIpPacket<'a>> { - match dns::parse(&self.resources, packet.as_immutable()) { - Some(dns::ResolveStrategy::LocalResponse(pkt)) => Ok(Some(pkt)), + match dns::parse( + &self.dns_resources, + &self.dns_resources_internal_ips, + packet.as_immutable(), + resolve_strategy, + ) { + Some(dns::ResolveStrategy::LocalResponse(query)) => Ok(Some(query)), Some(dns::ResolveStrategy::ForwardQuery(query)) => { self.add_pending_dns_query(query); Ok(None) } + Some(dns::ResolveStrategy::DeferredResponse(resource)) => { + self.on_connection_intent_dns(&resource.0); + self.deferred_dns_queries + .insert(resource, packet.as_immutable().to_owned()); + + Ok(None) + } None => Err(packet), } } + pub(crate) fn get_awaiting_connection_domain( + &self, + resource: &ResourceId, + ) -> Result<&Option, ConnlibError> { + Ok(&self + .awaiting_connection + .get(resource) + .ok_or(Error::UnexpectedConnectionDetails)? + .domain) + } + pub(crate) fn attempt_to_reuse_connection( &mut self, resource: ResourceId, gateway: GatewayId, expected_attempts: usize, - connected_peers: &mut IpNetworkTable>, ) -> Result, ConnlibError> { - if self.is_connected_to(resource, connected_peers) { + let desc = self + .resource_ids + .get(&resource) + .ok_or(Error::UnknownResource)?; + + let domain = self.get_awaiting_connection_domain(&resource)?.clone(); + + if self.is_connected_to(resource, &self.peers_by_ip, &domain) { return Err(Error::UnexpectedConnectionDetails); } - let desc = self - .resources - .get_by_id(&resource) - .ok_or(Error::UnknownResource)?; - let details = self .awaiting_connection .get_mut(&resource) @@ -208,18 +270,19 @@ impl ClientState { self.resources_gateways.insert(resource, gateway); - let Some(peer) = connected_peers.iter().find_map(|(_, p)| { + let Some(peer) = self.peers_by_ip.iter().find_map(|(_, p)| { (p.inner.conn_id == gateway).then_some(ConnectedPeer { inner: p.inner.clone(), channel: p.channel.clone(), }) }) else { + self.gateway_awaiting_connection.insert(gateway); return Ok(None); }; - for ip in desc.ips() { + for ip in self.get_resource_ip(desc, &domain) { peer.inner.add_allowed_ip(ip); - connected_peers.insert( + self.peers_by_ip.insert( ip, ConnectedPeer { inner: peer.inner.clone(), @@ -233,6 +296,7 @@ impl ClientState { Ok(Some(ReuseConnection { resource_id: resource, gateway_id: gateway, + payload: domain.clone(), })) } @@ -246,20 +310,20 @@ impl ClientState { self.gateway_awaiting_connection.remove(&gateway); } - pub fn on_connection_intent(&mut self, destination: IpAddr) { - if self.is_awaiting_connection_to(destination) { + fn is_awaiting_connection_to_dns(&self, resource: &DnsResource) -> bool { + self.awaiting_connection.contains_key(&resource.id) + } + + pub fn on_connection_intent_dns(&mut self, resource: &DnsResource) { + if self.is_awaiting_connection_to_dns(resource) { return; } - tracing::trace!(resource_ip = %destination, "resource_connection_intent"); - - let Some(resource) = self.get_resource_by_destination(destination) else { - return; - }; + tracing::trace!(?resource, "resource_connection_intent"); const MAX_SIGNAL_CONNECTION_DELAY: Duration = Duration::from_secs(2); - let resource_id = resource.id(); + let resource_id = resource.id; let gateways = self .gateway_awaiting_connection @@ -268,8 +332,6 @@ impl ClientState { .copied() .collect(); - tracing::trace!(?gateways, "connected_gateways"); - match self.awaiting_connection_timers.try_push( resource_id, stream::poll_fn({ @@ -292,6 +354,65 @@ impl ClientState { AwaitingConnectionDetails { total_attemps: 0, response_received: false, + domain: Some(resource.address.clone()), + gateways, + }, + ); + } + + pub fn on_connection_intent_ip(&mut self, destination: IpAddr) { + if self.is_awaiting_connection_to_cidr(destination) { + return; + } + + tracing::trace!(resource_ip = %destination, "resource_connection_intent"); + + let Some(resource) = self.get_cidr_resource_by_destination(destination) else { + if let Some(resource) = self + .dns_resources_internal_ips + .iter() + .find_map(|(r, i)| i.contains(&destination).then_some(r)) + .cloned() + { + self.on_connection_intent_dns(&resource); + } + return; + }; + + const MAX_SIGNAL_CONNECTION_DELAY: Duration = Duration::from_secs(2); + + let resource_id = resource.id(); + + let gateways = self + .gateway_awaiting_connection + .iter() + .chain(self.resources_gateways.values()) + .copied() + .collect(); + + match self.awaiting_connection_timers.try_push( + resource_id, + stream::poll_fn({ + let mut interval = tokio::time::interval(MAX_SIGNAL_CONNECTION_DELAY); + move |cx| interval.poll_tick(cx).map(Some) + }), + ) { + Ok(()) => {} + Err(PushError::BeyondCapacity(_)) => { + tracing::warn!(%resource_id, "Too many concurrent connection attempts"); + return; + } + Err(PushError::Replaced(_)) => { + // The timers are equivalent for our purpose so we don't really care about this one. + } + } + + self.awaiting_connection.insert( + resource_id, + AwaitingConnectionDetails { + total_attemps: 0, + response_received: false, + domain: None, gateways, }, ); @@ -301,6 +422,7 @@ impl ClientState { &mut self, resource: ResourceId, gateway: GatewayId, + domain: &Option, ) -> Result { let shared_key = self .gateway_preshared_keys @@ -316,14 +438,16 @@ impl ClientState { }; let desc = self - .resources - .get_by_id(&resource) + .resource_ids + .get(&resource) .ok_or(Error::ControlProtocolError)?; + let ips = self.get_resource_ip(desc, domain); + let config = PeerConfig { persistent_keepalive: None, public_key, - ips: desc.ips(), + ips, preshared_key: SecretKey::new(Key(shared_key.to_bytes())), }; @@ -367,8 +491,8 @@ impl ClientState { } } - fn is_awaiting_connection_to(&self, destination: IpAddr) -> bool { - let Some(resource) = self.get_resource_by_destination(destination) else { + fn is_awaiting_connection_to_cidr(&self, destination: IpAddr) -> bool { + let Some(resource) = self.get_cidr_resource_by_destination(destination) else { return false; }; @@ -378,32 +502,81 @@ impl ClientState { fn is_connected_to( &self, resource: ResourceId, - connected_peers: &IpNetworkTable>, + connected_peers: &IpNetworkTable>, + domain: &Option, ) -> bool { - let Some(resource) = self.resources.get_by_id(&resource) else { + let Some(resource) = self.resource_ids.get(&resource) else { return false; }; - resource - .ips() - .iter() + let ips = self.get_resource_ip(resource, domain); + ips.iter() .any(|ip| connected_peers.exact_match(*ip).is_some()) } - fn get_resource_by_destination(&self, destination: IpAddr) -> Option<&ResourceDescription> { - match destination { - IpAddr::V4(ipv4) => self.resources.get_by_ip(ipv4), - IpAddr::V6(ipv6) => self.resources.get_by_ip(ipv6), + fn get_resource_ip( + &self, + resource: &ResourceDescription, + domain: &Option, + ) -> Vec { + match resource { + ResourceDescription::Dns(dns_resource) => { + let Some(domain) = domain else { + return vec![]; + }; + + let description = DnsResource::from_description(dns_resource, domain.clone()); + self.dns_resources_internal_ips + .get(&description) + .cloned() + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect() + } + ResourceDescription::Cidr(cidr) => vec![cidr.address], } } - pub fn add_pending_dns_query(&mut self, query: DnsQuery) { - if self.dns_queries.push_back(query.into_owned()).is_err() { + fn get_cidr_resource_by_destination(&self, destination: IpAddr) -> Option { + self.cidr_resources + .longest_match(destination) + .map(|(_, res)| ResourceDescription::Cidr(res.clone())) + } + + fn add_pending_dns_query(&mut self, query: DnsQuery) { + if self + .forwarded_dns_queries + .push_back(query.into_owned()) + .is_err() + { tracing::warn!("Too many DNS queries, dropping new ones"); } } } +pub struct IpProvider { + ipv4: Box + Send + Sync>, + ipv6: Box + Send + Sync>, +} + +impl IpProvider { + fn new(ipv4: Ipv4Network, ipv6: Ipv6Network) -> Self { + Self { + ipv4: Box::new(ipv4.hosts()), + ipv6: Box::new(ipv6.subnets_with_prefix(128).map(|ip| ip.network_address())), + } + } + + pub fn next_ipv4(&mut self) -> Option { + self.ipv4.next() + } + + pub fn next_ipv6(&mut self) -> Option { + self.ipv6.next() + } +} + impl Default for ClientState { fn default() -> Self { Self { @@ -417,9 +590,20 @@ impl Default for ClientState { awaiting_connection_timers: StreamMap::new(Duration::from_secs(60), 100), gateway_public_keys: Default::default(), resources_gateways: Default::default(), - resources: Default::default(), - dns_queries: BoundedQueue::with_capacity(DNS_QUERIES_QUEUE_SIZE), + forwarded_dns_queries: BoundedQueue::with_capacity(DNS_QUERIES_QUEUE_SIZE), gateway_preshared_keys: Default::default(), + dns_strategy: Default::default(), + // TODO: decide ip ranges + ip_provider: IpProvider::new( + "100.96.0.0/11".parse().unwrap(), + "fd00:2021:1112::/106".parse().unwrap(), + ), + dns_resources_internal_ips: Default::default(), + dns_resources: Default::default(), + cidr_resources: IpNetworkTable::new(), + resource_ids: Default::default(), + peers_by_ip: IpNetworkTable::new(), + deferred_dns_queries: Default::default(), } } } @@ -467,8 +651,8 @@ impl RoleState for ClientState { return Poll::Ready(Event::ConnectionIntent { resource: self - .resources - .get_by_id(&resource) + .resource_ids + .get(&resource) .expect("inconsistent internal state") .clone(), connected_gateway_ids: entry.get().gateways.clone(), @@ -483,7 +667,41 @@ impl RoleState for ClientState { Poll::Pending => {} } - return self.dns_queries.poll(cx).map(Event::DnsQuery); + return self.forwarded_dns_queries.poll(cx).map(Event::DnsQuery); } } + + fn remove_peers(&mut self, conn_id: GatewayId) { + self.peers_by_ip.retain(|_, p| p.inner.conn_id != conn_id); + } + + fn refresh_peers(&mut self) -> VecDeque { + let mut peers_to_stop = VecDeque::new(); + for (_, peer) in self.peers_by_ip.iter().unique_by(|(_, p)| p.inner.conn_id) { + let conn_id = peer.inner.conn_id; + + let bytes = match peer.inner.update_timers() { + Ok(Some(bytes)) => bytes, + Ok(None) => continue, + Err(e) => { + tracing::error!("Failed to update timers for peer: {e}"); + if e.is_fatal_connection_error() { + peers_to_stop.push_back(conn_id); + } + + continue; + } + }; + + let peer_channel = peer.channel.clone(); + + tokio::spawn(async move { + if let Err(e) = peer_channel.send(bytes).await { + tracing::error!("Failed to send packet to peer: {e:#}"); + } + }); + } + + peers_to_stop + } } diff --git a/rust/connlib/tunnel/src/control_protocol.rs b/rust/connlib/tunnel/src/control_protocol.rs index c1c0138bd..29455b979 100644 --- a/rust/connlib/tunnel/src/control_protocol.rs +++ b/rust/connlib/tunnel/src/control_protocol.rs @@ -17,7 +17,11 @@ use webrtc::ice_transport::{ use webrtc::ice_transport::{ice_candidate_type::RTCIceCandidateType, RTCIceTransport}; use webrtc::ice_transport::{ice_credential_type::RTCIceCredentialType, ice_server::RTCIceServer}; -use crate::{device_channel::Device, peer::Peer, peer_handler, ConnectedPeer, RoleState, Tunnel}; +use crate::{ + device_channel::Device, + peer::{PacketTransform, Peer}, + peer_handler, ConnectedPeer, RoleState, Tunnel, +}; mod client; mod gateway; @@ -30,7 +34,6 @@ const MAX_RELAYS: usize = 2; const MAX_HOST_CANDIDATES: usize = 8; #[derive(Debug, Clone, PartialEq, Eq)] -#[allow(clippy::large_enum_variant)] pub enum Request { NewConnection(RequestConnection), ReuseConnection(ReuseConnection), @@ -61,7 +64,7 @@ where } pub(crate) struct IceConnection { - pub ice_params: RTCIceParameters, + pub ice_parameters: RTCIceParameters, pub ice_transport: Arc, pub ice_candidate_rx: mpsc::Receiver, } @@ -132,30 +135,31 @@ pub(crate) async fn new_ice_connection( gatherer.gather().await?; Ok(IceConnection { - ice_params: gatherer.get_local_parameters().await?, + ice_parameters: gatherer.get_local_parameters().await?, ice_transport, ice_candidate_rx, }) } -fn insert_peers( - peers_by_ip: &mut IpNetworkTable>, +fn insert_peers( + peers_by_ip: &mut IpNetworkTable>, ips: &Vec, - peer: ConnectedPeer, + peer: ConnectedPeer, ) { for ip in ips { peers_by_ip.insert(*ip, peer.clone()); } } -fn start_handlers( +fn start_handlers( device: Arc>, callbacks: impl Callbacks + 'static, - peer: Arc>, + peer: Arc>, ice: Arc, peer_receiver: tokio::sync::mpsc::Receiver, ) where TId: Copy + Send + Sync + fmt::Debug + 'static, + TTransform: Send + Sync + PacketTransform + 'static, { ice.on_connection_state_change(Box::new(|_| Box::pin(async {}))); tokio::spawn({ diff --git a/rust/connlib/tunnel/src/control_protocol/client.rs b/rust/connlib/tunnel/src/control_protocol/client.rs index 8bf8ea41a..b7071dd33 100644 --- a/rust/connlib/tunnel/src/control_protocol/client.rs +++ b/rust/connlib/tunnel/src/control_protocol/client.rs @@ -1,11 +1,16 @@ -use std::sync::Arc; +use std::{net::IpAddr, sync::Arc}; use boringtun::x25519::PublicKey; use connlib_shared::{ control::Reference, - messages::{GatewayId, Key, Relay, RequestConnection, ResourceId}, + messages::{ + ClientPayload, DomainResponse, GatewayId, Key, Relay, RequestConnection, + ResourceDescription, ResourceId, + }, Callbacks, }; +use domain::base::Rtype; +use ip_network::IpNetwork; use secrecy::Secret; use webrtc::ice_transport::{ ice_parameters::RTCIceParameters, ice_role::RTCIceRole, @@ -13,7 +18,11 @@ use webrtc::ice_transport::{ }; use crate::{ + client::DnsResource, control_protocol::{new_ice_connection, IceConnection}, + device_channel::Device, + dns, + peer::PacketTransformClient, PEER_QUEUE_SIZE, }; use crate::{peer::Peer, ClientState, ConnectedPeer, Error, Request, Result, Tunnel}; @@ -86,13 +95,18 @@ where resource_id, gateway_id, reference, - &mut self.peers_by_ip.write(), )? { return Ok(Request::ReuseConnection(connection)); } + let domain = self + .role_state + .lock() + .get_awaiting_connection_domain(&resource_id)? + .clone(); + let IceConnection { - ice_params, + ice_parameters, ice_transport, ice_candidate_rx, } = new_ice_connection(&self.webrtc_api, relays).await?; @@ -110,30 +124,44 @@ where resource_id, gateway_id, client_preshared_key: Secret::new(Key(preshared_key.to_bytes())), - client_rtc_session_description: ice_params, + client_payload: ClientPayload { + ice_parameters, + domain, + }, })) } fn new_tunnel( - &self, + self: &Arc, resource_id: ResourceId, gateway_id: GatewayId, ice: Arc, + domain_response: Option, ) -> Result<()> { let peer_config = self .role_state .lock() - .create_peer_config_for_new_connection(resource_id, gateway_id)?; + .create_peer_config_for_new_connection( + resource_id, + gateway_id, + &domain_response.as_ref().map(|d| d.domain.clone()), + )?; let peer = Arc::new(Peer::new( self.private_key.clone(), self.next_index(), peer_config.clone(), gateway_id, - None, self.rate_limiter.clone(), + Default::default(), )); + let peer_ips = if let Some(domain_response) = domain_response { + self.dns_response(&resource_id, &domain_response, &peer)? + } else { + peer_config.ips + }; + let (peer_sender, peer_receiver) = tokio::sync::mpsc::channel(PEER_QUEUE_SIZE); start_handlers( @@ -147,8 +175,8 @@ where // Partial reads of peers_by_ip can be problematic in the very unlikely case of an expiration // before inserting finishes. insert_peers( - &mut self.peers_by_ip.write(), - &peer_config.ips, + &mut self.role_state.lock().peers_by_ip, + &peer_ips, ConnectedPeer { inner: peer, channel: peer_sender, @@ -171,6 +199,7 @@ where self: &Arc, resource_id: ResourceId, rtc_ice_params: RTCIceParameters, + domain_response: Option, gateway_public_key: PublicKey, ) -> Result<()> { let gateway_id = self @@ -178,6 +207,7 @@ where .lock() .gateway_by_resource(&resource_id) .ok_or(Error::UnknownResource)?; + let peer_connection = self .peer_connections .lock() @@ -195,7 +225,9 @@ where .start(&rtc_ice_params, Some(RTCIceRole::Controlling)) .await .map_err(Into::into) - .and_then(|_| tunnel.new_tunnel(resource_id, gateway_id, peer_connection)) + .and_then(|_| { + tunnel.new_tunnel(resource_id, gateway_id, peer_connection, domain_response) + }) { tracing::warn!(%gateway_id, err = ?e, "Can't start tunnel: {e:#}"); tunnel.role_state.lock().on_connection_failed(resource_id); @@ -208,4 +240,113 @@ where Ok(()) } + + fn dns_response( + self: &Arc, + resource_id: &ResourceId, + domain_response: &DomainResponse, + peer: &Peer, + ) -> Result> { + let resource_description = self + .role_state + .lock() + .resource_ids + .get(resource_id) + .ok_or(Error::UnknownResource)? + .clone(); + + let ResourceDescription::Dns(resource_description) = resource_description else { + // We should never get a domain_response for a CIDR resource! + return Err(Error::ControlProtocolError); + }; + let resource_description = + DnsResource::from_description(&resource_description, domain_response.domain.clone()); + + let mut role_state = self.role_state.lock(); + let addrs: Vec<_> = domain_response + .address + .iter() + .filter_map(|addr| { + peer.transform + .get_or_assign_translation(addr, &mut role_state.ip_provider) + }) + .collect(); + + let dev = Arc::clone(self); + let ips = addrs.clone(); + let resource = resource_description.clone(); + tokio::spawn(async move { + for ip in &ips { + if let Err(e) = dev.add_route((*ip).into()).await { + tracing::error!(err = ?e, "add route failed"); + } + } + + if let Some(device) = dev.device.load().as_ref() { + let mut role_state = dev.role_state.lock(); + send_dns_answer(&mut role_state, Rtype::A, device, &resource, &ips); + + send_dns_answer(&mut role_state, Rtype::Aaaa, device, &resource, &ips); + } + + dev.role_state + .lock() + .dns_resources_internal_ips + .insert(resource, ips); + }); + + let ips: Vec = addrs.into_iter().map(Into::into).collect(); + for ip in &ips { + peer.add_allowed_ip(*ip); + } + + Ok(ips) + } + + #[tracing::instrument(level = "trace", skip(self))] + pub fn received_domain_parameters( + self: &Arc, + resource_id: ResourceId, + domain_response: DomainResponse, + ) -> Result<()> { + let gateway_id = self + .role_state + .lock() + .gateway_by_resource(&resource_id) + .ok_or(Error::UnknownResource)?; + + let Some(peer) = self + .role_state + .lock() + .peers_by_ip + .iter_mut() + .find_map(|(_, p)| (p.inner.conn_id == gateway_id).then_some(p.clone())) + else { + return Err(Error::ControlProtocolError); + }; + + let peer_ips = self.dns_response(&resource_id, &domain_response, &peer.inner)?; + insert_peers(&mut self.role_state.lock().peers_by_ip, &peer_ips, peer); + Ok(()) + } +} + +fn send_dns_answer( + role_state: &mut ClientState, + qtype: Rtype, + device: &Device, + resource_description: &DnsResource, + addrs: &[IpAddr], +) { + let packet = role_state + .deferred_dns_queries + .remove(&(resource_description.clone(), qtype)); + if let Some(packet) = packet { + let Some(packet) = dns::create_local_answer(addrs, packet) else { + return; + }; + if let Err(e) = device.write(packet) { + tracing::error!(err = ?e, "error writing packet: {e:#?}"); + } + } } diff --git a/rust/connlib/tunnel/src/control_protocol/gateway.rs b/rust/connlib/tunnel/src/control_protocol/gateway.rs index 2ebdf3fe9..de5ca861f 100644 --- a/rust/connlib/tunnel/src/control_protocol/gateway.rs +++ b/rust/connlib/tunnel/src/control_protocol/gateway.rs @@ -1,18 +1,21 @@ use crate::{ control_protocol::{insert_peers, start_handlers}, - peer::Peer, + peer::{PacketTransformGateway, Peer}, ConnectedPeer, GatewayState, PeerConfig, Tunnel, PEER_QUEUE_SIZE, }; use chrono::{DateTime, Utc}; use connlib_shared::{ - messages::{ClientId, Relay, ResourceDescription}, - Callbacks, Error, Result, + messages::{ + ClientId, ClientPayload, ConnectionAccepted, DomainResponse, Relay, ResourceAccepted, + ResourceDescription, + }, + Callbacks, Dname, Error, Result, }; -use std::sync::Arc; +use ip_network::IpNetwork; +use std::{net::ToSocketAddrs, sync::Arc}; use webrtc::ice_transport::{ - ice_parameters::RTCIceParameters, ice_role::RTCIceRole, - ice_transport_state::RTCIceTransportState, RTCIceTransport, + ice_role::RTCIceRole, ice_transport_state::RTCIceTransportState, RTCIceTransport, }; use super::{new_ice_connection, IceConnection}; @@ -52,18 +55,18 @@ where /// - `client_id`: UUID of the remote client. /// /// # Returns - /// An [RTCIceParameters] of the local sdp, with candidates gathered. + /// The connection details pub async fn set_peer_connection_request( self: &Arc, - remote_params: RTCIceParameters, + client_payload: ClientPayload, peer: PeerConfig, relays: Vec, client_id: ClientId, expires_at: DateTime, resource: ResourceDescription, - ) -> Result { + ) -> Result { let IceConnection { - ice_params: local_params, + ice_parameters: local_params, ice_transport: ice, ice_candidate_rx, } = new_ice_connection(&self.webrtc_api, relays).await?; @@ -77,7 +80,6 @@ where .peer_connections .lock() .insert(client_id, Arc::clone(&ice)); - if let Some(ice) = previous_ice { // If we had a previous on-going connection we stop it. // Note that ice.stop also closes the gatherer. @@ -87,35 +89,70 @@ where let _ = ice.stop().await; } - let tunnel = self.clone(); - tokio::spawn(async move { - if let Err(e) = ice - .start(&remote_params, Some(RTCIceRole::Controlled)) - .await - .map_err(Into::into) - .and_then(|_| tunnel.new_tunnel(peer, client_id, resource, expires_at, ice.clone())) - { - tracing::warn!(%client_id, err = ?e, "Can't start tunnel: {e:#}"); + let resource_addresses = match &resource { + ResourceDescription::Dns(_) => { + let Some(domain) = client_payload.domain.clone() else { + return Err(Error::ControlProtocolError); + }; + (domain.to_string(), 0) + .to_socket_addrs()? + .map(|addrs| addrs.ip().into()) + .collect() + } + ResourceDescription::Cidr(ref cidr) => vec![cidr.address], + }; + + { + let resource_addresses = resource_addresses.clone(); + let tunnel = self.clone(); + tokio::spawn(async move { + if let Err(e) = ice + .start(&client_payload.ice_parameters, Some(RTCIceRole::Controlled)) + .await + .map_err(Into::into) + .and_then(|_| { + tunnel.new_tunnel( + peer, + client_id, + resource, + expires_at, + ice.clone(), + resource_addresses, + ) + }) { - let mut peer_connections = tunnel.peer_connections.lock(); - if let Some(peer_connection) = peer_connections.get(&client_id).cloned() { - // We need to re-check this since it might have been replaced in between. - if matches!( - peer_connection.state(), - RTCIceTransportState::Failed - | RTCIceTransportState::Disconnected - | RTCIceTransportState::Closed - ) { - peer_connections.remove(&client_id); + tracing::warn!(%client_id, err = ?e, "Can't start tunnel: {e:#}"); + { + let mut peer_connections = tunnel.peer_connections.lock(); + if let Some(peer_connection) = peer_connections.get(&client_id).cloned() { + // We need to re-check this since it might have been replaced in between. + if matches!( + peer_connection.state(), + RTCIceTransportState::Failed + | RTCIceTransportState::Disconnected + | RTCIceTransportState::Closed + ) { + peer_connections.remove(&client_id); + } } } - } - // We only need to stop here because in case tunnel.new_tunnel failed. - let _ = ice.stop().await; - } - }); - Ok(local_params) + // We only need to stop here because in case tunnel.new_tunnel failed. + let _ = ice.stop().await; + } + }); + } + + Ok(ConnectionAccepted { + ice_parameters: local_params, + domain_response: client_payload.domain.map(|domain| DomainResponse { + domain, + address: resource_addresses + .into_iter() + .map(|ip| ip.network_address()) + .collect(), + }), + }) } pub fn allow_access( @@ -123,15 +160,48 @@ where resource: ResourceDescription, client_id: ClientId, expires_at: DateTime, - ) { + domain: Option, + ) -> Option { if let Some((_, peer)) = self + .role_state + .lock() .peers_by_ip - .write() .iter_mut() .find(|(_, p)| p.inner.conn_id == client_id) { - peer.inner.add_resource(resource, expires_at); + let addresses = match &resource { + ResourceDescription::Dns(_) => { + let Some(ref domain) = domain else { + return None; + }; + + (domain.to_string(), 0) + .to_socket_addrs() + .ok()? + .map(|a| a.ip()) + .map(Into::into) + .collect() + } + ResourceDescription::Cidr(cidr) => vec![cidr.address], + }; + + for address in &addresses { + peer.inner + .transform + .add_resource(*address, resource.clone(), expires_at); + } + + if let Some(domain) = domain { + return Some(ResourceAccepted { + domain_response: DomainResponse { + domain, + address: addresses.iter().map(|i| i.network_address()).collect(), + }, + }); + } } + + None } fn new_tunnel( @@ -141,11 +211,13 @@ where resource: ResourceDescription, expires_at: DateTime, ice: Arc, + resource_addresses: Vec, ) -> Result<()> { tracing::trace!(?peer_config.ips, "new_data_channel_open"); let device = self.device.load().clone().ok_or(Error::NoIface)?; let callbacks = self.callbacks.clone(); let ips = peer_config.ips.clone(); + // Worst thing if this is not run before peers_by_ip is that some packets are lost to the default route tokio::spawn(async move { for ip in ips { @@ -160,10 +232,15 @@ where self.next_index(), peer_config.clone(), client_id, - Some((resource, expires_at)), self.rate_limiter.clone(), + PacketTransformGateway::default(), )); + for address in resource_addresses { + peer.transform + .add_resource(address, resource.clone(), expires_at); + } + let (peer_sender, peer_receiver) = tokio::sync::mpsc::channel(PEER_QUEUE_SIZE); start_handlers( @@ -175,7 +252,7 @@ where ); insert_peers( - &mut self.peers_by_ip.write(), + &mut self.role_state.lock().peers_by_ip, &peer_config.ips, ConnectedPeer { inner: peer, diff --git a/rust/connlib/tunnel/src/device_channel/device_channel_unix.rs b/rust/connlib/tunnel/src/device_channel/device_channel_unix.rs index 3522fb527..bcacf20b1 100644 --- a/rust/connlib/tunnel/src/device_channel/device_channel_unix.rs +++ b/rust/connlib/tunnel/src/device_channel/device_channel_unix.rs @@ -12,6 +12,7 @@ use tokio::io::{unix::AsyncFd, Ready}; use tun::{IfaceDevice, IfaceStream}; use crate::device_channel::{Device, Packet}; +use crate::DnsFallbackStrategy; mod tun; @@ -83,8 +84,9 @@ impl IfaceConfig { pub(crate) async fn create_iface( config: &Interface, callbacks: &impl Callbacks, + fallback_strategy: DnsFallbackStrategy, ) -> Result { - let (iface, stream) = IfaceDevice::new(config, callbacks).await?; + let (iface, stream) = IfaceDevice::new(config, callbacks, fallback_strategy).await?; iface.up().await?; let io = DeviceIo(stream); let mtu = iface.mtu().await?; diff --git a/rust/connlib/tunnel/src/device_channel/device_channel_win.rs b/rust/connlib/tunnel/src/device_channel/device_channel_win.rs index 65a243206..2d2a0c4ef 100644 --- a/rust/connlib/tunnel/src/device_channel/device_channel_win.rs +++ b/rust/connlib/tunnel/src/device_channel/device_channel_win.rs @@ -1,5 +1,6 @@ use crate::device_channel::Packet; use crate::Device; +use crate::DnsFallbackStrategy; use connlib_shared::{messages::Interface, Callbacks, Result}; use ip_network::IpNetwork; use std::task::{Context, Poll}; @@ -47,7 +48,11 @@ impl IfaceConfig { } } -pub(crate) async fn create_iface(_: &Interface, _: &impl Callbacks) -> Result { +pub(crate) async fn create_iface( + _: &Interface, + _: &impl Callbacks, + _: DnsFallbackStrategy, +) -> Result { Ok(Device { config: IfaceConfig {}, io: DeviceIo {}, diff --git a/rust/connlib/tunnel/src/device_channel/tun/tun_android.rs b/rust/connlib/tunnel/src/device_channel/tun/tun_android.rs index 8080f3fbd..914c0466c 100644 --- a/rust/connlib/tunnel/src/device_channel/tun/tun_android.rs +++ b/rust/connlib/tunnel/src/device_channel/tun/tun_android.rs @@ -15,11 +15,10 @@ use std::{ }; use tokio::io::unix::AsyncFd; +use crate::DnsFallbackStrategy; + mod closeable; mod wrapped_socket; -// Android doesn't support Split DNS. So we intercept all requests and forward -// the non-Firezone name resolution requests to the upstream DNS resolver. -const DNS_FALLBACK_STRATEGY: &str = "upstream_resolver"; #[repr(C)] union IfrIfru { @@ -108,13 +107,14 @@ impl IfaceDevice { pub async fn new( config: &InterfaceConfig, callbacks: &impl Callbacks, + fallback_strategy: DnsFallbackStrategy, ) -> Result<(Self, Arc>)> { let fd = callbacks .on_set_interface_config( config.ipv4, config.ipv6, DNS_SENTINEL, - DNS_FALLBACK_STRATEGY.to_string(), + fallback_strategy.to_string(), )? .ok_or(Error::NoFd)?; let iface_stream = Arc::new(AsyncFd::new(IfaceStream { diff --git a/rust/connlib/tunnel/src/device_channel/tun/tun_darwin.rs b/rust/connlib/tunnel/src/device_channel/tun/tun_darwin.rs index 0bde0f2fd..1421f1fe3 100644 --- a/rust/connlib/tunnel/src/device_channel/tun/tun_darwin.rs +++ b/rust/connlib/tunnel/src/device_channel/tun/tun_darwin.rs @@ -16,6 +16,8 @@ use std::{ }; use tokio::io::unix::AsyncFd; +use crate::DnsFallbackStrategy; + const CTL_NAME: &[u8] = b"com.apple.net.utun_control"; const SIOCGIFMTU: u64 = 0x0000_0000_c020_6933; @@ -138,6 +140,7 @@ impl IfaceDevice { pub async fn new( config: &InterfaceConfig, callbacks: &impl Callbacks, + _: DnsFallbackStrategy, ) -> Result<(Self, Arc>)> { let mut info = ctl_info { ctl_id: 0, diff --git a/rust/connlib/tunnel/src/device_channel/tun/tun_linux.rs b/rust/connlib/tunnel/src/device_channel/tun/tun_linux.rs index 20ab903c2..3580866bb 100644 --- a/rust/connlib/tunnel/src/device_channel/tun/tun_linux.rs +++ b/rust/connlib/tunnel/src/device_channel/tun/tun_linux.rs @@ -15,6 +15,8 @@ use std::{ }; use tokio::io::unix::AsyncFd; +use crate::DnsFallbackStrategy; + const IFACE_NAME: &str = "tun-firezone"; const TUNSETIFF: u64 = 0x4004_54ca; const TUN_FILE: &[u8] = b"/dev/net/tun\0"; @@ -103,6 +105,7 @@ impl IfaceDevice { pub async fn new( config: &InterfaceConfig, cb: &impl Callbacks, + _: DnsFallbackStrategy, ) -> Result<(Self, Arc>)> { debug_assert!(IFACE_NAME.as_bytes().len() < IFNAMSIZ); diff --git a/rust/connlib/tunnel/src/dns.rs b/rust/connlib/tunnel/src/dns.rs index 1e2701f2b..0d7b4f35b 100644 --- a/rust/connlib/tunnel/src/dns.rs +++ b/rust/connlib/tunnel/src/dns.rs @@ -1,18 +1,20 @@ +use crate::client::DnsResource; use crate::device_channel::Packet; use crate::ip_packet::{to_dns, IpPacket, MutableIpPacket, Version}; -use crate::resource_table::ResourceTable; -use crate::DnsQuery; +use crate::{get_v4, get_v6, DnsFallbackStrategy, DnsQuery}; use connlib_shared::error::ConnlibError; -use connlib_shared::{messages::ResourceDescription, DNS_SENTINEL}; +use connlib_shared::messages::ResourceDescriptionDns; +use connlib_shared::{Dname, DNS_SENTINEL}; use domain::base::{ iana::{Class, Rcode, Rtype}, - Dname, Message, MessageBuilder, ParsedDname, Question, ToDname, + Message, MessageBuilder, Question, ToDname, }; use hickory_resolver::lookup::Lookup; use hickory_resolver::proto::op::Message as TrustDnsMessage; use hickory_resolver::proto::rr::RecordType; use itertools::Itertools; use pnet_packet::{udp::MutableUdpPacket, MutablePacket, Packet as UdpPacket, PacketSize}; +use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; const DNS_TTL: u32 = 300; @@ -22,9 +24,10 @@ const REVERSE_DNS_ADDRESS_V4: &str = "in-addr"; const REVERSE_DNS_ADDRESS_V6: &str = "ip6"; #[derive(Debug)] -pub(crate) enum ResolveStrategy { +pub(crate) enum ResolveStrategy { LocalResponse(T), ForwardQuery(U), + DeferredResponse(V), } struct DnsQueryParams { @@ -42,8 +45,8 @@ impl DnsQueryParams { } } -impl ResolveStrategy { - fn new(name: String, record_type: Rtype) -> ResolveStrategy { +impl ResolveStrategy { + fn forward(name: String, record_type: Rtype) -> ResolveStrategy { ResolveStrategy::ForwardQuery(DnsQueryParams { name, record_type: u16::from(record_type).into(), @@ -57,9 +60,11 @@ impl ResolveStrategy { // // See: https://stackoverflow.com/a/55093896 pub(crate) fn parse<'a>( - resources: &ResourceTable, + dns_resources: &HashMap, + dns_resources_internal_ips: &HashMap>, packet: IpPacket<'a>, -) -> Option, DnsQuery<'a>>> { + resolve_strategy: DnsFallbackStrategy, +) -> Option, DnsQuery<'a>, (DnsResource, Rtype)>> { if packet.destination() != IpAddr::from(DNS_SENTINEL) { return None; } @@ -68,19 +73,61 @@ pub(crate) fn parse<'a>( if message.header().qr() { return None; } + let question = message.first_question()?; - let resource = match resource_from_question(resources, &question)? { - ResolveStrategy::LocalResponse(resource) => resource, - ResolveStrategy::ForwardQuery(params) => { - return Some(ResolveStrategy::ForwardQuery(params.into_query(packet))) - } - }; - let response = build_dns_with_answer(message, question.qname(), question.qtype(), &resource)?; + // In general we prefer to always have a response NxDomain to deal with with domains we don't expect + // For systems with splitdns, in theory, we should only see Ptr queries we don't handle(e.g. apple's dns-sd) + let resource = + match resource_from_question(dns_resources, dns_resources_internal_ips, &question) { + Some(ResolveStrategy::LocalResponse(resource)) => Some(resource), + Some(ResolveStrategy::ForwardQuery(params)) => { + if resolve_strategy.is_upstream() { + return Some(ResolveStrategy::ForwardQuery(params.into_query(packet))); + } + None + } + Some(ResolveStrategy::DeferredResponse(resource)) => { + return Some(ResolveStrategy::DeferredResponse(( + resource, + question.qtype(), + ))) + } + None => None, + }; + let response = build_dns_with_answer(message, question.qname(), &resource)?; Some(ResolveStrategy::LocalResponse(build_response( packet, response, )?)) } +pub(crate) fn create_local_answer<'a>(ips: &[IpAddr], packet: IpPacket<'a>) -> Option> { + let datagram = packet.as_udp().unwrap(); + let message = to_dns(&datagram).unwrap(); + let question = message.first_question().unwrap(); + let qtype = question.qtype(); + let resource = match qtype { + Rtype::A => RecordData::A( + ips.iter() + .copied() + .filter_map(get_v4) + .map(domain::rdata::A::new) + .collect(), + ), + Rtype::Aaaa => RecordData::Aaaa( + ips.iter() + .copied() + .filter_map(get_v6) + .map(domain::rdata::Aaaa::new) + .collect(), + ), + _ => unreachable!(), + }; + + let response = build_dns_with_answer(message, question.qname(), &Some(resource.clone()))?; + + build_response(packet, response) +} + pub(crate) fn build_response_from_resolve_result( original_pkt: IpPacket<'_>, response: hickory_resolver::error::ResolveResult, @@ -148,8 +195,7 @@ fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec) -> Option fn build_dns_with_answer( message: &Message<[u8]>, qname: &N, - qtype: Rtype, - resource: &ResourceDescription, + resource: &Option>, ) -> Option> where N: ToDname + ?Sized, @@ -158,60 +204,110 @@ where let msg_builder = MessageBuilder::from_target(msg_buf).expect( "Developer error: we should be always be able to create a MessageBuilder from a Vec", ); + + let Some(resource) = resource else { + return Some( + msg_builder + .start_answer(message, Rcode::NXDomain) + .ok()? + .finish(), + ); + }; + let mut answer_builder = msg_builder.start_answer(message, Rcode::NoError).ok()?; - match qtype { - Rtype::A => answer_builder - .push(( - qname, - Class::In, - DNS_TTL, - domain::rdata::A::from(resource.ipv4()?), - )) - .ok()?, - Rtype::Aaaa => answer_builder - .push(( - qname, - Class::In, - DNS_TTL, - domain::rdata::Aaaa::from(resource.ipv6()?), - )) - .ok()?, - Rtype::Ptr => answer_builder - .push(( - qname, - Class::In, - DNS_TTL, - domain::rdata::Ptr::>::new( - resource.dns_name()?.parse::>>().ok()?.into(), - ), - )) - .ok()?, - _ => return None, + + // W/O object-safety there's no other way to access the inner type + // we could as well implement the ComposeRecordData trait for RecordData + // but the code would look like this but for each method instead + match resource { + RecordData::A(r) => r + .iter() + .try_for_each(|r| answer_builder.push((qname, Class::In, DNS_TTL, r))), + RecordData::Aaaa(r) => r + .iter() + .try_for_each(|r| answer_builder.push((qname, Class::In, DNS_TTL, r))), + RecordData::Ptr(r) => answer_builder.push((qname, Class::In, DNS_TTL, r)), } + .ok()?; Some(answer_builder.finish()) } +// No object safety =_= +#[derive(Clone)] +enum RecordData { + A(Vec), + Aaaa(Vec), + Ptr(domain::rdata::Ptr), +} + fn resource_from_question( - resources: &ResourceTable, + dns_resources: &HashMap, + dns_resources_internal_ips: &HashMap>, question: &Question, -) -> Option> { - let name = ToDname::to_cow(question.qname()).to_string(); +) -> Option, DnsQueryParams, DnsResource>> { + let name = ToDname::to_vec(question.qname()); let qtype = question.qtype(); - let resource = match qtype { - Rtype::A | Rtype::Aaaa => resources.get_by_name(&name), - Rtype::Ptr => { - let ip = reverse_dns_addr(&name)?; - resources.get_by_ip(ip) - } - _ => return None, - }; + match qtype { + Rtype::A => { + let Some(description) = name + .iter_suffixes() + .find_map(|n| dns_resources.get(&n.to_string())) + else { + return Some(ResolveStrategy::forward(name.to_string(), qtype)); + }; - resource - .cloned() - .map(ResolveStrategy::LocalResponse) - .unwrap_or(ResolveStrategy::new(name, qtype)) - .into() + let description = DnsResource::from_description(description, name); + let Some(ips) = dns_resources_internal_ips.get(&description) else { + // TODO!!: Sometimes we need to respond with nxdomain for this + // it might just not have this in the gateway. + // this is quite complicated, look at this again later + return Some(ResolveStrategy::DeferredResponse(description)); + }; + Some(ResolveStrategy::LocalResponse(RecordData::A( + ips.iter() + .cloned() + .filter_map(get_v4) + .map(domain::rdata::A::new) + .collect(), + ))) + } + Rtype::Aaaa => { + let Some(description) = name + .iter_suffixes() + .find_map(|n| dns_resources.get(&n.to_string())) + else { + return Some(ResolveStrategy::forward(name.to_string(), qtype)); + }; + let description = DnsResource::from_description(description, name); + let Some(ips) = dns_resources_internal_ips.get(&description) else { + return Some(ResolveStrategy::DeferredResponse(description)); + }; + + Some(ResolveStrategy::LocalResponse(RecordData::Aaaa( + ips.iter() + .cloned() + .filter_map(get_v6) + .map(domain::rdata::Aaaa::new) + .collect(), + ))) + } + Rtype::Ptr => { + let Some(ip) = reverse_dns_addr(&name.to_string()) else { + return Some(ResolveStrategy::forward(name.to_string(), qtype)); + }; + let Some(resource) = dns_resources_internal_ips + .iter() + .find_map(|(r, ips)| ips.contains(&ip).then_some(r)) + else { + return Some(ResolveStrategy::forward(name.to_string(), qtype)); + }; + Some(ResolveStrategy::LocalResponse(RecordData::Ptr( + domain::rdata::Ptr::new(resource.address.clone()), + ))) + } + _ => Some(ResolveStrategy::forward(name.to_string(), qtype)), + } } pub(crate) fn as_dns_message(pkt: &IpPacket) -> Option { diff --git a/rust/connlib/tunnel/src/gateway.rs b/rust/connlib/tunnel/src/gateway.rs index 58ed768bc..b78b95a4f 100644 --- a/rust/connlib/tunnel/src/gateway.rs +++ b/rust/connlib/tunnel/src/gateway.rs @@ -1,11 +1,16 @@ use crate::device_channel::create_iface; +use crate::peer::PacketTransformGateway; use crate::{ - Event, RoleState, Tunnel, ICE_GATHERING_TIMEOUT_SECONDS, MAX_CONCURRENT_ICE_GATHERING, + ConnectedPeer, DnsFallbackStrategy, Event, RoleState, Tunnel, ICE_GATHERING_TIMEOUT_SECONDS, + MAX_CONCURRENT_ICE_GATHERING, }; use connlib_shared::messages::{ClientId, Interface as InterfaceConfig}; use connlib_shared::Callbacks; use futures::channel::mpsc::Receiver; use futures_bounded::{PushError, StreamMap}; +use ip_network_table::IpNetworkTable; +use itertools::Itertools; +use std::collections::VecDeque; use std::sync::Arc; use std::task::{ready, Context, Poll}; use std::time::Duration; @@ -18,7 +23,9 @@ where /// Sets the interface configuration and starts background tasks. #[tracing::instrument(level = "trace", skip(self))] pub async fn set_interface(&self, config: &InterfaceConfig) -> connlib_shared::Result<()> { - let device = Arc::new(create_iface(config, self.callbacks()).await?); + // Note: the dns fallback strategy is irrelevant for gateways + let device = + Arc::new(create_iface(config, self.callbacks(), DnsFallbackStrategy::default()).await?); self.device.store(Some(device.clone())); self.no_device_waker.wake(); @@ -38,6 +45,8 @@ where /// [`Tunnel`] state specific to gateways. pub struct GatewayState { pub candidate_receivers: StreamMap, + #[allow(clippy::type_complexity)] + pub peers_by_ip: IpNetworkTable>, } impl GatewayState { @@ -61,6 +70,7 @@ impl Default for GatewayState { Duration::from_secs(ICE_GATHERING_TIMEOUT_SECONDS), MAX_CONCURRENT_ICE_GATHERING, ), + peers_by_ip: IpNetworkTable::new(), } } } @@ -84,4 +94,47 @@ impl RoleState for GatewayState { } } } + + fn remove_peers(&mut self, conn_id: ClientId) { + self.peers_by_ip.retain(|_, p| p.inner.conn_id != conn_id); + } + + fn refresh_peers(&mut self) -> VecDeque { + let mut peers_to_stop = VecDeque::new(); + for (_, peer) in self.peers_by_ip.iter().unique_by(|(_, p)| p.inner.conn_id) { + let conn_id = peer.inner.conn_id; + + peer.inner.transform.expire_resources(); + + if peer.inner.transform.is_emptied() { + tracing::trace!(%conn_id, "peer_expired"); + peers_to_stop.push_back(conn_id); + + continue; + } + + let bytes = match peer.inner.update_timers() { + Ok(Some(bytes)) => bytes, + Ok(None) => continue, + Err(e) => { + tracing::error!("Failed to update timers for peer: {e}"); + if e.is_fatal_connection_error() { + peers_to_stop.push_back(conn_id); + } + + continue; + } + }; + + let peer_channel = peer.channel.clone(); + + tokio::spawn(async move { + if let Err(e) = peer_channel.send(bytes).await { + tracing::error!("Failed to send packet to peer: {e:#}"); + } + }); + } + + peers_to_stop + } } diff --git a/rust/connlib/tunnel/src/ip_packet.rs b/rust/connlib/tunnel/src/ip_packet.rs index 48f1cced5..3c2627730 100644 --- a/rust/connlib/tunnel/src/ip_packet.rs +++ b/rust/connlib/tunnel/src/ip_packet.rs @@ -170,6 +170,15 @@ impl<'a> MutableIpPacket<'a> { } } + #[inline] + pub(crate) fn set_src(&mut self, src: IpAddr) { + match (self, src) { + (Self::MutableIpv4Packet(p), IpAddr::V4(s)) => p.set_source(s), + (Self::MutableIpv6Packet(p), IpAddr::V6(s)) => p.set_source(s), + _ => {} + } + } + pub(crate) fn set_len(&mut self, total_len: usize, payload_len: usize) { match self { Self::MutableIpv4Packet(p) => p.set_total_length(total_len as u16), @@ -201,6 +210,11 @@ impl<'a> IpPacket<'a> { Some(packet) } + pub(crate) fn to_owned(&self) -> IpPacket<'static> { + // This should never fail as the provided buffer is a vec (unless oom) + IpPacket::owned(self.packet().to_vec()).unwrap() + } + pub(crate) fn version(&self) -> Version { match self { IpPacket::Ipv4Packet(_) => Version::Ipv4, diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index ed50b6a03..1bf638346 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -15,8 +15,8 @@ use ip_packet::IpPacket; use pnet_packet::Packet; use hickory_resolver::proto::rr::RecordType; -use parking_lot::{Mutex, RwLock}; -use peer::{Peer, PeerStats}; +use parking_lot::Mutex; +use peer::{PacketTransform, Peer}; use tokio::time::MissedTickBehavior; use webrtc::{ api::{ @@ -29,11 +29,13 @@ use webrtc::{ use arc_swap::ArcSwapOption; use futures_util::task::AtomicWaker; -use itertools::Itertools; -use std::collections::VecDeque; use std::task::{ready, Context, Poll}; use std::{collections::HashMap, fmt, net::IpAddr, sync::Arc, time::Duration}; use std::{collections::HashSet, hash::Hash}; +use std::{ + collections::VecDeque, + net::{Ipv4Addr, Ipv6Addr}, +}; use tokio::time::Interval; use connlib_shared::{ @@ -61,7 +63,6 @@ mod index; mod ip_packet; mod peer; mod peer_handler; -mod resource_table; const MAX_UDP_SIZE: usize = (1 << 16) - 1; const DNS_QUERIES_QUEUE_SIZE: usize = 100; @@ -80,12 +81,68 @@ const ICE_GATHERING_TIMEOUT_SECONDS: u64 = 5 * 60; /// How many concurrent ICE gathering attempts we are allow. /// -/// Chosen arbitrarily. +/// Chosen arbitrarily, const MAX_CONCURRENT_ICE_GATHERING: usize = 100; // Note: Taken from boringtun const HANDSHAKE_RATE_LIMIT: u64 = 100; +// Note: the windows dns fallback strategy might change when implementing, however we prefer +// splitdns to trying to obtain the default server. +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "linux", + target_os = "windows" +))] +impl Default for DnsFallbackStrategy { + fn default() -> DnsFallbackStrategy { + Self::SystemResolver + } +} + +#[cfg(target_os = "android")] +impl Default for DnsFallbackStrategy { + fn default() -> DnsFallbackStrategy { + Self::UpstreamResolver + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DnsFallbackStrategy { + UpstreamResolver, + SystemResolver, +} + +impl DnsFallbackStrategy { + fn is_upstream(&self) -> bool { + self == &DnsFallbackStrategy::UpstreamResolver + } +} + +impl fmt::Display for DnsFallbackStrategy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DnsFallbackStrategy::UpstreamResolver => write!(f, "upstream_resolver"), + DnsFallbackStrategy::SystemResolver => write!(f, "system_resolver"), + } + } +} + +pub(crate) fn get_v4(ip: IpAddr) -> Option { + match ip { + IpAddr::V4(v4) => Some(v4), + IpAddr::V6(_) => None, + } +} + +pub(crate) fn get_v6(ip: IpAddr) -> Option { + match ip { + IpAddr::V4(_) => None, + IpAddr::V6(v6) => Some(v6), + } +} + /// Represent's the tunnel actual peer's config /// Obtained from connlib_shared's Peer #[derive(Clone)] @@ -113,8 +170,6 @@ pub struct Tunnel { rate_limiter: Arc, private_key: StaticSecret, public_key: PublicKey, - #[allow(clippy::type_complexity)] - peers_by_ip: RwLock>>, peer_connections: Mutex>>, webrtc_api: API, callbacks: CallbackErrorFacade, @@ -178,6 +233,8 @@ where cx: &mut Context<'_>, ) -> Poll>>> { loop { + let mut role_state = self.role_state.lock(); + let mut read_guard = self.read_buf.lock(); let mut write_guard = self.write_buf.lock(); let read_buf = read_guard.as_mut_slice(); @@ -189,9 +246,8 @@ where tracing::trace!(target: "wire", action = "read", from = "device", dest = %packet.destination()); - let mut role_state = self.role_state.lock(); - - let packet = match role_state.handle_dns(packet) { + let dns_strategy = role_state.dns_strategy; + let packet = match role_state.handle_dns(packet, dns_strategy) { Ok(Some(response)) => { device.write(response)?; continue; @@ -202,13 +258,12 @@ where let dest = packet.destination(); - let peers_by_ip = self.peers_by_ip.read(); - let Some(peer) = peer_by_ip(&peers_by_ip, dest) else { - role_state.on_connection_intent(dest); + let Some(peer) = peer_by_ip(&role_state.peers_by_ip, dest) else { + role_state.on_connection_intent_ip(dest); continue; }; - self.encapsulate(write_buf, packet, dest, peer); + self.encapsulate(write_buf, packet, peer); continue; } @@ -234,12 +289,12 @@ where Some(Poll::Ready(Ok(Some(packet)))) => { let dest = packet.destination(); - let peers_by_ip = self.peers_by_ip.read(); - let Some(peer) = peer_by_ip(&peers_by_ip, dest) else { + let role_state = self.role_state.lock(); + let Some(peer) = peer_by_ip(&role_state.peers_by_ip, dest) else { continue; }; - self.encapsulate(write_buf, packet, dest, peer); + self.encapsulate(write_buf, packet, peer); continue; } @@ -267,18 +322,24 @@ where } } -#[derive(Clone)] -pub struct ConnectedPeer { - inner: Arc>, +pub struct ConnectedPeer { + inner: Arc>, channel: tokio::sync::mpsc::Sender, } -// TODO: For now we only use these fields with debug +impl Clone for ConnectedPeer { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + channel: self.channel.clone(), + } + } +} + #[allow(dead_code)] #[derive(Debug, Clone)] pub struct TunnelStats { public_key: String, - peers_by_ip: HashMap>, peer_connections: Vec, } @@ -288,17 +349,10 @@ where TRoleState: RoleState, { pub fn stats(&self) -> TunnelStats { - let peers_by_ip = self - .peers_by_ip - .read() - .iter() - .map(|(ip, peer)| (ip, peer.inner.stats())) - .collect(); let peer_connections = self.peer_connections.lock().keys().cloned().collect(); TunnelStats { public_key: Key::from(self.public_key).to_string(), - peers_by_ip, peer_connections, } } @@ -306,9 +360,7 @@ where fn poll_next_event_common(&self, cx: &mut Context<'_>) -> Poll> { loop { if let Some(conn_id) = self.peers_to_stop.lock().pop_front() { - let mut peers = self.peers_by_ip.write(); - - peers.retain(|_, p| p.inner.conn_id != conn_id); + self.role_state.lock().remove_peers(conn_id); if let Some(conn) = self.peer_connections.lock().remove(&conn_id) { tokio::spawn({ @@ -334,44 +386,8 @@ where } if self.peer_refresh_interval.lock().poll_tick(cx).is_ready() { - let peers_by_ip = self.peers_by_ip.read(); - let mut peers_to_stop = self.peers_to_stop.lock(); - - for (_, peer) in peers_by_ip.iter().unique_by(|(_, p)| p.inner.conn_id) { - let conn_id = peer.inner.conn_id; - - peer.inner.expire_resources(); - - if peer.inner.is_emptied() { - tracing::trace!(%conn_id, "peer_expired"); - peers_to_stop.push_back(conn_id); - - continue; - } - - let bytes = match peer.inner.update_timers() { - Ok(Some(bytes)) => bytes, - Ok(None) => continue, - Err(e) => { - tracing::error!("Failed to update timers for peer: {e}"); - let _ = self.callbacks.on_error(&e); - - if e.is_fatal_connection_error() { - peers_to_stop.push_back(conn_id); - } - - continue; - } - }; - - let peer_channel = peer.channel.clone(); - - tokio::spawn(async move { - if let Err(e) = peer_channel.send(bytes).await { - tracing::error!("Failed to send packet to peer: {e:#}"); - } - }); - } + let mut peers_to_stop = self.role_state.lock().refresh_peers(); + self.peers_to_stop.lock().append(&mut peers_to_stop); continue; } @@ -402,25 +418,24 @@ where } } - fn encapsulate( + fn encapsulate( &self, write_buf: &mut [u8], packet: MutableIpPacket, - dest: IpAddr, - peer: &ConnectedPeer, + peer: &ConnectedPeer, ) { let peer_id = peer.inner.conn_id; match peer.inner.encapsulate(packet, write_buf) { Ok(None) => {} Ok(Some(b)) => { - tracing::trace!(target: "wire", action = "writing", to = "peer", %dest); + tracing::trace!(target: "wire", action = "writing", to = "peer"); if peer.channel.try_send(b).is_err() { - tracing::warn!(target: "wire", action = "dropped", to = "peer", %dest); + tracing::warn!(target: "wire", action = "dropped", to = "peer"); } } Err(e) => { - tracing::error!(resource_address = %dest, err = ?e, "failed to handle packet {e:#}"); + tracing::error!(err = ?e, "failed to handle packet {e:#}"); if e.is_fatal_connection_error() { self.peers_to_stop.lock().push_back(peer_id); @@ -430,10 +445,10 @@ where } } -pub(crate) fn peer_by_ip( - peers_by_ip: &IpNetworkTable>, +pub(crate) fn peer_by_ip( + peers_by_ip: &IpNetworkTable>, ip: IpAddr, -) -> Option<&ConnectedPeer> { +) -> Option<&ConnectedPeer> { peers_by_ip.longest_match(ip).map(|(_, peer)| peer) } @@ -492,7 +507,6 @@ where pub async fn new(private_key: StaticSecret, callbacks: CB) -> Result { let public_key = (&private_key).into(); let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); - let peers_by_ip = RwLock::new(IpNetworkTable::new()); let next_index = Default::default(); let peer_connections = Default::default(); let device = Default::default(); @@ -518,7 +532,6 @@ where private_key, peer_connections, public_key, - peers_by_ip, next_index, webrtc_api, device, @@ -579,4 +592,6 @@ pub trait RoleState: Default + Send + 'static { type Id: fmt::Debug + fmt::Display + Eq + Hash + Copy + Unpin + Send + Sync + 'static; fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll>; + fn remove_peers(&mut self, conn_id: Self::Id); + fn refresh_peers(&mut self) -> VecDeque; } diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs index eb4cf4ea3..fa85bf30f 100644 --- a/rust/connlib/tunnel/src/peer.rs +++ b/rust/connlib/tunnel/src/peer.rs @@ -1,79 +1,51 @@ use std::borrow::Cow; use std::collections::VecDeque; -use std::net::ToSocketAddrs; +use std::net::IpAddr; use std::sync::Arc; -use std::{collections::HashMap, net::IpAddr}; +use bimap::BiMap; use boringtun::noise::rate_limiter::RateLimiter; use boringtun::noise::{Tunn, TunnResult}; use boringtun::x25519::StaticSecret; use bytes::Bytes; use chrono::{DateTime, Utc}; -use connlib_shared::{ - messages::{ResourceDescription, ResourceId}, - Error, Result, -}; +use connlib_shared::{messages::ResourceDescription, Error, Result}; use ip_network::IpNetwork; use ip_network_table::IpNetworkTable; use parking_lot::{Mutex, RwLock}; use pnet_packet::Packet; use secrecy::ExposeSecret; +use crate::client::IpProvider; use crate::MAX_UDP_SIZE; -use crate::{ - device_channel, ip_packet::MutableIpPacket, resource_table::ResourceTable, PeerConfig, -}; +use crate::{device_channel, ip_packet::MutableIpPacket, PeerConfig}; type ExpiryingResource = (ResourceDescription, DateTime); -pub(crate) struct Peer { +pub(crate) struct Peer { tunnel: Mutex, allowed_ips: RwLock>, pub conn_id: TId, - resources: Option>>, - // Here we store the address that we obtained for the resource that the peer corresponds to. - // This can have the following problem: - // 1. Peer sends packet to address.com and it resolves to 1.1.1.1 - // 2. Now Peer sends another packet to address.com but it resolves to 2.2.2.2 - // 3. We receive an outstanding response(or push) from 1.1.1.1 - // This response(or push) is ignored, since we store only the last. - // so, TODO: store multiple ips and expire them. - // Note that this case is quite an unlikely edge case so I wouldn't prioritize this fix - // TODO: Also check if there's any case where we want to talk to ipv4 and ipv6 from the same peer. - translated_resource_addresses: RwLock>, + pub transform: TTransform, } -// TODO: For now we only use these fields with debug #[allow(dead_code)] #[derive(Debug, Clone)] pub(crate) struct PeerStats { pub allowed_ips: Vec, pub conn_id: TId, - pub dns_resources: HashMap, - pub network_resources: HashMap, - pub translated_resource_addresses: HashMap, } -impl Peer +impl Peer where TId: Copy, + TTransform: PacketTransform, { pub(crate) fn stats(&self) -> PeerStats { - let (network_resources, dns_resources) = self.resources.as_ref().map_or_else( - || (HashMap::new(), HashMap::new()), - |resources| { - let resources = resources.read(); - (resources.network_resources(), resources.dns_resources()) - }, - ); let allowed_ips = self.allowed_ips.read().iter().map(|(ip, _)| ip).collect(); - let translated_resource_addresses = self.translated_resource_addresses.read().clone(); PeerStats { allowed_ips, conn_id: self.conn_id, - dns_resources, - network_resources, - translated_resource_addresses, } } @@ -82,9 +54,9 @@ where index: u32, peer_config: PeerConfig, conn_id: TId, - resource: Option<(ResourceDescription, DateTime)>, rate_limiter: Arc, - ) -> Peer { + transform: TTransform, + ) -> Peer { let tunnel = Tunn::new( private_key.clone(), peer_config.public_key, @@ -99,28 +71,15 @@ where allowed_ips.insert(ip, ()); } let allowed_ips = RwLock::new(allowed_ips); - let resources = resource.map(|r| { - let mut resource_table = ResourceTable::new(); - resource_table.insert(r); - RwLock::new(resource_table) - }); Peer { tunnel: Mutex::new(tunnel), allowed_ips, conn_id, - resources, - translated_resource_addresses: Default::default(), + transform, } } - fn get_translation(&self, ip: IpAddr) -> Option { - let id = self.translated_resource_addresses.read().get(&ip).cloned(); - self.resources.as_ref().and_then(|resources| { - id.and_then(|id| resources.read().get_by_id(&id).map(|r| r.0.clone())) - }) - } - pub(crate) fn add_allowed_ip(&self, ip: IpNetwork) { self.allowed_ips.write().insert(ip, ()); } @@ -143,66 +102,26 @@ where Ok(Some(Bytes::copy_from_slice(packet))) } - pub(crate) fn is_emptied(&self) -> bool { - self.resources.as_ref().is_some_and(|r| r.read().is_empty()) - } - - pub(crate) fn expire_resources(&self) { - if let Some(resources) = &self.resources { - // TODO: We could move this to resource_table and make it way faster - let expire_resources: Vec<_> = resources - .read() - .values() - .filter(|(_, e)| e <= &Utc::now()) - .cloned() - .collect(); - { - // Oh oh! 2 Mutexes - let mut resources = resources.write(); - let mut translated_resource_addresses = self.translated_resource_addresses.write(); - for r in expire_resources { - resources.cleanup_resource(&r); - translated_resource_addresses.retain(|_, &mut i| r.0.id() != i); - } - } - } - } - - pub(crate) fn add_resource(&self, resource: ResourceDescription, expires_at: DateTime) { - if let Some(resources) = &self.resources { - resources.write().insert((resource, expires_at)) - } - } - - pub(crate) fn is_allowed(&self, addr: IpAddr) -> bool { + fn is_allowed(&self, addr: IpAddr) -> bool { self.allowed_ips.read().longest_match(addr).is_some() } - pub(crate) fn update_translated_resource_address(&self, id: ResourceId, addr: IpAddr) { - self.translated_resource_addresses.write().insert(addr, id); - } - /// Sends the given packet to this peer by encapsulating it in a wireguard packet. pub(crate) fn encapsulate( &self, - mut packet: MutableIpPacket, + packet: MutableIpPacket, buf: &mut [u8], ) -> Result> { - if let Some(resource) = self.get_translation(packet.to_immutable().source()) { - let ResourceDescription::Dns(resource) = resource else { - tracing::error!( - "Control protocol error: only dns resources should have a resource_address" - ); - return Err(Error::ControlProtocolError); - }; + let Some(packet) = self.transform.packet_transform(packet) else { + return Ok(None); + }; - match packet { - MutableIpPacket::MutableIpv4Packet(ref mut p) => p.set_source(resource.ipv4), - MutableIpPacket::MutableIpv6Packet(ref mut p) => p.set_source(resource.ipv6), - } + tracing::trace!( + "packet src: {}, packet dst {}", + packet.as_immutable().source(), + packet.as_immutable().destination() + ); - packet.update_checksum(); - } let packet = match self.tunnel.lock().encapsulate(packet.packet(), buf) { TunnResult::Done => return Ok(None), TunnResult::Err(e) => return Err(e.into()), @@ -237,36 +156,27 @@ where Ok(Some(WriteTo::Network(packets))) } TunnResult::WriteToTunnelV4(packet, addr) => { - let Some(packet) = make_packet_for_resource(self, addr.into(), packet)? else { - return Ok(None); - }; - - Ok(Some(WriteTo::Resource(packet))) + self.make_packet_for_resource(addr.into(), packet) } TunnResult::WriteToTunnelV6(packet, addr) => { - let Some(packet) = make_packet_for_resource(self, addr.into(), packet)? else { - return Ok(None); - }; - - Ok(Some(WriteTo::Resource(packet))) + self.make_packet_for_resource(addr.into(), packet) } } } - pub(crate) fn get_packet_resource( + fn make_packet_for_resource<'a>( &self, - packet: &mut [u8], - ) -> Option<(IpAddr, ResourceDescription)> { - let resources = self.resources.as_ref()?; + addr: IpAddr, + packet: &'a mut [u8], + ) -> Result>> { + let (packet, addr) = self.transform.packet_untransform(&addr, packet)?; - let dst = Tunn::dst_address(packet)?; + if !self.is_allowed(addr) { + tracing::warn!("packet not allowed: {addr}"); + return Ok(None); + } - let Some(resource) = resources.read().get_by_ip(dst).map(|r| r.0.clone()) else { - tracing::warn!("client tried to hijack the tunnel for resource itsn't allowed."); - return None; - }; - - Some((dst, resource)) + Ok(Some(WriteTo::Resource(packet))) } } @@ -275,103 +185,131 @@ pub enum WriteTo<'a> { Resource(device_channel::Packet<'a>), } -#[inline(always)] -pub(crate) fn make_packet_for_resource<'a, TId>( - peer: &Peer, - addr: IpAddr, - packet: &'a mut [u8], -) -> Result>> -where - TId: Copy, -{ - if !peer.is_allowed(addr) { - tracing::warn!(%addr, "Received packet from peer with an unallowed ip"); - return Ok(None); +pub struct PacketTransformGateway { + resources: RwLock>, +} + +impl Default for PacketTransformGateway { + fn default() -> Self { + Self { + resources: RwLock::new(IpNetworkTable::new()), + } + } +} + +#[derive(Default)] +pub struct PacketTransformClient { + // TODO: we need to refresh the translations ips periodically, just add a timer to resend allow access + translations: RwLock>, +} + +impl PacketTransformClient { + pub fn get_or_assign_translation( + &self, + external_ip: &IpAddr, + ip_provider: &mut IpProvider, + ) -> Option { + let mut translations = self.translations.write(); + if let Some(internal_ip) = translations.get_by_right(external_ip) { + return Some(*internal_ip); + } + let internal_ip = match external_ip { + IpAddr::V4(_) => ip_provider.next_ipv4()?.into(), + IpAddr::V6(_) => ip_provider.next_ipv6()?.into(), + }; + translations.insert(internal_ip, *external_ip); + Some(internal_ip) + } +} + +impl PacketTransformGateway { + pub(crate) fn is_emptied(&self) -> bool { + self.resources.read().is_empty() } - let Some((dst, resource)) = peer.get_packet_resource(packet) else { - // If there's no associated resource it means that we are in a client, then the packet comes from a gateway - // and we just trust gateways. - // In gateways this should never happen. - tracing::trace!(target: "wire", action = "writing", to = "iface", %addr, bytes = %packet.len()); + pub(crate) fn expire_resources(&self) { + self.resources.write().retain(|_, (_, e)| *e > Utc::now()); + } + + pub(crate) fn add_resource( + &self, + ip: IpNetwork, + resource: ResourceDescription, + expires_at: DateTime, + ) { + self.resources.write().insert(ip, (resource, expires_at)); + } +} + +pub(crate) trait PacketTransform { + fn packet_untransform<'a>( + &self, + addr: &IpAddr, + packet: &'a mut [u8], + ) -> Result<(device_channel::Packet<'a>, IpAddr)>; + + fn packet_transform<'a>(&self, packet: MutableIpPacket<'a>) -> Option>; +} + +impl PacketTransform for PacketTransformGateway { + fn packet_untransform<'a>( + &self, + addr: &IpAddr, + packet: &'a mut [u8], + ) -> Result<(device_channel::Packet<'a>, IpAddr)> { + let Some(dst) = Tunn::dst_address(packet) else { + return Err(Error::BadPacket); + }; + + if self.resources.read().longest_match(dst).is_some() { + let packet = make_packet(packet, addr); + Ok((packet, *addr)) + } else { + tracing::warn!(%dst, "unallowed packet"); + Err(Error::InvalidDst) + } + } + + fn packet_transform<'a>(&self, packet: MutableIpPacket<'a>) -> Option> { + Some(packet) + } +} + +impl PacketTransform for PacketTransformClient { + fn packet_untransform<'a>( + &self, + addr: &IpAddr, + packet: &'a mut [u8], + ) -> Result<(device_channel::Packet<'a>, IpAddr)> { + let translations = self.translations.read(); + let src = translations.get_by_right(addr).unwrap_or(addr); + + let Some(mut pkt) = MutableIpPacket::new(packet) else { + return Err(Error::BadPacket); + }; + + tracing::trace!("setting packet source from: {addr} to {src}"); + pkt.set_src(*src); + pkt.update_checksum(); let packet = make_packet(packet, addr); - return Ok(Some(packet)); - }; + Ok((packet, *src)) + } - let (dst_addr, _dst_port) = get_resource_addr_and_port(peer, &resource, &addr, &dst)?; - update_packet(packet, dst_addr); - let packet = make_packet(packet, addr); + fn packet_transform<'a>(&self, mut packet: MutableIpPacket<'a>) -> Option> { + if let Some(translated_ip) = self.translations.read().get_by_left(&packet.destination()) { + packet.set_dst(*translated_ip); + packet.update_checksum(); + tracing::trace!("translating to ip: {translated_ip}"); + } - Ok(Some(packet)) + Some(packet) + } } #[inline(always)] -fn make_packet(packet: &mut [u8], dst_addr: IpAddr) -> device_channel::Packet<'_> { +fn make_packet<'a>(packet: &'a mut [u8], dst_addr: &IpAddr) -> device_channel::Packet<'a> { match dst_addr { IpAddr::V4(_) => device_channel::Packet::Ipv4(Cow::Borrowed(packet)), IpAddr::V6(_) => device_channel::Packet::Ipv6(Cow::Borrowed(packet)), } } - -#[inline(always)] -fn update_packet(packet: &mut [u8], dst_addr: IpAddr) { - let Some(mut pkt) = MutableIpPacket::new(packet) else { - return; - }; - pkt.set_dst(dst_addr); - pkt.update_checksum(); -} - -fn get_matching_version_ip(addr: &IpAddr, ip: &IpAddr) -> Option { - ((addr.is_ipv4() && ip.is_ipv4()) || (addr.is_ipv6() && ip.is_ipv6())).then_some(*ip) -} - -fn get_resource_addr_and_port( - peer: &Peer, - resource: &ResourceDescription, - addr: &IpAddr, - dst: &IpAddr, -) -> Result<(IpAddr, Option)> -where - TId: Copy, -{ - match resource { - ResourceDescription::Dns(r) => { - let mut address = r.address.split(':'); - let Some(dst_addr) = address.next() else { - tracing::error!("invalid DNS name for resource: {}", r.address); - return Err(Error::InvalidResource); - }; - let Ok(mut dst_addr) = (dst_addr, 0).to_socket_addrs() else { - tracing::warn!(%addr, "Couldn't resolve name"); - return Err(Error::InvalidResource); - }; - let Some(dst_addr) = dst_addr.find_map(|d| get_matching_version_ip(addr, &d.ip())) - else { - tracing::warn!(%addr, "Couldn't resolve name addr"); - return Err(Error::InvalidResource); - }; - peer.update_translated_resource_address(r.id, dst_addr); - Ok(( - dst_addr, - address - .next() - .map(str::parse::) - .and_then(std::result::Result::ok), - )) - } - ResourceDescription::Cidr(r) => { - if r.address.contains(*dst) { - Ok(( - get_matching_version_ip(addr, dst).ok_or(Error::InvalidResource)?, - None, - )) - } else { - tracing::warn!( - "client tried to hijack the tunnel for range outside what it's allowed." - ); - Err(Error::InvalidSource) - } - } - } -} diff --git a/rust/connlib/tunnel/src/peer_handler.rs b/rust/connlib/tunnel/src/peer_handler.rs index ca0d1e6fb..2fffda9cb 100644 --- a/rust/connlib/tunnel/src/peer_handler.rs +++ b/rust/connlib/tunnel/src/peer_handler.rs @@ -9,16 +9,17 @@ use webrtc::mux::endpoint::Endpoint; use webrtc::util::Conn; use crate::device_channel::Device; -use crate::peer::WriteTo; +use crate::peer::{PacketTransform, WriteTo}; use crate::{peer::Peer, MAX_UDP_SIZE}; -pub(crate) async fn start_peer_handler( +pub(crate) async fn start_peer_handler( device: Arc>, callbacks: impl Callbacks + 'static, - peer: Arc>, + peer: Arc>, channel: Arc, ) where TId: Copy + fmt::Debug + Send + Sync + 'static, + TTransform: PacketTransform, { loop { let Some(device) = device.load().clone() else { @@ -43,14 +44,15 @@ pub(crate) async fn start_peer_handler( tracing::debug!(peer = ?peer.stats(), "peer_stopped"); } -async fn peer_handler( +async fn peer_handler( callbacks: &impl Callbacks, - peer: &Arc>, + peer: &Arc>, channel: Arc, device: &Device, ) -> std::io::Result<()> where TId: Copy, + TTransform: PacketTransform, { let mut src_buf = [0u8; MAX_UDP_SIZE]; let mut dst_buf = [0u8; MAX_UDP_SIZE]; diff --git a/rust/connlib/tunnel/src/resource_table.rs b/rust/connlib/tunnel/src/resource_table.rs deleted file mode 100644 index 625b10103..000000000 --- a/rust/connlib/tunnel/src/resource_table.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! A resource table is a custom type that allows us to store a resource under an id and possibly multiple ips or even network ranges -use std::{collections::HashMap, net::IpAddr, rc::Rc}; - -use chrono::{DateTime, Utc}; -use connlib_shared::messages::{ResourceDescription, ResourceId}; -use ip_network::IpNetwork; -use ip_network_table::IpNetworkTable; - -pub(crate) trait Resource { - fn description(&self) -> &ResourceDescription; -} - -impl Resource for ResourceDescription { - fn description(&self) -> &ResourceDescription { - self - } -} - -impl Resource for (ResourceDescription, DateTime) { - fn description(&self) -> &ResourceDescription { - &self.0 - } -} - -/// The resource table type -/// -/// This is specifically crafted for our use case, so the API is particularly made for us and not generic -pub(crate) struct ResourceTable { - id_table: HashMap>, - network_table: IpNetworkTable>, - dns_name: HashMap>, -} - -// SAFETY: This type is send since you can't obtain the underlying `Rc` and the only way to clone it is using `insert` which requires an &mut self -unsafe impl Send for ResourceTable {} -// SAFETY: This type is sync since you can't obtain the underlying `Rc` and the only way to clone it is using `insert` which requires an &mut self -unsafe impl Sync for ResourceTable {} - -impl Default for ResourceTable { - fn default() -> ResourceTable { - ResourceTable::new() - } -} - -impl ResourceTable { - /// Creates a new `ResourceTable` - pub fn new() -> ResourceTable { - ResourceTable { - network_table: IpNetworkTable::new(), - id_table: HashMap::new(), - dns_name: HashMap::new(), - } - } -} - -impl ResourceTable -where - T: Resource + Clone, -{ - pub fn values(&self) -> impl Iterator { - self.id_table.values().map(AsRef::as_ref) - } - - pub fn network_resources(&self) -> HashMap { - // Safety: Due to internal consistency, since the value is stored the reference should be valid - self.network_table - .iter() - .map(|(wg_ip, res)| (wg_ip, res.as_ref().clone())) - .collect() - } - - pub fn dns_resources(&self) -> HashMap { - // Safety: Due to internal consistency, since the value is stored the reference should be valid - self.dns_name - .iter() - .map(|(name, res)| (name.clone(), res.as_ref().clone())) - .collect() - } - - /// Tells you if it's empty - pub fn is_empty(&self) -> bool { - self.id_table.is_empty() - } - - /// Gets the resource by ip - pub fn get_by_ip(&self, ip: impl Into) -> Option<&T> { - self.network_table.longest_match(ip).map(|m| m.1.as_ref()) - } - - /// Gets the resource by id - pub fn get_by_id(&self, id: &ResourceId) -> Option<&T> { - self.id_table.get(id).map(AsRef::as_ref) - } - - /// Gets the resource by name - pub fn get_by_name(&self, name: impl AsRef) -> Option<&T> { - self.dns_name.get(name.as_ref()).map(AsRef::as_ref) - } - - fn remove_resource(&mut self, resource_description: &T) { - let id = { - match resource_description.description() { - ResourceDescription::Dns(r) => { - self.dns_name.remove(&r.address); - self.network_table.remove(r.ipv4); - self.network_table.remove(r.ipv6); - r.id - } - ResourceDescription::Cidr(r) => { - self.network_table.remove(r.address); - r.id - } - } - }; - self.id_table.remove(&id); - } - - pub(crate) fn cleanup_resource(&mut self, resource_description: &T) { - match resource_description.description() { - ResourceDescription::Dns(r) => { - if let Some(res) = self.id_table.remove(&r.id) { - self.remove_resource(res.as_ref()); - } - - if let Some(res) = self.dns_name.remove(&r.address) { - self.remove_resource(res.as_ref()); - } - - if let Some(res) = self.network_table.remove(r.ipv4) { - self.remove_resource(res.as_ref()); - } - - if let Some(res) = self.network_table.remove(r.ipv6) { - self.remove_resource(res.as_ref()); - } - } - ResourceDescription::Cidr(r) => { - if let Some(res) = self.id_table.remove(&r.id) { - self.remove_resource(res.as_ref()); - } - - if let Some(res) = self.network_table.remove(r.address) { - self.remove_resource(res.as_ref()); - } - } - } - } - - // For soundness it's very important that this API only takes a resource_description - // doing this, we can assume that when removing a resource from the id table we have all the info - // about all the tables. - /// Inserts a new resource_description - /// - /// If the id was used previously the old value will be deleted. - /// Same goes if any of the ip matches exactly an old ip or dns name. - /// This means that a match in IP or dns name will discard all old values. - /// - /// This is done so that we don't have dangling values. - pub fn insert(&mut self, resource_description: T) { - self.cleanup_resource(&resource_description); - let id = resource_description.description().id(); - let resource_description = Rc::new(resource_description); - self.id_table.insert(id, Rc::clone(&resource_description)); - // we just inserted it we can unwrap - let res = self.id_table.get(&id).unwrap(); - match res.description() { - ResourceDescription::Dns(r) => { - self.network_table - .insert(r.ipv4, Rc::clone(&resource_description)); - self.network_table - .insert(r.ipv6, Rc::clone(&resource_description)); - self.dns_name - .insert(r.address.clone(), resource_description); - } - ResourceDescription::Cidr(r) => { - self.network_table.insert(r.address, resource_description); - } - } - } - - pub fn resource_list(&self) -> Vec { - self.id_table - .values() - .map(|r| r.description()) - .cloned() - .collect() - } -} diff --git a/rust/gateway/Cargo.toml b/rust/gateway/Cargo.toml index d4f40a58c..a4f596a0c 100644 --- a/rust/gateway/Cargo.toml +++ b/rust/gateway/Cargo.toml @@ -27,6 +27,7 @@ tracing = { workspace = true } tracing-subscriber = "0.3.17" url = { version = "2.4.1", default-features = false } webrtc = { workspace = true } +domain = { workspace = true } [dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/rust/gateway/src/eventloop.rs b/rust/gateway/src/eventloop.rs index 23063905c..a6e9e3228 100644 --- a/rust/gateway/src/eventloop.rs +++ b/rust/gateway/src/eventloop.rs @@ -4,7 +4,7 @@ use crate::messages::{ }; use crate::CallbackHandler; use anyhow::Result; -use connlib_shared::messages::ClientId; +use connlib_shared::messages::{ClientId, GatewayResponse}; use connlib_shared::Error; use firezone_tunnel::{Event, GatewayState, Tunnel}; use phoenix_channel::PhoenixChannel; @@ -12,7 +12,6 @@ use std::convert::Infallible; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; -use webrtc::ice_transport::ice_parameters::RTCIceParameters; pub const PHOENIX_TOPIC: &str = "gateway"; @@ -22,7 +21,7 @@ pub struct Eventloop { // TODO: Strongly type request reference (currently `String`) connection_request_tasks: - futures_bounded::FuturesMap<(ClientId, String), Result>, + futures_bounded::FuturesMap<(ClientId, String), Result>, add_ice_candidate_tasks: futures_bounded::FuturesSet>, print_stats_timer: tokio::time::Interval, @@ -53,14 +52,14 @@ impl Eventloop { pub fn poll(&mut self, cx: &mut Context<'_>) -> Poll> { loop { match self.connection_request_tasks.poll_unpin(cx) { - Poll::Ready(((client, reference), Ok(Ok(gateway_rtc_session_description)))) => { + Poll::Ready(((client, reference), Ok(Ok(gateway_payload)))) => { tracing::debug!(%client, %reference, "Connection is ready"); let _id = self.portal.send( PHOENIX_TOPIC, EgressMessages::ConnectionReady(ConnectionReady { reference, - gateway_rtc_session_description, + gateway_payload, }), ); @@ -110,16 +109,17 @@ impl Eventloop { match self.connection_request_tasks.try_push( (req.client.id, req.reference.clone()), async move { - tunnel + let conn = tunnel .set_peer_connection_request( - req.client.rtc_session_description, + req.client.payload, req.client.peer.into(), req.relays, req.client.id, req.expires_at, req.resource, ) - .await + .await?; + Ok(GatewayResponse::ConnectionAccepted(conn)) }, ) { Err(futures_bounded::PushError::BeyondCapacity(_)) => { @@ -138,12 +138,26 @@ impl Eventloop { client_id, resource, expires_at, + payload, + reference, }), .. }) => { tracing::debug!(client = %client_id, resource = %resource.id(), expires = %expires_at.to_rfc3339() ,"Allowing access to resource"); - self.tunnel.allow_access(resource, client_id, expires_at); + if let Some(res) = self + .tunnel + .allow_access(resource, client_id, expires_at, payload) + { + tracing::trace!("sending response"); + self.portal.send( + PHOENIX_TOPIC, + EgressMessages::ConnectionReady(ConnectionReady { + reference, + gateway_payload: GatewayResponse::ResourceAccepted(res), + }), + ); + } continue; } Poll::Ready(phoenix_channel::Event::InboundMessage { diff --git a/rust/gateway/src/messages.rs b/rust/gateway/src/messages.rs index 5339bff11..a12aaa8c9 100644 --- a/rust/gateway/src/messages.rs +++ b/rust/gateway/src/messages.rs @@ -1,11 +1,14 @@ -use std::net::IpAddr; - use chrono::{serde::ts_seconds, DateTime, Utc}; -use connlib_shared::messages::{ - ActorId, ClientId, Interface, Peer, Relay, ResourceDescription, ResourceId, +use connlib_shared::{ + messages::{ + ActorId, ClientId, ClientPayload, GatewayResponse, Interface, Peer, Relay, + ResourceDescription, ResourceId, + }, + Dname, }; use serde::{Deserialize, Serialize}; -use webrtc::ice_transport::{ice_candidate::RTCIceCandidate, ice_parameters::RTCIceParameters}; +use std::net::IpAddr; +use webrtc::ice_transport::ice_candidate::RTCIceCandidate; // TODO: Should this have a resource? #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] @@ -23,7 +26,7 @@ pub struct Actor { #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Client { pub id: ClientId, - pub rtc_session_description: RTCIceParameters, + pub payload: ClientPayload, pub peer: Peer, } @@ -79,6 +82,9 @@ pub struct AllowAccess { pub resource: ResourceDescription, #[serde(with = "ts_seconds")] pub expires_at: DateTime, + pub payload: Option, + #[serde(rename = "ref")] + pub reference: String, } // These messages are the messages that can be received @@ -127,7 +133,7 @@ pub enum EgressMessages { pub struct ConnectionReady { #[serde(rename = "ref")] pub reference: String, - pub gateway_rtc_session_description: RTCIceParameters, + pub gateway_payload: GatewayResponse, } #[cfg(test)] @@ -153,10 +159,12 @@ mod test { "persistent_keepalive": 25, "preshared_key": "sMeTuiJ3mezfpVdan948CmisIWbwBZ1z7jBNnbVtfVg=" }, - "rtc_session_description": { - "ice_lite":false, - "password": "xEwoXEzHuSyrcgOCSRnwOXQVnbnbeGeF", - "username_fragment": "PvCPFevCOgkvVCtH" + "payload": { + "ice_parameters": { + "ice_lite":false, + "password": "xEwoXEzHuSyrcgOCSRnwOXQVnbnbeGeF", + "username_fragment": "PvCPFevCOgkvVCtH" + } } }, "resource": { diff --git a/rust/windows-client/src-tauri/src/debug_commands.rs b/rust/windows-client/src-tauri/src/debug_commands.rs index 16f99805a..ff69eeef8 100644 --- a/rust/windows-client/src-tauri/src/debug_commands.rs +++ b/rust/windows-client/src-tauri/src/debug_commands.rs @@ -112,7 +112,7 @@ pub fn device_id() -> Result<()> { pub use details::wintun; -#[cfg(target_os = "linux")] +#[cfg(target_family = "unix")] mod details { use super::*; diff --git a/rust/windows-client/src-tauri/src/main.rs b/rust/windows-client/src-tauri/src/main.rs index f75f3db7e..3ea0c9ce0 100755 --- a/rust/windows-client/src-tauri/src/main.rs +++ b/rust/windows-client/src-tauri/src/main.rs @@ -10,7 +10,7 @@ use cli::CliCommands as Cmd; mod cli; mod debug_commands; mod device_id; -#[cfg(target_os = "linux")] +#[cfg(target_family = "unix")] mod gui { use super::*;