I'm writing a UDP transparent proxy in Rust, I'm using several crates
- socket2 => Access to the creation of a RAW socket
- nix::sys::socket => Access to all of the low level socket API calls
- std::net::socket => Access to the standard socket
the machine that is supposed to act as a proxy has been set as the gateway for the interested machines.
The proxy machine has the linux kernel redirection capabilities enabled, see snippet below
#!/bin/sh
# Redirect packets coming to the computer
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
# Disable ICMP redirection
sysctl -w net.ipv4.conf.all.send_redirects=0
The machine also has an NFTABLES rule set up to redirect all of the incoming traffic to the proxy program, see snippet below
#!/bin/sh
# Setting up NFTABLES
nft add table filter
nft add chain filter divert "{ type filter hook prerouting priority -150; }"
nft add rule filter divert meta l4proto udp socket transparent 1 meta mark set 1 accept
#nft add table ip nat
#nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
# Creating a new rule that redirects UDP traffic from P1-P2 to port 35
nft add rule filter divert udp dport 80-65525 tproxy to :35 meta mark set 1 accept
#nft add rule ip nat prerouting udp dport 80-65525 redirect to :35
As you can see from the commented out parts, I've experimented with both NAT PREROUTING and TPROXY.
After extensive research and testing I have found out that when I use NAT PREROUTING my proxy (that's listening on port 35) gets ALL of the incoming traffic from the clients but when I read the packets from the socket the original destination address get overwritten with the proxy machine's ip (which I don't want).
But if I instead use TPROXY I don't get all of the incoming traffic, I only get what my machine is "supposed" to see, e.g. broadcast traffic and traffic that has the proxy machine's mac address as the destination, this means that I can't see all of the traffic from the connected clients.
I've followed the instructions given in the linux kernel's tproxy manual.
It has to be noted that I do set IP_TRANSPARENT.
These are the things that I set in my program
use socket2::{Socket, Domain, Type, Protocol};
use std::os::fd::{OwnedFd, RawFd, AsRawFd};
use nix::sys::socket::{
...
sockopt::{IpTransparent, Ipv4OrigDstAddr, Ipv4PacketInfo},
setsockopt,
...
};
...
let client_to_proxy_socket = match Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::UDP)) { ... };
let socket_ref: OwnedFd = client_to_proxy_socket.into();
let active: bool = true;
match setsockopt(&socket_ref, IpTransparent, &active) { ... };
match setsockopt(&socket_ref, Ipv4OrigDstAddr, &active) { ... };
match setsockopt(&socket_ref, Ipv4PacketInfo, &active) { ... };
...
When it comes to what I've tried, I must say that I've tried a lot of possible combinations of both rules and code with variations and different structs.
I'm also gonna post a short list of links that I've already visited and read multiple times
- Implement async socket recvmsg for RX and TX timestamping in Rust
- Get destination address of a received UDP packet
- System-wide redirect traffic to local proxy server using iptables
- Using iptables TPROXY instead of REDIRECT
- IPTables configuration for Transparent Proxy
As for what I expect, I expect to get all of the incoming traffic and have that redirected by TPROXY to port 35.
This is the call to the recvmsg function
let target_address: SockaddrIn = match recvmsg::<SockaddrIn>(*socket_clone, &mut iov, Some(&mut cmsg_space!(nix_msghdr)), flags) {
Ok(t) => {
let ctrl_msg: Vec<ControlMessageOwned> = t.cmsgs().collect();
let mut res: SockaddrIn = SockaddrIn::new(0, 0, 0, 0, 0);
for c in ctrl_msg {
if let ControlMessageOwned::Ipv4OrigDstAddr(b) = c {
let addr: [u8; 4] = convert_u32_to_4u8(b.sin_addr.s_addr);
let port: u16 = b.sin_port;
let new_port: u16 = ((port & (0xFF << 8)) >> 8) | ((port & 0xFF) << 8);
res = SockaddrIn::new(addr[0], addr[1], addr[2], addr[3], new_port);
}
if let ControlMessageOwned::Ipv4PacketInfo(b) = c {
debug!("IP_PKTINFO => {:?}", b);
}
}
res
},
Err(e) => {
error!("Could not extract the original destination from the socket ; Error => {}", e);
std::process::exit(15);
}
};
I know that someone is probably going to ask "Show us some more code", the code I believe to be relevant, I've posted it, the whole program is 400 lines long, I don't think SO allows me to post that many lines of code.