connlib: add client dns interception support (#1807)

This commit is contained in:
Gabi
2023-07-24 18:41:42 -03:00
committed by GitHub
parent 52898a69af
commit 7ad2fb623a
11 changed files with 450 additions and 16 deletions

View File

@@ -118,7 +118,7 @@ services:
client:
environment:
FZ_URL: "ws://api:8081/"
FZ_SECRET: "SFMyNTY.g2gDaANkAAhpZGVudGl0eW0AAAAkN2RhN2QxY2QtMTExYy00NGE3LWI1YWMtNDAyN2I5ZDIzMGU1bQAAACAZ_F7tY7RZcWcaeGbwM9H9EBDdj2U4QPu2sBzD8Z_7R24GAMH8mfqIAWIB4TOA.2IZ089fjvNLoCsirq2PwNTfMHXf3285F6YcNquk6niU"
FZ_SECRET: "SFMyNTY.g2gDaAN3CGlkZW50aXR5bQAAACQ3ZGE3ZDFjZC0xMTFjLTQ0YTctYjVhYy00MDI3YjlkMjMwZTVtAAAAIBn8Xu1jtFlxZxp4ZvAz0f0QEN2PZThA-7awHMPxn_tHbgYAbLRvQokBYgHhM38.pM-prhb7uvvCVKf51-tAUMEtMzLPZk1n3nLsY44dGFA"
RUST_LOG: headless=trace,firezone_client_connlib=trace,firezone_tunnel=trace,libs_common=trace,warn
build:
context: rust
@@ -126,6 +126,9 @@ services:
args:
PACKAGE: headless
image: firezone-headless
dns:
- 100.100.111.1
- 8.8.8.8
cap_add:
- NET_ADMIN
sysctls:
@@ -142,7 +145,7 @@ services:
gateway:
environment:
FZ_URL: "ws://api:8081/"
FZ_SECRET: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAFvAb_mIAWIAAVGA.1DaY3H3fKzW5sqcciJqlHyG0uFctzOewofsVRGS7NrQ"
FZ_SECRET: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAEC0b0KJAWIAAVGA.9Oirn9t8rvQpfOhW7hwGBFVzeMm9di0xYGTlwf9cFFk"
RUST_LOG: gateway=trace,firezone_gateway_connlib=trace,firezone_tunnel=trace,libs_common=trace,warn
ENABLE_MASQUERADE: 1
build:
@@ -178,7 +181,7 @@ services:
PUBLIC_IP4_ADDR: 172.28.0.101
LISTEN_IP4_ADDR: 172.28.0.101
PORTAL_WS_URL: "ws://api:8081/"
PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAJZ5vfiIAWIAAVGA.F1J6PxmFwmlSYtsUnkw2Z7IjpMkB1oS7wxtzQBqlFFM"
PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAFSzb0KJAWIAAVGA.waeGE26tbgkgIcMrWyck0ysv9SHIoHr0zqoM3wao84M"
RUST_LOG: "debug"
RUST_BACKTRACE: 1
build:

View File

@@ -208,7 +208,7 @@ IO.puts("")
Resources.create_resource(
%{
type: :dns,
address: "gitlab.mycorp.com",
address: "google.com",
connections: [%{gateway_group_id: gateway_group.id}]
},
admin_subject

67
rust/Cargo.lock generated
View File

@@ -986,6 +986,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "domain"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c9fdb317c54875188fea3e4ae483d4b3cf71b131888e9b7665fd6b020fb327"
dependencies = [
"octseq",
"rand",
"time 0.3.22",
]
[[package]]
name = "ecdsa"
version = "0.14.8"
@@ -1124,6 +1135,7 @@ dependencies = [
"boringtun",
"bytes",
"chrono",
"domain",
"futures",
"futures-util",
"ip_network",
@@ -1135,6 +1147,7 @@ dependencies = [
"netlink-packet-core",
"netlink-packet-route",
"parking_lot",
"pnet_packet",
"rand_core 0.6.4",
"rtnetlink",
"serde",
@@ -1874,6 +1887,12 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "no-std-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1944,6 +1963,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "octseq"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190e3482f38446ee3f3ab50b049a16b072b6111cba008381b816598478ba65d"
[[package]]
name = "oid-registry"
version = "0.4.0"
@@ -2115,6 +2140,48 @@ version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630"
[[package]]
name = "pnet_base"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e"
dependencies = [
"no-std-net",
]
[[package]]
name = "pnet_macros"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
]
[[package]]
name = "pnet_macros_support"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc"
dependencies = [
"pnet_base",
]
[[package]]
name = "pnet_packet"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706"
dependencies = [
"glob",
"pnet_base",
"pnet_macros",
"pnet_macros_support",
]
[[package]]
name = "poly1305"
version = "0.8.0"

View File

@@ -19,7 +19,7 @@ ENV RUST_BACKTRACE=1
ENV PATH "/app:$PATH"
ENV PACKAGE_NAME ${PACKAGE}
RUN apt-get update -y \
&& apt-get install -y iputils-ping iptables lsof \
&& apt-get install -y iputils-ping iptables lsof iproute2 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Some black magics here:

View File

@@ -90,6 +90,19 @@ impl ResourceDescription {
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::Cidr(_) => None,
}
}
pub fn id(&self) -> Id {
match self {

View File

@@ -20,8 +20,10 @@ libs-common = { path = "../common" }
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.8"
boringtun = { workspace = true }
chrono = { workspace = true }
pnet_packet = { version = "0.33" }
# TODO: research replacing for https://github.com/algesten/str0m
webrtc = { version = "0.8" }

View File

@@ -0,0 +1,120 @@
use std::{net::IpAddr, sync::Arc};
use crate::{
ip_packet::{to_dns, IpPacket, MutableIpPacket},
ControlSignal, Tunnel,
};
use domain::base::{
iana::{Class, Rcode, Rtype},
Message, MessageBuilder, ToDname,
};
use libs_common::{messages::ResourceDescription, Callbacks, DNS_SENTINEL};
use pnet_packet::{udp::MutableUdpPacket, MutablePacket, Packet as UdpPacket, PacketSize};
const DNS_TTL: u32 = 300;
const UDP_HEADER_SIZE: usize = 8;
// We don't need to support multiple questions/qname in a single query because
// nobody does it and since this run with each packet we want to squeeze as much optimization
// as we can therefore we won't do it.
//
// See: https://stackoverflow.com/a/55093896
impl<C, CB> Tunnel<C, CB>
where
C: ControlSignal + Send + Sync + 'static,
CB: Callbacks + 'static,
{
fn build_response(
self: &Arc<Self>,
original_buf: &[u8],
mut dns_answer: Vec<u8>,
) -> Option<Vec<u8>> {
let response_len = dns_answer.len();
let original_pkt = IpPacket::new(original_buf)?;
let original_dgm = original_pkt.as_udp()?;
let hdr_len = original_pkt.packet_size() - original_dgm.payload().len();
let mut res_buf = Vec::with_capacity(hdr_len + response_len);
res_buf.extend_from_slice(&original_buf[..hdr_len]);
res_buf.append(&mut dns_answer);
let mut pkt = MutableIpPacket::new(&mut res_buf)?;
let dgm_len = UDP_HEADER_SIZE + response_len;
pkt.set_len(hdr_len + response_len, dgm_len);
pkt.swap_src_dst();
let mut dgm = MutableUdpPacket::new(pkt.payload_mut())?;
dgm.set_length(dgm_len as u16);
dgm.set_source(original_dgm.get_destination());
dgm.set_destination(original_dgm.get_source());
let mut pkt = MutableIpPacket::new(&mut res_buf)?;
let udp_checksum = pkt.to_immutable().udp_checksum(&pkt.as_immutable_udp()?);
pkt.as_udp()?.set_checksum(udp_checksum);
pkt.set_checksum();
Some(res_buf)
}
fn build_dns_with_answer<N>(
self: &Arc<Self>,
message: &Message<[u8]>,
qname: &N,
qtype: Rtype,
resource: &ResourceDescription,
) -> Option<Vec<u8>>
where
N: ToDname + ?Sized,
{
let msg_buf = Vec::with_capacity(message.as_slice().len() * 2);
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 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()?,
_ => todo!(),
}
Some(answer_builder.finish())
}
pub(crate) fn check_for_dns(self: &Arc<Self>, buf: &[u8]) -> Option<Vec<u8>> {
let packet = IpPacket::new(buf)?;
if packet.destination() != IpAddr::from(DNS_SENTINEL) {
return None;
}
let datagram = packet.as_udp()?;
let message = to_dns(&datagram)?;
let question = message.first_question()?;
if matches!(question.qtype(), Rtype::A | Rtype::Aaaa) && !message.header().qr() {
if let Some(resource) = self
.resources
.read()
.get_by_name(&ToDname::to_cow(question.qname()).to_string())
{
let response = self.build_dns_with_answer(
message,
question.qname(),
question.qtype(),
resource,
)?;
return self.build_response(buf, response);
}
}
None
}
}

View File

@@ -0,0 +1,217 @@
use std::net::IpAddr;
use domain::base::message::Message;
use pnet_packet::{
ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
ipv4::{checksum, Ipv4Packet, MutableIpv4Packet},
ipv6::{Ipv6Packet, MutableIpv6Packet},
udp::{ipv4_checksum, ipv6_checksum, MutableUdpPacket, UdpPacket},
MutablePacket, Packet, PacketSize,
};
const DNS_PORT: u16 = 53;
#[derive(Debug, PartialEq)]
pub(crate) enum MutableIpPacket<'a> {
MutableIpv4Packet(MutableIpv4Packet<'a>),
MutableIpv6Packet(MutableIpv6Packet<'a>),
}
// no std::mem:;swap? no problem
macro_rules! swap_src_dst {
($p:expr) => {
let src = $p.get_source();
let dst = $p.get_destination();
$p.set_source(dst);
$p.set_destination(src);
};
}
impl<'a> MutableIpPacket<'a> {
pub(crate) fn new(data: &mut [u8]) -> Option<MutableIpPacket> {
match data[0] >> 4 {
4 => MutableIpv4Packet::new(data).map(Into::into),
6 => MutableIpv6Packet::new(data).map(Into::into),
_ => None,
}
}
pub(crate) fn set_checksum(&mut self) {
if let Self::MutableIpv4Packet(p) = self {
p.set_checksum(checksum(&p.to_immutable()));
}
}
pub(crate) fn to_immutable(&self) -> IpPacket {
match self {
Self::MutableIpv4Packet(p) => p.to_immutable().into(),
Self::MutableIpv6Packet(p) => p.to_immutable().into(),
}
}
pub(crate) fn as_udp(&mut self) -> Option<MutableUdpPacket> {
self.to_immutable()
.is_udp()
.then(|| MutableUdpPacket::new(self.payload_mut()))
.flatten()
}
pub(crate) fn as_immutable_udp(&self) -> Option<UdpPacket> {
self.to_immutable()
.is_udp()
.then(|| UdpPacket::new(self.payload()))
.flatten()
}
pub(crate) fn swap_src_dst(&mut self) {
match self {
Self::MutableIpv4Packet(p) => {
swap_src_dst!(p);
}
Self::MutableIpv6Packet(p) => {
swap_src_dst!(p);
}
}
}
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),
Self::MutableIpv6Packet(p) => p.set_payload_length(payload_len as u16),
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum IpPacket<'a> {
Ipv4Packet(Ipv4Packet<'a>),
Ipv6Packet(Ipv6Packet<'a>),
}
impl<'a> IpPacket<'a> {
pub(crate) fn new(data: &[u8]) -> Option<IpPacket> {
match data[0] >> 4 {
4 => Ipv4Packet::new(data).map(Into::into),
6 => Ipv6Packet::new(data).map(Into::into),
_ => None,
}
}
pub(crate) fn next_header(&self) -> IpNextHeaderProtocol {
match self {
Self::Ipv4Packet(p) => p.get_next_level_protocol(),
Self::Ipv6Packet(p) => p.get_next_header(),
}
}
fn is_udp(&self) -> bool {
self.next_header() == IpNextHeaderProtocols::Udp
}
pub(crate) fn as_udp(&self) -> Option<UdpPacket> {
self.is_udp()
.then(|| UdpPacket::new(self.payload()))
.flatten()
}
pub(crate) fn destination(&self) -> IpAddr {
match self {
Self::Ipv4Packet(p) => p.get_destination().into(),
Self::Ipv6Packet(p) => p.get_destination().into(),
}
}
pub(crate) fn udp_checksum(&self, dgm: &UdpPacket<'_>) -> u16 {
match self {
Self::Ipv4Packet(p) => ipv4_checksum(dgm, &p.get_source(), &p.get_destination()),
Self::Ipv6Packet(p) => ipv6_checksum(dgm, &p.get_source(), &p.get_destination()),
}
}
}
pub(crate) fn to_dns<'a>(pkt: &'a UdpPacket<'a>) -> Option<&'a Message<[u8]>> {
(pkt.get_destination() == DNS_PORT)
.then(|| Message::from_slice(pkt.payload()).ok())
.flatten()
}
impl<'a> Packet for IpPacket<'a> {
fn packet(&self) -> &[u8] {
match self {
Self::Ipv4Packet(p) => p.packet(),
Self::Ipv6Packet(p) => p.packet(),
}
}
fn payload(&self) -> &[u8] {
match self {
Self::Ipv4Packet(p) => p.payload(),
Self::Ipv6Packet(p) => p.payload(),
}
}
}
impl<'a> PacketSize for IpPacket<'a> {
fn packet_size(&self) -> usize {
match self {
Self::Ipv4Packet(p) => p.packet_size(),
Self::Ipv6Packet(p) => p.packet_size(),
}
}
}
impl<'a> Packet for MutableIpPacket<'a> {
fn packet(&self) -> &[u8] {
match self {
Self::MutableIpv4Packet(p) => p.packet(),
Self::MutableIpv6Packet(p) => p.packet(),
}
}
fn payload(&self) -> &[u8] {
match self {
Self::MutableIpv4Packet(p) => p.payload(),
Self::MutableIpv6Packet(p) => p.payload(),
}
}
}
impl<'a> MutablePacket for MutableIpPacket<'a> {
fn packet_mut(&mut self) -> &mut [u8] {
match self {
Self::MutableIpv4Packet(p) => p.packet_mut(),
Self::MutableIpv6Packet(p) => p.packet_mut(),
}
}
fn payload_mut(&mut self) -> &mut [u8] {
match self {
Self::MutableIpv4Packet(p) => p.payload_mut(),
Self::MutableIpv6Packet(p) => p.payload_mut(),
}
}
}
impl<'a> From<Ipv4Packet<'a>> for IpPacket<'a> {
fn from(pkt: Ipv4Packet<'a>) -> Self {
Self::Ipv4Packet(pkt)
}
}
impl<'a> From<Ipv6Packet<'a>> for IpPacket<'a> {
fn from(pkt: Ipv6Packet<'a>) -> Self {
Self::Ipv6Packet(pkt)
}
}
impl<'a> From<MutableIpv4Packet<'a>> for MutableIpPacket<'a> {
fn from(pkt: MutableIpv4Packet<'a>) -> Self {
Self::MutableIpv4Packet(pkt)
}
}
impl<'a> From<MutableIpv6Packet<'a>> for MutableIpPacket<'a> {
fn from(pkt: MutableIpv6Packet<'a>) -> Self {
Self::MutableIpv6Packet(pkt)
}
}

View File

@@ -11,7 +11,7 @@ use boringtun::{
};
use ip_network::IpNetwork;
use ip_network_table::IpNetworkTable;
use libs_common::Callbacks;
use libs_common::{Callbacks, DNS_SENTINEL};
use async_trait::async_trait;
use bytes::Bytes;
@@ -49,7 +49,9 @@ pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription
use index::{check_packet_index, IndexLfsr};
mod control_protocol;
mod dns;
mod index;
mod ip_packet;
mod peer;
mod resource_table;
@@ -241,6 +243,9 @@ where
.up()
.await
.expect("Couldn't initiate interface");
iface_config
.add_route(&DNS_SENTINEL.into(), self.callbacks())
.await?;
}
self.start_timers();
@@ -479,6 +484,14 @@ where
}
};
tracing::trace!("Reading from iface {res} bytes");
if let Some(r) = dev.check_for_dns(&src[..res]) {
// TODO(ipv4/ipv6)!
dev.write4_device_infallible(&r[..]).await;
continue;
}
let dst_addr = match Tunn::dst_address(&src[..res]) {
Some(addr) => addr,
None => continue,

View File

@@ -52,6 +52,14 @@ impl ResourceTable {
self.id_table.get(id)
}
/// Gets the resource by name
pub fn get_by_name(&self, name: impl AsRef<str>) -> Option<&ResourceDescription> {
// SAFETY: if we found the pointer, due to our internal consistency rules it is in the id_table
self.dns_name
.get(name.as_ref())
.map(|m| unsafe { m.as_ref() })
}
// SAFETY: resource_description must still be in storage since we are going to reference it.
unsafe fn remove_resource(&mut self, resource_description: NonNull<ResourceDescription>) {
let id = {

View File

@@ -222,6 +222,7 @@ impl IfaceConfig {
Ok(())
}
#[tracing::instrument(level = "trace", skip(self, _callbacks))]
pub async fn set_iface_config(
&mut self,
@@ -254,16 +255,6 @@ impl IfaceConfig {
.execute()
.await?;
//TODO!
/*
let name: String = self.name.clone().try_into()?;
for dns in &config.dns {
//resolvconf::set_dns(&name, dns).await?;
}
*/
//nftables::enable_masquerade((config.ipv4_masquerade, config.ipv6_masquerade)).await?;
Ok(())
}