Skip to content

Commit

Permalink
Get DNS server(s) from FortiVPN and send to IKEv2.
Browse files Browse the repository at this point in the history
macOS refuses to use IKEv2 connections if there's no DNS server
available, and the traffic selector is large.
  • Loading branch information
zlogic committed Sep 4, 2024
1 parent f2fc023 commit dc17ded
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 38 deletions.
81 changes: 58 additions & 23 deletions src/fortivpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,14 @@ pub async fn get_oauth_cookie(config: &Config) -> Result<String, FortiError> {
Ok(cookie)
}

struct IpConfig {
addr: IpAddr,
dns: Vec<IpAddr>,
}

pub struct FortiVPNTunnel {
socket: BufTlsStream,
addr: IpAddr,
ip_config: IpConfig,
mtu: u16,
ppp_state: PPPState,
ppp_magic: u32,
Expand All @@ -151,16 +156,16 @@ impl FortiVPNTunnel {
let mut socket =
Self::connect(&config.destination_addr, domain, config.tls_config.clone()).await?;
debug!("Connected to VPN host");
let addr = Self::request_vpn_allocation(domain, &mut socket, &cookie).await?;
let ip_config = Self::request_vpn_allocation(domain, &mut socket, &cookie).await?;
Self::start_vpn_tunnel(domain, &mut socket, &cookie).await?;

let mtu = config.mtu;
let mut ppp_state = PPPState::new();
let ppp_magic = Self::start_ppp(&mut socket, &mut ppp_state, mtu).await?;
Self::start_ipcp(&mut socket, &mut ppp_state, addr).await?;
Self::start_ipcp(&mut socket, &mut ppp_state, ip_config.addr).await?;
Ok(FortiVPNTunnel {
socket,
addr,
ip_config,
mtu,
ppp_state,
ppp_magic,
Expand All @@ -170,7 +175,11 @@ impl FortiVPNTunnel {
}

pub fn ip_addr(&self) -> IpAddr {
self.addr
self.ip_config.addr
}

pub fn dns(&self) -> &[IpAddr] {
&self.ip_config.dns
}

pub fn mtu(&self) -> u16 {
Expand All @@ -194,7 +203,7 @@ impl FortiVPNTunnel {
domain: &str,
socket: &mut BufTlsStream,
cookie: &str,
) -> Result<IpAddr, FortiError> {
) -> Result<IpConfig, FortiError> {
let req = http::build_request("GET /remote/fortisslvpn_xml", domain, Some(cookie), 0);
socket.write_all(req.as_bytes()).await?;
socket.flush().await?;
Expand All @@ -203,24 +212,50 @@ impl FortiVPNTunnel {
http::validate_response_code(&headers)?;
let content = http::read_content(socket, headers.as_str()).await?;

const IPV4_ADDRESS_PREFIX: &str = "<assigned-addr ipv4='";
let ipv4_addr_start = if let Some(start) = content.find(IPV4_ADDRESS_PREFIX) {
start
} else {
debug!("Unsupported config format: {}", content);
return Err("Cannot find IPv4 address in config".into());
};
let content = &content[ipv4_addr_start + IPV4_ADDRESS_PREFIX.len()..];
let ipv4_addr_end = if let Some(start) = content.find("'") {
start
} else {
debug!("Unsupported config format: {}", content);
return Err("Cannot find IPv4 address in config".into());
let addr = {
const IPV4_ADDRESS_PREFIX: &str = "<assigned-addr ipv4='";
let ipv4_addr_start = if let Some(start) = content.find(IPV4_ADDRESS_PREFIX) {
start
} else {
debug!("Unsupported config format: {}", content);
return Err("Cannot find IPv4 address in config".into());
};
let content = &content[ipv4_addr_start + IPV4_ADDRESS_PREFIX.len()..];
let ipv4_addr_end = if let Some(start) = content.find("'") {
start
} else {
debug!("Unsupported config format: {}", content);
return Err("Cannot find IPv4 address in config".into());
};
IpAddr::from_str(&content[..ipv4_addr_end]).map_err(|err| {
debug!("Failed to parse IPv4 address: {}", err);
"Failed to parse IPv4 address"
})?
};
Ok(IpAddr::from_str(&content[..ipv4_addr_end]).map_err(|err| {
debug!("Failed to parse IPv4 address: {}", err);
"Failed to parse IPv4 address"
})?)

const DNS_PREFIX: &str = "<dns ip='";
let mut dns = vec![];
let mut content = content.as_str();
loop {
let dns_start = if let Some(start) = content.find(DNS_PREFIX) {
start
} else {
break;
};
content = &content[dns_start + DNS_PREFIX.len()..];
let dns_end = if let Some(start) = content.find("'") {
start
} else {
debug!("Unsupported config format: {}", content);
return Err("Cannot find DNS address in config".into());
};
let dns_addr = IpAddr::from_str(&content[..dns_end]).map_err(|err| {
debug!("Failed to parse DNS address: {}", err);
"Failed to parse DNS address"
})?;
dns.push(dns_addr);
}
Ok(IpConfig { addr, dns })
}

async fn start_vpn_tunnel(
Expand Down
41 changes: 38 additions & 3 deletions src/ikev2/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,17 +578,29 @@ impl MessageWriter<'_> {
Ok(())
}

pub fn write_configuration_payload(&mut self, addr: IpAddr) -> Result<(), NotEnoughSpaceError> {
pub fn write_configuration_payload(
&mut self,
addr: IpAddr,
dns: &[IpAddr],
) -> Result<(), NotEnoughSpaceError> {
let addr_length = match addr {
IpAddr::V4(_) => (4 + 4) * 2,
IpAddr::V6(_) => 4 + 17,
};
let dns_length = dns
.iter()
.map(|dns| match dns {
IpAddr::V4(_) => 4 + 4,
IpAddr::V6(_) => 4 + 16,
})
.sum::<usize>();

let next_payload_slice =
self.next_payload_slice(PayloadType::CONFIGURATION, 4 + addr_length)?;
self.next_payload_slice(PayloadType::CONFIGURATION, 4 + addr_length + dns_length)?;
next_payload_slice[0] = ConfigurationType::CFG_REPLY.0;

// Write IP address.
let data = &mut next_payload_slice[4..4 + addr_length];
let mut data = &mut next_payload_slice[4..];
match addr {
IpAddr::V4(addr) => {
data[0..2].copy_from_slice(
Expand All @@ -606,6 +618,7 @@ impl MessageWriter<'_> {
);
data[10..12].copy_from_slice(&4u16.to_be_bytes());
data[12..16].fill_with(|| 255);
data = &mut data[16..];
}
IpAddr::V6(addr) => {
data[0..2].copy_from_slice(
Expand All @@ -617,8 +630,30 @@ impl MessageWriter<'_> {
data[4..20].copy_from_slice(&addr.octets());
// Assume only current address can be reached.
data[20] = 128;
data = &mut data[21..];
}
};
// Write all DNS servers.
for addr in dns {
match addr {
IpAddr::V4(addr) => {
data[0..2].copy_from_slice(
&ConfigurationAttributeType::INTERNAL_IP4_DNS.0.to_be_bytes(),
);
data[2..4].copy_from_slice(&4u16.to_be_bytes());
data[4..8].copy_from_slice(&addr.octets());
data = &mut data[8..];
}
IpAddr::V6(addr) => {
data[0..2].copy_from_slice(
&ConfigurationAttributeType::INTERNAL_IP6_DNS.0.to_be_bytes(),
);
data[2..4].copy_from_slice(&16u16.to_be_bytes());
data[4..20].copy_from_slice(&addr.octets());
data = &mut data[20..];
}
}
}

Ok(())
}
Expand Down
22 changes: 12 additions & 10 deletions src/ikev2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,11 @@ impl Sessions {
} else {
session::SessionID::from_message(&ikev2_request)?
};
let client_ip = if ikev2_request.read_exchange_type()? == message::ExchangeType::IKE_AUTH
let ip_configuration = if ikev2_request.read_exchange_type()?
== message::ExchangeType::IKE_AUTH
&& !ikev2_request.read_flags()?.has(message::Flags::RESPONSE)
{
self.vpn_service.client_ip().await?
self.vpn_service.ip_configuration().await?
} else {
None
};
Expand All @@ -569,8 +570,8 @@ impl Sessions {
} else {
return Err("Session not found".into());
};
if let Some(client_ip) = client_ip {
session.update_internal_addr(client_ip);
if let Some((client_ip, dns_addrs)) = ip_configuration {
session.update_ip(client_ip, dns_addrs);
}

let mut response_bytes = [0u8; MAX_DATAGRAM_SIZE];
Expand Down Expand Up @@ -888,7 +889,7 @@ impl FortiService {
debug!("Received packet from closed FortiClient channel");
}
FortiServiceCommand::SendEcho => {}
FortiServiceCommand::RequestIp(tx) => {
FortiServiceCommand::RequestIpConfiguration(tx) => {
debug!("Received IP request for closed FortiClient channel");
let _ = tx.send(None);
}
Expand Down Expand Up @@ -955,8 +956,9 @@ impl FortiService {
}
last_echo_sent = next_echo_sent;
}
FortiServiceCommand::RequestIp(tx) => {
let _ = tx.send(Some(forti_client.ip_addr()));
FortiServiceCommand::RequestIpConfiguration(tx) => {
let _ =
tx.send(Some((forti_client.ip_addr(), forti_client.dns().to_vec())));
}
FortiServiceCommand::Shutdown => {
forti_client.terminate().await?;
Expand Down Expand Up @@ -1014,11 +1016,11 @@ impl FortiService {
}
}

async fn client_ip(&self) -> Result<Option<IpAddr>, IKEv2Error> {
async fn ip_configuration(&self) -> Result<Option<(IpAddr, Vec<IpAddr>)>, IKEv2Error> {
if let Some(command_sender) = self.command_sender.as_ref() {
let (tx, rx) = oneshot::channel();
command_sender
.send(FortiServiceCommand::RequestIp(tx))
.send(FortiServiceCommand::RequestIpConfiguration(tx))
.await
.map_err(|_| "VPN client command channel closed")?;
Ok(rx.await.map_err(|_| "IP address receiver closed")?)
Expand Down Expand Up @@ -1052,7 +1054,7 @@ impl FortiService {

enum FortiServiceCommand {
HandleConnection(Result<fortivpn::FortiVPNTunnel, fortivpn::FortiError>),
RequestIp(oneshot::Sender<Option<IpAddr>>),
RequestIpConfiguration(oneshot::Sender<Option<(IpAddr, Vec<IpAddr>)>>),
SendPacket(Vec<u8>),
ReceivePacket,
SendEcho,
Expand Down
8 changes: 6 additions & 2 deletions src/ikev2/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub struct IKEv2Session {
local_addr: SocketAddr,
state: SessionState,
internal_addr: Option<IpAddr>,
dns_addrs: Vec<IpAddr>,
ts_local: Vec<message::TrafficSelector>,
child_sas: HashSet<ChildSessionID>,
pki_processing: Arc<pki::PkiProcessing>,
Expand Down Expand Up @@ -144,6 +145,7 @@ impl IKEv2Session {
local_addr,
state: SessionState::Empty,
internal_addr: None,
dns_addrs: vec![],
ts_local: ts_local.to_vec(),
child_sas: HashSet::new(),
pki_processing,
Expand Down Expand Up @@ -188,8 +190,9 @@ impl IKEv2Session {
NextRetransmission::Delay(time::Duration::from_millis(next_delay))
}

pub fn update_internal_addr(&mut self, internal_addr: IpAddr) {
pub fn update_ip(&mut self, internal_addr: IpAddr, dns_addrs: Vec<IpAddr>) {
self.internal_addr = Some(internal_addr);
self.dns_addrs = dns_addrs;
}

pub fn process_request(
Expand Down Expand Up @@ -951,7 +954,7 @@ impl IKEv2Session {

if ipv4_address_requested {
if let Some(internal_addr) = self.internal_addr {
response.write_configuration_payload(internal_addr)?;
response.write_configuration_payload(internal_addr, &self.dns_addrs)?;
} else {
warn!("No IP address is available, notifying client");
response.write_notify_payload(
Expand Down Expand Up @@ -1463,6 +1466,7 @@ impl IKEv2Session {
session_id,
remote_addr: self.remote_addr,
local_addr: self.local_addr,
dns_addrs: self.dns_addrs.clone(),
state: SessionState::Established,
internal_addr: self.internal_addr,
ts_local: self.ts_local.clone(),
Expand Down

0 comments on commit dc17ded

Please sign in to comment.