mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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 <andrew@dryga.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -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}}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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} <-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label for="resource_type" class="block mb-2 text-sm font-medium text-neutral-900">
|
||||
Type
|
||||
</label>
|
||||
<div class="flex text-sm leading-6 text-zinc-600">
|
||||
<div class="flex items-center me-4">
|
||||
<.input
|
||||
id="resource-type--dns"
|
||||
type="radio"
|
||||
field={@form[:type]}
|
||||
value="dns"
|
||||
label="DNS"
|
||||
checked={@form[:type].value == :dns}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center me-4">
|
||||
<.input
|
||||
id="resource-type--ip"
|
||||
type="radio"
|
||||
field={@form[:type]}
|
||||
value="ip"
|
||||
label="IP"
|
||||
checked={@form[:type].value == :ip}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center me-4">
|
||||
<.input
|
||||
id="resource-type--cidr"
|
||||
type="radio"
|
||||
field={@form[:type]}
|
||||
value="cidr"
|
||||
label="CIDR"
|
||||
checked={@form[:type].value == :cidr}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.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", %{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
282
rust/Cargo.lock
generated
282
rust/Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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<CB: Callbacks + 'static> ControlPlane<CB> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,6 +25,8 @@ use url::Url;
|
||||
|
||||
pub const DNS_SENTINEL: Ipv4Addr = Ipv4Addr::new(100, 100, 111, 1);
|
||||
|
||||
pub type Dname = domain::base::Dname<Vec<u8>>;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const LIB_NAME: &str = "connlib";
|
||||
|
||||
|
||||
@@ -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<Dname>,
|
||||
}
|
||||
|
||||
/// 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<Dname>,
|
||||
}
|
||||
|
||||
// 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<IpAddr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ConnectionAccepted {
|
||||
pub ice_parameters: RTCIceParameters,
|
||||
pub domain_response: Option<DomainResponse>,
|
||||
}
|
||||
|
||||
#[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<IpNetwork> {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => vec![r.ipv4.into(), r.ipv6.into()],
|
||||
ResourceDescription::Cidr(r) => vec![r.address],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipv4(&self) -> Option<Ipv4Addr> {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => Some(r.ipv4),
|
||||
ResourceDescription::Cidr(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipv6(&self) -> Option<Ipv6Addr> {
|
||||
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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<CB> Tunnel<CB, ClientState>
|
||||
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<GatewayId, PublicKey>,
|
||||
pub gateway_preshared_keys: HashMap<GatewayId, StaticSecret>,
|
||||
resources_gateways: HashMap<ResourceId, GatewayId>,
|
||||
resources: ResourceTable<ResourceDescription>,
|
||||
dns_queries: BoundedQueue<DnsQuery<'static>>,
|
||||
|
||||
pub dns_resources_internal_ips: HashMap<DnsResource, Vec<IpAddr>>,
|
||||
dns_resources: HashMap<String, ResourceDescriptionDns>,
|
||||
cidr_resources: IpNetworkTable<ResourceDescriptionCidr>,
|
||||
pub resource_ids: HashMap<ResourceId, ResourceDescription>,
|
||||
pub deferred_dns_queries: HashMap<(DnsResource, Rtype), IpPacket<'static>>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub peers_by_ip: IpNetworkTable<ConnectedPeer<GatewayId, PacketTransformClient>>,
|
||||
|
||||
pub dns_strategy: DnsFallbackStrategy,
|
||||
forwarded_dns_queries: BoundedQueue<DnsQuery<'static>>,
|
||||
|
||||
pub ip_provider: IpProvider,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AwaitingConnectionDetails {
|
||||
total_attemps: usize,
|
||||
response_received: bool,
|
||||
domain: Option<Dname>,
|
||||
gateways: HashSet<GatewayId>,
|
||||
}
|
||||
|
||||
@@ -161,34 +198,59 @@ impl ClientState {
|
||||
pub(crate) fn handle_dns<'a>(
|
||||
&mut self,
|
||||
packet: MutableIpPacket<'a>,
|
||||
resolve_strategy: DnsFallbackStrategy,
|
||||
) -> Result<Option<Packet<'a>>, 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<Dname>, 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<ConnectedPeer<GatewayId>>,
|
||||
) -> Result<Option<ReuseConnection>, 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<Dname>,
|
||||
) -> Result<PeerConfig, ConnlibError> {
|
||||
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<ConnectedPeer<GatewayId>>,
|
||||
connected_peers: &IpNetworkTable<ConnectedPeer<GatewayId, PacketTransformClient>>,
|
||||
domain: &Option<Dname>,
|
||||
) -> 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<Dname>,
|
||||
) -> Vec<IpNetwork> {
|
||||
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<ResourceDescription> {
|
||||
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<dyn Iterator<Item = Ipv4Addr> + Send + Sync>,
|
||||
ipv6: Box<dyn Iterator<Item = Ipv6Addr> + 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<Ipv4Addr> {
|
||||
self.ipv4.next()
|
||||
}
|
||||
|
||||
pub fn next_ipv6(&mut self) -> Option<Ipv6Addr> {
|
||||
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<Self::Id> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RTCIceTransport>,
|
||||
pub ice_candidate_rx: mpsc::Receiver<RTCIceCandidate>,
|
||||
}
|
||||
@@ -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<TId: Copy>(
|
||||
peers_by_ip: &mut IpNetworkTable<ConnectedPeer<TId>>,
|
||||
fn insert_peers<TId: Copy, TTransform>(
|
||||
peers_by_ip: &mut IpNetworkTable<ConnectedPeer<TId, TTransform>>,
|
||||
ips: &Vec<IpNetwork>,
|
||||
peer: ConnectedPeer<TId>,
|
||||
peer: ConnectedPeer<TId, TTransform>,
|
||||
) {
|
||||
for ip in ips {
|
||||
peers_by_ip.insert(*ip, peer.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn start_handlers<TId>(
|
||||
fn start_handlers<TId, TTransform>(
|
||||
device: Arc<ArcSwapOption<Device>>,
|
||||
callbacks: impl Callbacks + 'static,
|
||||
peer: Arc<Peer<TId>>,
|
||||
peer: Arc<Peer<TId, TTransform>>,
|
||||
ice: Arc<RTCIceTransport>,
|
||||
peer_receiver: tokio::sync::mpsc::Receiver<Bytes>,
|
||||
) 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({
|
||||
|
||||
@@ -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<Self>,
|
||||
resource_id: ResourceId,
|
||||
gateway_id: GatewayId,
|
||||
ice: Arc<RTCIceTransport>,
|
||||
domain_response: Option<DomainResponse>,
|
||||
) -> 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<Self>,
|
||||
resource_id: ResourceId,
|
||||
rtc_ice_params: RTCIceParameters,
|
||||
domain_response: Option<DomainResponse>,
|
||||
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<Self>,
|
||||
resource_id: &ResourceId,
|
||||
domain_response: &DomainResponse,
|
||||
peer: &Peer<GatewayId, PacketTransformClient>,
|
||||
) -> Result<Vec<IpNetwork>> {
|
||||
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<IpNetwork> = 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<Self>,
|
||||
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:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Self>,
|
||||
remote_params: RTCIceParameters,
|
||||
client_payload: ClientPayload,
|
||||
peer: PeerConfig,
|
||||
relays: Vec<Relay>,
|
||||
client_id: ClientId,
|
||||
expires_at: DateTime<Utc>,
|
||||
resource: ResourceDescription,
|
||||
) -> Result<RTCIceParameters> {
|
||||
) -> Result<ConnectionAccepted> {
|
||||
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<Utc>,
|
||||
) {
|
||||
domain: Option<Dname>,
|
||||
) -> Option<ResourceAccepted> {
|
||||
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<Utc>,
|
||||
ice: Arc<RTCIceTransport>,
|
||||
resource_addresses: Vec<IpNetwork>,
|
||||
) -> 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,
|
||||
|
||||
@@ -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<Error = Error>,
|
||||
fallback_strategy: DnsFallbackStrategy,
|
||||
) -> Result<Device> {
|
||||
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?;
|
||||
|
||||
@@ -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<Device> {
|
||||
pub(crate) async fn create_iface(
|
||||
_: &Interface,
|
||||
_: &impl Callbacks,
|
||||
_: DnsFallbackStrategy,
|
||||
) -> Result<Device> {
|
||||
Ok(Device {
|
||||
config: IfaceConfig {},
|
||||
io: DeviceIo {},
|
||||
|
||||
@@ -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<Error = Error>,
|
||||
fallback_strategy: DnsFallbackStrategy,
|
||||
) -> Result<(Self, Arc<AsyncFd<IfaceStream>>)> {
|
||||
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 {
|
||||
|
||||
@@ -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<Error = Error>,
|
||||
_: DnsFallbackStrategy,
|
||||
) -> Result<(Self, Arc<AsyncFd<IfaceStream>>)> {
|
||||
let mut info = ctl_info {
|
||||
ctl_id: 0,
|
||||
|
||||
@@ -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<AsyncFd<IfaceStream>>)> {
|
||||
debug_assert!(IFACE_NAME.as_bytes().len() < IFNAMSIZ);
|
||||
|
||||
|
||||
@@ -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<T, U> {
|
||||
pub(crate) enum ResolveStrategy<T, U, V> {
|
||||
LocalResponse(T),
|
||||
ForwardQuery(U),
|
||||
DeferredResponse(V),
|
||||
}
|
||||
|
||||
struct DnsQueryParams {
|
||||
@@ -42,8 +45,8 @@ impl DnsQueryParams {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResolveStrategy<T, DnsQueryParams> {
|
||||
fn new(name: String, record_type: Rtype) -> ResolveStrategy<T, DnsQueryParams> {
|
||||
impl<T, V> ResolveStrategy<T, DnsQueryParams, V> {
|
||||
fn forward(name: String, record_type: Rtype) -> ResolveStrategy<T, DnsQueryParams, V> {
|
||||
ResolveStrategy::ForwardQuery(DnsQueryParams {
|
||||
name,
|
||||
record_type: u16::from(record_type).into(),
|
||||
@@ -57,9 +60,11 @@ impl<T> ResolveStrategy<T, DnsQueryParams> {
|
||||
//
|
||||
// See: https://stackoverflow.com/a/55093896
|
||||
pub(crate) fn parse<'a>(
|
||||
resources: &ResourceTable<ResourceDescription>,
|
||||
dns_resources: &HashMap<String, ResourceDescriptionDns>,
|
||||
dns_resources_internal_ips: &HashMap<DnsResource, Vec<IpAddr>>,
|
||||
packet: IpPacket<'a>,
|
||||
) -> Option<ResolveStrategy<Packet<'static>, DnsQuery<'a>>> {
|
||||
resolve_strategy: DnsFallbackStrategy,
|
||||
) -> Option<ResolveStrategy<Packet<'static>, 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<Packet<'a>> {
|
||||
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<Lookup>,
|
||||
@@ -148,8 +195,7 @@ fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec<u8>) -> Option
|
||||
fn build_dns_with_answer<N>(
|
||||
message: &Message<[u8]>,
|
||||
qname: &N,
|
||||
qtype: Rtype,
|
||||
resource: &ResourceDescription,
|
||||
resource: &Option<RecordData<Dname>>,
|
||||
) -> Option<Vec<u8>>
|
||||
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::<ParsedDname<_>>::new(
|
||||
resource.dns_name()?.parse::<Dname<Vec<u8>>>().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<T> {
|
||||
A(Vec<domain::rdata::A>),
|
||||
Aaaa(Vec<domain::rdata::Aaaa>),
|
||||
Ptr(domain::rdata::Ptr<T>),
|
||||
}
|
||||
|
||||
fn resource_from_question<N: ToDname>(
|
||||
resources: &ResourceTable<ResourceDescription>,
|
||||
dns_resources: &HashMap<String, ResourceDescriptionDns>,
|
||||
dns_resources_internal_ips: &HashMap<DnsResource, Vec<IpAddr>>,
|
||||
question: &Question<N>,
|
||||
) -> Option<ResolveStrategy<ResourceDescription, DnsQueryParams>> {
|
||||
let name = ToDname::to_cow(question.qname()).to_string();
|
||||
) -> Option<ResolveStrategy<RecordData<Dname>, 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<TrustDnsMessage> {
|
||||
|
||||
@@ -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<ClientId, RTCIceCandidate>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub peers_by_ip: IpNetworkTable<ConnectedPeer<ClientId, PacketTransformGateway>>,
|
||||
}
|
||||
|
||||
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<Self::Id> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Ipv4Addr> {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => Some(v4),
|
||||
IpAddr::V6(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_v6(ip: IpAddr) -> Option<Ipv6Addr> {
|
||||
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<CB: Callbacks, TRoleState: RoleState> {
|
||||
rate_limiter: Arc<RateLimiter>,
|
||||
private_key: StaticSecret,
|
||||
public_key: PublicKey,
|
||||
#[allow(clippy::type_complexity)]
|
||||
peers_by_ip: RwLock<IpNetworkTable<ConnectedPeer<TRoleState::Id>>>,
|
||||
peer_connections: Mutex<HashMap<TRoleState::Id, Arc<RTCIceTransport>>>,
|
||||
webrtc_api: API,
|
||||
callbacks: CallbackErrorFacade<CB>,
|
||||
@@ -178,6 +233,8 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Option<Event<GatewayId>>>> {
|
||||
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<TId> {
|
||||
inner: Arc<Peer<TId>>,
|
||||
pub struct ConnectedPeer<TId, TTransform> {
|
||||
inner: Arc<Peer<TId, TTransform>>,
|
||||
channel: tokio::sync::mpsc::Sender<Bytes>,
|
||||
}
|
||||
|
||||
// TODO: For now we only use these fields with debug
|
||||
impl<TId, TTranform> Clone for ConnectedPeer<TId, TTranform> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TunnelStats<TId> {
|
||||
public_key: String,
|
||||
peers_by_ip: HashMap<IpNetwork, PeerStats<TId>>,
|
||||
peer_connections: Vec<TId>,
|
||||
}
|
||||
|
||||
@@ -288,17 +349,10 @@ where
|
||||
TRoleState: RoleState,
|
||||
{
|
||||
pub fn stats(&self) -> TunnelStats<TRoleState::Id> {
|
||||
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<Event<TRoleState::Id>> {
|
||||
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<TTransform: PacketTransform>(
|
||||
&self,
|
||||
write_buf: &mut [u8],
|
||||
packet: MutableIpPacket,
|
||||
dest: IpAddr,
|
||||
peer: &ConnectedPeer<TRoleState::Id>,
|
||||
peer: &ConnectedPeer<TRoleState::Id, TTransform>,
|
||||
) {
|
||||
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<Id>(
|
||||
peers_by_ip: &IpNetworkTable<ConnectedPeer<Id>>,
|
||||
pub(crate) fn peer_by_ip<Id, TTransform>(
|
||||
peers_by_ip: &IpNetworkTable<ConnectedPeer<Id, TTransform>>,
|
||||
ip: IpAddr,
|
||||
) -> Option<&ConnectedPeer<Id>> {
|
||||
) -> Option<&ConnectedPeer<Id, TTransform>> {
|
||||
peers_by_ip.longest_match(ip).map(|(_, peer)| peer)
|
||||
}
|
||||
|
||||
@@ -492,7 +507,6 @@ where
|
||||
pub async fn new(private_key: StaticSecret, callbacks: CB) -> Result<Self> {
|
||||
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<Event<Self::Id>>;
|
||||
fn remove_peers(&mut self, conn_id: Self::Id);
|
||||
fn refresh_peers(&mut self) -> VecDeque<Self::Id>;
|
||||
}
|
||||
|
||||
@@ -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<Utc>);
|
||||
|
||||
pub(crate) struct Peer<TId> {
|
||||
pub(crate) struct Peer<TId, TTransform> {
|
||||
tunnel: Mutex<Tunn>,
|
||||
allowed_ips: RwLock<IpNetworkTable<()>>,
|
||||
pub conn_id: TId,
|
||||
resources: Option<RwLock<ResourceTable<ExpiryingResource>>>,
|
||||
// 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<HashMap<IpAddr, ResourceId>>,
|
||||
pub transform: TTransform,
|
||||
}
|
||||
|
||||
// TODO: For now we only use these fields with debug
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PeerStats<TId> {
|
||||
pub allowed_ips: Vec<IpNetwork>,
|
||||
pub conn_id: TId,
|
||||
pub dns_resources: HashMap<String, ExpiryingResource>,
|
||||
pub network_resources: HashMap<IpNetwork, ExpiryingResource>,
|
||||
pub translated_resource_addresses: HashMap<IpAddr, ResourceId>,
|
||||
}
|
||||
|
||||
impl<TId> Peer<TId>
|
||||
impl<TId, TTransform> Peer<TId, TTransform>
|
||||
where
|
||||
TId: Copy,
|
||||
TTransform: PacketTransform,
|
||||
{
|
||||
pub(crate) fn stats(&self) -> PeerStats<TId> {
|
||||
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<Utc>)>,
|
||||
rate_limiter: Arc<RateLimiter>,
|
||||
) -> Peer<TId> {
|
||||
transform: TTransform,
|
||||
) -> Peer<TId, TTransform> {
|
||||
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<ResourceDescription> {
|
||||
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<Utc>) {
|
||||
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<Option<Bytes>> {
|
||||
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<Option<WriteTo<'a>>> {
|
||||
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<TId>,
|
||||
addr: IpAddr,
|
||||
packet: &'a mut [u8],
|
||||
) -> Result<Option<device_channel::Packet<'a>>>
|
||||
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<IpNetworkTable<ExpiryingResource>>,
|
||||
}
|
||||
|
||||
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<BiMap<IpAddr, IpAddr>>,
|
||||
}
|
||||
|
||||
impl PacketTransformClient {
|
||||
pub fn get_or_assign_translation(
|
||||
&self,
|
||||
external_ip: &IpAddr,
|
||||
ip_provider: &mut IpProvider,
|
||||
) -> Option<IpAddr> {
|
||||
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<Utc>,
|
||||
) {
|
||||
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<MutableIpPacket<'a>>;
|
||||
}
|
||||
|
||||
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<MutableIpPacket<'a>> {
|
||||
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<MutableIpPacket<'a>> {
|
||||
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<IpAddr> {
|
||||
((addr.is_ipv4() && ip.is_ipv4()) || (addr.is_ipv6() && ip.is_ipv6())).then_some(*ip)
|
||||
}
|
||||
|
||||
fn get_resource_addr_and_port<TId>(
|
||||
peer: &Peer<TId>,
|
||||
resource: &ResourceDescription,
|
||||
addr: &IpAddr,
|
||||
dst: &IpAddr,
|
||||
) -> Result<(IpAddr, Option<u16>)>
|
||||
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::<u16>)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TId>(
|
||||
pub(crate) async fn start_peer_handler<TId, TTransform>(
|
||||
device: Arc<ArcSwapOption<Device>>,
|
||||
callbacks: impl Callbacks + 'static,
|
||||
peer: Arc<Peer<TId>>,
|
||||
peer: Arc<Peer<TId, TTransform>>,
|
||||
channel: Arc<Endpoint>,
|
||||
) 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<TId>(
|
||||
tracing::debug!(peer = ?peer.stats(), "peer_stopped");
|
||||
}
|
||||
|
||||
async fn peer_handler<TId>(
|
||||
async fn peer_handler<TId, TTransform>(
|
||||
callbacks: &impl Callbacks,
|
||||
peer: &Arc<Peer<TId>>,
|
||||
peer: &Arc<Peer<TId, TTransform>>,
|
||||
channel: Arc<Endpoint>,
|
||||
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];
|
||||
|
||||
@@ -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<Utc>) {
|
||||
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<T> {
|
||||
id_table: HashMap<ResourceId, Rc<T>>,
|
||||
network_table: IpNetworkTable<Rc<T>>,
|
||||
dns_name: HashMap<String, Rc<T>>,
|
||||
}
|
||||
|
||||
// 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<T> Send for ResourceTable<T> {}
|
||||
// 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<T> Sync for ResourceTable<T> {}
|
||||
|
||||
impl<T> Default for ResourceTable<T> {
|
||||
fn default() -> ResourceTable<T> {
|
||||
ResourceTable::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResourceTable<T> {
|
||||
/// Creates a new `ResourceTable`
|
||||
pub fn new() -> ResourceTable<T> {
|
||||
ResourceTable {
|
||||
network_table: IpNetworkTable::new(),
|
||||
id_table: HashMap::new(),
|
||||
dns_name: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResourceTable<T>
|
||||
where
|
||||
T: Resource + Clone,
|
||||
{
|
||||
pub fn values(&self) -> impl Iterator<Item = &T> {
|
||||
self.id_table.values().map(AsRef::as_ref)
|
||||
}
|
||||
|
||||
pub fn network_resources(&self) -> HashMap<IpNetwork, T> {
|
||||
// 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<String, T> {
|
||||
// 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<IpAddr>) -> 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<str>) -> 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<ResourceDescription> {
|
||||
self.id_table
|
||||
.values()
|
||||
.map(|r| r.description())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<RTCIceParameters, Error>>,
|
||||
futures_bounded::FuturesMap<(ClientId, String), Result<GatewayResponse, Error>>,
|
||||
add_ice_candidate_tasks: futures_bounded::FuturesSet<Result<(), Error>>,
|
||||
|
||||
print_stats_timer: tokio::time::Interval,
|
||||
@@ -53,14 +52,14 @@ impl Eventloop {
|
||||
pub fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<Infallible>> {
|
||||
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 {
|
||||
|
||||
@@ -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<Utc>,
|
||||
pub payload: Option<Dname>,
|
||||
#[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": {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user