public release

This commit is contained in:
chayleaf 2024-08-13 15:39:14 +07:00
parent 76ffd7af5b
commit da7f82a6dd
Signed by: chayleaf
GPG key ID: 78171AD46227E68E
18 changed files with 2294 additions and 22157 deletions

4
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target
/result
/unbound-mod-test-config
/unbound-mod-test-data
unbound-mod-test-config
unbound-mod-test-data

51
Cargo.lock generated
View file

@ -39,7 +39,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.72",
"syn 2.0.74",
]
[[package]]
@ -62,6 +62,25 @@ dependencies = [
"synstructure",
]
[[package]]
name = "example"
version = "0.1.0"
dependencies = [
"boxcar",
"filetime",
"ipnet",
"iptrie",
"libc",
"mnl",
"nftnl",
"nix",
"radix_trie",
"serde",
"serde_json",
"smallvec",
"unbound-mod",
]
[[package]]
name = "filetime"
version = "0.2.24"
@ -273,29 +292,29 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.205"
version = "1.0.207"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.205"
version = "1.0.207"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
"syn 2.0.74",
]
[[package]]
name = "serde_json"
version = "1.0.122"
version = "1.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
dependencies = [
"itoa",
"memchr",
@ -322,9 +341,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.72"
version = "2.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
dependencies = [
"proc-macro2",
"quote",
@ -347,19 +366,7 @@ dependencies = [
name = "unbound-mod"
version = "0.1.0"
dependencies = [
"boxcar",
"ctor",
"filetime",
"ipnet",
"iptrie",
"libc",
"mnl",
"nftnl",
"nix",
"radix_trie",
"serde",
"serde_json",
"smallvec",
]
[[package]]

View file

@ -1,42 +1,3 @@
[package]
name = "unbound-mod"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib", "cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
boxcar = { version = "0.2.5", optional = true }
ctor = { version = "0.2.8", optional = true }
filetime = { version = "0.2.24", optional = true }
ipnet = { version = "2.9.0", features = ["serde"], optional = true }
iptrie = { version = "0.8.5", optional = true }
libc = { version = "0.2.155", optional = true }
mnl = { version = "0.2.2", features = ["mnl-1-0-4"], optional = true }
nftnl = { version = "0.6.2", features = ["nftnl-1-1-2"], optional = true }
nix = { version = "0.29.0", features = ["poll", "user"], optional = true }
radix_trie = { version = "0.2.1", optional = true }
serde = { version = "1.0.205", features = ["derive"], optional = true }
serde_json = { version = "1.0.122", optional = true }
smallvec = { version = "1.13.2", optional = true }
[features]
example = [
"boxcar",
"ctor",
"filetime",
"ipnet",
"iptrie",
"libc",
"mnl",
"nftnl",
"nix",
"radix_trie",
"serde",
"serde_json",
"smallvec",
]
default = ["example"]
[workspace]
members = ["example", "unbound-mod"]
resolver = "2"

28
LICENSE Normal file
View file

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, chayleaf
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# unbound-rust-mod
This is a library for writing Unbound modules in Rust. See
[`example`](./example) for an example module.
Most of Unbound's features don't have safe bindings, so you might have
to write some yourself - in that case, PRs are appreciated.
To regenerate the bindings, there's a small problem - you actually
need to run Unbound's configure script for that. That's why I provide a
Nix file to generate them (running `nix build .#bindings` in project
root will produce the bindings at the `result` symlink). Alternatively,
you may call rust-bindgen manually.

24
example/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib", "cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
boxcar = "0.2.5"
filetime = "0.2.24"
ipnet = { version = "2.9.0", features = ["serde"] }
iptrie = "0.8.5"
libc = "0.2.155"
mnl = { version = "0.2.2", features = ["mnl-1-0-4"] }
nftnl = { version = "0.6.2", features = ["nftnl-1-1-2"] }
nix = { version = "0.29.0", features = ["poll", "user"] }
radix_trie = "0.2.1"
serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
smallvec = "1.13.2"
unbound-mod = { path = "../unbound-mod", default-features = false }

12
example/README.md Normal file
View file

@ -0,0 +1,12 @@
# unbound-rust-mod Example
This is an example module written using unbound-rust-mod. It
automatically populates nft sets using IP and domain info from .json
files. On start, it checks various environment variables, then loads
the .json files. The IPs are added immediately, but the domains are only
added if they're already in the module's cache (stored on the
filesystem) or whenever Unbound sends a response. Additionally, it
optionally supports live editing certain domain sets by sending a
command (that is, a specially formatted DNS request). This could be done
using an HTTP server, but that is a holdover from when this was still a
Python module (which took 100 seconds to load...)

View file

@ -109,21 +109,11 @@ mod tests {
assert!(!tree.contains([&"a", &"c"]));
let mut it = tree.iter();
assert!(matches!(
it.next()
.unwrap()
.into_iter()
.copied()
.collect::<String>()
.as_str(),
it.next().unwrap().copied().collect::<String>().as_str(),
"ab" | "bcd"
));
assert!(matches!(
it.next()
.unwrap()
.into_iter()
.copied()
.collect::<String>()
.as_str(),
it.next().unwrap().copied().collect::<String>().as_str(),
"ab" | "bcd"
));
}

View file

@ -1,3 +1,4 @@
#![allow(clippy::type_complexity)]
use std::{
collections::HashMap,
fmt::Display,
@ -12,7 +13,6 @@ use std::{
},
};
use ctor::ctor;
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use iptrie::IpPrefix;
use serde::{
@ -21,12 +21,15 @@ use serde::{
};
use smallvec::SmallVec;
use crate::{
unbound::{rr_class, rr_type, ModuleEvent, ModuleExtState, ReplyInfo},
UnboundMod,
};
use domain_tree::PrefixSet;
use nftables::{nftables_thread, NftData};
use unbound_mod::{
unbound::{
rr_class, rr_type, ModuleEnvMut, ModuleEvent, ModuleExtState, ModuleQstateMut,
OutboundEntryMut, ReplyInfo,
},
UnboundMod,
};
mod domain_tree;
mod nftables;
@ -34,10 +37,7 @@ mod nftables;
type Domain = SmallVec<[u8; 32]>;
type DomainSeg = SmallVec<[u8; 16]>;
#[ctor]
fn setup() {
crate::set_unbound_mod::<ExampleMod>();
}
unbound_mod::set_module!(ExampleMod);
struct IpNetDeser(IpNet);
struct IpNetVisitor;
@ -708,15 +708,15 @@ impl UnboundMod for ExampleMod {
type EnvData = ();
type QstateData = ();
fn init(_env: &mut crate::unbound::ModuleEnvMut<Self::EnvData>) -> Result<Self, ()> {
fn init(_env: &mut ModuleEnvMut<Self::EnvData>) -> Result<Self, ()> {
Self::new()
}
fn operate(
&self,
qstate: &mut crate::unbound::ModuleQstateMut<Self::QstateData>,
qstate: &mut ModuleQstateMut<Self::QstateData>,
event: ModuleEvent,
_entry: Option<&mut crate::unbound::OutboundEntryMut>,
_entry: Option<&mut OutboundEntryMut>,
) -> Option<ModuleExtState> {
match event {
ModuleEvent::New | ModuleEvent::Pass => {
@ -755,10 +755,8 @@ mod test {
use ipnet::IpNet;
use smallvec::smallvec;
use crate::{
example::{ignore, ExampleMod, IpCacheKey, IpNetDeser, DATA_PREFIX},
unbound::ModuleExtState,
};
use super::{ignore, ExampleMod, IpCacheKey, IpNetDeser, DATA_PREFIX};
use unbound_mod::unbound::ModuleExtState;
#[test]
fn test() {

View file

@ -9,7 +9,7 @@ use std::{
sync::mpsc,
};
use crate::example::{Helper, DATA_PREFIX};
use crate::{Helper, DATA_PREFIX};
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use iptrie::RTrieSet;
use mnl::mnl_sys;

View file

@ -19,7 +19,12 @@
outputs = [ "out" ];
installPhase = ''
cp ${./dummy.h} ./dummy.h
bindgen ./dummy.h -- -I $PWD > $out
opts=()
for file in **/*.h; do
opts+=(--allowlist-file "$PWD/$file")
done
bindgen --no-layout-tests "''${opts[@]}" dummy.h -- -I "$PWD" > "$out"
'';
});
unbound-mod = let
@ -28,13 +33,18 @@
in craneLib.buildPackage {
pname = "unbound-mod";
version = "0.1.0";
cargoExtraArgs = "--package example";
postPatch = ''
cp ${bindings} src/bindings.rs
ls -la
cp ${bindings} unbound-mod/src/bindings.rs
'';
src = nixpkgs.lib.cleanSourceWith {
src = ./.;
filter = path: type: lib.hasSuffix ".h" path || craneLib.filterCargoSources path type;
};
postInstall = ''
mv $out/lib/libexample.so $out/lib/libunbound_mod.so
'';
doCheck = false;
LIBMNL_LIB_DIR = "${nixpkgs.lib.getLib pkgs.libmnl}/lib";
LIBNFTNL_LIB_DIR = "${nixpkgs.lib.getLib pkgs.libnftnl}/lib";

File diff suppressed because it is too large Load diff

9
unbound-mod/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "unbound-mod"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ctor = "0.2.8"

2110
unbound-mod/src/bindings.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
#![allow(clippy::type_complexity)]
#![allow(clippy::type_complexity, clippy::missing_safety_doc)]
use std::panic::{RefUnwindSafe, UnwindSafe};
use unbound::ModuleExtState;
@ -13,12 +13,27 @@ use unbound::ModuleExtState;
clippy::nursery,
clippy::pedantic
)]
mod bindings;
#[doc(hidden)]
pub mod bindings;
mod combine;
#[cfg(feature = "example")]
mod example;
mod exports;
mod unbound;
pub mod unbound;
pub use bindings as sys;
#[doc(hidden)]
pub use ctor;
#[macro_export]
macro_rules! set_module {
($mod:ty) => {
use unbound_mod::ctor::ctor;
#[ctor]
fn _internal_module_setup() {
unbound_mod::set_unbound_mod::<$mod>();
}
};
}
pub trait UnboundMod: Send + Sync + Sized + RefUnwindSafe + UnwindSafe {
type EnvData;

View file

@ -448,32 +448,32 @@ type RrsetIdType = rrset_id_type;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ModuleEvent {
/// new query
New = 0,
New = bindings::module_ev_module_event_new,
/// query passed by other module
Pass = 1,
Pass = bindings::module_ev_module_event_pass,
/// reply inbound from server
Reply = 2,
Reply = bindings::module_ev_module_event_reply,
/// no reply, timeout or other error
NoReply = 3,
NoReply = bindings::module_ev_module_event_noreply,
/// reply is there, but capitalisation check failed
CapsFail = 4,
CapsFail = bindings::module_ev_module_event_capsfail,
/// next module is done, and its reply is awaiting you
ModDone = 5,
ModDone = bindings::module_ev_module_event_moddone,
/// error
Error = 6,
Unknown = 7,
Error = bindings::module_ev_module_event_error,
Unknown = 99,
}
impl From<module_ev> for ModuleEvent {
fn from(value: module_ev) -> Self {
match value {
0 => Self::New,
1 => Self::Pass,
2 => Self::Reply,
3 => Self::NoReply,
4 => Self::CapsFail,
5 => Self::ModDone,
6 => Self::Error,
bindings::module_ev_module_event_new => Self::New,
bindings::module_ev_module_event_pass => Self::Pass,
bindings::module_ev_module_event_reply => Self::Reply,
bindings::module_ev_module_event_noreply => Self::NoReply,
bindings::module_ev_module_event_capsfail => Self::CapsFail,
bindings::module_ev_module_event_moddone => Self::ModDone,
bindings::module_ev_module_event_error => Self::Error,
_ => Self::Unknown,
}
}