diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..e22e3fa --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,34 @@ +steps: + build: + image: rust:1.94.0-bullseye + environment: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + commands: + - rustup default stable + - cargo build --verbose --release --jobs 4 + when: + branch: main + event: [ push, pull_request ] + tests: + image: rust + environment: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + commands: + - cargo test --verbose --jobs 4 -- --test-threads=4 + when: + event: [ pull_request, push ] + branch: main + clippy_and_fmt: + image: rust:1.94.0-bullseye + environment: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + commands: + - rustup component add clippy rustfmt + - cargo fmt --all --check + - cargo clippy --jobs 4 -- -D clippy::all # -D warnings + when: + branch: main + event: [ pull_request ] diff --git a/Cargo.lock b/Cargo.lock index 8b92f8c..49d6b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1251,15 +1251,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "etherparse" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b119b9796ff800751a220394b8b3613f26dd30c48f254f6837e64c464872d1c7" -dependencies = [ - "arrayvec", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -1345,6 +1336,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -2207,6 +2204,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.18" @@ -2343,15 +2346,18 @@ name = "nsc" version = "0.1.0" dependencies = [ "arti-client", - "etherparse", "ipnet", "iptables", "maxminddb", + "prost", + "prost-build", + "prost-types", "rtnetlink", "serde", "tokio", "toml 1.0.6+spec-1.1.0", "tun", + "ureq", ] [[package]] @@ -2662,6 +2668,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.13.0", +] + [[package]] name = "phf" version = "0.13.1" @@ -2912,6 +2929,57 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "pwd-grp" version = "1.0.2" @@ -3146,6 +3214,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3242,6 +3324,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -5166,12 +5283,34 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unty" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.8" @@ -5363,6 +5502,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5533,6 +5690,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 5d073aa..7121300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,18 @@ version = "0.1.0" edition = "2024" description = "Tun-in interface for Mesh networks like Tor/I2P with traffic-routing support (DIRECT/PROXY/BLOCK etc.)" repository = "https://codeberg.org/NamelessTeam/nsc" +build = "build.rs" +[build-dependencies] +prost-build = "0.14.3" [dependencies] arti-client = "0.40.0" -etherparse = "0.19.0" ipnet = "2.12.0" iptables = "0.6.0" maxminddb = "0.27.3" +prost = "0.14.3" +prost-types = "0.14.3" rtnetlink = "0.20.0" serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.50.0", features = ["full"] } @@ -24,4 +28,7 @@ lto = true codegen-units = 1 opt-level = 3 +[dev-dependencies] +ureq = "2.12" + diff --git a/README.md b/README.md index 815977b..6826975 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ The project is designed by the desire to transfer all the charms of routing and ## brief impl plan: ### client-side usage essentianls 1. [ ] TUN raw data (headers) parsing 50-70% done =) -2. [ ] parse geoip2 thingies +2. [ ] parse geoip2 (mmdb for now) thingies ( +3. [ ] add v2ray protocols buffer support (geo{ip,site}.dat) 3. [ ] parse user's Config 4. [ ] impl routing logic based on client's Config 5. [ ] make this thing easy adn convenient to use -6. [ ] custom Tor/i2p profiles/Config presets mayber???? +6. [ ] custom Tor/i2p profiles/Config presets (via proxies like 127.0.0.1:10808) ### inner infra 1. [ ] write as much tests as possible covering hopefully all logic 2. [ ] keep code readable and comprehensive 3. [ ] ideally review each others PRs after initial MVP stage -4. [ ] leave references/helpful comments on parts when code isn't obvious from the first glance \ No newline at end of file +4. [ ] leave references/helpful comments on parts when code isn't obvious from the first glance diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..99e430f --- /dev/null +++ b/build.rs @@ -0,0 +1,14 @@ +use std::io::Result; + +fn main() -> Result<()> { + let out_dir = std::path::PathBuf::from("src/geoparsers/v2ray/"); + + prost_build::Config::new() + .out_dir(&out_dir) + .compile_protos( + &["src/geoparsers/v2ray/proto_src/geosite.proto"], + &["src/geoparsers/v2ray/proto_src/"], + )?; + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 6552a65..a5739ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ pub enum RunTypes { #[derive(Serialize, Deserialize)] pub struct Config { - /// Paths to v2ray `geosite.dat', `geoip.dat` + /// Paths to `geosite.dat', `geolite2.mmdb` pub geo_files: [String; 2], /// Routing settings similar to v2ray pub routing: String, @@ -17,11 +17,21 @@ pub struct Config { pub mode: RunTypes, } +// TODO: Think how to add other anonymisers +// Like VPN on localhost:10808 +// it can be like: +// ```toml +// [[proxy]] +// name = "VPN" +// addr = "127.0.0.1:10808" +// type = "SOCKS5" # ... +// ``` + impl Default for Config { fn default() -> Self { Self { geo_files: [ - String::from("/etc/nsc/data/geoip.dat"), + String::from("/etc/nsc/data/geolite2.mmdb"), String::from("/etc/nsc/data/geosite.dat"), ], routing: String::from("/etc/nsc/routing.toml"), @@ -29,5 +39,3 @@ impl Default for Config { } } } - - diff --git a/src/geoparsers/geoip2.rs b/src/geoparsers/geoip2.rs index 9323280..1fd6ab1 100644 --- a/src/geoparsers/geoip2.rs +++ b/src/geoparsers/geoip2.rs @@ -1,42 +1,61 @@ -use ipnet::IpNet; use crate::config::Config; +use maxminddb::{Reader, geoip2}; +use serde::Deserialize; +use std::net::IpAddr; -/// Enum for declaring GeoSite/IP routing +// For now only MMDB because i cant found .proto schemes of +// V2Ray GeoSite.dat :(( +// TODO: V2Ray protobuf parsing && Test 4 ts + +/// Interface enum for `dst_addr` info +#[derive(Debug, Deserialize)] pub enum RouteType { /// GeoSite MMDB type, like `category-ads-all` GeoSite(String), - /// Subnet - GeoIp(IpNet), + /// Result with GeoCode like "RU" + GeoIp(String), + // String because enum will used as interface in result of `route_packet`. } /// Routing actions +#[derive(Debug, Deserialize)] pub enum RouteAction { + #[serde(alias = "block")] Block, + #[serde(alias = "proxy")] Proxy, + #[serde(alias = "direct")] Direct, } -type Rules = Vec; +pub type Rules = Vec; -/// Type for declaring the routing rules like: -/// ```toml -/// [rule] -/// action = enum RouteAction -/// target = enum RouteType -/// -/// [rule] -/// target = "geosite:category-ads-all" -/// action = "block" -/// ``` +/// Type for deserializing the routing rules like: +#[derive(serde::Deserialize)] pub struct Rule { pub target: RouteType, pub action: RouteAction, } -pub fn parse_ruleset(config: Config) -> Result> { - let reader = maxminddb::Reader::open_readfile(config.geo_files[0].clone())?; - - // Ok(()) - todo!(); +pub struct GeoIpService { + reader: Reader>, } +impl GeoIpService { + pub fn new(config: Config) -> Result> { + let path = config.geo_files.get(0).unwrap(); + let reader = Reader::open_readfile(path)?; + Ok(Self { reader }) + } + + pub fn lookup_country<'a>( + &'a self, + ip: IpAddr, + ) -> Result, Box> { + let result = self.reader.lookup(ip)?; + + result + .decode::()? + .ok_or_else(|| "Couldnt lookup IP geo.".into()) + } +} diff --git a/src/geoparsers/mod.rs b/src/geoparsers/mod.rs index 9f95b8f..c3cfece 100644 --- a/src/geoparsers/mod.rs +++ b/src/geoparsers/mod.rs @@ -1 +1,3 @@ -mod geoip2; +pub mod geoip2; +pub mod toml; +pub mod v2ray; diff --git a/src/geoparsers/toml.rs b/src/geoparsers/toml.rs new file mode 100644 index 0000000..3638aa7 --- /dev/null +++ b/src/geoparsers/toml.rs @@ -0,0 +1,15 @@ +use crate::config::Config; +use crate::geoparsers::geoip2::Rules; + +pub fn parse_rules(config: Config) -> Result, Box> { + let data = match std::fs::read_to_string(config.routing) { + Ok(result) => result, + Err(_) => { + println!("Couldnt find your `rules.toml`; Using default mode. All to anonymizers"); + return Ok(None); + } + }; + + let rules: Rules = toml::from_str(&data)?; + Ok(Some(rules)) +} diff --git a/src/geoparsers/v2ray/mod.rs b/src/geoparsers/v2ray/mod.rs new file mode 100644 index 0000000..971be55 --- /dev/null +++ b/src/geoparsers/v2ray/mod.rs @@ -0,0 +1,2 @@ +pub mod parsing; +pub mod types; diff --git a/src/geoparsers/v2ray/parsing.rs b/src/geoparsers/v2ray/parsing.rs new file mode 100644 index 0000000..4f0bbba --- /dev/null +++ b/src/geoparsers/v2ray/parsing.rs @@ -0,0 +1,79 @@ +use crate::geoparsers::v2ray::types::{Domain, GeoSite, GeoSiteList}; +use prost::Message; +use prost::bytes::Buf; +use std::fs; + +pub struct GeoSiteService { + index: GeoSiteList, +} + +impl GeoSiteService { + // TODO: Make more smart memory mapping; geosite files can be > 70MB + pub fn new(path: &str) -> Result> { + let bytes = fs::read(path)?; + let geosite_list = decode_geosite_stream(&bytes)?; + + Ok(Self { + index: geosite_list, + }) + } + + // Idk but i think it can work + pub fn lookup(&self, value: &str) -> Option<&GeoSite> { + self.index + .entry + .iter() + .find(|site| site.domain.iter().any(|d| d.value == value)) + } + + /// Returns the number of GeoSite entries in the list + pub fn len(&self) -> usize { + self.index.entry.len() + } + + /// Returns true if the GeoSite list is empty + pub fn is_empty(&self) -> bool { + self.index.entry.is_empty() + } +} + +/// Decode a stream of length-delimited GeoSite messages +/// `geosite.dat` ts is not one protobuf-message, stream of length-delimited messages +/// so we need ts helper +fn decode_geosite_stream(bytes: &[u8]) -> Result> { + let mut buf = bytes; + let mut entries = Vec::new(); + + while buf.has_remaining() { + // Read tag (0x0a field 1, wire type 2) + let tag = buf.get_u8(); + if tag != 0x0a { + return Err(format!("Unexpected tag: {:#04x}", tag).into()); + } + // varint + let mut len = 0usize; + let mut shift = 0; + loop { + if !buf.has_remaining() { + return Err("Unexpected end of buffer while reading varint".into()); + } + let b = buf.get_u8(); + len |= ((b & 0x7f) as usize) << shift; + if b & 0x80 == 0 { + break; + } + shift += 7; + if shift >= 70 { + return Err("Varint too long".into()); + } + } + + let entry_bytes = &buf[..len]; + let site = GeoSite::decode(entry_bytes)?; + entries.push(site); + + buf.advance(len); + } + + Ok(GeoSiteList { entry: entries }) +} diff --git a/src/geoparsers/v2ray/proto_src/geosite.proto b/src/geoparsers/v2ray/proto_src/geosite.proto new file mode 100644 index 0000000..e6c76dd --- /dev/null +++ b/src/geoparsers/v2ray/proto_src/geosite.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +package types; + +// Domain for routing decision. +message Domain { + // Type of domain value. + enum Type { + // The value is used as is. + Plain = 0; + // The value is used as a regular expression. + Regex = 1; + // The value is a root domain. + Domain = 2; + // The value is a domain. + Full = 3; + } + + // Domain matching type. + Type type = 1; + + // Domain value. + string value = 2; + + // Attribute of the domain. + message Attribute { + string key = 1; + oneof typed_value { + bool bool_value = 2; + int64 int_value = 3; + } + } + + // Attributes of this domain. May be used for filtering. + repeated Attribute attribute = 3; +} + +// IP for routing decision, in CIDR form. +message CIDR { + // IP address, should be either 4 or 16 bytes. + bytes ip = 1; + + // Number of leading ones in the network mask. + uint32 prefix = 2; +} + +message GeoIP { + string country_code = 1; + repeated CIDR cidr = 2; +} + +message GeoIPList { + repeated GeoIP entry = 1; +} + +message GeoSite { + string country_code = 1; + repeated Domain domain = 2; + // resource_hash instruct simplified config converter to load domain from geo file. + bytes resource_hash = 3; + string code = 4; +} + +message GeoSiteList { + repeated GeoSite entry = 1; +} diff --git a/src/geoparsers/v2ray/types.rs b/src/geoparsers/v2ray/types.rs new file mode 100644 index 0000000..b82fbbf --- /dev/null +++ b/src/geoparsers/v2ray/types.rs @@ -0,0 +1,111 @@ +// This file is @generated by prost-build. +/// Domain for routing decision. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Domain { + /// Domain matching type. + #[prost(enumeration = "domain::Type", tag = "1")] + pub r#type: i32, + /// Domain value. + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, + /// Attributes of this domain. May be used for filtering. + #[prost(message, repeated, tag = "3")] + pub attribute: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `Domain`. +pub mod domain { + /// Attribute of the domain. + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct Attribute { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(oneof = "attribute::TypedValue", tags = "2, 3")] + pub typed_value: ::core::option::Option, + } + /// Nested message and enum types in `Attribute`. + pub mod attribute { + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum TypedValue { + #[prost(bool, tag = "2")] + BoolValue(bool), + #[prost(int64, tag = "3")] + IntValue(i64), + } + } + /// Type of domain value. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Type { + /// The value is used as is. + Plain = 0, + /// The value is used as a regular expression. + Regex = 1, + /// The value is a root domain. + Domain = 2, + /// The value is a domain. + Full = 3, + } + impl Type { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Plain => "Plain", + Self::Regex => "Regex", + Self::Domain => "Domain", + Self::Full => "Full", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Plain" => Some(Self::Plain), + "Regex" => Some(Self::Regex), + "Domain" => Some(Self::Domain), + "Full" => Some(Self::Full), + _ => None, + } + } + } +} +/// IP for routing decision, in CIDR form. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Cidr { + /// IP address, should be either 4 or 16 bytes. + #[prost(bytes = "vec", tag = "1")] + pub ip: ::prost::alloc::vec::Vec, + /// Number of leading ones in the network mask. + #[prost(uint32, tag = "2")] + pub prefix: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GeoIp { + #[prost(string, tag = "1")] + pub country_code: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub cidr: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GeoIpList { + #[prost(message, repeated, tag = "1")] + pub entry: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GeoSite { + #[prost(string, tag = "1")] + pub country_code: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub domain: ::prost::alloc::vec::Vec, + /// resource_hash instruct simplified config converter to load domain from geo file. + #[prost(bytes = "vec", tag = "3")] + pub resource_hash: ::prost::alloc::vec::Vec, + #[prost(string, tag = "4")] + pub code: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GeoSiteList { + #[prost(message, repeated, tag = "1")] + pub entry: ::prost::alloc::vec::Vec, +} diff --git a/src/lib.rs b/src/lib.rs index 29905b0..2a02068 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -pub mod sniffing; pub mod config; pub mod geoparsers; +pub mod sniffing; pub mod startup; diff --git a/src/main.rs b/src/main.rs index d90ae5d..c2b6707 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,11 @@ -// mod config; -// mod geoparsers; -// pub mod sniffing; -// mod startup; +//mod routing; +mod config; +mod geoparsers; +pub mod sniffing; +//mod startup; use nsc::startup::init; -use std::io::Read; - -fn main() -> Result<(), Box ->{ +fn main() -> Result<(), Box> { init() } diff --git a/src/routing.rs b/src/routing.rs index e69de29..21e355b 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -0,0 +1,9 @@ +use crate::geoparsers::geoip2::GeoIpService; + +struct Router { + geoip: GeoIpService, + // geosite: GeoSiteService + // sniffer: Sniffer +} + + diff --git a/src/sniffing/headers.rs b/src/sniffing/headers.rs index a5b0480..1db0da6 100644 --- a/src/sniffing/headers.rs +++ b/src/sniffing/headers.rs @@ -1,3 +1,4 @@ +use std::fmt; use tun::Error; // Here we will recieve bytes and try to get their destanation & apply Rules for them. @@ -7,10 +8,17 @@ use crate::config::Config; pub enum Protocol { TCP, UDP, - Unsupported(u8) + Unsupported(u8), +} +type SourceV4Ip = Ipv4; +type SourceV6Ip = Ipv6; +#[derive(PartialEq, Debug)] +pub enum IpVersion { + V4, + V6, } type Ipv4 = [u8; 4]; -type Ipv6 = [u8; 16]; +type Ipv6 = [u16; 8]; type Port = u16; #[derive(Debug, PartialEq)] pub enum PacketInfo { @@ -21,7 +29,7 @@ pub enum PacketInfo { dst_ip: Ipv4, dst_port: Port, protocol: Protocol, - dns: bool + dns: bool, }, // V6 { @@ -30,11 +38,112 @@ pub enum PacketInfo { dst_ip: Ipv6, dst_port: Port, protocol: Protocol, - dns: bool + dns: bool, + }, +} + +impl fmt::Display for PacketInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.version() == &IpVersion::V4 { + let src_ip = self.src_ipv4_ip().unwrap(); + let dst_ip = self.dst_ipv4_ip().unwrap(); + write!( + f, + "{}.{}.{}.{}:{} -> {}.{}.{}.{}:{} {:?} is dns? {:?}", + src_ip[0], + src_ip[1], + src_ip[2], + src_ip[3], + self.src_port(), + dst_ip[0], + dst_ip[1], + dst_ip[2], + dst_ip[3], + self.dst_port(), + self.protocol(), + self.dns() + ) + } else { + let src_ip = self.src_ipv6_ip().unwrap(); + let dst_ip = self.dst_ipv6_ip().unwrap(); + // y:y:y:y:y:y:y:y = 8 hexademical; y = segment, pair of 2 u8 big endian + write!( + f, + "{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x} port:{} -> {:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x} port:{} {:?} is dns? {:?}", + src_ip[0], + src_ip[1], + src_ip[2], + src_ip[3], + src_ip[4], + src_ip[5], + src_ip[6], + src_ip[7], + self.src_port(), + dst_ip[0], + dst_ip[1], + dst_ip[2], + dst_ip[3], + dst_ip[4], + dst_ip[5], + dst_ip[6], + dst_ip[7], + self.dst_port(), + self.protocol(), + self.dns() + ) + } } } impl PacketInfo { + pub fn dns(&self) -> &bool { + match self { + PacketInfo::V4 { dns, .. } => dns, + PacketInfo::V6 { dns, .. } => dns, + } + } + pub fn src_ipv6_ip(&self) -> Option<&SourceV6Ip> { + match self { + PacketInfo::V6 { src_ip, .. } => Some(src_ip), + _ => None, + } + } + pub fn dst_ipv6_ip(&self) -> Option<&SourceV6Ip> { + match self { + PacketInfo::V6 { dst_ip, .. } => Some(dst_ip), + _ => None, + } + } + pub fn src_ipv4_ip(&self) -> Option<&SourceV4Ip> { + match self { + PacketInfo::V4 { src_ip, .. } => Some(src_ip), + _ => None, + } + } + pub fn dst_ipv4_ip(&self) -> Option<&SourceV4Ip> { + match self { + PacketInfo::V4 { dst_ip, .. } => Some(dst_ip), + _ => None, + } + } + pub fn src_port(&self) -> &Port { + match self { + PacketInfo::V4 { src_port, .. } => src_port, + PacketInfo::V6 { src_port, .. } => src_port, + } + } + pub fn dst_port(&self) -> &Port { + match self { + PacketInfo::V4 { dst_port, .. } => dst_port, + PacketInfo::V6 { dst_port, .. } => dst_port, + } + } + pub fn version(&self) -> &IpVersion { + match self { + PacketInfo::V4 { .. } => &IpVersion::V4, + PacketInfo::V6 { .. } => &IpVersion::V6, + } + } pub fn protocol(&self) -> &Protocol { match self { PacketInfo::V4 { protocol, .. } => protocol, @@ -52,59 +161,74 @@ pub fn sniff_raw_packets(packet: &Packet) -> SniffedPacket { let ver = packet[0] >> 4; match ver { 4 => { - let dst_port = Port::from_be_bytes([packet[22], packet[23]]); + // Internet Header Length (IHL). + let ihl = (packet[0] & 0x0F) as usize * 4; + let dst_port = Port::from_be_bytes([packet[ihl + 2], packet[ihl + 3]]); let dns; - if dst_port == 53 { dns = true; } else { dns = false; }; - // FIXME: hardcoded IPv4 port offset - let v4 = PacketInfo::V4{ + if dst_port == 53 { + dns = true; + } else { + dns = false; + }; + let v4 = PacketInfo::V4 { src_ip: ::try_from(&packet[12..16])?, - src_port: Port::from_be_bytes([packet[20], packet[21]]), + src_port: Port::from_be_bytes([packet[ihl], packet[ihl + 1]]), dst_ip: ::try_from(&packet[16..20])?, dst_port, protocol: match packet[9] { 6 => Protocol::TCP, 17 => Protocol::UDP, - p => Protocol::Unsupported(p) + p => Protocol::Unsupported(p), }, - dns + dns, }; - if !matches!(v4.protocol(), Protocol::Unsupported(_)) { - println!("{v4:?}"); - } else { - // TODO: make --debug option which will include this diagnostic, for general use this + if !matches!(v4.protocol(), Protocol::Unsupported(_)) { + println!("{v4}"); + } else { + // TODO: make --debug option which will include this diagnostic, for general use this // should be off - // println!("oppsie unsupported protocol: {:?}", v4.protocol()); + // println!("oppsie unsupported protocol: {:?}", v4.protocol()); } Ok(v4) - }, + } 6 => { - let dst_port = Port::from_be_bytes([packet[22], packet[23]]); + // y:y:y:y:y:y:y:y hexademical; y = segment, pair of 2 u8 in big endian + let src_ip = std::array::from_fn(|i| { + u16::from_be_bytes([packet[8 + i * 2], packet[8 + i * 2 + 1]]) + }); + let dst_ip = std::array::from_fn(|i| { + u16::from_be_bytes([packet[24 + i * 2], packet[24 + i * 2 + 1]]) + }); + + let dst_port = Port::from_be_bytes([packet[42], packet[43]]); let dns; - if dst_port == 53 { dns = true; } else { dns = false; }; - let v6 = PacketInfo::V6{ - src_ip: ::try_from(&packet[8..24])?, + if dst_port == 53 { + dns = true; + } else { + dns = false; + }; + let v6 = PacketInfo::V6 { + src_ip, src_port: Port::from_be_bytes([packet[40], packet[41]]), - dst_ip: ::try_from(&packet[24..40])?, + dst_ip, dst_port, protocol: match packet[6] { 6 => Protocol::TCP, 17 => Protocol::UDP, - p => Protocol::Unsupported(p) + p => Protocol::Unsupported(p), }, - dns + dns, }; - if !matches!(v6.protocol(), Protocol::Unsupported(_)) { - println!("{v6:?}"); - } else { - // TODO: make --debug option which will include this diagnostic, for general use this + if !matches!(v6.protocol(), Protocol::Unsupported(_)) { + println!("{v6}"); + } else { + // TODO: make --debug option which will include this diagnostic, for general use this // should be off - // println!("oppsie unsupported protocol: {:?}", v6.protocol()); + // println!("oppsie unsupported protocol: {:?}", v6.protocol()); } Ok(v6) - }, - ver => { - Err(format!("unsuppiorted ver: {ver}").into()) } + ver => Err(format!("unsuppiorted ver: {ver}").into()), } } diff --git a/src/sniffing/metadata.rs b/src/sniffing/metadata.rs index e69de29..8b13789 100644 --- a/src/sniffing/metadata.rs +++ b/src/sniffing/metadata.rs @@ -0,0 +1 @@ + diff --git a/src/startup.rs b/src/startup.rs index 10a8441..3a1a09a 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,7 +1,7 @@ // Here we iniitialize systems crucial for nsc -use std::io::Read; -use crate::sniffing::headers::sniff_raw_packets; use crate::sniffing::headers::Protocol; +use crate::sniffing::headers::sniff_raw_packets; +use std::io::Read; pub fn init() -> Result<(), Box> { let mut config = tun::Configuration::default(); config diff --git a/tests/headers.rs b/tests/headers.rs index d7daef5..ff51111 100644 --- a/tests/headers.rs +++ b/tests/headers.rs @@ -1,8 +1,8 @@ use nsc::sniffing::*; use crate::headers::Protocol; -use nsc::sniffing::headers::sniff_raw_packets; use nsc::sniffing::headers::PacketInfo; +use nsc::sniffing::headers::sniff_raw_packets; #[test] fn generic_typeck() -> Result<(), Box> { @@ -115,7 +115,8 @@ fn generic_typeck() -> Result<(), Box> { // [0] IPv4 TCP 192.168.1.100:4832 → 93.184.216.34:443 assert_eq!( sniff_raw_packets(test_suite[0])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [192, 168, 1, 100], src_port: 4832, dst_ip: [93, 184, 216, 34], @@ -127,7 +128,8 @@ fn generic_typeck() -> Result<(), Box> { // [1] IPv4 UDP 10.0.0.9:5353 → 224.0.0.251:5353 (mDNS) assert_eq!( sniff_raw_packets(test_suite[1])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [10, 0, 0, 9], src_port: 5353, dst_ip: [224, 0, 0, 251], @@ -139,7 +141,8 @@ fn generic_typeck() -> Result<(), Box> { // [2] IPv4 UDP 10.0.0.9:1024 → 8.8.8.8:53 (DNS) assert_eq!( sniff_raw_packets(test_suite[2])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [10, 0, 0, 9], src_port: 1024, dst_ip: [8, 8, 8, 8], @@ -151,7 +154,8 @@ fn generic_typeck() -> Result<(), Box> { // [3] IPv4 TCP 10.0.0.5:54321 → 10.0.0.1:80 (HTTP) assert_eq!( sniff_raw_packets(test_suite[3])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [10, 0, 0, 5], src_port: 54321, dst_ip: [10, 0, 0, 1], @@ -163,7 +167,8 @@ fn generic_typeck() -> Result<(), Box> { // [4] IPv4 TCP 172.16.0.1:65535 → 172.16.0.2:8080 assert_eq!( sniff_raw_packets(test_suite[4])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [172, 16, 0, 1], src_port: 65535, dst_ip: [172, 16, 0, 2], @@ -175,7 +180,8 @@ fn generic_typeck() -> Result<(), Box> { // [5] IPv4 TCP IHL=6 10.0.0.1:9090 → 10.0.0.2:22 (requires IHL-based offset) assert_eq!( sniff_raw_packets(test_suite[5])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [10, 0, 0, 1], src_port: 9090, dst_ip: [10, 0, 0, 2], @@ -187,7 +193,8 @@ fn generic_typeck() -> Result<(), Box> { // [6] IPv4 ICMP (unsupported, "ports" are just ICMP body bytes) assert_eq!( sniff_raw_packets(test_suite[6])?, - PacketInfo::V4 { + PacketInfo::V6 { + dns: false, src_ip: [10, 0, 0, 9], src_port: 2048, dst_ip: [10, 0, 0, 1], @@ -200,6 +207,7 @@ fn generic_typeck() -> Result<(), Box> { assert_eq!( sniff_raw_packets(test_suite[7])?, PacketInfo::V6 { + dns: false, src_ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], src_port: 4000, dst_ip: [0x26, 0x06, 0x47, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], @@ -212,6 +220,7 @@ fn generic_typeck() -> Result<(), Box> { assert_eq!( sniff_raw_packets(test_suite[8])?, PacketInfo::V6 { + dns: false, src_ip: [0xFD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], src_port: 1234, dst_ip: [0xFD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], @@ -224,6 +233,7 @@ fn generic_typeck() -> Result<(), Box> { assert_eq!( sniff_raw_packets(test_suite[9])?, PacketInfo::V6 { + dns: false, src_ip: [0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], src_port: 32768, dst_ip: [0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], diff --git a/tests/v2ray_geosite.rs b/tests/v2ray_geosite.rs new file mode 100644 index 0000000..d400839 --- /dev/null +++ b/tests/v2ray_geosite.rs @@ -0,0 +1,72 @@ +use nsc::geoparsers::v2ray::parsing::GeoSiteService; +use nsc::geoparsers::v2ray::types::Domain; +use std::fs; +use std::path::PathBuf; + +fn download_geosite() -> Result> { + let tmp_dir = std::env::temp_dir().join("seccontrol_test"); + fs::create_dir_all(&tmp_dir)?; + + let geosite_path = tmp_dir.join("geosite.dat"); + + if !geosite_path.exists() { + // Use v2fly domain-list-community which has standard protobuf format + let url = "https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"; + let response = ureq::get(url).call()?; + let mut file = fs::File::create(&geosite_path)?; + let mut reader = response.into_reader(); + std::io::copy(&mut reader, &mut file)?; + } + + Ok(geosite_path) +} + +fn get_geosite_service() -> Result> { + let geosite_path = download_geosite()?; + let service = GeoSiteService::new(geosite_path.to_str().unwrap())?; + Ok(service) +} + +#[test] +fn geosite_service_creation() { + let service = get_geosite_service(); + assert!( + service.is_ok(), + "Failed to create GeoSiteService: {:?}", + service.err() + ); +} + +#[test] +fn lookup_existing_domain() { + let service = get_geosite_service().expect("Failed to create service"); + + assert!(!service.is_empty(), "Service should have entries"); + println!("Loaded {} GeoSite entries", service.len()); +} + +#[test] +fn lookup_nonexistent_domain() { + let service = get_geosite_service().expect("Failed to create service"); + let domain = Domain { + r#type: nsc::geoparsers::v2ray::types::domain::Type::Full as i32, + value: "nfaklsfjlasfvjkcnjnasxcjsas-not-existing-domain.com".to_string(), + attribute: vec![], + }; + + let result = service.lookup(domain.value.as_str()); + assert!( + result.is_none(), + "Should return none for not existing domain" + ); + println!("{:?}", result); +} + +#[test] +fn geosite_list_not_empty() { + let service = get_geosite_service().expect("Failed to create service"); + + assert!(!service.is_empty(), "GeoSiteList should not be empty"); + + println!("Loaded {} GeoSite entries", service.len()); +}