use crate::nat::helper::{Expectation, Helper, NatMapping, PacketHelper, PROTO_TCP};
use crate::nat::nat::Nat;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Mutex;
use std::time::{Duration, Instant};
const PPTP_PORT: u16 = 1723;
const PPTP_MAGIC_COOKIE: u32 = 0x1A2B_3C4D;
const PPTP_GRE_TIMEOUT: Duration = Duration::from_secs(120);
const PPTP_OUTGOING_CALL_REQ: u16 = 7;
const PPTP_OUTGOING_CALL_REPLY: u16 = 8;
pub(crate) const PROTO_GRE: u8 = 47;
#[derive(Debug, Clone, Copy)]
struct PptpCallInfo {
#[allow(dead_code)]
inside_call_id: u16,
#[allow(dead_code)]
outside_call_id: u16,
peer_call_id: u16,
#[allow(dead_code)]
inside_ip: Ipv4Addr,
created: Instant,
}
#[derive(Debug)]
pub struct PptpHelper {
calls: Mutex<HashMap<u16, PptpCallInfo>>,
}
impl Default for PptpHelper {
fn default() -> Self {
PptpHelper::new()
}
}
impl PptpHelper {
pub fn new() -> PptpHelper {
PptpHelper {
calls: Mutex::new(HashMap::new()),
}
}
fn cleanup_calls(calls: &mut HashMap<u16, PptpCallInfo>) {
let now = Instant::now();
calls.retain(|_, info| now.duration_since(info.created) <= PPTP_GRE_TIMEOUT);
}
}
impl Helper for PptpHelper {
fn name(&self) -> &str {
"pptp"
}
}
impl PacketHelper for PptpHelper {
fn match_outbound(&self, proto: u8, dst_port: u16) -> bool {
proto == PROTO_TCP && dst_port == PPTP_PORT
}
fn process_outbound(&self, nat: &Nat, pkt: Vec<u8>, m: &NatMapping) -> Vec<u8> {
let payload = match payload(&pkt) {
Some(p) => p,
None => return pkt,
};
let (msg_type, ctrl_type) = match pptp_parse_header(payload) {
Some(v) => v,
None => return pkt,
};
if msg_type != 1 {
return pkt;
}
let inside_ip = match m.inside_ip {
IpAddr::V4(a) => a,
_ => return pkt,
};
match ctrl_type {
PPTP_OUTGOING_CALL_REQ => {
if payload.len() < 14 {
return pkt;
}
let call_id = u16::from_be_bytes([payload[12], payload[13]]);
if call_id == 0 {
return pkt;
}
{
let mut calls = self.calls.lock().unwrap();
Self::cleanup_calls(&mut calls);
calls.insert(
call_id,
PptpCallInfo {
inside_call_id: call_id,
outside_call_id: call_id,
peer_call_id: 0,
inside_ip,
created: Instant::now(),
},
);
}
add_gre_expectation(nat, inside_ip, call_id);
}
PPTP_OUTGOING_CALL_REPLY => {
if payload.len() < 16 {
return pkt;
}
let call_id = u16::from_be_bytes([payload[12], payload[13]]);
let peer_call_id = u16::from_be_bytes([payload[14], payload[15]]);
{
let mut calls = self.calls.lock().unwrap();
calls.insert(
call_id,
PptpCallInfo {
inside_call_id: call_id,
outside_call_id: call_id,
peer_call_id,
inside_ip,
created: Instant::now(),
},
);
}
add_gre_expectation(nat, inside_ip, call_id);
}
_ => {}
}
pkt
}
fn process_inbound(&self, nat: &Nat, pkt: Vec<u8>, m: &NatMapping) -> Vec<u8> {
let payload = match payload(&pkt) {
Some(p) => p,
None => return pkt,
};
let (msg_type, ctrl_type) = match pptp_parse_header(payload) {
Some(v) => v,
None => return pkt,
};
if msg_type != 1 {
return pkt;
}
let inside_ip = match m.inside_ip {
IpAddr::V4(a) => a,
_ => return pkt,
};
match ctrl_type {
PPTP_OUTGOING_CALL_REPLY => {
if payload.len() < 16 {
return pkt;
}
let server_call_id = u16::from_be_bytes([payload[12], payload[13]]);
let peer_call_id = u16::from_be_bytes([payload[14], payload[15]]);
{
let mut calls = self.calls.lock().unwrap();
if let Some(info) = calls.get_mut(&peer_call_id) {
info.peer_call_id = server_call_id;
}
}
add_gre_expectation(nat, inside_ip, peer_call_id);
}
PPTP_OUTGOING_CALL_REQ => {
if payload.len() < 14 {
return pkt;
}
let call_id = u16::from_be_bytes([payload[12], payload[13]]);
let mut calls = self.calls.lock().unwrap();
calls.insert(
call_id,
PptpCallInfo {
inside_call_id: call_id,
outside_call_id: call_id,
peer_call_id: 0,
inside_ip,
created: Instant::now(),
},
);
}
_ => {}
}
pkt
}
}
fn add_gre_expectation(nat: &Nat, inside_ip: Ipv4Addr, call_id: u16) {
nat.add_expectation(Expectation {
proto: PROTO_GRE,
remote_ip: Ipv4Addr::UNSPECIFIED,
remote_port: 0,
inside_ip,
inside_port: call_id,
expires: Instant::now() + PPTP_GRE_TIMEOUT,
});
}
fn payload(pkt: &[u8]) -> Option<&[u8]> {
let ihl = (pkt[0] & 0x0F) as usize * 4;
if ihl < 20 || pkt.len() < ihl + 20 {
return None;
}
let tcp_hdr_len = (pkt[ihl + 12] >> 4) as usize * 4;
let off = ihl + tcp_hdr_len;
if off >= pkt.len() {
return None;
}
Some(&pkt[off..])
}
fn pptp_parse_header(payload: &[u8]) -> Option<(u16, u16)> {
if payload.len() < 10 {
return None;
}
let length = u16::from_be_bytes([payload[0], payload[1]]);
if length as usize > payload.len() || length < 10 {
return None;
}
let msg_type = u16::from_be_bytes([payload[2], payload[3]]);
let magic = u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
if magic != PPTP_MAGIC_COOKIE {
return None;
}
let ctrl_type = u16::from_be_bytes([payload[8], payload[9]]);
Some((msg_type, ctrl_type))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nat::nat::Nat;
use crate::{checksum, combine_checksums, pseudo_header_checksum, IpPrefix, Protocol};
fn pfx(s: &str) -> IpPrefix {
s.parse().unwrap()
}
fn pptp_payload(ctrl_type: u16, call_id: u16, peer_call_id: u16) -> Vec<u8> {
let mut p = vec![0u8; 16];
p[0..2].copy_from_slice(&16u16.to_be_bytes()); p[2..4].copy_from_slice(&1u16.to_be_bytes()); p[4..8].copy_from_slice(&PPTP_MAGIC_COOKIE.to_be_bytes());
p[8..10].copy_from_slice(&ctrl_type.to_be_bytes());
p[12..14].copy_from_slice(&call_id.to_be_bytes());
p[14..16].copy_from_slice(&peer_call_id.to_be_bytes());
p
}
fn build_pptp(src: Ipv4Addr, sport: u16, dst: Ipv4Addr, dport: u16, payload: &[u8]) -> Vec<u8> {
let total = 20 + 20 + payload.len();
let mut p = vec![0u8; total];
p[0] = 0x45;
p[2..4].copy_from_slice(&(total as u16).to_be_bytes());
p[8] = 64;
p[9] = PROTO_TCP;
p[12..16].copy_from_slice(&src.octets());
p[16..20].copy_from_slice(&dst.octets());
let ic = checksum(&p[..20]);
p[10..12].copy_from_slice(&ic.to_be_bytes());
p[20..22].copy_from_slice(&sport.to_be_bytes());
p[22..24].copy_from_slice(&dport.to_be_bytes());
p[32] = 0x50;
p[33] = 0x18;
p[40..].copy_from_slice(payload);
let ph = pseudo_header_checksum(
Protocol::TCP,
IpAddr::V4(src),
IpAddr::V4(dst),
(20 + payload.len()) as u16,
);
let seg = checksum(&p[20..]);
let cs = combine_checksums(ph, seg);
p[36..38].copy_from_slice(&cs.to_be_bytes());
p
}
fn mapping(inside: Ipv4Addr) -> NatMapping {
NatMapping {
proto: PROTO_TCP,
inside_ip: IpAddr::V4(inside),
inside_port: 60000,
outside_port: 20000,
}
}
#[test]
fn pptp_header_parse_validates_magic() {
let good = pptp_payload(PPTP_OUTGOING_CALL_REQ, 0x1111, 0);
assert_eq!(pptp_parse_header(&good), Some((1, PPTP_OUTGOING_CALL_REQ)));
let mut bad = good.clone();
bad[4] = 0; assert_eq!(pptp_parse_header(&bad), None);
assert_eq!(pptp_parse_header(&[0u8; 4]), None);
}
#[test]
fn pptp_outgoing_call_req_tracks_call_and_gre_expectation() {
let nat = Nat::new(pfx("10.0.0.1/24"), pfx("203.0.113.1/24"));
let h = PptpHelper::new();
let inside = Ipv4Addr::new(10, 0, 0, 5);
let pkt = build_pptp(
inside,
60000,
Ipv4Addr::new(198, 51, 100, 9),
PPTP_PORT,
&pptp_payload(PPTP_OUTGOING_CALL_REQ, 0x2222, 0),
);
let out = h.process_outbound(&nat, pkt.clone(), &mapping(inside));
assert_eq!(out, pkt);
assert!(h.calls.lock().unwrap().contains_key(&0x2222));
let e = nat
.take_expectation(PROTO_GRE, inside, 0x2222)
.expect("GRE expectation should be registered");
assert_eq!(e.proto, PROTO_GRE);
assert_eq!(e.inside_ip, inside);
assert_eq!(e.inside_port, 0x2222);
}
#[test]
fn pptp_inbound_reply_updates_peer_call_id() {
let nat = Nat::new(pfx("10.0.0.1/24"), pfx("203.0.113.1/24"));
let h = PptpHelper::new();
let inside = Ipv4Addr::new(10, 0, 0, 5);
let req = build_pptp(
inside,
60000,
Ipv4Addr::new(198, 51, 100, 9),
PPTP_PORT,
&pptp_payload(PPTP_OUTGOING_CALL_REQ, 0x3333, 0),
);
h.process_outbound(&nat, req, &mapping(inside));
let reply = build_pptp(
Ipv4Addr::new(198, 51, 100, 9),
PPTP_PORT,
Ipv4Addr::new(203, 0, 113, 1),
20000,
&pptp_payload(PPTP_OUTGOING_CALL_REPLY, 0x9999, 0x3333),
);
h.process_inbound(&nat, reply, &mapping(inside));
let calls = h.calls.lock().unwrap();
let info = calls.get(&0x3333).expect("call should still be tracked");
assert_eq!(info.peer_call_id, 0x9999);
}
}