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:
Gabi
2023-12-08 00:16:42 -05:00
committed by GitHub
parent 6ab445555a
commit 8e34457340
70 changed files with 2333 additions and 1568 deletions

View File

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

View File

@@ -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"}
```

View File

@@ -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}}
)

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

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

View File

@@ -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,

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("")

View File

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

View File

@@ -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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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} <-

View File

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

View File

@@ -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", %{

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
View File

@@ -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",
]

View File

@@ -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"}

View File

@@ -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);
}
}
}
}

View File

@@ -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(),
}),
],

View File

@@ -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"

View File

@@ -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,

View File

@@ -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";

View File

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

View File

@@ -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]

View File

@@ -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
}
}

View File

@@ -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({

View File

@@ -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:#?}");
}
}
}

View File

@@ -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,

View File

@@ -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?;

View File

@@ -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 {},

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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>;
}

View File

@@ -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)
}
}
}
}

View File

@@ -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];

View File

@@ -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()
}
}

View File

@@ -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"] }

View File

@@ -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 {

View File

@@ -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": {

View File

@@ -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::*;

View File

@@ -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::*;