Skip to content

Commit 3148282

Browse files
author
root
committed
pre-clean stale state at startup, bump to 0.0.5
Drop-based rollback only runs on clean shutdown; SIGKILL / panic=abort / OOM leave iptables / ip rule / route / TUN state behind, and the next run fails with "File exists". Add a best-effort pre_clean pass that wipes exactly the state this config would install, before apply().
1 parent c3be061 commit 3148282

4 files changed

Lines changed: 97 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "transparent-udp-obfus"
3-
version = "0.0.4"
3+
version = "0.0.5"
44
edition = "2021"
55

66
[[bin]]

src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ async fn async_main(cfg: Config) -> Result<()> {
5454
let remote_rules = Arc::new(CompiledRule::compile_many(&cfg.remote_traffic)?);
5555
let keystream = Arc::new(Keystream::from_passphrase(&cfg.passphrase, cfg.cipher_mode));
5656

57+
// Wipe any stale state from a previously crashed run (SIGKILL / panic=abort
58+
// / OOM) before we try to install our own. The Drop-based rollback in
59+
// setup::apply only runs on clean shutdown; this covers the rest.
60+
setup::pre_clean(&cfg, &local_rules, &remote_rules);
61+
5762
// Create TUN queues: one read queue per worker for the sender, plus one
5863
// dedicated write queue shared across all receiver tasks.
5964
let sender_fds: Vec<OwnedFd> = tun::open_queues(&cfg.tun, cfg.num_threads)

src/setup.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,96 @@ fn s(x: impl Into<String>) -> String {
5353
x.into()
5454
}
5555

56+
/// Run a command silently, ignoring exit status and output. Used by the
57+
/// pre-clean pass where "not present" is the expected outcome on a fresh box.
58+
fn try_cmd(args: &[&str]) {
59+
let _ = Command::new(args[0]).args(&args[1..]).output();
60+
}
61+
62+
/// Best-effort removal of any leftover state this config would install, so we
63+
/// survive a previous crashed run (SIGKILL / panic=abort / OOM) that didn't
64+
/// run the Drop-based rollback. All failures are expected on a clean system
65+
/// and silently ignored.
66+
pub fn pre_clean(cfg: &Config, local_rules: &[CompiledRule], remote_rules: &[CompiledRule]) {
67+
let tun = &cfg.tun;
68+
let table = cfg.table.to_string();
69+
let tproxy_table = (cfg.table + 1).to_string();
70+
let mark_tproxy = format!("0x{:x}", cfg.mark_tproxy);
71+
72+
// iptables / ip6tables: delete the exact TPROXY rule we'd add. iptables -D
73+
// only removes an exactly-matching rule, so we rebuild the full arg list.
74+
for r in remote_rules {
75+
let v6 = matches!(r.af, AfSpec::Ip6);
76+
let bin = if v6 { "ip6tables" } else { "iptables" };
77+
let on_ip = if v6 { "::1" } else { "127.0.0.1" };
78+
let mut args: Vec<String> = vec![
79+
s(bin), s("-t"), s("mangle"), s("-D"), s("PREROUTING"),
80+
s("!"), s("-i"), tun.clone(),
81+
s("-p"), s("udp"),
82+
];
83+
if let Some(dp) = r.dstport {
84+
args.extend([s("--dport"), dp.to_string()]);
85+
}
86+
if let Some(net) = &r.srcip {
87+
args.extend([s("-s"), net.to_string()]);
88+
}
89+
if let Some(net) = &r.dstip {
90+
args.extend([s("-d"), net.to_string()]);
91+
}
92+
if let Some((lo, hi)) = r.srcport {
93+
args.extend([s("--sport"), format!("{}:{}", lo, hi)]);
94+
}
95+
args.extend([s("-m"), s("mark"), s("!"), s("--mark"), format!("0x{:x}", cfg.mark_reinject)]);
96+
args.extend([s("-m"), s("addrtype"), s("--dst-type"), s("LOCAL")]);
97+
args.extend([s("-j"), s("TPROXY")]);
98+
args.extend([s("--on-ip"), s(on_ip)]);
99+
args.extend([s("--on-port"), cfg.tproxy_port.to_string()]);
100+
args.extend([s("--tproxy-mark"), mark_tproxy.clone()]);
101+
let refs: Vec<&str> = args.iter().map(|x| x.as_str()).collect();
102+
try_cmd(&refs);
103+
}
104+
105+
// Per-local_traffic ip rule: build the same spec as apply() but with `del`.
106+
for r in local_rules {
107+
let v6 = matches!(r.af, AfSpec::Ip6);
108+
let mut args: Vec<String> = vec![s("ip")];
109+
if v6 {
110+
args.push(s("-6"));
111+
}
112+
args.extend([s("rule"), s("del"), s("iif"), s("lo")]);
113+
if let Some(net) = &r.dstip {
114+
args.extend([s("to"), net.to_string()]);
115+
}
116+
if let Some(net) = &r.srcip {
117+
args.extend([s("from"), net.to_string()]);
118+
}
119+
if let Some(dp) = r.dstport {
120+
args.extend([s("dport"), dp.to_string()]);
121+
}
122+
if let Some((lo, hi)) = r.srcport {
123+
args.extend([s("sport"), format!("{}-{}", lo, hi)]);
124+
}
125+
args.extend([s("fwmark"), format!("0x0/0x{:x}", cfg.mark_reinject)]);
126+
args.extend([s("lookup"), table.clone()]);
127+
let refs: Vec<&str> = args.iter().map(|x| x.as_str()).collect();
128+
try_cmd(&refs);
129+
}
130+
131+
// tproxy fwmark rule (v4 + v6)
132+
try_cmd(&["ip", "rule", "del", "fwmark", &mark_tproxy, "lookup", &tproxy_table]);
133+
try_cmd(&["ip", "-6", "rule", "del", "fwmark", &mark_tproxy, "lookup", &tproxy_table]);
134+
135+
// Routes: flushing our own tables is cheaper + more robust than reverse-
136+
// engineering each entry, and the tables are ours by config so it is safe.
137+
try_cmd(&["ip", "route", "flush", "table", &table]);
138+
try_cmd(&["ip", "-6", "route", "flush", "table", &table]);
139+
try_cmd(&["ip", "route", "flush", "table", &tproxy_table]);
140+
try_cmd(&["ip", "-6", "route", "flush", "table", &tproxy_table]);
141+
142+
// Leftover TUN device from a crashed run. The new apply() will re-open it.
143+
try_cmd(&["ip", "link", "del", tun]);
144+
}
145+
56146
fn read_sysctl(path: &str, fallback: &str) -> String {
57147
std::fs::read_to_string(path)
58148
.map(|x| x.trim().to_string())

0 commit comments

Comments
 (0)