mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-03 09:04:07 +02:00
basic netns management
This commit is contained in:
commit
a4c7b3c1c9
17 changed files with 1555 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
16
.forgejo/workflows/ci.yaml
Normal file
16
.forgejo/workflows/ci.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: "CI"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://codeberg.org/icewind/attic-action@v1
|
||||
with:
|
||||
name: link
|
||||
instance: https://cache.icewind.link
|
||||
authToken: "${{ secrets.ATTIC_TOKEN }}"
|
||||
- run: nix flake check --keep-going
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
target
|
||||
/data
|
||||
.direnv
|
||||
.env
|
||||
result
|
||||
config.toml
|
||||
627
Cargo.lock
generated
Normal file
627
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "main_error"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "155db5e86c6e45ee456bf32fad5a290ee1f7151c2faca27ea27097568da67d1a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netnsd"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"main_error",
|
||||
"nix",
|
||||
"sd-notify",
|
||||
"serde",
|
||||
"serde_test",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-notify"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_test"
|
||||
version = "1.0.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "netnsd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "signal"] }
|
||||
toml = "0.9.8"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.20"
|
||||
main_error = "0.1.2"
|
||||
nix = { version = "0.30.1", features = ["mount", "sched"] }
|
||||
sd-notify = "0.4.5"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_test = "1.0.177"
|
||||
107
flake.lock
generated
Normal file
107
flake.lock
generated
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1742394900,
|
||||
"narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flakelight": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748868585,
|
||||
"narHash": "sha256-DrrbahOQAwvNM8l5EuGxxkVS7X5/S59zcG0N9ZWQFhk=",
|
||||
"owner": "nix-community",
|
||||
"repo": "flakelight",
|
||||
"rev": "dfbecd12d99c1bf82906521a6a7d5b75d2aa1ca2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "flakelight",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"mill-scale": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flakelight": [
|
||||
"flakelight"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748205441,
|
||||
"narHash": "sha256-W+UUBT/l1DSTZo5G43494mRNNspJ2i9jW2QELC9JuMQ=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "dac3b74a89cebbeb21cc6602e4a346604adbee8b",
|
||||
"revCount": 49,
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/icewind/mill-scale"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/icewind/mill-scale"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1748708770,
|
||||
"narHash": "sha256-q8jG2HJWgooWa9H0iatZqBPF3bp0504e05MevFmnFLY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a59eb7800787c926045d51b70982ae285faa2346",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-25.05",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flakelight": "flakelight",
|
||||
"mill-scale": "mill-scale",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"mill-scale",
|
||||
"flakelight",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1742697269,
|
||||
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
30
flake.nix
Normal file
30
flake.nix
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-25.05";
|
||||
flakelight = {
|
||||
url = "github:nix-community/flakelight";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
mill-scale = {
|
||||
url = "git+https://codeberg.org/icewind/mill-scale";
|
||||
inputs.flakelight.follows = "flakelight";
|
||||
};
|
||||
};
|
||||
outputs = {mill-scale, ...}:
|
||||
mill-scale ./. {
|
||||
withOverlays = [(import ./nix/overlay.nix)];
|
||||
nixosModules = {outputs, ...}: {
|
||||
default = {
|
||||
pkgs,
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
imports = [./nix/module.nix];
|
||||
config = lib.mkIf config.networking.netnsd.enable {
|
||||
networking.netnsd.package = lib.mkDefault pkgs.netnsd;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
73
nix/module.nix
Normal file
73
nix/module.nix
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.networking.netnsd;
|
||||
hasNamespaces = cfg.namespaces != {};
|
||||
format = pkgs.formats.toml {};
|
||||
configFile = format.generate "netnsd.toml" {
|
||||
inherit (cfg) namespaces;
|
||||
};
|
||||
in {
|
||||
options.networking.netnsd = {
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
description = "package to use";
|
||||
};
|
||||
|
||||
namespaces = mkOption {
|
||||
type = types.attrsOf (types.submodule ({name, ...}: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "target port inside the namespace";
|
||||
};
|
||||
forward = mkOption {
|
||||
type = types.listOf (types.submodule ({config, ...}: {
|
||||
options = {
|
||||
source = mkOption {
|
||||
type = types.oneOf[types.port types.str];
|
||||
default = config.destination;
|
||||
defaultText = "<destination>";
|
||||
description = "source port, address or socket outside the namespace";
|
||||
};
|
||||
destination = mkOption {
|
||||
type = types.oneOf[types.port types.str];
|
||||
description = "target port or address inside the namespace";
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "ports to forward into the namespace";
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "namespaces to setup";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf hasNamespaces {
|
||||
# symlink instead of passing `configFile` directly to netnsd to allow changing the config without changing the path
|
||||
environment.etc."netnsd/netnsd.toml".source = configFile;
|
||||
|
||||
|
||||
systemd.services.netcsctl = {
|
||||
reloadTriggers = [configFile];
|
||||
|
||||
wantedBy = ["multi-user.target"];
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
Type = "notify";
|
||||
ExecStart = "${getExec cfg.pkg} daemon -c /etc/netnsd/netnsd.toml";
|
||||
ExecReload = "${getExec cfg.pkg} reload";
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "full";
|
||||
ProtectHome = true;
|
||||
NoNewPrivileges = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
3
nix/overlay.nix
Normal file
3
nix/overlay.nix
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
final: prev: {
|
||||
netnsd = final.callPackage ./package.nix {};
|
||||
}
|
||||
19
nix/package.nix
Normal file
19
nix/package.nix
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
rustPlatform,
|
||||
lib,
|
||||
}: let
|
||||
inherit (lib.sources) sourceByRegex;
|
||||
inherit (builtins) fromTOML readFile;
|
||||
src = sourceByRegex ../. ["Cargo.*" "(src|templates)(/.*)?"];
|
||||
cargoPackage = (fromTOML (readFile ../Cargo.toml)).package;
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = cargoPackage.name;
|
||||
inherit (cargoPackage) version;
|
||||
|
||||
inherit src;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ../Cargo.lock;
|
||||
};
|
||||
}
|
||||
126
src/config/destination.rs
Normal file
126
src/config/destination.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
use serde::de::{Error, Unexpected, Visitor};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct ForwardDestination {
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl From<ForwardDestination> for SocketAddr {
|
||||
fn from(value: ForwardDestination) -> Self {
|
||||
value.addr
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ForwardDestination {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ForwardDestination {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ForwardDestinationVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ForwardDestinationVisitor {
|
||||
type Value = ForwardDestination;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter
|
||||
.write_str("Either a port as integer, or a string containing a socket address")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let v = v
|
||||
.try_into()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Signed(v), &self))?;
|
||||
self.visit_u16(v)
|
||||
}
|
||||
|
||||
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let ip = IpAddr::from([127, 0, 0, 1]);
|
||||
Ok(ForwardDestination {
|
||||
addr: SocketAddr::from((ip, v)),
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let v = v
|
||||
.try_into()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Unsigned(v), &self))?;
|
||||
self.visit_u16(v)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
if let Ok(port) = u16::from_str(v) {
|
||||
return self.visit_u16(port);
|
||||
}
|
||||
let addr = v
|
||||
.parse()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Str(&v), &self))?;
|
||||
Ok(ForwardDestination { addr })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ForwardDestinationVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_de() {
|
||||
use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
|
||||
|
||||
let addr_str = "127.0.0.1:80";
|
||||
let addr = SocketAddr::from_str("127.0.0.1:80").unwrap();
|
||||
fn port_addr(port: u16) -> ForwardDestination {
|
||||
ForwardDestination {
|
||||
addr: SocketAddr::new(IpAddr::from([127, 0, 0, 1]), port),
|
||||
}
|
||||
}
|
||||
|
||||
assert_de_tokens(&ForwardDestination { addr }, &[Token::String(addr_str)]);
|
||||
assert_de_tokens(&ForwardDestination { addr }, &[Token::Str(addr_str)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::Str("80")]);
|
||||
|
||||
assert_de_tokens(&port_addr(80), &[Token::U8(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::U16(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::U64(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I8(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I16(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I64(80)]);
|
||||
|
||||
assert_de_tokens_error::<ForwardDestination>(
|
||||
&[Token::I64(-80)],
|
||||
"invalid value: integer `-80`, expected Either a port as integer, or a string containing a socket address",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardDestination>(
|
||||
&[Token::U64(12345678)],
|
||||
"invalid value: integer `12345678`, expected Either a port as integer, or a string containing a socket address",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardDestination>(
|
||||
&[Token::Str("hello world")],
|
||||
"invalid value: string \"hello world\", expected Either a port as integer, or a string containing a socket address",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardDestination>(
|
||||
&[Token::Str("localhost:80")],
|
||||
"invalid value: string \"localhost:80\", expected Either a port as integer, or a string containing a socket address",
|
||||
);
|
||||
}
|
||||
109
src/config/mod.rs
Normal file
109
src/config/mod.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
mod destination;
|
||||
mod name;
|
||||
mod source;
|
||||
|
||||
pub use crate::config::destination::ForwardDestination;
|
||||
pub use crate::config::name::NamespaceName;
|
||||
pub use crate::config::source::ForwardSource;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use toml::from_str;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
path: PathBuf,
|
||||
pub namespaces: Vec<NamespaceConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Config, ConfigError> {
|
||||
let path = path.as_ref();
|
||||
let raw = read_to_string(path).map_err(|error| ConfigError::Read {
|
||||
error,
|
||||
path: path.to_owned(),
|
||||
})?;
|
||||
let config: RawConfig = from_str(&raw).map_err(|error| ConfigError::Parse {
|
||||
error,
|
||||
path: path.to_owned(),
|
||||
})?;
|
||||
Ok(config
|
||||
.validate(path)
|
||||
.map_err(|error| ConfigError::Validation {
|
||||
error,
|
||||
path: path.to_owned(),
|
||||
})?)
|
||||
}
|
||||
|
||||
pub fn reload(&self) -> Result<Config, ConfigError> {
|
||||
Self::load(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawConfig {
|
||||
#[serde(default, rename = "namespace")]
|
||||
pub namespaces: Vec<NamespaceConfig>,
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn validate(self, path: &Path) -> Result<Config, ValidationError> {
|
||||
let mut sources = HashSet::new();
|
||||
for source in self
|
||||
.namespaces
|
||||
.iter()
|
||||
.flat_map(|namespace| namespace.forward.iter())
|
||||
.map(|forward| &forward.source)
|
||||
{
|
||||
if !sources.insert(source.clone()) {
|
||||
return Err(ValidationError::DuplicateSource {
|
||||
forward_source: source.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Config {
|
||||
path: path.into(),
|
||||
namespaces: self.namespaces,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NamespaceConfig {
|
||||
pub name: NamespaceName,
|
||||
pub forward: Vec<ForwardConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ForwardConfig {
|
||||
pub source: ForwardSource,
|
||||
pub destination: ForwardDestination,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("Error while reading config from {}: {error:#}", path.display())]
|
||||
Read {
|
||||
error: std::io::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[error("Error while parsing config from {}: {error:#}", path.display())]
|
||||
Parse {
|
||||
error: toml::de::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[error("Error while validating config from {}: {error:#}", path.display())]
|
||||
Validation {
|
||||
error: ValidationError,
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ValidationError {
|
||||
#[error("Duplicate source in forwards: {forward_source}")]
|
||||
DuplicateSource { forward_source: ForwardSource },
|
||||
}
|
||||
91
src/config/name.rs
Normal file
91
src/config/name.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::path::Path;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde::de::{Error, Unexpected, Visitor};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct NamespaceName(String);
|
||||
|
||||
impl Display for NamespaceName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for NamespaceName {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for NamespaceName {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamespaceName> for String {
|
||||
fn from(value: NamespaceName) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NamespaceName {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct NamespaceNameVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for NamespaceNameVisitor {
|
||||
type Value = NamespaceName;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter
|
||||
.write_str("A valid namespace name")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
if !validate_name(v) {
|
||||
return Err(E::invalid_value(Unexpected::Str(v), &self));
|
||||
}
|
||||
Ok(NamespaceName(v.into()))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error
|
||||
{
|
||||
if !validate_name(&v) {
|
||||
return Err(E::invalid_value(Unexpected::Str(&v), &self));
|
||||
}
|
||||
Ok(NamespaceName(v))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(NamespaceNameVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a name follows the portable filename character set
|
||||
fn validate_name(name: &str) -> bool {
|
||||
if name.is_empty() {
|
||||
return false;
|
||||
}
|
||||
name.bytes().all(|b| b.is_ascii_alphanumeric() || [b'_', b'.', b'-'].contains(&b))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_de() {
|
||||
use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
|
||||
|
||||
assert_de_tokens(&NamespaceName("foo".into()), &[Token::String("foo")]);;
|
||||
|
||||
assert_de_tokens_error::<NamespaceName>(
|
||||
&[Token::String("foo/bar")],
|
||||
"invalid value: integer `-80`, expected Either a port as integer, or a string containing a socket address",
|
||||
);
|
||||
}
|
||||
143
src/config/source.rs
Normal file
143
src/config/source.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use serde::de::{Error, Unexpected, Visitor};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||
pub enum ForwardSource {
|
||||
Unix(PathBuf),
|
||||
Ip(SocketAddr),
|
||||
}
|
||||
|
||||
impl Display for ForwardSource {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ForwardSource::Unix(a) => write!(f, "{}", a.display()),
|
||||
ForwardSource::Ip(a) => write!(f, "{a}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ForwardSource {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ForwardSourceVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ForwardSourceVisitor {
|
||||
type Value = ForwardSource;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("Either a port as integer, or a string containing a socket address or unix path")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let v = v
|
||||
.try_into()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Signed(v), &self))?;
|
||||
self.visit_u16(v)
|
||||
}
|
||||
|
||||
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let ip = IpAddr::from([0, 0, 0, 0]);
|
||||
Ok(ForwardSource::Ip(SocketAddr::from((ip, v))))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
let v = v
|
||||
.try_into()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Unsigned(v), &self))?;
|
||||
self.visit_u16(v)
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
if v.starts_with('/') {
|
||||
Ok(ForwardSource::Unix(v.into()))
|
||||
} else {
|
||||
self.visit_str(&v)
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
if v.starts_with('/') {
|
||||
Ok(ForwardSource::Unix(v.into()))
|
||||
} else {
|
||||
if let Ok(port) = u16::from_str(v) {
|
||||
return self.visit_u16(port);
|
||||
}
|
||||
let addr = v
|
||||
.parse()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Str(&v), &self))?;
|
||||
Ok(ForwardSource::Ip(addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ForwardSourceVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_de() {
|
||||
use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
|
||||
|
||||
let addr_str = "127.0.0.1:80";
|
||||
let addr = SocketAddr::from_str("127.0.0.1:80").unwrap();
|
||||
fn port_addr(port: u16) -> ForwardSource {
|
||||
ForwardSource::Ip(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), port))
|
||||
}
|
||||
|
||||
assert_de_tokens(
|
||||
&ForwardSource::Unix("/test/foo".into()),
|
||||
&[Token::String("/test/foo")],
|
||||
);
|
||||
assert_de_tokens(
|
||||
&ForwardSource::Unix("/test/foo".into()),
|
||||
&[Token::Str("/test/foo")],
|
||||
);
|
||||
assert_de_tokens(&ForwardSource::Ip(addr), &[Token::String(addr_str)]);
|
||||
assert_de_tokens(&ForwardSource::Ip(addr), &[Token::Str(addr_str)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::Str("80")]);
|
||||
|
||||
assert_de_tokens(&port_addr(80), &[Token::U8(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::U16(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::U64(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I8(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I16(80)]);
|
||||
assert_de_tokens(&port_addr(80), &[Token::I64(80)]);
|
||||
|
||||
assert_de_tokens_error::<ForwardSource>(
|
||||
&[Token::I64(-80)],
|
||||
"invalid value: integer `-80`, expected Either a port as integer, or a string containing a socket address or unix path",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardSource>(
|
||||
&[Token::U64(12345678)],
|
||||
"invalid value: integer `12345678`, expected Either a port as integer, or a string containing a socket address or unix path",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardSource>(
|
||||
&[Token::Str("hello world")],
|
||||
"invalid value: string \"hello world\", expected Either a port as integer, or a string containing a socket address or unix path",
|
||||
);
|
||||
assert_de_tokens_error::<ForwardSource>(
|
||||
&[Token::Str("localhost:80")],
|
||||
"invalid value: string \"localhost:80\", expected Either a port as integer, or a string containing a socket address or unix path",
|
||||
);
|
||||
}
|
||||
55
src/daemon/mod.rs
Normal file
55
src/daemon/mod.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
mod namespace;
|
||||
|
||||
use crate::config::{Config, NamespaceName};
|
||||
use crate::daemon::namespace::{NamespaceError, NetNs};
|
||||
use main_error::MainResult;
|
||||
use sd_notify::{notify, NotifyState};
|
||||
use std::io::Error as IoError;
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::signal::ctrl_c;
|
||||
|
||||
pub fn daemon(config: Config) -> MainResult {
|
||||
let rt = Runtime::new()?;
|
||||
Ok(rt.block_on(daemon_async(config))?)
|
||||
}
|
||||
|
||||
async fn daemon_async(config: Config) -> Result<(), DaemonError> {
|
||||
for namespace in &config.namespaces {
|
||||
println!("{}:", namespace.name);
|
||||
for forward in &namespace.forward {
|
||||
println!(" {} => {}", forward.source, forward.destination);
|
||||
}
|
||||
}
|
||||
|
||||
let namespaces = config
|
||||
.namespaces
|
||||
.iter()
|
||||
.map(|ns| ActiveNamespace::new(&ns.name))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// now the namespaces are setup, we can tell systemd to start any service depending on them
|
||||
notify(true, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||
|
||||
let _ = ctrl_c().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ActiveNamespace {
|
||||
ns: NetNs,
|
||||
}
|
||||
|
||||
impl ActiveNamespace {
|
||||
pub fn new(name: &NamespaceName) -> Result<Self, DaemonError> {
|
||||
let ns = NetNs::new(name)?;
|
||||
Ok(ActiveNamespace { ns })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DaemonError {
|
||||
#[error(transparent)]
|
||||
Namespace(#[from] NamespaceError),
|
||||
#[error("Error sending notification to systemd: {0:#}")]
|
||||
Notify(IoError)
|
||||
}
|
||||
87
src/daemon/namespace.rs
Normal file
87
src/daemon/namespace.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::config::NamespaceName;
|
||||
use nix::errno::Errno;
|
||||
use nix::mount::{MsFlags, mount, umount};
|
||||
use nix::sched::{CloneFlags, unshare};
|
||||
use std::fs::{File, create_dir_all, remove_file};
|
||||
use std::io::{Error as IoError, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread::{JoinHandle, spawn};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub struct NetNs {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl NetNs {
|
||||
/// Create a new named network namespace that will be removed when dropped
|
||||
pub fn new(name: &NamespaceName) -> Result<Self, NamespaceError> {
|
||||
debug!(%name, "creating network namespace");
|
||||
let parent = Path::new("/var/run/netns");
|
||||
create_dir_all(parent).map_err(NamespaceError::Parent)?;
|
||||
let path = parent.join(name);
|
||||
let mount_path = path.clone();
|
||||
|
||||
let _ =
|
||||
File::create_new(&path).map_err(|error| NamespaceError::from_create(name, error))?;
|
||||
|
||||
let handle: JoinHandle<Result<(), NamespaceError>> = spawn(move || {
|
||||
unshare(CloneFlags::CLONE_NEWNET).map_err(NamespaceError::Unshare)?;
|
||||
mount(
|
||||
Some("/proc/self/ns/net"),
|
||||
&mount_path,
|
||||
Option::<&str>::None,
|
||||
MsFlags::MS_BIND,
|
||||
Option::<&str>::None,
|
||||
)
|
||||
.map_err(NamespaceError::from_mount)?;
|
||||
Ok(())
|
||||
});
|
||||
handle.join().unwrap()?;
|
||||
Ok(NetNs { path })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetNs {
|
||||
fn drop(&mut self) {
|
||||
let name = self.path.file_name().unwrap().to_str().unwrap();
|
||||
debug!(name, "deleting network namespace");
|
||||
if let Err(error) = umount(&self.path) {
|
||||
error!(%error, path = %self.path.display(), "Failed to unmount network namespace");
|
||||
}
|
||||
if let Err(error) = remove_file(&self.path) {
|
||||
error!(%error, path = %self.path.display(), "Failed to remove namespace file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NamespaceError {
|
||||
#[error("Failed to create parent directory for namespaces (/var/run/netns): {0:#}")]
|
||||
Parent(IoError),
|
||||
#[error("Network namespace {0} already exists")]
|
||||
AlreadyExists(NamespaceName),
|
||||
#[error("Failed to create namespace file {}: {error:#}", path.display())]
|
||||
Create { path: PathBuf, error: IoError },
|
||||
#[error("Unexpected error while creating new network namespace: {0:}")]
|
||||
Unshare(Errno),
|
||||
#[error("Unexpected error while binding new network namespace: {0:}")]
|
||||
Bind(Errno),
|
||||
}
|
||||
|
||||
impl NamespaceError {
|
||||
fn from_mount(errno: Errno) -> Self {
|
||||
// todo more specific errors?
|
||||
NamespaceError::Bind(errno)
|
||||
}
|
||||
|
||||
fn from_create(name: &NamespaceName, error: IoError) -> Self {
|
||||
match error.kind() {
|
||||
ErrorKind::AlreadyExists => NamespaceError::AlreadyExists(name.clone()),
|
||||
_ => NamespaceError::Create {
|
||||
path: Path::new("/var/run/netns").join(name),
|
||||
error,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main.rs
Normal file
43
src/main.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use std::path::{PathBuf};
|
||||
use clap::{Parser, Subcommand};
|
||||
use main_error::MainResult;
|
||||
use crate::config::Config;
|
||||
use crate::daemon::daemon;
|
||||
|
||||
mod config;
|
||||
mod daemon;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Start the netnsd daemon
|
||||
Daemon {
|
||||
/// Location of the config file
|
||||
#[clap(short, long, default_value = "/etc/netnsd/netnsd")]
|
||||
config: PathBuf,
|
||||
},
|
||||
/// Signal a running daemon to reload it's configuration
|
||||
Reload,
|
||||
}
|
||||
|
||||
fn main() -> MainResult {
|
||||
let args: Args = Args::parse();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
match args.command {
|
||||
Commands::Daemon { config } => {
|
||||
let config = Config::load(config)?;
|
||||
daemon(config)
|
||||
}
|
||||
Commands::Reload => reload()
|
||||
}
|
||||
}
|
||||
|
||||
fn reload() -> MainResult {
|
||||
todo!()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue