-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: transparent proxy support (linux only) #343
Conversation
@@ -37,6 +37,8 @@ base64 = "0.22" | |||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } | |||
boring = "4.5.0" | |||
boring-sys = "4.5.0" | |||
unix-udp-sock = { git = "https://github.com/Watfaq/unix-udp-sock.git", rev = "cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18"} | |||
cmd_lib = "1.9.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a big fan of executing system commands in a program - at lease should we print out what we are executing so the users know what's happening under the hood?
clash_lib/src/app/inbound/manager.rs
Outdated
@@ -88,7 +88,7 @@ impl InboundManager { | |||
}; | |||
self.network_listeners | |||
.values() | |||
.for_each(|x| match x.listener_type { | |||
.for_each(|x: &NetworkInboundListener| match x.listener_type { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this necessary
@@ -181,6 +181,7 @@ impl Dispatcher { | |||
|
|||
/// Dispatch a UDP packet to outbound handler | |||
/// returns the close sender | |||
/// will ignore the source and destination in `Session` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this comment still valid?
let mut buf = vec![0_u8; 1024 * 64]; | ||
while let Ok(meta) = socket.recv_msg(&mut buf).await { | ||
match meta.orig_dst { | ||
Some(orig_dst) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would tproxy override the dst so we need to patch the socket to get the orig_dst?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i see it now IP_ORIGDSTADDR
if command_exists("iptables") { | ||
TProxyStrategy::Iptables | ||
} else { | ||
TProxyStrategy::None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it still work if iptables was not installed? should we prompt user?
); | ||
|
||
run_cmd!(ip rule add fwmark $DEFAULT_TPROXY_MARK lookup $POLICY_ROUTING_TABLE_NUM); | ||
run_cmd!(ip route add local "0.0.0.0/0" dev lo table $POLICY_ROUTING_TABLE_NUM); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would be lowest priority, is it necessary/useful to do what openvpn does? https://serverfault.com/a/312977 (i think mainly for having higher priority than the system default route)
and do we need to delete this route in clean up?
@@ -1,4 +1,3 @@ | |||
pub mod inbound; | |||
pub use netstack_lwip as netstack; | |||
mod datagram; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this intentional?
clash_lib/src/proxy/vmess/mod.rs
Outdated
@@ -36,6 +36,7 @@ pub enum VmessTransport { | |||
|
|||
pub struct HandlerOptions { | |||
pub name: String, | |||
// TODO: @VendettaReborn, delete this after confirmed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where do you think better to put this?
clash_lib/src/session.rs
Outdated
@@ -12,6 +13,13 @@ use tokio::io::{AsyncRead, AsyncReadExt}; | |||
|
|||
use erased_serde::Serialize as ESerialize; | |||
|
|||
// mark of the packet from clash-rs | |||
static DEFAULT_PACKET_MARK: AtomicU32 = AtomicU32::new(0xff); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this just a fixed u32, why you need an atomic?
)) | ||
} | ||
|
||
// only support v4 now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plans for supporting v6? is it huge amount of work?
and does v4 support mean we can't proxy requests targeting a remote v6 addr or we just can't bind v6 locally
clash_lib/src/proxy/mod.rs
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am actually confused here. Shouldn't every connect from clash-rs use the same so_mark?
pub(crate) fn get_packet_mark() -> u32 { | ||
DEFAULT_PACKET_MARK | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What i thought is:
static GLOBAL_PACKET_MARK: AtomicU32 = AtomicU32::new(0);
pub(crate) fn get_packet_mark() -> Option<u32> {
let mark = GLOBAL_PACKET_MARK.load(Ordering::Relex)
if mark != 0 {
return Some(mark)
} else {
return None
}
}
if tun auto-route or tproxy auto-route is enabled, tun or tproxy can generate mark itself or read from conf, then set GLOBAL_PACKET_MARK
if tun auto-route or tproxy auto-route is disabled, we can just set this to 0.
Then anywhere we constructing Session, we can(must) use it(get_packet_mark
).
Signed-off-by: Yuwei Ba <dev@watfaq.com>
i find this post is useful https://powerdns.org/tproxydoc/tproxy.md.html |
let mut buf = vec![0_u8; 1024 * 64]; | ||
while let Ok(meta) = socket.recv_msg(&mut buf).await { | ||
match meta.orig_dst { | ||
Some(orig_dst) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i see it now IP_ORIGDSTADDR
pub fn new_tcp_listener(addr: SocketAddr) -> io::Result<TcpListener> { | ||
let socket = TcpSocket::new_v4()?; | ||
set_ip_transparent(socket.as_raw_fd())?; | ||
socket.set_reuseaddr(true)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we need to reuseaddr?
this is the iptables created by the tproxy
|
) { | ||
match self { | ||
TProxyStrategy::Iptables => { | ||
clean_iptables_tproxy(output_chain_name, prerouting_chain_name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need to cleanup everytime before setting up? it also seems a bit counter single responsitiblity to have clean up and set up in setup
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" LOG "--log-prefix" "\"LOCAL IN\""); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" RETURN); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" mark "--mark" $skip_mark "-j" RETURN); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-m" socket "-j" divert_chain_name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-m" socket "-j" divert_chain_name); | |
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-m" socket "-j" $divert_chain_name); |
); | ||
} | ||
TProxyStrategy::None => { | ||
error!("No tproxy command found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
error!("No tproxy command found"); | |
error!("No iptables command found"); |
for addr in RESERVED_ADDRS { | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-d" $addr "-j" RETURN); | ||
} | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" LOG "--log-prefix" "\"LOCAL IN\""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this log useful in any case?
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" LOG "--log-prefix" "\"LOCAL IN\""); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" RETURN); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" mark "--mark" $skip_mark "-j" RETURN); | ||
run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-m" socket "-j" divert_chain_name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why you need to redirect it to a "divert" chain rather than just setting mark here inplace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have tried this out on my box and it's working as expected!
overall looks good to me.
would be great please see the comments and clean up the code a bit esp the #[allow(warning)]
dead_code related things.
@@ -374,6 +374,7 @@ pub struct Config { | |||
/// device-id: "dev://utun1989" | |||
/// ``` | |||
pub tun: Option<HashMap<String, Value>>, | |||
pub iptables: Iptables, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not seeing this used anywhere
to be discussed
|
i will try my best to resolve some trivial issues and warnings here. |
@VendettaReborn What's still missing for this to merge? |
I created a new on on top of this without the iptables integration since the code base has changed a lot and it takes some effort to rebase #615 |
🤔 This is a ...
🔗 Related issue link
#73
💡 Background and solution
on linux, the system provides a native way of handling the global proxy: IP_TRANSPARENT & tproxy.
compared with tun, it has a better performance, since it will not need one more traverse of network stack.
some introductions and documents can be found bellow:
there are 3 main parts of this PR:
iptables
to redirect the socket to tproxy listenerso_mark
and policy routing to avoid the endless loopi have test this PR on my linux machine, but the compatibility tests with other tools that depend on iptables, like docker, wireguard haven't been done
to be discussed
IP_RECVTOS
option in socket?📝 Changelog
Support Transparent proxy on linux(Ipv4 only)
☑️ Self-Check before Merge