pub mod cache;
pub mod chnroute;
pub mod dns;
pub mod dnsconf;
pub mod geoip;
pub mod gfwlist;
pub mod proxy;
pub mod route;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
pub use proxy::{chinadns_decision, Decision, IpSink, Resolver};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Full,
GfwList,
ChinaDns,
}
impl Mode {
pub fn from_name(name: &str) -> Result<Self, PolicyError> {
match name.to_ascii_lowercase().as_str() {
"full" | "off" | "none" => Ok(Mode::Full),
"gfwlist" => Ok(Mode::GfwList),
"chinadns" | "china" => Ok(Mode::ChinaDns),
other => Err(PolicyError::UnknownMode(other.to_string())),
}
}
pub fn name(self) -> &'static str {
match self {
Mode::Full => "full",
Mode::GfwList => "gfwlist",
Mode::ChinaDns => "chinadns",
}
}
pub fn is_enabled(self) -> bool {
!matches!(self, Mode::Full)
}
}
#[derive(Debug, thiserror::Error)]
pub enum PolicyError {
#[error("unknown policy mode: {0} (expected full|gfwlist|chinadns)")]
UnknownMode(String),
}
pub const DEFAULT_PREWARM: &[&str] = &[
"google.com",
"www.google.com",
"www.gstatic.com",
"fonts.gstatic.com",
"ssl.gstatic.com",
"www.googleapis.com",
"ajax.googleapis.com",
"accounts.google.com",
"www.youtube.com",
"youtube.com",
"i.ytimg.com",
"googlevideo.com",
"twitter.com",
"x.com",
"abs.twimg.com",
"pbs.twimg.com",
"www.facebook.com",
"static.xx.fbcdn.net",
"www.instagram.com",
"www.wikipedia.org",
"en.wikipedia.org",
"upload.wikimedia.org",
"github.com",
"raw.githubusercontent.com",
"objects.githubusercontent.com",
"telegram.org",
"web.telegram.org",
"www.reddit.com",
"duckduckgo.com",
];
#[derive(Debug, Clone)]
pub struct PolicyConfig {
pub mode: Mode,
pub dns_listen: SocketAddr,
pub dns_local: SocketAddr,
pub dns_remote: SocketAddr,
pub gfwlist: Option<PathBuf>,
pub chnroute: Option<PathBuf>,
pub geoip: Option<PathBuf>,
pub geoip_country: String,
pub set_dns: bool,
pub prewarm: Vec<String>,
pub cache_file: Option<PathBuf>,
pub dns_timeout: Duration,
}
pub struct PolicyHandle {
pub task: tokio::task::JoinHandle<anyhow::Result<()>>,
_guard: route::RouteGuard,
_dns_guard: Option<dnsconf::DnsGuard>,
_cache_guard: Option<CacheGuard>,
}
struct CacheGuard {
cache: std::sync::Arc<cache::DnsCache>,
path: PathBuf,
}
impl Drop for CacheGuard {
fn drop(&mut self) {
self.cache.save(&self.path);
}
}
pub async fn spawn(
cfg: &PolicyConfig,
tun_name: &str,
tun_ip: std::net::Ipv4Addr,
direct_src: std::net::IpAddr,
) -> anyhow::Result<PolicyHandle> {
use anyhow::Context;
use std::net::IpAddr;
use std::sync::Arc;
let gfwlist = match cfg.mode {
Mode::GfwList => {
let p = cfg
.gfwlist
.as_ref()
.context("gfwlist mode requires a gfwlist file")?;
let list = gfwlist::GfwList::load(p)
.with_context(|| format!("loading gfwlist from {}", p.display()))?;
log::info!("loaded {} gfwlist domains from {}", list.len(), p.display());
list
}
Mode::ChinaDns if cfg.gfwlist.is_some() => {
let p = cfg.gfwlist.as_ref().unwrap();
let list = gfwlist::GfwList::load(p)
.with_context(|| format!("loading gfwlist from {}", p.display()))?;
log::info!(
"loaded {} gfwlist force-tunnel domains from {}",
list.len(),
p.display()
);
list
}
_ => gfwlist::GfwList::default(),
};
let chnroute = if matches!(cfg.mode, Mode::ChinaDns) {
if let Some(db) = cfg.geoip.as_ref() {
let routes = geoip::load_country_routes(db, &cfg.geoip_country).with_context(|| {
format!(
"building {} routes from {}",
cfg.geoip_country,
db.display()
)
})?;
log::info!(
"loaded {} {} routes from GeoIP database {}",
routes.len(),
cfg.geoip_country,
db.display()
);
routes
} else {
let p = cfg
.chnroute
.as_ref()
.context("chinadns mode requires a chnroute file or a geoip database")?;
let routes = chnroute::ChnRoute::load(p)
.with_context(|| format!("loading chnroute from {}", p.display()))?;
log::info!("loaded {} china routes from {}", routes.len(), p.display());
routes
}
} else {
chnroute::ChnRoute::default()
};
let router = Arc::new(
route::TunRouter::new(tun_name, tun_ip)
.with_context(|| format!("setting up routing for tun device {tun_name}"))?,
);
if let IpAddr::V4(v4) = cfg.dns_remote.ip() {
router
.add_route(v4)
.with_context(|| format!("routing clean DNS upstream {v4} through the tunnel"))?;
}
let dns_cache = Arc::new(cache::DnsCache::new());
if let Some(path) = cfg.cache_file.as_ref() {
dns_cache.load(path);
}
let sink: Arc<dyn IpSink> = router.clone();
let resolver = Resolver::new(
cfg.mode,
gfwlist,
chnroute,
cfg.dns_local,
cfg.dns_remote,
cfg.dns_timeout,
sink,
Arc::clone(&dns_cache),
);
#[cfg(windows)]
let resolver = resolver.with_bind_sources(direct_src, IpAddr::V4(tun_ip));
let resolver = Arc::new(resolver);
let listener = tokio::net::UdpSocket::bind(cfg.dns_listen)
.await
.with_context(|| format!("binding DNS proxy on {}", cfg.dns_listen))?;
log::info!(
"policy routing active (mode={}); DNS proxy on {}",
cfg.mode.name(),
cfg.dns_listen
);
let dns_guard = if cfg.set_dns {
dnsconf::apply(cfg.dns_listen.ip(), cfg.dns_listen.port(), direct_src)
.context("configuring the system resolver")?
} else {
log::info!(
"point this host's resolver at {} (e.g. nameserver {}) to use policy routing",
cfg.dns_listen,
cfg.dns_listen.ip()
);
None
};
if !cfg.prewarm.is_empty() {
log::info!("pre-warming {} common domains", cfg.prewarm.len());
tokio::spawn(proxy::prewarm(Arc::clone(&resolver), cfg.prewarm.clone()));
}
let cache_guard = cfg.cache_file.as_ref().map(|path| CacheGuard {
cache: Arc::clone(&dns_cache),
path: path.clone(),
});
let task = tokio::spawn(proxy::serve(listener, resolver));
Ok(PolicyHandle {
task,
_guard: route::RouteGuard::new(router),
_dns_guard: dns_guard,
_cache_guard: cache_guard,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_modes() {
assert_eq!(Mode::from_name("full").unwrap(), Mode::Full);
assert_eq!(Mode::from_name("OFF").unwrap(), Mode::Full);
assert_eq!(Mode::from_name("gfwlist").unwrap(), Mode::GfwList);
assert_eq!(Mode::from_name("ChinaDNS").unwrap(), Mode::ChinaDns);
assert_eq!(Mode::from_name("china").unwrap(), Mode::ChinaDns);
assert!(Mode::from_name("bogus").is_err());
}
#[test]
fn enabled_flag() {
assert!(!Mode::Full.is_enabled());
assert!(Mode::GfwList.is_enabled());
assert!(Mode::ChinaDns.is_enabled());
}
}