├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── CONTRIBUTING.md ├── codecov.yml ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── user.rs ├── ci ├── libnss_whatami.c ├── test.sh ├── test_nsncd.sh └── test_nspawn.sh ├── cla └── TSOS CLA - Individual - nsncd.pdf ├── debian ├── .gitignore ├── Cargo.toml.append ├── README.source ├── changelog ├── compat ├── control ├── copyright ├── install ├── rules └── source │ └── format ├── nsncd.service └── src ├── config.rs ├── ffi.rs ├── handlers.rs ├── handlers └── nixish.rs ├── main.rs ├── protocol.rs └── work_group.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.154.0/containers/rust/.devcontainer/base.Dockerfile 2 | 3 | FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1 4 | 5 | # [Optional] Uncomment this section to install additional packages. 6 | #RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.154.0/containers/rust 3 | { 4 | "name": "Rust", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | "runArgs": [ 9 | "--cap-add=SYS_PTRACE", 10 | "--security-opt", 11 | "seccomp=unconfined" 12 | ], 13 | "customizations": { 14 | "vscode": { 15 | // Set *default* container specific settings.json values on container create. 16 | "settings": { 17 | "terminal.integrated.profiles.linux": { 18 | "bash": { 19 | "path": "bash", 20 | "icon": "terminal-bash" 21 | }, 22 | "zsh": { 23 | "path": "zsh" 24 | }, 25 | "fish": { 26 | "path": "fish" 27 | }, 28 | "tmux": { 29 | "path": "tmux", 30 | "icon": "terminal-tmux" 31 | }, 32 | "pwsh": { 33 | "path": "pwsh", 34 | "icon": "terminal-powershell" 35 | } 36 | }, 37 | "terminal.integrated.defaultProfile.linux": "bash", 38 | "lldb.executable": "/usr/bin/lldb", 39 | // VS Code don't watch files under ./target 40 | "files.watcherExclude": { 41 | "**/target/**": true 42 | }, 43 | "git.autofetch": true, 44 | "git.branchProtection": [ 45 | "main" 46 | ] 47 | }, 48 | // Add the IDs of extensions you want installed when the container is created. 49 | "extensions": [ 50 | "matklad.rust-analyzer", 51 | "tamasfe.even-better-toml", 52 | "vadimcn.vscode-lldb", 53 | "mutantdino.resourcemonitor", 54 | "stkb.rewrap", 55 | "github.vscode-pull-request-github", 56 | "dawidd6.debian-vscode", 57 | "coolbear.systemd-unit-file", 58 | "ms-vscode.makefile-tools" 59 | ] 60 | } 61 | }, 62 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 63 | // "forwardPorts": [], 64 | // Use 'postCreateCommand' to run commands after the container is created. 65 | // "postCreateCommand": "rustc --version", 66 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 67 | "remoteUser": "vscode" 68 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | In order to accept your code contributions, please fill out the appropriate Contributor License Agreement in the `cla` folder and submit it to tsos@twosigma.com. 4 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | # Check for updates to GitHub Actions every week 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | release: 8 | types: [published] 9 | schedule: 10 | - cron: '25 3 * * *' 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | 17 | build-debian-11: 18 | runs-on: ubuntu-latest 19 | container: debian:11 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install dependencies 24 | run: | 25 | apt-get update 26 | apt-get -y install build-essential dpkg-dev ca-certificates sudo curl 27 | apt-get -y build-dep . 28 | - uses: actions-rust-lang/setup-rust-toolchain@v1 29 | with: 30 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 31 | - name: Build 32 | run: cargo build --verbose 33 | 34 | build-debian-12: 35 | runs-on: ubuntu-latest 36 | container: debian:12 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Install dependencies 41 | run: | 42 | apt-get update 43 | apt-get -y install build-essential dpkg-dev ca-certificates sudo curl 44 | apt-get -y build-dep . 45 | - uses: actions-rust-lang/setup-rust-toolchain@v1 46 | with: 47 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 48 | - name: Build 49 | run: cargo build --verbose 50 | 51 | build-debian-13: 52 | runs-on: ubuntu-latest 53 | container: debian:trixie 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Install dependencies 58 | run: | 59 | apt-get update 60 | apt-get -y install build-essential dpkg-dev ca-certificates sudo curl 61 | apt-get -y build-dep . 62 | - uses: actions-rust-lang/setup-rust-toolchain@v1 63 | with: 64 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 65 | - name: Build 66 | run: cargo build --verbose 67 | 68 | clippy: 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions-rust-lang/setup-rust-toolchain@v1 74 | with: 75 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 76 | - run: rustup component add clippy 77 | - name: rust-clippy-check 78 | uses: actions-rs/clippy-check@v1 79 | with: 80 | token: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | 83 | build-debian-package-11: 84 | runs-on: ubuntu-latest 85 | container: debian:11 86 | 87 | steps: 88 | - uses: actions/checkout@v4 89 | with: 90 | path: clone 91 | - name: Install dependencies 92 | run: | 93 | apt-get update 94 | apt-get install -y build-essential dpkg-dev ca-certificates sudo curl 95 | cd clone 96 | apt-get build-dep -y . 97 | - uses: actions-rust-lang/setup-rust-toolchain@v1 98 | with: 99 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 100 | - name: Build package 101 | run: | 102 | debian/rules vendor 103 | dpkg-buildpackage --no-sign 104 | working-directory: clone 105 | - uses: actions/upload-artifact@v3 106 | with: 107 | name: deb-package-debian-11 108 | path: | 109 | ./* 110 | !./clone/** 111 | 112 | build-debian-package-12: 113 | runs-on: ubuntu-latest 114 | container: debian:12 115 | 116 | steps: 117 | - uses: actions/checkout@v4 118 | with: 119 | path: clone 120 | - name: Install dependencies 121 | run: | 122 | apt-get update 123 | apt-get install -y build-essential dpkg-dev ca-certificates sudo curl 124 | cd clone 125 | apt-get build-dep -y . 126 | - uses: actions-rust-lang/setup-rust-toolchain@v1 127 | with: 128 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 129 | - name: Build package 130 | run: | 131 | debian/rules vendor 132 | dpkg-buildpackage --no-sign 133 | working-directory: clone 134 | - uses: actions/upload-artifact@v3 135 | with: 136 | name: deb-package-debian-12 137 | path: | 138 | ./* 139 | !./clone/** 140 | 141 | build-debian-package-13: 142 | runs-on: ubuntu-latest 143 | container: debian:trixie 144 | 145 | steps: 146 | - uses: actions/checkout@v4 147 | with: 148 | path: clone 149 | - name: Install dependencies 150 | run: | 151 | apt-get update 152 | apt-get install -y build-essential dpkg-dev ca-certificates sudo curl 153 | cd clone 154 | apt-get build-dep -y . 155 | - uses: actions-rust-lang/setup-rust-toolchain@v1 156 | with: 157 | toolchain: ${{ github.event.schedule && 'nightly' || 'stable' }} 158 | - name: Build package 159 | run: | 160 | debian/rules vendor 161 | dpkg-buildpackage --no-sign 162 | working-directory: clone 163 | - uses: actions/upload-artifact@v3 164 | with: 165 | name: deb-package-debian-13 166 | path: | 167 | ./* 168 | !./clone/** 169 | 170 | run-ci-ubuntu-latest: 171 | runs-on: ubuntu-latest 172 | needs: [build-debian-package-11, build-debian-package-12, build-debian-package-13] 173 | 174 | steps: 175 | - uses: actions/checkout@v4 176 | - uses: actions/download-artifact@v3 177 | with: 178 | name: deb-package-debian-12 179 | - name: CI 180 | run: ci/test.sh 181 | env: 182 | HAVE_SYSTEMD: "1" 183 | 184 | 185 | run-ci-debian-11: 186 | runs-on: ubuntu-latest 187 | container: debian:11 188 | needs: [build-debian-package-11] 189 | 190 | steps: 191 | - uses: actions/checkout@v4 192 | - name: Install dependencies 193 | run: | 194 | apt-get update 195 | apt-get -y install build-essential 196 | - uses: actions/download-artifact@v3 197 | with: 198 | name: deb-package-debian-11 199 | - name: CI 200 | run: ci/test.sh 201 | env: 202 | HAVE_SYSTEMD: "0" 203 | 204 | run-ci-debian-12: 205 | runs-on: ubuntu-latest 206 | container: debian:12 207 | needs: [build-debian-package-12] 208 | 209 | steps: 210 | - uses: actions/checkout@v4 211 | - name: Install dependencies 212 | run: | 213 | apt-get update 214 | apt-get -y install build-essential 215 | - uses: actions/download-artifact@v3 216 | with: 217 | name: deb-package-debian-12 218 | - name: CI 219 | run: ci/test.sh 220 | env: 221 | HAVE_SYSTEMD: "0" 222 | 223 | run-ci-debian-13: 224 | runs-on: ubuntu-latest 225 | container: debian:trixie 226 | needs: [build-debian-package-13] 227 | 228 | steps: 229 | - uses: actions/checkout@v4 230 | - name: Install dependencies 231 | run: | 232 | apt-get update 233 | apt-get -y install build-essential 234 | - uses: actions/download-artifact@v3 235 | with: 236 | name: deb-package-debian-13 237 | - name: CI 238 | run: ci/test.sh 239 | env: 240 | HAVE_SYSTEMD: "0" 241 | 242 | # This step deliberately uses a vm 243 | run-enhanced-ci-debian-11: 244 | runs-on: ubuntu-latest 245 | needs: [build-debian-package-11] 246 | 247 | steps: 248 | - uses: actions/checkout@v4 249 | - name: Install dependencies 250 | run: | 251 | sudo apt-get update 252 | sudo apt-get -y install build-essential systemd-container debootstrap 253 | - uses: actions/download-artifact@v3 254 | with: 255 | name: deb-package-debian-11 256 | - name: CI 257 | run: ci/test_nspawn.sh 258 | 259 | create-release: 260 | runs-on: ubuntu-latest 261 | needs: [run-ci-ubuntu-latest, run-ci-debian-11, run-ci-debian-12, run-ci-debian-13, run-enhanced-ci-debian-11] 262 | if: github.event.release 263 | 264 | steps: 265 | - uses: actions/download-artifact@v3 266 | with: 267 | name: deb-package-debian-11 268 | - uses: softprops/action-gh-release@v2 269 | with: 270 | files: "*" 271 | tag_name: ${{ github.event.release.tag_name }} 272 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | cobertura.xml 3 | vendor 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.8" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 25 | 26 | [[package]] 27 | name = "anyhow" 28 | version = "1.0.89" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" 31 | 32 | [[package]] 33 | name = "atoi" 34 | version = "2.0.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 37 | dependencies = [ 38 | "num-traits", 39 | ] 40 | 41 | [[package]] 42 | name = "autocfg" 43 | version = "1.4.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.6.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 52 | 53 | [[package]] 54 | name = "bumpalo" 55 | version = "3.16.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 58 | 59 | [[package]] 60 | name = "cast" 61 | version = "0.3.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "cfg_aliases" 73 | version = "0.1.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 76 | 77 | [[package]] 78 | name = "ciborium" 79 | version = "0.2.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 82 | dependencies = [ 83 | "ciborium-io", 84 | "ciborium-ll", 85 | "serde", 86 | ] 87 | 88 | [[package]] 89 | name = "ciborium-io" 90 | version = "0.2.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 93 | 94 | [[package]] 95 | name = "ciborium-ll" 96 | version = "0.2.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 99 | dependencies = [ 100 | "ciborium-io", 101 | "half", 102 | ] 103 | 104 | [[package]] 105 | name = "clap" 106 | version = "4.5.19" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" 109 | dependencies = [ 110 | "clap_builder", 111 | ] 112 | 113 | [[package]] 114 | name = "clap_builder" 115 | version = "4.5.19" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" 118 | dependencies = [ 119 | "anstyle", 120 | "clap_lex", 121 | ] 122 | 123 | [[package]] 124 | name = "clap_lex" 125 | version = "0.7.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 128 | 129 | [[package]] 130 | name = "criterion" 131 | version = "0.5.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 134 | dependencies = [ 135 | "anes", 136 | "cast", 137 | "ciborium", 138 | "clap", 139 | "criterion-plot", 140 | "is-terminal", 141 | "itertools", 142 | "num-traits", 143 | "once_cell", 144 | "oorandom", 145 | "plotters", 146 | "rayon", 147 | "regex", 148 | "serde", 149 | "serde_derive", 150 | "serde_json", 151 | "tinytemplate", 152 | "walkdir", 153 | ] 154 | 155 | [[package]] 156 | name = "criterion-plot" 157 | version = "0.5.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 160 | dependencies = [ 161 | "cast", 162 | "itertools", 163 | ] 164 | 165 | [[package]] 166 | name = "crossbeam-channel" 167 | version = "0.5.13" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 170 | dependencies = [ 171 | "crossbeam-utils", 172 | ] 173 | 174 | [[package]] 175 | name = "crossbeam-deque" 176 | version = "0.8.5" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 179 | dependencies = [ 180 | "crossbeam-epoch", 181 | "crossbeam-utils", 182 | ] 183 | 184 | [[package]] 185 | name = "crossbeam-epoch" 186 | version = "0.9.18" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 189 | dependencies = [ 190 | "crossbeam-utils", 191 | ] 192 | 193 | [[package]] 194 | name = "crossbeam-utils" 195 | version = "0.8.20" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 198 | 199 | [[package]] 200 | name = "crunchy" 201 | version = "0.2.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 204 | 205 | [[package]] 206 | name = "deranged" 207 | version = "0.3.11" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 210 | dependencies = [ 211 | "powerfmt", 212 | ] 213 | 214 | [[package]] 215 | name = "dirs-next" 216 | version = "2.0.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 219 | dependencies = [ 220 | "cfg-if", 221 | "dirs-sys-next", 222 | ] 223 | 224 | [[package]] 225 | name = "dirs-sys-next" 226 | version = "0.1.2" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 229 | dependencies = [ 230 | "libc", 231 | "redox_users", 232 | "winapi", 233 | ] 234 | 235 | [[package]] 236 | name = "dns-lookup" 237 | version = "2.0.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" 240 | dependencies = [ 241 | "cfg-if", 242 | "libc", 243 | "socket2", 244 | "windows-sys 0.48.0", 245 | ] 246 | 247 | [[package]] 248 | name = "either" 249 | version = "1.13.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 252 | 253 | [[package]] 254 | name = "getrandom" 255 | version = "0.2.15" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 258 | dependencies = [ 259 | "cfg-if", 260 | "libc", 261 | "wasi", 262 | ] 263 | 264 | [[package]] 265 | name = "half" 266 | version = "2.4.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 269 | dependencies = [ 270 | "cfg-if", 271 | "crunchy", 272 | ] 273 | 274 | [[package]] 275 | name = "hermit-abi" 276 | version = "0.4.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 279 | 280 | [[package]] 281 | name = "is-terminal" 282 | version = "0.4.13" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 285 | dependencies = [ 286 | "hermit-abi", 287 | "libc", 288 | "windows-sys 0.52.0", 289 | ] 290 | 291 | [[package]] 292 | name = "itertools" 293 | version = "0.10.5" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 296 | dependencies = [ 297 | "either", 298 | ] 299 | 300 | [[package]] 301 | name = "itoa" 302 | version = "1.0.11" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 305 | 306 | [[package]] 307 | name = "js-sys" 308 | version = "0.3.70" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 311 | dependencies = [ 312 | "wasm-bindgen", 313 | ] 314 | 315 | [[package]] 316 | name = "libc" 317 | version = "0.2.159" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" 320 | 321 | [[package]] 322 | name = "libredox" 323 | version = "0.1.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 326 | dependencies = [ 327 | "bitflags", 328 | "libc", 329 | ] 330 | 331 | [[package]] 332 | name = "lock_api" 333 | version = "0.4.12" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 336 | dependencies = [ 337 | "autocfg", 338 | "scopeguard", 339 | ] 340 | 341 | [[package]] 342 | name = "log" 343 | version = "0.4.22" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 346 | 347 | [[package]] 348 | name = "memchr" 349 | version = "2.7.4" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 352 | 353 | [[package]] 354 | name = "memoffset" 355 | version = "0.9.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 358 | dependencies = [ 359 | "autocfg", 360 | ] 361 | 362 | [[package]] 363 | name = "nix" 364 | version = "0.28.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 367 | dependencies = [ 368 | "bitflags", 369 | "cfg-if", 370 | "cfg_aliases", 371 | "libc", 372 | "memoffset", 373 | ] 374 | 375 | [[package]] 376 | name = "nsncd" 377 | version = "1.4.1" 378 | dependencies = [ 379 | "anyhow", 380 | "atoi", 381 | "criterion", 382 | "crossbeam-channel", 383 | "dns-lookup", 384 | "nix", 385 | "num-derive", 386 | "num-traits", 387 | "sd-notify", 388 | "slog", 389 | "slog-async", 390 | "slog-term", 391 | "static_assertions", 392 | "temp-env", 393 | ] 394 | 395 | [[package]] 396 | name = "num-conv" 397 | version = "0.1.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 400 | 401 | [[package]] 402 | name = "num-derive" 403 | version = "0.3.3" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 406 | dependencies = [ 407 | "proc-macro2", 408 | "quote", 409 | "syn 1.0.109", 410 | ] 411 | 412 | [[package]] 413 | name = "num-traits" 414 | version = "0.2.19" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 417 | dependencies = [ 418 | "autocfg", 419 | ] 420 | 421 | [[package]] 422 | name = "once_cell" 423 | version = "1.20.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" 426 | dependencies = [ 427 | "portable-atomic", 428 | ] 429 | 430 | [[package]] 431 | name = "oorandom" 432 | version = "11.1.4" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" 435 | 436 | [[package]] 437 | name = "parking_lot" 438 | version = "0.12.3" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 441 | dependencies = [ 442 | "lock_api", 443 | "parking_lot_core", 444 | ] 445 | 446 | [[package]] 447 | name = "parking_lot_core" 448 | version = "0.9.10" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 451 | dependencies = [ 452 | "cfg-if", 453 | "libc", 454 | "redox_syscall", 455 | "smallvec", 456 | "windows-targets 0.52.6", 457 | ] 458 | 459 | [[package]] 460 | name = "plotters" 461 | version = "0.3.7" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 464 | dependencies = [ 465 | "num-traits", 466 | "plotters-backend", 467 | "plotters-svg", 468 | "wasm-bindgen", 469 | "web-sys", 470 | ] 471 | 472 | [[package]] 473 | name = "plotters-backend" 474 | version = "0.3.7" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 477 | 478 | [[package]] 479 | name = "plotters-svg" 480 | version = "0.3.7" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 483 | dependencies = [ 484 | "plotters-backend", 485 | ] 486 | 487 | [[package]] 488 | name = "portable-atomic" 489 | version = "1.9.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 492 | 493 | [[package]] 494 | name = "powerfmt" 495 | version = "0.2.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 498 | 499 | [[package]] 500 | name = "proc-macro2" 501 | version = "1.0.86" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 504 | dependencies = [ 505 | "unicode-ident", 506 | ] 507 | 508 | [[package]] 509 | name = "quote" 510 | version = "1.0.37" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 513 | dependencies = [ 514 | "proc-macro2", 515 | ] 516 | 517 | [[package]] 518 | name = "rayon" 519 | version = "1.10.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 522 | dependencies = [ 523 | "either", 524 | "rayon-core", 525 | ] 526 | 527 | [[package]] 528 | name = "rayon-core" 529 | version = "1.12.1" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 532 | dependencies = [ 533 | "crossbeam-deque", 534 | "crossbeam-utils", 535 | ] 536 | 537 | [[package]] 538 | name = "redox_syscall" 539 | version = "0.5.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 542 | dependencies = [ 543 | "bitflags", 544 | ] 545 | 546 | [[package]] 547 | name = "redox_users" 548 | version = "0.4.6" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 551 | dependencies = [ 552 | "getrandom", 553 | "libredox", 554 | "thiserror", 555 | ] 556 | 557 | [[package]] 558 | name = "regex" 559 | version = "1.11.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" 562 | dependencies = [ 563 | "aho-corasick", 564 | "memchr", 565 | "regex-automata", 566 | "regex-syntax", 567 | ] 568 | 569 | [[package]] 570 | name = "regex-automata" 571 | version = "0.4.8" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 574 | dependencies = [ 575 | "aho-corasick", 576 | "memchr", 577 | "regex-syntax", 578 | ] 579 | 580 | [[package]] 581 | name = "regex-syntax" 582 | version = "0.8.5" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 585 | 586 | [[package]] 587 | name = "rustversion" 588 | version = "1.0.17" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 591 | 592 | [[package]] 593 | name = "ryu" 594 | version = "1.0.18" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 597 | 598 | [[package]] 599 | name = "same-file" 600 | version = "1.0.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 603 | dependencies = [ 604 | "winapi-util", 605 | ] 606 | 607 | [[package]] 608 | name = "scopeguard" 609 | version = "1.2.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 612 | 613 | [[package]] 614 | name = "sd-notify" 615 | version = "0.4.2" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "4646d6f919800cd25c50edb49438a1381e2cd4833c027e75e8897981c50b8b5e" 618 | 619 | [[package]] 620 | name = "serde" 621 | version = "1.0.210" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 624 | dependencies = [ 625 | "serde_derive", 626 | ] 627 | 628 | [[package]] 629 | name = "serde_derive" 630 | version = "1.0.210" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 633 | dependencies = [ 634 | "proc-macro2", 635 | "quote", 636 | "syn 2.0.79", 637 | ] 638 | 639 | [[package]] 640 | name = "serde_json" 641 | version = "1.0.128" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 644 | dependencies = [ 645 | "itoa", 646 | "memchr", 647 | "ryu", 648 | "serde", 649 | ] 650 | 651 | [[package]] 652 | name = "slog" 653 | version = "2.7.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" 656 | 657 | [[package]] 658 | name = "slog-async" 659 | version = "2.8.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" 662 | dependencies = [ 663 | "crossbeam-channel", 664 | "slog", 665 | "take_mut", 666 | "thread_local", 667 | ] 668 | 669 | [[package]] 670 | name = "slog-term" 671 | version = "2.9.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" 674 | dependencies = [ 675 | "is-terminal", 676 | "slog", 677 | "term", 678 | "thread_local", 679 | "time", 680 | ] 681 | 682 | [[package]] 683 | name = "smallvec" 684 | version = "1.13.2" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 687 | 688 | [[package]] 689 | name = "socket2" 690 | version = "0.5.7" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 693 | dependencies = [ 694 | "libc", 695 | "windows-sys 0.52.0", 696 | ] 697 | 698 | [[package]] 699 | name = "static_assertions" 700 | version = "1.1.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 703 | 704 | [[package]] 705 | name = "syn" 706 | version = "1.0.109" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 709 | dependencies = [ 710 | "proc-macro2", 711 | "quote", 712 | "unicode-ident", 713 | ] 714 | 715 | [[package]] 716 | name = "syn" 717 | version = "2.0.79" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" 720 | dependencies = [ 721 | "proc-macro2", 722 | "quote", 723 | "unicode-ident", 724 | ] 725 | 726 | [[package]] 727 | name = "take_mut" 728 | version = "0.2.2" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" 731 | 732 | [[package]] 733 | name = "temp-env" 734 | version = "0.3.6" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" 737 | dependencies = [ 738 | "parking_lot", 739 | ] 740 | 741 | [[package]] 742 | name = "term" 743 | version = "0.7.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 746 | dependencies = [ 747 | "dirs-next", 748 | "rustversion", 749 | "winapi", 750 | ] 751 | 752 | [[package]] 753 | name = "thiserror" 754 | version = "1.0.64" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 757 | dependencies = [ 758 | "thiserror-impl", 759 | ] 760 | 761 | [[package]] 762 | name = "thiserror-impl" 763 | version = "1.0.64" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 766 | dependencies = [ 767 | "proc-macro2", 768 | "quote", 769 | "syn 2.0.79", 770 | ] 771 | 772 | [[package]] 773 | name = "thread_local" 774 | version = "1.1.8" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 777 | dependencies = [ 778 | "cfg-if", 779 | "once_cell", 780 | ] 781 | 782 | [[package]] 783 | name = "time" 784 | version = "0.3.36" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 787 | dependencies = [ 788 | "deranged", 789 | "itoa", 790 | "num-conv", 791 | "powerfmt", 792 | "serde", 793 | "time-core", 794 | "time-macros", 795 | ] 796 | 797 | [[package]] 798 | name = "time-core" 799 | version = "0.1.2" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 802 | 803 | [[package]] 804 | name = "time-macros" 805 | version = "0.2.18" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 808 | dependencies = [ 809 | "num-conv", 810 | "time-core", 811 | ] 812 | 813 | [[package]] 814 | name = "tinytemplate" 815 | version = "1.2.1" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 818 | dependencies = [ 819 | "serde", 820 | "serde_json", 821 | ] 822 | 823 | [[package]] 824 | name = "unicode-ident" 825 | version = "1.0.13" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 828 | 829 | [[package]] 830 | name = "walkdir" 831 | version = "2.5.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 834 | dependencies = [ 835 | "same-file", 836 | "winapi-util", 837 | ] 838 | 839 | [[package]] 840 | name = "wasi" 841 | version = "0.11.0+wasi-snapshot-preview1" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 844 | 845 | [[package]] 846 | name = "wasm-bindgen" 847 | version = "0.2.93" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 850 | dependencies = [ 851 | "cfg-if", 852 | "once_cell", 853 | "wasm-bindgen-macro", 854 | ] 855 | 856 | [[package]] 857 | name = "wasm-bindgen-backend" 858 | version = "0.2.93" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 861 | dependencies = [ 862 | "bumpalo", 863 | "log", 864 | "once_cell", 865 | "proc-macro2", 866 | "quote", 867 | "syn 2.0.79", 868 | "wasm-bindgen-shared", 869 | ] 870 | 871 | [[package]] 872 | name = "wasm-bindgen-macro" 873 | version = "0.2.93" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 876 | dependencies = [ 877 | "quote", 878 | "wasm-bindgen-macro-support", 879 | ] 880 | 881 | [[package]] 882 | name = "wasm-bindgen-macro-support" 883 | version = "0.2.93" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 886 | dependencies = [ 887 | "proc-macro2", 888 | "quote", 889 | "syn 2.0.79", 890 | "wasm-bindgen-backend", 891 | "wasm-bindgen-shared", 892 | ] 893 | 894 | [[package]] 895 | name = "wasm-bindgen-shared" 896 | version = "0.2.93" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 899 | 900 | [[package]] 901 | name = "web-sys" 902 | version = "0.3.70" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" 905 | dependencies = [ 906 | "js-sys", 907 | "wasm-bindgen", 908 | ] 909 | 910 | [[package]] 911 | name = "winapi" 912 | version = "0.3.9" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 915 | dependencies = [ 916 | "winapi-i686-pc-windows-gnu", 917 | "winapi-x86_64-pc-windows-gnu", 918 | ] 919 | 920 | [[package]] 921 | name = "winapi-i686-pc-windows-gnu" 922 | version = "0.4.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 925 | 926 | [[package]] 927 | name = "winapi-util" 928 | version = "0.1.9" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 931 | dependencies = [ 932 | "windows-sys 0.59.0", 933 | ] 934 | 935 | [[package]] 936 | name = "winapi-x86_64-pc-windows-gnu" 937 | version = "0.4.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 940 | 941 | [[package]] 942 | name = "windows-sys" 943 | version = "0.48.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 946 | dependencies = [ 947 | "windows-targets 0.48.5", 948 | ] 949 | 950 | [[package]] 951 | name = "windows-sys" 952 | version = "0.52.0" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 955 | dependencies = [ 956 | "windows-targets 0.52.6", 957 | ] 958 | 959 | [[package]] 960 | name = "windows-sys" 961 | version = "0.59.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 964 | dependencies = [ 965 | "windows-targets 0.52.6", 966 | ] 967 | 968 | [[package]] 969 | name = "windows-targets" 970 | version = "0.48.5" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 973 | dependencies = [ 974 | "windows_aarch64_gnullvm 0.48.5", 975 | "windows_aarch64_msvc 0.48.5", 976 | "windows_i686_gnu 0.48.5", 977 | "windows_i686_msvc 0.48.5", 978 | "windows_x86_64_gnu 0.48.5", 979 | "windows_x86_64_gnullvm 0.48.5", 980 | "windows_x86_64_msvc 0.48.5", 981 | ] 982 | 983 | [[package]] 984 | name = "windows-targets" 985 | version = "0.52.6" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 988 | dependencies = [ 989 | "windows_aarch64_gnullvm 0.52.6", 990 | "windows_aarch64_msvc 0.52.6", 991 | "windows_i686_gnu 0.52.6", 992 | "windows_i686_gnullvm", 993 | "windows_i686_msvc 0.52.6", 994 | "windows_x86_64_gnu 0.52.6", 995 | "windows_x86_64_gnullvm 0.52.6", 996 | "windows_x86_64_msvc 0.52.6", 997 | ] 998 | 999 | [[package]] 1000 | name = "windows_aarch64_gnullvm" 1001 | version = "0.48.5" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1004 | 1005 | [[package]] 1006 | name = "windows_aarch64_gnullvm" 1007 | version = "0.52.6" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1010 | 1011 | [[package]] 1012 | name = "windows_aarch64_msvc" 1013 | version = "0.48.5" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1016 | 1017 | [[package]] 1018 | name = "windows_aarch64_msvc" 1019 | version = "0.52.6" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1022 | 1023 | [[package]] 1024 | name = "windows_i686_gnu" 1025 | version = "0.48.5" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1028 | 1029 | [[package]] 1030 | name = "windows_i686_gnu" 1031 | version = "0.52.6" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1034 | 1035 | [[package]] 1036 | name = "windows_i686_gnullvm" 1037 | version = "0.52.6" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1040 | 1041 | [[package]] 1042 | name = "windows_i686_msvc" 1043 | version = "0.48.5" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1046 | 1047 | [[package]] 1048 | name = "windows_i686_msvc" 1049 | version = "0.52.6" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1052 | 1053 | [[package]] 1054 | name = "windows_x86_64_gnu" 1055 | version = "0.48.5" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1058 | 1059 | [[package]] 1060 | name = "windows_x86_64_gnu" 1061 | version = "0.52.6" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1064 | 1065 | [[package]] 1066 | name = "windows_x86_64_gnullvm" 1067 | version = "0.48.5" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1070 | 1071 | [[package]] 1072 | name = "windows_x86_64_gnullvm" 1073 | version = "0.52.6" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1076 | 1077 | [[package]] 1078 | name = "windows_x86_64_msvc" 1079 | version = "0.48.5" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1082 | 1083 | [[package]] 1084 | name = "windows_x86_64_msvc" 1085 | version = "0.52.6" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1088 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nsncd" 3 | version = "1.4.1" 4 | authors = [ 5 | "Ben Linsay ", 6 | "Geoffrey Thomas ", 7 | "Leif Walsh ", 8 | ] 9 | edition = "2018" 10 | description = "The name service non-caching daemon" 11 | readme = "README.md" 12 | repository = "https://github.com/twosigma/nsncd" 13 | license = "Apache-2.0" 14 | 15 | [dependencies] 16 | anyhow = "^1.0" 17 | atoi = "^2.0" 18 | slog = "^2.7" 19 | slog-async = "^2.8" 20 | slog-term = "^2.9" 21 | crossbeam-channel = "^0.5" 22 | nix = { version = "^0.28", features = ["socket", "user"]} 23 | num-derive = "^0.3" 24 | num-traits = "^0.2" 25 | sd-notify = "^0.4" 26 | static_assertions = "1.1.0" 27 | dns-lookup = "2.0.4" 28 | 29 | [dev-dependencies] 30 | criterion = "^0.5" 31 | temp-env = "^0.3" 32 | 33 | [[bench]] 34 | name = "user" 35 | harness = false 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nsncd - the name service non-caching daemon 2 | 3 | `nsncd` is a nscd-compatible daemon that proxies lookups, without caching. 4 | 5 | ## Why? 6 | 7 | `nsncd` can be used in situations where you want to make an application use nss 8 | plugins available to a different libc than the one the application will load. 9 | Since most (all?) libc implementations will try to use `/var/run/nscd/socket` if 10 | it exists, you can make all lookups on a machine attempt to use the libc that 11 | nsncd is running with (and any nss plugins available to it), regardless of the 12 | libc used by a particular application. 13 | 14 | It is also a fairly minimal and clean implementation of (a part of) the `nscd` 15 | protocol, which is otherwise only really documented in implementations of libc, 16 | and mailing lists. 17 | 18 | ## Installing 19 | 20 | Just run the `nsncd` binary and it will listen at `/var/run/nscd/socket`. 21 | There's a simple `systemd` unit file, too. 22 | 23 | If you're on a Debian-based system, you can use the provided Debian package to 24 | install `nsncd` to run under `systemd`. See `debian/README.source` for how to 25 | build it - we use a few Rust crates that aren't packaged for stable Debian 26 | releases. 27 | 28 | ## Configuration 29 | 30 | `nsncd` looks in its environment for configuration. 31 | 32 | There are two integer variables we pay attention to: `NSNCD_WORKER_COUNT` and 33 | `NSNCD_HANDOFF_TIMEOUT`. Both must be positive (non-zero), and the timeout is 34 | in seconds. 35 | 36 | We also pay attention to variables `NSNCD_IGNORE_` where `` 37 | is one of the database names from `nsswitch.conf(5)`, capitalized: 38 | 39 | - NSNCD_IGNORE_GROUP 40 | - NSNCD_IGNORE_HOSTS 41 | - NSNCD_IGNORE_INITGROUPS 42 | - NSNCD_IGNORE_NETGROUP 43 | - NSNCD_IGNORE_PASSWD 44 | - NSNCD_IGNORE_SERVICES 45 | 46 | These variables must be either `true` or `false`. The default is `false` (don't 47 | ignore any requests). If one of these variables is set to true, `nsncd` will 48 | not respond to the requests related to that database. 49 | 50 | Some request types may be ignored by the implementation (e.g. the ones that 51 | request a file descriptor pointing into internal cache structures). 52 | 53 | `NSNCD_SOCKET_PATH` may be set to override the default location of the socket. 54 | 55 | ## Bug Reports and Contributions 56 | 57 | Please create GitHub issues and/or pull requests. 58 | 59 | ## License 60 | 61 | `nsncd` is licensed under the [Apache License 2.0](./LICENSE). 62 | -------------------------------------------------------------------------------- /benches/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 18 | use nix::unistd::{Uid, User}; 19 | 20 | pub fn criterion_benchmark(c: &mut Criterion) { 21 | c.bench_function("User::from_uid", |b| { 22 | b.iter(|| User::from_uid(Uid::from_raw(black_box(1000)))) 23 | }); 24 | } 25 | 26 | criterion_group!(benches, criterion_benchmark); 27 | criterion_main!(benches); 28 | -------------------------------------------------------------------------------- /ci/libnss_whatami.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | enum nss_status 26 | _nss_whatami_getpwnam_r(const char *name, struct passwd *result, char *buffer, size_t buflen, int *errnop) 27 | { 28 | if (strcmp(name, "whatami") == 0 || strncmp(name, "am_i_", 5) == 0) { 29 | if (buflen < 16) { 30 | *errnop = ERANGE; 31 | return NSS_STATUS_TRYAGAIN; 32 | } 33 | prctl(PR_GET_NAME, buffer); 34 | result->pw_name = "whatami"; 35 | result->pw_passwd = "*"; 36 | result->pw_uid = 65534; 37 | result->pw_gid = 65534; 38 | result->pw_gecos = buffer; 39 | result->pw_dir = "/"; 40 | result->pw_shell = "/sbin/nologin"; 41 | return NSS_STATUS_SUCCESS; 42 | } else { 43 | return NSS_STATUS_NOTFOUND; 44 | } 45 | } 46 | 47 | enum nss_status 48 | _nss_whatami_initgroups_dyn(const char *user, gid_t group, long int *start, long int *size, gid_t **groups, long int limit, int *errnop) 49 | { 50 | char buffer[21] = "am_i_"; 51 | prctl(PR_GET_NAME, buffer + 5); 52 | if (strcmp(user, buffer) != 0) { 53 | return NSS_STATUS_SUCCESS; 54 | } 55 | 56 | if (*size - *start < 20) { 57 | if (limit > 0 && *size + 20 > limit) { 58 | *errnop = ERANGE; 59 | return NSS_STATUS_TRYAGAIN; 60 | } 61 | gid_t *newgroups = realloc(*groups, (*size + 20) * sizeof(**groups)); 62 | if (newgroups == NULL) { 63 | *errnop = ENOMEM; 64 | return NSS_STATUS_TRYAGAIN; 65 | } 66 | *groups = newgroups; 67 | *size += 20; 68 | } 69 | 70 | for (int i = 0; i < 20; i++) { 71 | (*groups)[*start + i] = 100001 + i; 72 | } 73 | *start += 20; 74 | 75 | return NSS_STATUS_SUCCESS; 76 | } 77 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2020-2023 Two Sigma Open Source, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -ex 18 | 19 | gcc -fPIC -shared -o ci/libnss_whatami.so.2 ci/libnss_whatami.c 20 | if [ "${HAVE_SYSTEMD}" = "1" ]; then 21 | sudo cp ci/libnss_whatami.so.2 /lib 22 | sudo sed -i 's/\(passwd\|group\):/& whatami/' /etc/nsswitch.conf 23 | sudo dpkg -i nsncd*.deb 24 | else 25 | cp ci/libnss_whatami.so.2 /lib 26 | sed -i 's/\(passwd\|group\):/& whatami/' /etc/nsswitch.conf 27 | dpkg -i nsncd*.deb 28 | /usr/lib/nsncd & 29 | NSNCD_PID=$! 30 | trap "kill $NSNCD_PID" EXIT 31 | sleep 1 # Give it a moment to start up 32 | fi 33 | getent passwd whatami | grep nsncd 34 | getent initgroups am_i_nsncd | grep '100001.*100020' 35 | -------------------------------------------------------------------------------- /ci/test_nsncd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # this script tests sample calls inside a container 4 | # nsncd has to be running outside, with suitable test data configured 5 | 6 | rc=0 7 | 8 | # basic lookups 9 | getent passwd nsncdtest || rc=1 10 | getent group nsncdtest || rc=1 11 | 12 | # we expect all of these to succeed 13 | for i in $(seq 1 100); do 14 | getent services 65000 || rc=1 15 | getent services 65000/tcp || rc=1 16 | getent services 65000/udp || rc=1 17 | getent services foo1/tcp || rc=1 18 | getent services foo1/udp || rc=1 19 | getent services bufresize/tcp > /dev/null|| rc=1 20 | netgroup trusted-machines || rc=1 21 | getent netgroup trusted-machines || rc=1 22 | innetgr -h machine1 trusted-machines || rc=1 23 | innetgr -u user1 trusted-machines || rc=1 24 | innetgr -d domain1 trusted-machines || rc=1 25 | innetgr -h machine1 -u user1 -d domain1 trusted-machines || rc=1 26 | done 27 | 28 | exit ${rc} 29 | -------------------------------------------------------------------------------- /ci/test_nspawn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo debootstrap stable /stable-chroot http://deb.debian.org/debian/ &> /dev/null 4 | sudo dpkg -i nsncd*.deb 5 | 6 | sdns="sudo systemd-nspawn --quiet --no-pager --bind-ro /var/run/nscd/socket:/var/run/nscd/socket -D /stable-chroot" 7 | 8 | # Install the tooling required for netgroup and innetgr 9 | # Ensure nsswitch knows to read files for netgroup 10 | ${sdns} apt-get update 11 | ${sdns} apt-get install ng-utils 12 | ${sdns} sed '/netgroup/d' -i /etc/nsswitch.conf 13 | ${sdns} sed '$ a netgroup: files' -i /etc/nsswitch.conf 14 | 15 | # Similar nsswitch config for the host system so nsncd can access our test data 16 | sudo sed '/netgroup/d' -i /etc/nsswitch.conf 17 | sudo sed '$ a netgroup: files' -i /etc/nsswitch.conf 18 | 19 | rc=0 20 | 21 | sudo useradd nsncdtest 22 | cp /etc/services ./services 23 | 24 | # simple service lookups 25 | echo -e "foo1\t65000/tcp" >> services 26 | echo -e "foo1\t65000/udp" >> services 27 | # huge service lookup to exercise buffer resize 28 | echo -en "bufresize\t65001/tcp " >> services 29 | for i in $(seq 1000); do echo -n "alias${i} "; done >> services 30 | echo "" >> services 31 | sudo mv services /etc/services 32 | 33 | tail -5 /etc/services 34 | echo -e "trusted-machines (machine1,user1,domain1), (machine2,user2,domain2), (machine3,user3,domain3)\n" | sudo tee -a /etc/netgroup 35 | 36 | # copy in and execute tests inside the chroot 37 | sudo cp ci/test_nsncd.sh /stable-chroot/ 38 | sudo chmod a+x /stable-chroot/test_nsncd.sh 39 | ${sdns} /test_nsncd.sh 40 | 41 | -------------------------------------------------------------------------------- /cla/TSOS CLA - Individual - nsncd.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twosigma/nsncd/81615a8473d8bf3e422a03407e9fcf42712094ed/cla/TSOS CLA - Individual - nsncd.pdf -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | .debhelper/ 2 | debhelper-build-stamp 3 | files 4 | *.substvars 5 | *.debhelper 6 | nsncd/ 7 | -------------------------------------------------------------------------------- /debian/Cargo.toml.append: -------------------------------------------------------------------------------- 1 | 2 | 3 | [patch.crates-io] 4 | syn = { path = "vendor/syn" } 5 | -------------------------------------------------------------------------------- /debian/README.source: -------------------------------------------------------------------------------- 1 | Because Debian Stretch does not have a complete Rust packaging 2 | environment yet, we build the Debian package by first using "cargo 3 | vendor" to download the dependencies. To run this automatically and set 4 | things up, manually run 5 | 6 | debian/rules vendor 7 | 8 | in an environment with internet accesss. 9 | 10 | You can commit the resulting changes to your local clone, if you build 11 | packages in CI from git sources, or you can build a source package. (You 12 | may want to first run "debian/rules build clean" to trigger a small 13 | change to Cargo.toml, too.) The resulting git repo / source package will 14 | build like an ordinary Debian package, without needing to access the 15 | internet. 16 | 17 | The "debian/rules vendor" target does the following: 18 | - Runs "cargo vendor" and sets up Cargo configuration 19 | - Works around an issue where the "syn" crate includes a .gitignore 20 | file, which is automatically ignored by dpkg-source when building the 21 | source package 22 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | nsncd (1.5.1) unstable; urgency=low 2 | 3 | * Correctly handle lookups of missing netgroups 4 | * Getservbyport should refuse port 0 5 | 6 | -- James Morris Wed, 23 Oct 2024 06:25:31 -0400 7 | 8 | nsncd (1.5) unstable; urgency=low 9 | 10 | * Support netgroup and service lookups 11 | 12 | -- James Morris Fri, 11 Oct 2024 15:58:51 -0400 13 | 14 | nsncd (1.4.1) unstable; urgency=low 15 | 16 | * Update to stable rust (1.69.0) and build on Debian 10 with its 17 | glibc version requirement (#61). 18 | 19 | -- Leif Walsh Thu, 04 May 2023 21:44:00 -0400 20 | 21 | nsncd (1.4) unstable; urgency=low 22 | 23 | * Added environment-based runtime configuration (#51). 24 | * Updating how debs are released. 25 | 26 | -- Leif Walsh Sun, 16 Apr 2023 20:14:00 -0400 27 | 28 | nsncd (1.3) unstable; urgency=medium 29 | 30 | * Notify systemd of startup readiness via sd_notify and Type=notify. 31 | 32 | -- Geoffrey Thomas Tue, 18 Oct 2021 11:46:08 -0400 33 | 34 | nsncd (1.2.1) unstable; urgency=medium 35 | 36 | * Bump nix dependency to 0.21.2 to pick up fix for nix-rust/nix#1541 37 | aka RUSTSEC-2021-0119, memory corruption when using initgroups on a 38 | user in more than 16 groups. 39 | 40 | -- Geoffrey Thomas Thu, 07 Oct 2021 17:30:16 -0400 41 | 42 | nsncd (1.2) unstable; urgency=medium 43 | 44 | * Add initgroups support. 45 | * Use a fixed pool of threads to handle incoming requests. 46 | * Improve logging and do not log dropped client connections (which 47 | happen in normal operation). 48 | * Use nix v0.21, which increases the maximum passwd/group buffer size 49 | from 16kB to 1MB. 50 | 51 | -- Geoffrey Thomas Wed, 11 Aug 2021 18:20:31 -0400 52 | 53 | nsncd (1.1) unstable; urgency=medium 54 | 55 | * Handle requests with internal NULs (fixes twosigma/nsncd#7). 56 | * Remove systemd socket activation (fixes twosigma/nsncd#9, internal 57 | issue SPDE-8114). 58 | 59 | -- Geoffrey Thomas Mon, 08 Mar 2021 14:06:57 -0500 60 | 61 | nsncd (1.0) unstable; urgency=medium 62 | 63 | * Initial release. 64 | 65 | -- Geoffrey Thomas Tue, 19 Jan 2021 20:41:41 -0500 66 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nsncd 2 | Section: misc 3 | Priority: optional 4 | Maintainer: Geoffrey Thomas 5 | Build-Depends: debhelper (>= 12), cargo, pkg-config 6 | Standards-Version: 3.9.8 7 | Homepage: https://github.com/twosigma/nsncd 8 | VCS-Git: https://github.com/twosigma/nsncd 9 | VCS-Browser: https://github.com/twosigma/nsncd 10 | XC-Multidist: yes 11 | Rules-Requires-Root: no 12 | 13 | Package: nsncd 14 | Architecture: any 15 | Depends: ${misc:Depends}, ${shlibs:Depends} 16 | Description: Name service non-caching daemon 17 | nsncd implements the NSCD (name-service caching daemon) protocol to 18 | provide out-of-process NSS lookups but does not implement caching. 19 | . 20 | It is designed to provide high-performance NSS lookups for programs 21 | that are not using the system libc, while providing semantics as if 22 | NSCD were not being used. 23 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/twosigma/nsncd 3 | 4 | Files: * 5 | Copyright: 2020 Two Sigma Open Source, LLC 6 | License: Apache-2.0 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | . 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | . 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | . 19 | On Debian systems, the full text of the Apache License 2.0 can be found 20 | in the file `/usr/share/common-licenses/Apache-2.0'. 21 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | target/release/nsncd usr/lib 2 | *.service lib/systemd/system 3 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | override_dh_auto_build: 7 | cargo build --release 8 | 9 | override_dh_auto_clean: 10 | cargo clean 11 | 12 | # We use `__nss_disable_nscd` which is `GLIBC_PRIVATE`, which means the debian 13 | # tools don't know how to pick a minimum version of libc6 for it. It instead 14 | # picks a version range like `libc6 (>> 2.31), libc6 (<< 2.32)` if built on a 15 | # system with 2.31. 16 | # 17 | # The effect of this that we don't like is that we can't install the .deb on an 18 | # OS with a newer libc than the build machine, because that version bound is 19 | # strict. 20 | # 21 | # This rule somewhat rudely edits them out of substvars after they're generated. 22 | # We're taking a chance here that glibc isn't going to delete or change 23 | # `__nss_disable_nscd` soon in the future. 24 | # 25 | # See https://github.com/twosigma/nsncd/pull/61#issuecomment-1529166183 for the 26 | # original discovery of this. 27 | override_dh_shlibdeps: 28 | dh_shlibdeps 29 | sed -i -e '/^shlibs:Depends=/s/, libc6 (\(<<\|>>\) [0-9\.]*)//g' debian/nsncd.substvars 30 | 31 | # See README.source. 32 | vendor: 33 | mkdir -p .cargo 34 | cargo vendor > .cargo/config 35 | cat debian/Cargo.toml.append >> Cargo.toml 36 | echo 'nsncd: source-is-missing vendor/*' > debian/source/lintian-overrides 37 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /nsncd.service: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Two Sigma Open Source, LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [Unit] 16 | Description=name-service non-caching daemon 17 | 18 | [Service] 19 | ExecStart=/usr/lib/nsncd 20 | Restart=always 21 | Type=notify 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022-2023 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //! Configuration for nsncd. 18 | 19 | use std::time::Duration; 20 | use std::{collections::BTreeMap, env}; 21 | 22 | use anyhow::{Context, Result}; 23 | use num_traits::FromPrimitive; 24 | use static_assertions::const_assert; 25 | 26 | use super::protocol::RequestType; 27 | 28 | /// Size of the bitset for request types. Smaller values tend to exhibit worse 29 | /// cache performance in some quick benchmarks: 30 | /// https://gist.github.com/blinsay/3d233a09c59c083d8d27ccba4e322f04 31 | const BITSET_SIZE: usize = 256; 32 | const_assert!((RequestType::LASTREQ as usize) < BITSET_SIZE); 33 | 34 | #[derive(Clone, Copy)] 35 | pub struct RequestTypeSet { 36 | bits: [bool; BITSET_SIZE], 37 | } 38 | 39 | impl RequestTypeSet { 40 | pub fn new() -> Self { 41 | Self { 42 | bits: [Default::default(); BITSET_SIZE], 43 | } 44 | } 45 | 46 | pub fn insert(&mut self, val: &RequestType) -> bool { 47 | let val = *val as usize; 48 | if self.bits[val] { 49 | false 50 | } else { 51 | self.bits[val] = true; 52 | true 53 | } 54 | } 55 | 56 | pub fn contains(&self, val: &RequestType) -> bool { 57 | let val = *val as usize; 58 | self.bits[val] 59 | } 60 | } 61 | 62 | impl Default for RequestTypeSet { 63 | fn default() -> Self { 64 | Self::new() 65 | } 66 | } 67 | 68 | impl std::fmt::Debug for RequestTypeSet { 69 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 70 | let mut f = f.debug_set(); 71 | for i in 0..(RequestType::LASTREQ as i32) { 72 | let ty = &FromPrimitive::from_i32(i).unwrap(); 73 | if self.contains(ty) { 74 | f.entry(ty); 75 | } 76 | } 77 | f.finish() 78 | } 79 | } 80 | 81 | #[derive(Clone, Copy, Debug)] 82 | pub struct Config { 83 | pub ignored_request_types: RequestTypeSet, 84 | pub worker_count: usize, 85 | pub handoff_timeout: Duration, 86 | } 87 | 88 | /// Mapping from nsswitch.conf "database" name to the request types related to 89 | /// that database. 90 | const OPS_BY_DATABASE: &[(&str, &[RequestType])] = &[ 91 | ( 92 | "group", 93 | &[RequestType::GETGRBYNAME, RequestType::GETGRBYGID], 94 | ), 95 | ( 96 | "hosts", 97 | &[ 98 | RequestType::GETHOSTBYADDR, 99 | RequestType::GETHOSTBYADDRv6, 100 | RequestType::GETHOSTBYNAME, 101 | RequestType::GETHOSTBYNAMEv6, 102 | RequestType::GETAI, 103 | ], 104 | ), 105 | ("initgroups", &[RequestType::INITGROUPS]), 106 | ( 107 | "netgroup", 108 | &[RequestType::GETNETGRENT, RequestType::INNETGR], 109 | ), 110 | ( 111 | "passwd", 112 | &[RequestType::GETPWBYNAME, RequestType::GETPWBYUID], 113 | ), 114 | ( 115 | "services", 116 | &[RequestType::GETSERVBYNAME, RequestType::GETSERVBYPORT], 117 | ), 118 | ]; 119 | 120 | impl Config { 121 | /// Parse config out of the environment. 122 | /// 123 | /// There are two integer variables we pay attention to: 124 | /// `NSNCD_WORKER_COUNT` and `NSNCD_HANDOFF_TIMEOUT`. Both must be positive 125 | /// (non-zero). 126 | /// 127 | /// We also pay attention to variables `NSNCD_IGNORE_` where 128 | /// `` is one of the database names from `nsswitch.conf(5)`, 129 | /// capitalized: 130 | /// 131 | /// - NSNCD_IGNORE_GROUP 132 | /// - NSNCD_IGNORE_HOSTS 133 | /// - NSNCD_IGNORE_INITGROUPS 134 | /// - NSNCD_IGNORE_NETGROUP 135 | /// - NSNCD_IGNORE_PASSWD 136 | /// - NSNCD_IGNORE_SERVICES 137 | /// 138 | /// These variables must be either `true` or `false`. The default is 139 | /// `false` (don't ignore any requests). If one of these variables is set 140 | /// to true, `nsncd` will not respond to the requests related to that 141 | /// database. 142 | /// 143 | /// Some request types may be ignored by the implementation (e.g. the ones 144 | /// that request a file descriptor pointing into internal cache 145 | /// structures). 146 | pub fn from_env() -> Result { 147 | let ops_map = { 148 | let mut ops_map = BTreeMap::new(); 149 | for (op_group, types) in OPS_BY_DATABASE.iter() { 150 | ops_map.insert(op_group.to_uppercase().into_boxed_str(), *types); 151 | } 152 | ops_map 153 | }; 154 | 155 | let mut ignored_request_types = RequestTypeSet::new(); 156 | 157 | for (key, value) in env::vars() { 158 | if let Some(op_group) = key.strip_prefix("NSNCD_IGNORE_") { 159 | let types = ops_map.get(op_group).ok_or_else(|| { 160 | let groups = ops_map.keys().map(|s| &**s).collect::>().join(", "); 161 | anyhow::format_err!("Unknown group '{}'. Choose from: {}", op_group, groups) 162 | })?; 163 | let value = value 164 | .parse() 165 | .with_context(|| format!("parsing bool from {}", value))?; 166 | if value { 167 | for ty in types.iter() { 168 | ignored_request_types.insert(ty); 169 | } 170 | } 171 | } 172 | } 173 | 174 | Ok(Self { 175 | ignored_request_types, 176 | worker_count: env_positive_usize("NSNCD_WORKER_COUNT", 8)?, 177 | handoff_timeout: Duration::from_secs( 178 | env_positive_usize("NSNCD_HANDOFF_TIMEOUT", 3)? as u64 179 | ), 180 | }) 181 | } 182 | 183 | pub fn should_ignore(&self, ty: &RequestType) -> bool { 184 | self.ignored_request_types.contains(ty) 185 | } 186 | } 187 | 188 | impl Default for Config { 189 | fn default() -> Self { 190 | Self { 191 | worker_count: 8, 192 | handoff_timeout: Duration::from_secs(3), 193 | ignored_request_types: Default::default(), 194 | } 195 | } 196 | } 197 | 198 | fn env_positive_usize(var: &str, default: usize) -> Result { 199 | let s = match env::var(var) { 200 | Ok(s) => s, 201 | Err(_) => return Ok(default), 202 | }; 203 | let val = s 204 | .parse() 205 | .with_context(|| format!("parsing int from {}", s))?; 206 | if val > 0 { 207 | Ok(val) 208 | } else { 209 | Err(anyhow::format_err!("variable {} cannot be 0", var)) 210 | } 211 | } 212 | 213 | #[cfg(test)] 214 | mod test { 215 | use std::time::Duration; 216 | 217 | use temp_env::{with_var, with_var_unset, with_vars}; 218 | 219 | use super::Config; 220 | use super::RequestType; 221 | 222 | #[test] 223 | fn test_defaults() { 224 | let config = Config::default(); 225 | assert_eq!(config.worker_count, 8); 226 | assert_eq!(config.handoff_timeout, Duration::from_secs(3)); 227 | assert!(!config.should_ignore(&RequestType::GETPWBYNAME)); 228 | assert!(!config.should_ignore(&RequestType::GETPWBYUID)); 229 | } 230 | 231 | #[test] 232 | fn test_worker_count() { 233 | with_var_unset("NSNCD_WORKER_COUNT", || { 234 | let config = Config::from_env().unwrap(); 235 | assert_eq!(config.worker_count, Config::default().worker_count); 236 | }); 237 | with_var("NSNCD_WORKER_COUNT", Some("13"), || { 238 | let config = Config::from_env().unwrap(); 239 | assert_eq!(config.worker_count, 13); 240 | }); 241 | with_var("NSNCD_WORKER_COUNT", Some("0"), || { 242 | assert!(Config::from_env().is_err()); 243 | }); 244 | with_var("NSNCD_WORKER_COUNT", Some("-1"), || { 245 | assert!(Config::from_env().is_err()); 246 | }); 247 | with_var("NSNCD_WORKER_COUNT", Some("ten"), || { 248 | assert!(Config::from_env().is_err()); 249 | }); 250 | with_var( 251 | "NSNCD_WORKER_COUNT", 252 | Some("1000000000000000000000000"), 253 | || { 254 | assert!(Config::from_env().is_err()); 255 | }, 256 | ); 257 | with_var("NSNCD_WORKER_COUNT", Some(""), || { 258 | assert!(Config::from_env().is_err()); 259 | }); 260 | } 261 | 262 | #[test] 263 | fn test_handoff_timeout() { 264 | with_var_unset("NSNCD_HANDOFF_TIMEOUT", || { 265 | let config = Config::from_env().unwrap(); 266 | assert_eq!(config.handoff_timeout, Config::default().handoff_timeout); 267 | }); 268 | with_var("NSNCD_HANDOFF_TIMEOUT", Some("13"), || { 269 | let config = Config::from_env().unwrap(); 270 | assert_eq!(config.handoff_timeout, Duration::from_secs(13)); 271 | }); 272 | with_var("NSNCD_HANDOFF_TIMEOUT", Some("13s"), || { 273 | assert!(Config::from_env().is_err()); 274 | }); 275 | with_var("NSNCD_HANDOFF_TIMEOUT", Some("0"), || { 276 | assert!(Config::from_env().is_err()); 277 | }); 278 | with_var("NSNCD_HANDOFF_TIMEOUT", Some("-1"), || { 279 | assert!(Config::from_env().is_err()); 280 | }); 281 | with_var("NSNCD_HANDOFF_TIMEOUT", Some("ten"), || { 282 | assert!(Config::from_env().is_err()); 283 | }); 284 | with_var( 285 | "NSNCD_HANDOFF_TIMEOUT", 286 | Some("1000000000000000000000000"), 287 | || { 288 | assert!(Config::from_env().is_err()); 289 | }, 290 | ); 291 | with_var("NSNCD_HANDOFF_TIMEOUT", Some(""), || { 292 | assert!(Config::from_env().is_err()); 293 | }); 294 | } 295 | 296 | #[test] 297 | fn test_ignore_vars() { 298 | with_var_unset("NSNCD_IGNORE_INITGROUPS", || { 299 | let config = Config::from_env().unwrap(); 300 | assert!(!config.should_ignore(&RequestType::GETPWBYNAME)); 301 | assert!(!config.should_ignore(&RequestType::INITGROUPS)); 302 | }); 303 | with_var("NSNCD_IGNORE_INITGROUPS", Some("true"), || { 304 | let config = Config::from_env().unwrap(); 305 | assert!(!config.should_ignore(&RequestType::GETPWBYNAME)); 306 | assert!(config.should_ignore(&RequestType::INITGROUPS)); 307 | }); 308 | with_var("NSNCD_IGNORE_INITGROUPS", Some("false"), || { 309 | let config = Config::from_env().unwrap(); 310 | assert!(!config.should_ignore(&RequestType::GETPWBYNAME)); 311 | assert!(!config.should_ignore(&RequestType::INITGROUPS)); 312 | }); 313 | 314 | // Invalid values 315 | with_var("NSNCD_IGNORE_INITGROUPS", Some("yes"), || { 316 | assert!(Config::from_env().is_err()); 317 | }); 318 | with_var("NSNCD_IGNORE_INITGROUPS", Some("y"), || { 319 | assert!(Config::from_env().is_err()); 320 | }); 321 | with_var("NSNCD_IGNORE_INITGROUPS", Some("no"), || { 322 | assert!(Config::from_env().is_err()); 323 | }); 324 | with_var("NSNCD_IGNORE_INITGROUPS", Some("n"), || { 325 | assert!(Config::from_env().is_err()); 326 | }); 327 | with_var("NSNCD_IGNORE_INITGROUPS", Some("1"), || { 328 | assert!(Config::from_env().is_err()); 329 | }); 330 | with_var("NSNCD_IGNORE_INITGROUPS", Some("0"), || { 331 | assert!(Config::from_env().is_err()); 332 | }); 333 | with_var("NSNCD_IGNORE_INITGROUPS", Some(""), || { 334 | assert!(Config::from_env().is_err()); 335 | }); 336 | 337 | // Invalid keys 338 | with_var("NSNCD_IGNORE_initgroups", Some("true"), || { 339 | assert!(Config::from_env().is_err()); 340 | }); 341 | with_var("NSNCD_IGNORE_ZZZNOTAGROUP", Some("true"), || { 342 | assert!(Config::from_env().is_err()); 343 | }); 344 | 345 | // Some combinations 346 | with_vars( 347 | vec![ 348 | ("NSNCD_IGNORE_PASSWD", Some("false")), 349 | ("NSNCD_IGNORE_INITGROUPS", Some("true")), 350 | ], 351 | || { 352 | let config = Config::from_env().unwrap(); 353 | assert!(!config.should_ignore(&RequestType::GETPWBYNAME)); 354 | assert!(config.should_ignore(&RequestType::INITGROUPS)); 355 | }, 356 | ); 357 | with_vars( 358 | vec![ 359 | ("NSNCD_IGNORE_PASSWD", Some("true")), 360 | ("NSNCD_IGNORE_INITGROUPS", Some("false")), 361 | ], 362 | || { 363 | let config = Config::from_env().unwrap(); 364 | assert!(config.should_ignore(&RequestType::GETPWBYNAME)); 365 | assert!(!config.should_ignore(&RequestType::INITGROUPS)); 366 | }, 367 | ); 368 | with_vars( 369 | vec![ 370 | ("NSNCD_IGNORE_PASSWD", Some("true")), 371 | ("NSNCD_IGNORE_INITGROUPS", Some("true")), 372 | ], 373 | || { 374 | let config = Config::from_env().unwrap(); 375 | assert!(config.should_ignore(&RequestType::GETPWBYNAME)); 376 | assert!(config.should_ignore(&RequestType::GETPWBYUID)); 377 | assert!(!config.should_ignore(&RequestType::GETGRBYGID)); 378 | assert!(config.should_ignore(&RequestType::INITGROUPS)); 379 | }, 380 | ); 381 | with_vars( 382 | vec![ 383 | ("NSNCD_IGNORE_PASSWD", Some("1")), 384 | ("NSNCD_IGNORE_INITGROUPS", Some("true")), 385 | ], 386 | || { 387 | assert!(Config::from_env().is_err()); 388 | }, 389 | ); 390 | with_vars( 391 | vec![ 392 | ("NSNCD_IGNORE_PASSWD", Some("true")), 393 | ("NSNCD_IGNORE_ZZZNOTAGROUP", Some("true")), 394 | ], 395 | || { 396 | assert!(Config::from_env().is_err()); 397 | }, 398 | ); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | use anyhow::anyhow; 18 | use nix::libc::{self, dlsym, RTLD_DEFAULT}; 19 | use std::convert::TryInto; 20 | use std::ffi::{CStr, CString}; 21 | use std::mem; 22 | use std::ptr; 23 | 24 | #[allow(non_camel_case_types)] 25 | type size_t = ::std::os::raw::c_ulonglong; 26 | 27 | /// Copied from 28 | /// [unscd](https://github.com/bytedance/unscd/blob/3a4df8de6723bc493e9cd94bb3e3fd831e48b8ca/nscd.c#L2469) 29 | /// 30 | /// This internal glibc function is called to disable trying to contact nscd. 31 | /// We _are_ nscd, so we need to do the lookups, and not recurse. 32 | /// Until 2.14, this function was taking no parameters. 33 | /// In 2.15, it takes a function pointer from hell. 34 | unsafe extern "C" fn do_nothing(_dbidx: size_t, _finfo: *mut libc::c_void) {} 35 | 36 | /// Disable nscd inside our own glibc to prevent recursion. 37 | /// Some versions of glibc, like the one Arch Linux provides, are built without 38 | /// support for nscd, but the possibility remains that this support is 39 | /// re-enabled in a later update. 40 | /// 41 | /// This function loads the __nss_disable_nscd function through dlopen() with 42 | /// RTLD_DEFAULT (to find it in libc) and calls it only if it was found. 43 | pub fn disable_internal_nscd() { 44 | unsafe { 45 | let sym_name = CString::new("__nss_disable_nscd").unwrap(); 46 | let sym_ptr = dlsym(RTLD_DEFAULT, sym_name.as_ptr()); 47 | if !sym_ptr.is_null() { 48 | let __nss_disable_nscd = mem::transmute::< 49 | *mut libc::c_void, 50 | extern "C" fn(hell: unsafe extern "C" fn(size_t, *mut libc::c_void)), 51 | >(sym_ptr); 52 | __nss_disable_nscd(do_nothing); 53 | } 54 | } 55 | } 56 | 57 | pub enum LibcIp { 58 | V4([u8; 4]), 59 | V6([u8; 16]), 60 | } 61 | 62 | mod glibcffi { 63 | use nix::libc; 64 | extern "C" { 65 | pub fn gethostbyname2_r( 66 | name: *const libc::c_char, 67 | af: libc::c_int, 68 | result_buf: *mut libc::hostent, 69 | buf: *mut libc::c_char, 70 | buflen: libc::size_t, 71 | result: *mut *mut libc::hostent, 72 | h_errnop: *mut libc::c_int, 73 | ) -> libc::c_int; 74 | 75 | pub fn gethostbyaddr_r( 76 | addr: *const libc::c_void, 77 | len: libc::socklen_t, 78 | af: libc::c_int, 79 | ret: *mut libc::hostent, 80 | buf: *mut libc::c_char, 81 | buflen: libc::size_t, 82 | result: *mut *mut libc::hostent, 83 | h_errnop: *mut libc::c_int, 84 | ) -> libc::c_int; 85 | } 86 | } 87 | 88 | /// This structure is the Rust counterpart of the `libc::hostent` C 89 | /// function the Libc hostent struct. 90 | /// 91 | /// It's mostly used to perform the gethostbyaddr and gethostbyname 92 | /// operations. 93 | /// 94 | /// This struct can be serialized to the wire through the 95 | /// `serialize_hostent` function or retrieved from the C boundary using the 96 | /// TryFrom `libc:hostent` trait. 97 | #[allow(dead_code)] 98 | #[derive(Clone, Debug)] 99 | pub struct Hostent { 100 | pub name: CString, 101 | pub aliases: Vec, 102 | pub addr_type: i32, 103 | pub addr_list: Vec, 104 | pub herrno: i32, 105 | } 106 | 107 | impl Hostent { 108 | /// Given a herrno, constructs the hostent header we're supposed to use to 109 | /// convey a lookup error. 110 | /// NOTE: herrno is different from errno.h. 111 | /// This is a glibc quirk, I have nothing to do with that, don't blame me :) 112 | pub fn error_value(herrno: i32) -> Self { 113 | // This is a default hostent header 114 | Hostent { 115 | name: CString::default(), 116 | aliases: Vec::new(), 117 | addr_type: -1, 118 | addr_list: Vec::new(), 119 | herrno, 120 | } 121 | } 122 | } 123 | 124 | /// Structure used to represent a gethostbyxxx error. 125 | /// 126 | /// These operations can fail in two major ways: either they'll fail 127 | /// returning a Hostent, in which case they return a HError code that 128 | /// should be returned to the glibc client together with a dummy error 129 | /// Hostent. Either as an "internal failure". In that case, we won't 130 | /// be able to return anything to the GLibc client. 131 | #[derive(Debug)] 132 | pub enum HostentError { 133 | HError(i32), 134 | Other(anyhow::Error), 135 | } 136 | 137 | fn from_libc_hostent(value: libc::hostent) -> Result { 138 | // validate value.h_addtype, and bail out if it's unsupported 139 | if value.h_addrtype != libc::AF_INET && value.h_addrtype != libc::AF_INET6 { 140 | return Err(HostentError::Other(anyhow!( 141 | "unsupported address type: {}", 142 | value.h_addrtype 143 | ))); 144 | } 145 | 146 | // ensure value.h_length matches what we know from this address family 147 | if value.h_addrtype == libc::AF_INET && value.h_length != 4 { 148 | return Err(HostentError::Other(anyhow!( 149 | "unsupported h_length for AF_INET: {}", 150 | value.h_length 151 | ))); 152 | } 153 | if value.h_addrtype == libc::AF_INET6 && value.h_length != 16 { 154 | return Err(HostentError::Other(anyhow!( 155 | "unsupported h_length for AF_INET6: {}", 156 | value.h_length 157 | ))); 158 | } 159 | 160 | // construct the name field. 161 | // Be careful about null pointers or invalid utf-8 strings, even though this 162 | // shouldn't happen. 163 | if value.h_name.is_null() { 164 | return Err(HostentError::Other(anyhow!("h_name is null"))); 165 | } 166 | let name = unsafe { CStr::from_ptr(value.h_name) }; 167 | 168 | // construct the list of aliases. keep adding to value.h_aliases until we encounter a null pointer. 169 | let mut aliases: Vec = Vec::new(); 170 | let mut h_alias_ptr = value.h_aliases as *const *const libc::c_char; 171 | while !(unsafe { *h_alias_ptr }).is_null() { 172 | aliases.push(unsafe { CStr::from_ptr(*h_alias_ptr).to_owned() }); 173 | // increment 174 | unsafe { 175 | h_alias_ptr = h_alias_ptr.add(1); 176 | } 177 | } 178 | 179 | // construct the list of addresses. 180 | let mut addr_list: Vec = Vec::new(); 181 | 182 | // [value.h_addr_list] is a pointer to a list of pointers to addresses. 183 | // h_addr_list[0] => ptr to first address 184 | // h_addr_list[n] => null pointer (end of list) 185 | let mut h_addr_ptr = value.h_addr_list as *const *const libc::c_void; 186 | while !(unsafe { *h_addr_ptr }).is_null() { 187 | if value.h_addrtype == libc::AF_INET { 188 | let octets: [u8; 4] = unsafe { std::ptr::read((*h_addr_ptr) as *const [u8; 4]) }; 189 | addr_list.push(std::net::IpAddr::V4(std::net::Ipv4Addr::from(octets))); 190 | } else { 191 | let octets: [u8; 16] = unsafe { std::ptr::read((*h_addr_ptr) as *const [u8; 16]) }; 192 | addr_list.push(std::net::IpAddr::V6(std::net::Ipv6Addr::from(octets))); 193 | } 194 | unsafe { h_addr_ptr = h_addr_ptr.add(1) }; 195 | } 196 | 197 | Ok(Hostent { 198 | name: name.to_owned(), 199 | aliases, 200 | addr_type: value.h_addrtype, 201 | addr_list, 202 | // If we're here, glibc gave us an hostent. We should discard herrno to match the nscd behaviour. 203 | herrno: 0, 204 | }) 205 | } 206 | 207 | /// Decodes the result of a gethostbyname/addr call into a `Hostent` 208 | /// Rust struct. 209 | /// 210 | /// This decoding algorithm is quite confusing, but that's how it's 211 | /// implemented in Nscd and what the client Glibc expects. We 212 | /// basically always ignore `herrno` except if the resulting 213 | /// `libc::hostent` is set to null by glibc. 214 | fn unmarshal_gethostbyxx( 215 | hostent: *mut libc::hostent, 216 | herrno: libc::c_int, 217 | ) -> Result { 218 | if !hostent.is_null() { 219 | let res = from_libc_hostent(unsafe { *hostent })?; 220 | Ok(res) 221 | } else { 222 | Err(HostentError::HError(herrno)) 223 | } 224 | } 225 | 226 | pub fn gethostbyaddr_r(addr: LibcIp) -> Result { 227 | let (addr, len, af) = match addr { 228 | LibcIp::V4(ref ipv4) => (ipv4 as &[u8], 4, libc::AF_INET), 229 | LibcIp::V6(ref ipv6) => (ipv6 as &[u8], 16, libc::AF_INET6), 230 | }; 231 | 232 | let mut ret_hostent: libc::hostent = libc::hostent { 233 | h_name: ptr::null_mut(), 234 | h_aliases: ptr::null_mut(), 235 | h_addrtype: 0, 236 | h_length: 0, 237 | h_addr_list: ptr::null_mut(), 238 | }; 239 | let mut herrno: libc::c_int = 0; 240 | let mut hostent_result = ptr::null_mut(); 241 | // We start with a 1024 bytes buffer, the nscd default. See 242 | // scratch_buffer.h in the glibc codebase 243 | let mut buf: Vec = Vec::with_capacity(1024); 244 | loop { 245 | let ret = unsafe { 246 | glibcffi::gethostbyaddr_r( 247 | addr.as_ptr() as *const libc::c_void, 248 | len, 249 | af, 250 | &mut ret_hostent, 251 | buf.as_mut_ptr() as *mut libc::c_char, 252 | (buf.capacity() as size_t).try_into().unwrap(), 253 | &mut hostent_result, 254 | &mut herrno, 255 | ) 256 | }; 257 | 258 | if ret == libc::ERANGE && buf.capacity() < 10 * 1000 * 1000 { 259 | buf.reserve(buf.capacity() * 2); 260 | } else { 261 | break; 262 | } 263 | } 264 | unmarshal_gethostbyxx(hostent_result, herrno) 265 | } 266 | 267 | /// Typesafe wrapper around the gethostbyname2_r glibc function 268 | /// 269 | /// af is either nix::libc::AF_INET or nix::libc::AF_INET6 270 | pub fn gethostbyname2_r(name: String, af: libc::c_int) -> Result { 271 | let name = CString::new(name).unwrap(); 272 | 273 | // Prepare a libc::hostent and the pointer to the result list, 274 | // which will be passed to the glibcffi::gethostbyname2_r call. 275 | let mut ret_hostent: libc::hostent = libc::hostent { 276 | h_name: ptr::null_mut(), 277 | h_aliases: ptr::null_mut(), 278 | h_addrtype: 0, 279 | h_length: 0, 280 | h_addr_list: ptr::null_mut(), 281 | }; 282 | let mut herrno: libc::c_int = 0; 283 | // The 1024 initial size comes from the Glibc default. It fit most 284 | // of the requests in practice. 285 | let mut buf: Vec = Vec::with_capacity(1024); 286 | let mut hostent_result = ptr::null_mut(); 287 | loop { 288 | let ret = unsafe { 289 | glibcffi::gethostbyname2_r( 290 | name.as_ptr(), 291 | af, 292 | &mut ret_hostent, 293 | buf.as_mut_ptr() as *mut libc::c_char, 294 | (buf.capacity() as size_t).try_into().unwrap(), 295 | &mut hostent_result, 296 | &mut herrno, 297 | ) 298 | }; 299 | if ret == libc::ERANGE { 300 | // The buffer is too small. Let's x2 its capacity and retry. 301 | buf.reserve(buf.capacity() * 2); 302 | } else { 303 | break; 304 | } 305 | } 306 | unmarshal_gethostbyxx(hostent_result, herrno) 307 | } 308 | 309 | #[test] 310 | fn test_gethostbyname2_r() { 311 | disable_internal_nscd(); 312 | 313 | let result = gethostbyname2_r("localhost.".to_string(), libc::AF_INET); 314 | result.expect("Should resolve IPv4 localhost."); 315 | 316 | let result = gethostbyname2_r("localhost.".to_string(), libc::AF_INET6); 317 | result.expect("Should resolve IPv6 localhost."); 318 | } 319 | 320 | #[test] 321 | fn test_gethostbyaddr_r() { 322 | disable_internal_nscd(); 323 | 324 | let v4test = LibcIp::V4([127, 0, 0, 1]); 325 | let _ = gethostbyaddr_r(v4test).expect("Should resolve IPv4 localhost with gethostbyaddr"); 326 | 327 | let v6test = LibcIp::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); 328 | let _ = gethostbyaddr_r(v6test).expect("Should resolve IPv6 localhost with gethostbyaddr"); 329 | } 330 | -------------------------------------------------------------------------------- /src/handlers.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2023 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | use std::convert::TryInto; 18 | use std::ffi::{CStr, CString}; 19 | use std::net::IpAddr; 20 | use std::os::unix::ffi::OsStrExt; 21 | 22 | use anyhow::{bail, Context, Result}; 23 | use atoi::atoi; 24 | use dns_lookup::AddrInfoHints; 25 | use nix::libc::{AI_CANONNAME, SOCK_STREAM}; 26 | use nix::sys::socket::AddressFamily; 27 | use nix::unistd::{getgrouplist, Gid, Group, Uid, User}; 28 | use slog::{debug, error, Logger}; 29 | use std::mem; 30 | use std::mem::size_of; 31 | use std::num::ParseIntError; 32 | use std::ptr; 33 | use std::str::FromStr; 34 | use std::sync::{LazyLock, Mutex}; 35 | 36 | use crate::ffi::{gethostbyaddr_r, gethostbyname2_r, Hostent, HostentError, LibcIp}; 37 | use crate::protocol::{AiResponse, AiResponseHeader}; 38 | 39 | use super::config::Config; 40 | use super::protocol; 41 | use super::protocol::RequestType; 42 | 43 | use nix::libc::{c_char, c_int, servent, size_t}; 44 | 45 | mod nixish; 46 | use nixish::{Netgroup, Service}; 47 | 48 | const ERANGE: i32 = 34; 49 | 50 | fn call_with_erange_handling(buf: &mut Vec, mut f: F) -> i32 51 | where 52 | F: FnMut(&mut Vec) -> i32, 53 | { 54 | loop { 55 | let ret = f(buf); 56 | if ret == ERANGE { 57 | if buf.len() > 10 << 20 { 58 | // Let's not let this get much bigger than 10MB 59 | return ret; 60 | } 61 | buf.resize(buf.len() * 2, 0 as c_char); 62 | } else { 63 | return ret; 64 | } 65 | } 66 | } 67 | 68 | // these functions are not available in the nix::libc crate 69 | extern "C" { 70 | fn setnetgrent(netgroup: *const c_char) -> i32; 71 | fn endnetgrent(); 72 | fn getnetgrent_r( 73 | hostp: *mut *mut c_char, 74 | userp: *mut *mut c_char, 75 | domainp: *mut *mut c_char, 76 | buffer: *mut c_char, 77 | buflen: size_t, 78 | ) -> c_int; 79 | fn innetgr( 80 | netgroup: *const c_char, 81 | host: *const c_char, 82 | user: *const c_char, 83 | domain: *const c_char, 84 | ) -> c_int; 85 | fn getservbyname_r( 86 | name: *const c_char, 87 | proto: *const c_char, 88 | result_buf: *mut servent, 89 | buf: *mut c_char, 90 | buflen: size_t, 91 | result: *mut *mut servent, 92 | ) -> c_int; 93 | fn getservbyport_r( 94 | port: c_int, 95 | proto: *const c_char, 96 | result_buf: *mut servent, 97 | buf: *mut c_char, 98 | buflen: size_t, 99 | result: *mut *mut servent, 100 | ) -> c_int; 101 | } 102 | #[derive(Debug)] 103 | pub struct ServiceWithName { 104 | pub proto: Option, 105 | pub service: String, 106 | } 107 | #[derive(Debug)] 108 | pub struct ServiceWithPort { 109 | pub proto: Option, 110 | pub port: u16, 111 | } 112 | 113 | #[derive(Debug)] 114 | pub struct NetgroupWithName { 115 | pub name: String, 116 | } 117 | #[derive(Debug, PartialEq)] 118 | pub struct InNetGroup { 119 | pub netgroup: String, 120 | pub host: Option, 121 | pub user: Option, 122 | pub domain: Option, 123 | } 124 | 125 | impl FromStr for ServiceWithName { 126 | type Err = String; 127 | 128 | fn from_str(s: &str) -> Result { 129 | let parts: Vec<&str> = s.split('/').collect(); 130 | 131 | if parts.len() != 2 { 132 | return Err("Input must be in the format 'service/proto'".into()); 133 | } 134 | 135 | let service = parts[0].to_owned(); 136 | let proto = if parts[1].is_empty() { 137 | None 138 | } else { 139 | Some(parts[1].to_owned()) 140 | }; 141 | 142 | Ok(ServiceWithName { proto, service }) 143 | } 144 | } 145 | 146 | impl ServiceWithName { 147 | fn lookup(&self) -> Result> { 148 | let service_name = CString::new(self.service.clone())?; 149 | let proto = match &self.proto { 150 | Some(p) => Some(CString::new(p.clone())?), 151 | None => None, 152 | }; 153 | 154 | let mut result_buf: servent = unsafe { mem::zeroed() }; 155 | let mut buffer: Vec = vec![0; 1024]; 156 | let mut result: *mut servent = ptr::null_mut(); 157 | 158 | let ret = call_with_erange_handling(&mut buffer, |buffer| unsafe { 159 | getservbyname_r( 160 | service_name.as_ptr(), 161 | proto.as_ref().map_or(ptr::null(), |p| p.as_ptr()), 162 | &mut result_buf, 163 | buffer.as_mut_ptr(), 164 | buffer.len(), 165 | &mut result, 166 | ) 167 | }); 168 | // lookup was successful 169 | if ret == 0 { 170 | if !result.is_null() { 171 | let service: Service = unsafe { *result }.try_into()?; 172 | Ok(Some(service)) 173 | } else { 174 | Ok(None) 175 | } 176 | } else { 177 | anyhow::bail!("Error: getservbyname_r failed with code {}", ret); 178 | } 179 | } 180 | } 181 | 182 | impl FromStr for ServiceWithPort { 183 | type Err = String; 184 | 185 | fn from_str(s: &str) -> Result { 186 | let parts: Vec<&str> = s.split('/').collect(); 187 | 188 | if parts.len() != 2 { 189 | return Err("Input must be in the format 'port/proto'".into()); 190 | } 191 | 192 | let port: u16 = parts[0] 193 | .parse() 194 | .map_err(|err: ParseIntError| err.to_string())?; 195 | let proto = if parts[1].is_empty() { 196 | None 197 | } else { 198 | Some(parts[1].to_owned()) 199 | }; 200 | 201 | Ok(ServiceWithPort { proto, port }) 202 | } 203 | } 204 | 205 | impl ServiceWithPort { 206 | fn lookup(&self) -> Result> { 207 | //issue-142 208 | //port 0 lookups to sssd return ENOMEM 209 | if self.port == 0 { 210 | return Ok(None); 211 | } 212 | let proto = match &self.proto { 213 | Some(p) => Some(CString::new(p.clone())?), 214 | None => None, 215 | }; 216 | 217 | let mut result_buf: servent = unsafe { mem::zeroed() }; 218 | let mut buffer: Vec = vec![0; 1024]; 219 | let mut result: *mut servent = ptr::null_mut(); 220 | 221 | let ret = call_with_erange_handling(&mut buffer, |buffer| unsafe { 222 | getservbyport_r( 223 | self.port as c_int, 224 | proto.as_ref().map_or(ptr::null(), |p| p.as_ptr()), 225 | &mut result_buf, 226 | buffer.as_mut_ptr(), 227 | buffer.len(), 228 | &mut result, 229 | ) 230 | }); 231 | if ret == 0 { 232 | if !result.is_null() { 233 | let service: Service = unsafe { *result }.try_into()?; 234 | Ok(Some(service)) 235 | } else { 236 | Ok(None) 237 | } 238 | } else { 239 | anyhow::bail!("Error: getservbyport_r failed with code {}", ret); 240 | } 241 | } 242 | } 243 | 244 | impl InNetGroup { 245 | pub fn from_bytes(bytes: &[u8]) -> Result { 246 | let mut args: [Option; 3] = [None, None, None]; 247 | 248 | /* 249 | For innegroup -h h -u u -d d netgroup the input bytes string looks like this 250 | 6e 65 74 67 72 6f 75 70 00 01 68 00 01 75 00 01 64 00 251 | h u d 252 | The host, user, domain arguments are always in the same order 253 | Split the input by nul byte, generate strings as appropriate, skipping the SOH byte 254 | */ 255 | 256 | let parts: Vec<&[u8]> = bytes.split(|&b| b == 0).collect(); 257 | 258 | // netgroup is always present 259 | let netgroup = if let Ok(string) = std::str::from_utf8(parts[0]) { 260 | string.to_string() 261 | } else { 262 | anyhow::bail!("Parsing of netgroup failed"); 263 | }; 264 | 265 | // The remainder are optional 266 | // if len 0, just a NUL char 267 | // else, SOH char followed by arg, skip element 0 when making the string 268 | for idx in 0..3 { 269 | if !parts[idx + 1].is_empty() { 270 | args[idx] = if let Ok(string) = std::str::from_utf8(&parts[idx + 1][1..]) { 271 | Some(string.to_string()) 272 | } else { 273 | None 274 | }; 275 | } 276 | } 277 | 278 | Ok(InNetGroup { 279 | netgroup, 280 | host: args[0].clone(), 281 | user: args[1].clone(), 282 | domain: args[2].clone(), 283 | }) 284 | } 285 | 286 | fn lookup(&self) -> Result { 287 | let netgroup_name = CString::new(self.netgroup.clone())?; 288 | 289 | let host = match &self.host { 290 | Some(s) => Some(CString::new(s.clone())?), 291 | None => None, 292 | }; 293 | let user = match &self.user { 294 | Some(s) => Some(CString::new(s.clone())?), 295 | None => None, 296 | }; 297 | let domain = match &self.domain { 298 | Some(s) => Some(CString::new(s.clone())?), 299 | None => None, 300 | }; 301 | 302 | let ret = unsafe { 303 | innetgr( 304 | netgroup_name.as_ptr() as *const c_char, 305 | host.as_ref().map_or(ptr::null(), |s| s.as_ptr()), 306 | user.as_ref().map_or(ptr::null(), |s| s.as_ptr()), 307 | domain.as_ref().map_or(ptr::null(), |s| s.as_ptr()), 308 | ) != 0 309 | }; 310 | Ok(ret) 311 | } 312 | } 313 | 314 | impl FromStr for NetgroupWithName { 315 | type Err = String; 316 | 317 | fn from_str(s: &str) -> Result { 318 | let name = s.to_owned(); 319 | 320 | Ok(NetgroupWithName { name }) 321 | } 322 | } 323 | 324 | //Required for use of setnetgrent in multi threaded apps 325 | //https://docs.oracle.com/cd/E88353_01/html/E37843/setnetgrent-3c.html 326 | 327 | //Note that while setnetgrent() and endnetgrent() are safe for use in multi-threaded applications, the effect of each is process-wide. 328 | //Calling setnetgrent() resets the enumeration position for all threads. 329 | //If multiple threads interleave calls to getnetgrent_r() each will enumerate a disjoint subset of the netgroup. 330 | //Thus the effective use of these functions in multi-threaded applications may require coordination by the caller. 331 | 332 | //Make a Mutex to ensure that setnetgrent and getnetgrent_r are called in sequence 333 | static SETNETGRENT_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(0)); 334 | impl NetgroupWithName { 335 | fn lookup(&self) -> Result> { 336 | let mut results: Vec = vec![]; 337 | 338 | let netgroup_name = CString::new(self.name.clone())?; 339 | 340 | // if the mutex thinks it was poisoned (e.g by a thread panicing) 341 | // that thread is not running, thus we can take the lock 342 | // There is no need to explicitly unlock, this happens automatically 343 | // at the conclusion of the function 344 | let _guard = match SETNETGRENT_LOCK.lock() { 345 | Ok(g) => g, 346 | Err(poisoned) => poisoned.into_inner(), 347 | }; 348 | 349 | if unsafe { setnetgrent(netgroup_name.as_ptr() as *const c_char) } != 1 { 350 | //setnetgrent returns 0 if the netgroup cannot be found 351 | return Ok(results); 352 | } 353 | let mut buffer = vec![0 as c_char; 4096]; 354 | let mut host: *mut c_char = std::ptr::null_mut(); 355 | let mut user: *mut c_char = std::ptr::null_mut(); 356 | let mut domain: *mut c_char = std::ptr::null_mut(); 357 | 358 | loop { 359 | let ret = call_with_erange_handling(&mut buffer, |buffer| unsafe { 360 | getnetgrent_r( 361 | &mut host, 362 | &mut user, 363 | &mut domain, 364 | buffer.as_mut_ptr(), 365 | buffer.len() as size_t, 366 | ) 367 | }); 368 | if ret == 1 { 369 | let host_str = if !host.is_null() { 370 | Some(unsafe { CStr::from_ptr(host) }.to_owned()) 371 | } else { 372 | None 373 | }; 374 | let user_str = if !user.is_null() { 375 | Some(unsafe { CStr::from_ptr(user) }.to_owned()) 376 | } else { 377 | None 378 | }; 379 | let domain_str = if !domain.is_null() { 380 | Some(unsafe { CStr::from_ptr(domain) }.to_owned()) 381 | } else { 382 | None 383 | }; 384 | 385 | results.push(Netgroup { 386 | host: host_str, 387 | user: user_str, 388 | domain: domain_str, 389 | }); 390 | 391 | continue; 392 | } else if ret == 0 { 393 | unsafe { endnetgrent() }; 394 | break; 395 | } else { 396 | // Handle other errors 397 | anyhow::bail!("Error: getnetgrent_r failed with code {}", ret); 398 | } 399 | } 400 | 401 | Ok(results) 402 | } 403 | } 404 | 405 | /// Handle a request by performing the appropriate lookup and sending the 406 | /// serialized response back to the client. 407 | /// 408 | /// # Arguments 409 | /// 410 | /// * `log` - A `slog` Logger. 411 | /// * `config` - The nsncd configuration (which request types to ignore). 412 | /// * `request` - The request to handle. 413 | pub fn handle_request( 414 | log: &Logger, 415 | config: &Config, 416 | request: &protocol::Request, 417 | ) -> Result> { 418 | if config.should_ignore(&request.ty) { 419 | debug!(log, "ignoring request"; "request" => ?request); 420 | return Ok(vec![]); 421 | } 422 | debug!(log, "handling request"; "request" => ?request); 423 | match request.ty { 424 | RequestType::GETPWBYUID => { 425 | let key = CStr::from_bytes_with_nul(request.key)?; 426 | let uid = atoi(key.to_bytes()).context("invalid uid string")?; 427 | let user = User::from_uid(Uid::from_raw(uid))?; 428 | debug!(log, "got user"; "user" => ?user); 429 | serialize_user(user) 430 | } 431 | RequestType::GETPWBYNAME => { 432 | let key = CStr::from_bytes_with_nul(request.key)?; 433 | let user = User::from_name(key.to_str()?)?; 434 | debug!(log, "got user"; "user" => ?user); 435 | serialize_user(user) 436 | } 437 | RequestType::GETGRBYGID => { 438 | let key = CStr::from_bytes_with_nul(request.key)?; 439 | let gid = atoi(key.to_bytes()).context("invalid gid string")?; 440 | let group = Group::from_gid(Gid::from_raw(gid))?; 441 | debug!(log, "got group"; "group" => ?group); 442 | serialize_group(group) 443 | } 444 | RequestType::GETGRBYNAME => { 445 | let key = CStr::from_bytes_with_nul(request.key)?; 446 | let group = Group::from_name(key.to_str()?)?; 447 | debug!(log, "got group"; "group" => ?group); 448 | serialize_group(group) 449 | } 450 | RequestType::INITGROUPS => { 451 | // initgroups is a little strange: in the public libc API, the 452 | // interface is getgrouplist(), which requires that you pass one 453 | // extra GID (intended to be the user's primary GID) in, which is 454 | // returned as part of the result. In the glibc NSS implementation, 455 | // NSS backends can implement initgroups_dyn(), which is not 456 | // expected to find the primary GID (for example, 457 | // _nss_files_initgroups_dyn() only looks at /etc/group); 458 | // alternatively, both glibc itself and its NSCD implementation will 459 | // fall back to enumerating all groups with getgrent(). It will then 460 | // tack on the provided GID before returning, if it's not already in 461 | // the list. 462 | // 463 | // There's no public API to just get the supplementary groups, so we 464 | // need to get the primary group and pass it to getgrouplist() 465 | // (since we don't want to implement the NSS API ourselves). 466 | // 467 | // One corollary is that getting supplementary groups never fails; 468 | // if you ask for a nonexistent user, they just happen not to be in 469 | // any groups. So the "found" value is mostly used to indicate 470 | // whether the response is valid - in other words, we return found = 471 | // 1 and an empty list if User::from_name fails, meaning the 472 | // client can be happy with the response we provide. 473 | // 474 | // nix::getgrouplist can fail, in theory, if the number of groups is 475 | // greater than NGROUPS_MAX. (On Linux this is 65536 and therefore 476 | // pretty unlikely in practice.) There are only two things we can do 477 | // here: return a false reply or refuse the lookup. (Even if we 478 | // return found=0, glibc appears to treat that just like found=1 479 | // ngrps=0, i.e., successful empty reply. It would be useful for 480 | // glibc to fall back to NSS here, but it does not.) If we refuse 481 | // the lookup, glibc caches the fact that we don't support 482 | // INITGROUPS - and uses the same variable for whether we support 483 | // GETGR*, which causes the process to skip nsncd for all future 484 | // lookups. So, in this theoretical case, we log our perfidy and 485 | // return an empty list. 486 | let key = CStr::from_bytes_with_nul(request.key)?; 487 | let user = User::from_name(key.to_str()?)?; 488 | debug!(log, "got user"; "user" => ?user); 489 | let groups = if let Some(user) = user { 490 | getgrouplist(key, user.gid).unwrap_or_else(|e| { 491 | error!(log, "nix::getgrouplist failed, returning empty list"; "err" => %e); 492 | vec![] 493 | }) 494 | } else { 495 | vec![] 496 | }; 497 | serialize_initgroups(groups) 498 | } 499 | 500 | // There's no cache to invalidate 501 | RequestType::INVALIDATE => { 502 | debug!(log, "received invalidate request, ignoring"); 503 | Ok(vec![]) 504 | } 505 | 506 | // We don't want clients to be able to shut down nsncd. 507 | RequestType::SHUTDOWN => { 508 | debug!(log, "received shutdown request, ignoring"); 509 | Ok(vec![]) 510 | } 511 | 512 | RequestType::GETAI => { 513 | let hostname = CStr::from_bytes_with_nul(request.key)?.to_str()?; 514 | // Boths hints are necessary to mimick the glibc behaviour. 515 | let hints = AddrInfoHints { 516 | // The canonical name will be filled in the first 517 | // addrinfo struct returned by getaddrinfo. 518 | flags: AI_CANONNAME, 519 | // There's no way to convey socktype in the the Nscd 520 | // protocol, neither in the request nor response. 521 | // 522 | // Set this to SOCK_STREAM to match glibc and unscd 523 | // behaviour. 524 | socktype: SOCK_STREAM, 525 | address: 0, 526 | protocol: 0, 527 | }; 528 | let resp = dns_lookup::getaddrinfo(Some(hostname), None, Some(hints)); 529 | let ai_resp_empty = AiResponse { 530 | canon_name: hostname.to_string(), 531 | addrs: vec![], 532 | }; 533 | let ai_resp: AiResponse = match resp { 534 | Ok(ai_resp_iter) => { 535 | let mut ai_resp_iter = ai_resp_iter.filter_map(|e| e.ok()).peekable(); 536 | // According to man 3 getaddrinfo, the resulting 537 | // canonical name should be stored in the first 538 | // addrinfo struct. 539 | // Re-using the request hostname if we don't get a 540 | // canonical name. 541 | let canon_name = ai_resp_iter 542 | .peek() 543 | .and_then(|e| e.canonname.to_owned()) 544 | .unwrap_or(hostname.to_string()); 545 | let addrs: Vec = ai_resp_iter.map(|e| e.sockaddr.ip()).collect(); 546 | 547 | AiResponse { canon_name, addrs } 548 | } 549 | Err(_) => ai_resp_empty, 550 | }; 551 | 552 | serialize_address_info(ai_resp) 553 | } 554 | 555 | // GETHOSTBYADDR and GETHOSTBYADDRv6 implement reverse lookup 556 | // The key contains the address to look for. 557 | RequestType::GETHOSTBYADDR => { 558 | let key = request.key; 559 | 560 | if key.len() != 4 { 561 | bail!("Invalid key len: {}, expected 4", key.len()); 562 | } 563 | let address_bytes: [u8; 4] = key.try_into()?; 564 | let hostent = match gethostbyaddr_r(LibcIp::V4(address_bytes)) { 565 | Ok(hostent) => hostent, 566 | Err(HostentError::HError(herror)) => Hostent::error_value(herror), 567 | Err(HostentError::Other(e)) => 568 | // We shouldn't end up in that branch. Something 569 | // got very very wrong on the glibc client side if 570 | // we do. It's okay to bail, there's nothing much 571 | // we can do. 572 | { 573 | bail!("unexpected gethostbyaddr error: {}", e) 574 | } 575 | }; 576 | 577 | serialize_hostent(hostent) 578 | } 579 | RequestType::GETHOSTBYADDRv6 => { 580 | let key = request.key; 581 | 582 | if key.len() != 16 { 583 | bail!("Invalid key len: {}, expected 16", key.len()); 584 | } 585 | let address_bytes: [u8; 16] = key.try_into()?; 586 | let hostent = match gethostbyaddr_r(LibcIp::V6(address_bytes)) { 587 | Ok(hostent) => hostent, 588 | Err(HostentError::HError(herror)) => Hostent::error_value(herror), 589 | Err(HostentError::Other(e)) => 590 | // We shouldn't end up in that branch. Something 591 | // got very very wrong on the glibc client side if 592 | // we do. It's okay to bail, there's nothing much 593 | // we can do. 594 | { 595 | bail!("unexpected gethostbyaddrv6 error: {}", e) 596 | } 597 | }; 598 | serialize_hostent(hostent) 599 | } 600 | 601 | RequestType::GETHOSTBYNAME => { 602 | let hostname = CStr::from_bytes_with_nul(request.key)?.to_str()?; 603 | let hostent = match gethostbyname2_r(hostname.to_string(), nix::libc::AF_INET) { 604 | Ok(hostent) => hostent, 605 | Err(HostentError::HError(herror)) => Hostent::error_value(herror), 606 | Err(HostentError::Other(e)) => 607 | // We shouldn't end up in that branch. Something 608 | // got very very wrong on the glibc client side if 609 | // we do. It's okay to bail, there's nothing much 610 | // we can do. 611 | { 612 | bail!("unexpected gethostbyname error: {:?}", e) 613 | } 614 | }; 615 | serialize_hostent(hostent) 616 | } 617 | 618 | RequestType::GETHOSTBYNAMEv6 => { 619 | let hostname = CStr::from_bytes_with_nul(request.key)?.to_str()?; 620 | let hostent = match gethostbyname2_r(hostname.to_string(), nix::libc::AF_INET6) { 621 | Ok(hostent) => hostent, 622 | Err(HostentError::HError(herror)) => Hostent::error_value(herror), 623 | Err(HostentError::Other(e)) => 624 | // We shouldn't end up in that branch. Something 625 | // got very very wrong on the glibc client side if 626 | // we do. It's okay to bail, there's nothing much 627 | // we can do. 628 | { 629 | bail!("unexpected gethostbynamev6 error: {:?}", e) 630 | } 631 | }; 632 | serialize_hostent(hostent) 633 | } 634 | 635 | RequestType::GETSERVBYNAME => { 636 | /* 637 | Sample requests 638 | $ getent services biff 639 | biff 512/udp comsat 640 | $ getent services exec 641 | exec 512/tcp 642 | $ getent services exec/tcp 643 | exec 512/tcp 644 | $ getent services exec/udp 645 | $ 646 | */ 647 | 648 | //CStr is a borrowed reference to a CStyle string 649 | //Is is immutable, null terminated, string slice 650 | //CStr is used as the input is coming from a c ffi function 651 | let key = CStr::from_bytes_with_nul(request.key)?; 652 | let str_slice = key.to_str()?; 653 | // Use the FromStr trait 654 | match str_slice.parse::() { 655 | Ok(service_with_name) => { 656 | debug!(log, "got getservbyname {:?}", service_with_name); 657 | let service = service_with_name.lookup()?; 658 | serialize_service(service) 659 | } 660 | Err(_e) => { 661 | anyhow::bail!("Could not parse service request"); 662 | } 663 | } 664 | } 665 | RequestType::GETSERVBYPORT => { 666 | /* 667 | Sample requests 668 | $ getent services 512 669 | exec 512/tcp 670 | $ getent services 512/tcp 671 | exec 512/tcp 672 | $ getent services 512/udp 673 | biff 512/udp comsat 674 | 675 | If /proto is not provided, defaults to tcp 676 | 677 | When the request is received over the socket, the port is in network order 678 | 679 | */ 680 | let key = CStr::from_bytes_with_nul(request.key)?; 681 | let str_slice: &str = key.to_str()?; 682 | // Use the FromStr trait 683 | match str_slice.parse::() { 684 | Ok(service_with_port) => { 685 | debug!(log, "got getservbyport {:?}", service_with_port); 686 | let service = service_with_port.lookup()?; 687 | serialize_service(service) 688 | } 689 | Err(_e) => { 690 | anyhow::bail!("Could not parse service request"); 691 | } 692 | } 693 | } 694 | RequestType::GETNETGRENT => { 695 | let key = CStr::from_bytes_with_nul(request.key)?; 696 | let str_slice: &str = key.to_str()?; 697 | 698 | debug!(log, "got netgroup"; "netgroup" => ?key.to_str()); 699 | 700 | match str_slice.parse::() { 701 | Ok(netgroup_with_name) => { 702 | let netgroups = netgroup_with_name.lookup()?; 703 | serialize_netgroup(netgroups) 704 | } 705 | Err(_e) => { 706 | anyhow::bail!("Could not parse netgroup request"); 707 | } 708 | } 709 | } 710 | RequestType::INNETGR => { 711 | let in_netgroup = InNetGroup::from_bytes(request.key)?; 712 | debug!(log, "got innetgr {:?}", in_netgroup); 713 | serialize_innetgr(in_netgroup.lookup()?) 714 | } 715 | 716 | // These will normally send an FD pointing to the internal cache structure, 717 | // which clients use to look into the cache contents on their own. 718 | // We don't cache, and we don't want clients to poke around in cache structures either. 719 | // Luckily clients fall back to explicit queries if no FDs are sent over. 720 | RequestType::GETFDPW 721 | | RequestType::GETFDGR 722 | | RequestType::GETFDHST 723 | | RequestType::GETFDSERV 724 | | RequestType::GETFDNETGR => { 725 | debug!(log, "received GETFD* request, ignoring"); 726 | Ok(vec![]) 727 | } 728 | // Not implemented (yet) 729 | RequestType::GETSTAT | RequestType::LASTREQ => Ok(vec![]), 730 | } 731 | } 732 | 733 | fn serialize_innetgr(innetgr: bool) -> Result> { 734 | let mut result = vec![]; 735 | 736 | if innetgr { 737 | let header = protocol::InNetgroupResponseHeader { 738 | version: protocol::VERSION, 739 | found: 1, 740 | result: 1, 741 | }; 742 | result.extend_from_slice(header.as_slice()); 743 | } 744 | Ok(result) 745 | } 746 | 747 | //Take a list of NixNetGroup objects and serialize to send back 748 | fn serialize_netgroup(netgroups: Vec) -> Result> { 749 | let mut result = vec![]; 750 | 751 | // first we need to count the size of the return data to populate the header 752 | let mut result_len: i32 = 0; 753 | let mut field_len: i32; 754 | for netgroup in netgroups.iter() { 755 | if let Some(host) = &netgroup.host { 756 | field_len = host.to_bytes_with_nul().len().try_into()?; 757 | result_len += field_len; 758 | } else { 759 | result_len += 1; 760 | } 761 | if let Some(user) = &netgroup.user { 762 | field_len = user.to_bytes_with_nul().len().try_into()?; 763 | result_len += field_len; 764 | } else { 765 | result_len += 1; 766 | } 767 | if let Some(domain) = &netgroup.domain { 768 | field_len = domain.to_bytes_with_nul().len().try_into()?; 769 | result_len += field_len; 770 | } else { 771 | result_len += 1; 772 | } 773 | } 774 | 775 | // make the header first 776 | // This approach supports a 0 length list 777 | let header = protocol::NetgroupResponseHeader { 778 | version: protocol::VERSION, 779 | found: 1, 780 | nresults: netgroups.len().try_into()?, 781 | result_len, 782 | }; 783 | // TODO - this should if netgroups.len() ==0 return [].. at the top. 784 | // not sure of the syntax to early return 785 | if !netgroups.is_empty() { 786 | result.extend_from_slice(header.as_slice()); 787 | } 788 | 789 | //send all the results 790 | //netgroup all, 11641 members appears to work 791 | let null_string: &[u8] = b"\0"; 792 | for netgroup in netgroups.iter() { 793 | // TODO - another loop and getattr style 794 | 795 | if let Some(host) = &netgroup.host { 796 | result.extend_from_slice(host.to_bytes_with_nul()); 797 | } else { 798 | result.extend_from_slice(null_string); 799 | } 800 | if let Some(user) = &netgroup.user { 801 | result.extend_from_slice(user.to_bytes_with_nul()); 802 | } else { 803 | result.extend_from_slice(null_string); 804 | } 805 | if let Some(domain) = &netgroup.domain { 806 | result.extend_from_slice(domain.to_bytes_with_nul()); 807 | } else { 808 | result.extend_from_slice(null_string); 809 | } 810 | } 811 | Ok(result) 812 | } 813 | 814 | /// Send a service entry back to the client, or a response indicating the 815 | /// lookup found no such service. 816 | fn serialize_service(service: Option) -> Result> { 817 | let mut result = vec![]; 818 | 819 | if let Some(data) = service { 820 | let name = CString::new(data.name)?; 821 | let name_bytes = name.to_bytes_with_nul(); 822 | 823 | let proto = CString::new(data.proto)?; 824 | let proto_bytes = proto.to_bytes_with_nul(); 825 | 826 | let port = data.port; 827 | 828 | let aliases: Vec = data 829 | .aliases 830 | .iter() 831 | .map(|alias| CString::new((*alias).as_bytes())) 832 | .collect::, _>>()?; 833 | let aliases_bytes: Vec<&[u8]> = aliases 834 | .iter() 835 | .map(|alias| alias.to_bytes_with_nul()) 836 | .collect(); 837 | 838 | let header = protocol::ServResponseHeader { 839 | version: protocol::VERSION, 840 | found: 1, 841 | s_name_len: name_bytes.len().try_into()?, 842 | s_proto_len: proto_bytes.len().try_into()?, 843 | s_aliases_cnt: aliases.len().try_into()?, 844 | s_port: port, 845 | }; 846 | result.extend_from_slice(header.as_slice()); 847 | result.extend_from_slice(name_bytes); 848 | result.extend_from_slice(proto_bytes); 849 | // first indicate the length of each subsequent alias 850 | for alias_bytes in aliases_bytes.iter() { 851 | result.extend_from_slice(&i32::to_ne_bytes(alias_bytes.len().try_into()?)); 852 | } 853 | // serialize the value of the string 854 | for alias_bytes in aliases_bytes.iter() { 855 | result.extend_from_slice(alias_bytes); 856 | } 857 | } else { 858 | let header = protocol::ServResponseHeader::default(); 859 | result.extend_from_slice(header.as_slice()); 860 | } 861 | Ok(result) 862 | } 863 | 864 | /// Send a user (passwd entry) back to the client, or a response indicating the 865 | /// lookup found no such user. 866 | fn serialize_user(user: Option) -> Result> { 867 | let mut result = vec![]; 868 | if let Some(data) = user { 869 | let name = CString::new(data.name)?; 870 | let name_bytes = name.to_bytes_with_nul(); 871 | let passwd_bytes = data.passwd.to_bytes_with_nul(); 872 | let gecos_bytes = data.gecos.to_bytes_with_nul(); 873 | let dir = CString::new(data.dir.as_os_str().as_bytes())?; 874 | let dir_bytes = dir.to_bytes_with_nul(); 875 | let shell = CString::new(data.shell.as_os_str().as_bytes())?; 876 | let shell_bytes = shell.to_bytes_with_nul(); 877 | 878 | let header = protocol::PwResponseHeader { 879 | version: protocol::VERSION, 880 | found: 1, 881 | pw_name_len: name_bytes.len().try_into()?, 882 | pw_passwd_len: passwd_bytes.len().try_into()?, 883 | pw_uid: data.uid.as_raw(), 884 | pw_gid: data.gid.as_raw(), 885 | pw_gecos_len: gecos_bytes.len().try_into()?, 886 | pw_dir_len: dir_bytes.len().try_into()?, 887 | pw_shell_len: shell_bytes.len().try_into()?, 888 | }; 889 | result.extend_from_slice(header.as_slice()); 890 | result.extend_from_slice(name_bytes); 891 | result.extend_from_slice(passwd_bytes); 892 | result.extend_from_slice(gecos_bytes); 893 | result.extend_from_slice(dir_bytes); 894 | result.extend_from_slice(shell_bytes); 895 | } else { 896 | let header = protocol::PwResponseHeader::default(); 897 | result.extend_from_slice(header.as_slice()); 898 | } 899 | Ok(result) 900 | } 901 | 902 | /// Send a group (group entry) back to the client, or a response indicating the 903 | /// lookup found no such group. 904 | fn serialize_group(group: Option) -> Result> { 905 | let mut result = vec![]; 906 | if let Some(data) = group { 907 | let name = CString::new(data.name)?; 908 | let name_bytes = name.to_bytes_with_nul(); 909 | let mem_cnt = data.mem.len(); 910 | let passwd_bytes = data.passwd.to_bytes_with_nul(); 911 | let members: Vec = data 912 | .mem 913 | .into_iter() 914 | .map(CString::new) 915 | .collect::, _>>()?; 916 | let members_bytes: Vec<&[u8]> = members 917 | .iter() 918 | .map(|member| member.to_bytes_with_nul()) 919 | .collect(); 920 | 921 | let header = protocol::GrResponseHeader { 922 | version: protocol::VERSION, 923 | found: 1, 924 | gr_name_len: name_bytes.len().try_into()?, 925 | gr_passwd_len: passwd_bytes.len().try_into()?, 926 | gr_gid: data.gid.as_raw(), 927 | gr_mem_cnt: mem_cnt.try_into()?, 928 | }; 929 | result.extend_from_slice(header.as_slice()); 930 | for member_bytes in members_bytes.iter() { 931 | result.extend_from_slice(&i32::to_ne_bytes(member_bytes.len().try_into()?)); 932 | } 933 | result.extend_from_slice(name_bytes); 934 | result.extend_from_slice(passwd_bytes); 935 | for member_bytes in members_bytes.iter() { 936 | result.extend_from_slice(member_bytes); 937 | } 938 | } else { 939 | let header = protocol::GrResponseHeader::default(); 940 | result.extend_from_slice(header.as_slice()); 941 | } 942 | Ok(result) 943 | } 944 | 945 | /// Send a user's group list (initgroups/getgrouplist response) back to the 946 | /// client. 947 | fn serialize_initgroups(groups: Vec) -> Result> { 948 | let mut result = vec![]; 949 | let header = protocol::InitgroupsResponseHeader { 950 | version: protocol::VERSION, 951 | found: 1, 952 | ngrps: groups.len().try_into()?, 953 | }; 954 | 955 | result.extend_from_slice(header.as_slice()); 956 | for group in groups.iter() { 957 | result.extend_from_slice(&i32::to_ne_bytes(group.as_raw().try_into()?)); 958 | } 959 | 960 | Ok(result) 961 | } 962 | 963 | fn serialize_hostent(hostent: Hostent) -> Result> { 964 | // Loop over all addresses. 965 | // Serialize them into a slice, which is used later in the payload. 966 | // Take note of the number of addresses (by AF). 967 | let mut num_v4 = 0; 968 | let mut num_v6 = 0; 969 | let mut buf_addrs = vec![]; 970 | let mut buf_aliases = vec![]; 971 | // Memory segment used to convey the size of the different 972 | // aliases. The sizes are expressed in native endian encoded 32 973 | // bits integer. 974 | let mut buf_aliases_size = vec![]; 975 | 976 | for address in hostent.addr_list.iter() { 977 | match address { 978 | IpAddr::V4(ip4) => { 979 | num_v4 += 1; 980 | for octet in ip4.octets() { 981 | buf_addrs.push(octet) 982 | } 983 | } 984 | IpAddr::V6(ip6) => { 985 | num_v6 += 1; 986 | for octet in ip6.octets() { 987 | buf_addrs.push(octet) 988 | } 989 | } 990 | } 991 | } 992 | 993 | // this can only ever express one address family 994 | if num_v4 != 0 && num_v6 != 0 { 995 | bail!("unable to serialize mixed AF") 996 | } 997 | 998 | // if there's no addresses, early-return the "empty result" response. 999 | if hostent.addr_list.is_empty() { 1000 | return Ok(Vec::from( 1001 | protocol::HstResponseHeader { 1002 | version: protocol::VERSION, 1003 | found: 0, 1004 | h_name_len: 0, 1005 | h_aliases_cnt: 0, 1006 | h_addrtype: -1, 1007 | h_length: -1, 1008 | h_addr_list_cnt: 0, 1009 | error: hostent.herrno, 1010 | } 1011 | .as_slice(), 1012 | )); 1013 | } 1014 | 1015 | for alias in hostent.aliases.iter() { 1016 | buf_aliases_size.extend_from_slice(&(alias.as_bytes_with_nul().len() as i32).to_ne_bytes()); 1017 | buf_aliases.extend_from_slice(alias.as_bytes_with_nul()); 1018 | } 1019 | 1020 | let hostname_bytes = hostent.name.into_bytes_with_nul(); 1021 | 1022 | let header = protocol::HstResponseHeader { 1023 | version: protocol::VERSION, 1024 | found: 1, 1025 | h_name_len: hostname_bytes.len() as i32, 1026 | h_aliases_cnt: hostent.aliases.len() as i32, 1027 | h_addrtype: if num_v4 != 0 { 1028 | nix::sys::socket::AddressFamily::Inet as i32 1029 | } else { 1030 | nix::sys::socket::AddressFamily::Inet6 as i32 1031 | }, 1032 | h_length: if num_v4 != 0 { 4 } else { 16 }, 1033 | h_addr_list_cnt: hostent.addr_list.len() as i32, 1034 | error: hostent.herrno, 1035 | }; 1036 | 1037 | let total_len = std::mem::size_of::() 1038 | + hostname_bytes.len() 1039 | + buf_addrs.len() 1040 | + buf_aliases.len() 1041 | + buf_aliases_size.len(); 1042 | 1043 | let mut buf = Vec::with_capacity(total_len); 1044 | 1045 | // add header 1046 | buf.extend_from_slice(header.as_slice()); 1047 | 1048 | // add hostname 1049 | buf.extend_from_slice(&hostname_bytes); 1050 | 1051 | // add aliases sizes 1052 | buf.extend_from_slice(buf_aliases_size.as_slice()); 1053 | 1054 | // add serialized addresses from buf_addrs 1055 | buf.extend_from_slice(buf_addrs.as_slice()); 1056 | 1057 | // add aliases 1058 | buf.extend_from_slice(buf_aliases.as_slice()); 1059 | 1060 | debug_assert_eq!(buf.len(), total_len); 1061 | 1062 | Ok(buf) 1063 | } 1064 | 1065 | /// Serialize a [RequestType::GETAI] response to the wire. 1066 | /// 1067 | /// This wire format has been implemented by reading the `addhstaiX` 1068 | /// function living in the `nscd/aicache.c` glibc file. We copy the 1069 | /// exact same behaviour, aside from the caching part. 1070 | /// 1071 | /// The wire getaddrinfo call result is serialized like this: 1072 | /// 1073 | /// 1. version: int32. Hardcoded to 2. 1074 | /// 2. found: int32. 1 if we have a result, 0 if we don't. 1075 | /// 3. naddrs: int32. Number of IPv4/6 adresses we're about to write. 1076 | /// 4. addrslen: int32. Total length of the IPv4/6 adresses we're 1077 | /// about to write. 1078 | /// 5. canonlen: int32. Total length of the null-terminated canonical 1079 | /// name string. 1080 | /// 6. error: int32. Error code. Always 0 in the current nscd 1081 | /// implementation. 1082 | /// 7. addrs: \[BE-encoded IPv4/IPv6\]. We sequentially write the 1083 | /// IPv4 and IPv6 bytes using a big endian encoding. There's no 1084 | /// padding, an IPv4 will be 4 bytes wide, an IPv6 16 bytes wide. 1085 | /// 8. addr_family: \[uint8\]. This array mirrors the addrs array. Each 1086 | /// addr element will be mirrored in this array, except we'll write 1087 | /// the associated IP addr family number. AF_INET for an IPv4, 1088 | /// AF_INET6 for a v6. 1089 | /// 9. canon_name: Canonical name of the host. Null-terminated string. 1090 | fn serialize_address_info(resp: AiResponse) -> Result> { 1091 | let mut b_families: Vec = Vec::with_capacity(2); 1092 | let mut b_addrs: Vec = Vec::with_capacity(2); 1093 | for addr in &resp.addrs { 1094 | match addr { 1095 | IpAddr::V4(ip) => { 1096 | b_families.push(AddressFamily::Inet as u8); 1097 | b_addrs.extend(ip.octets()) 1098 | } 1099 | IpAddr::V6(ip) => { 1100 | b_families.push(AddressFamily::Inet6 as u8); 1101 | b_addrs.extend(ip.octets()) 1102 | } 1103 | } 1104 | } 1105 | let addrslen = b_addrs.len(); 1106 | if addrslen > 0 { 1107 | let b_canon_name = CString::new(resp.canon_name)?.into_bytes_with_nul(); 1108 | let ai_response_header = AiResponseHeader { 1109 | version: protocol::VERSION, 1110 | found: 1, 1111 | naddrs: resp.addrs.len() as i32, 1112 | addrslen: addrslen as i32, 1113 | canonlen: b_canon_name.len() as i32, 1114 | error: protocol::H_ERRNO_NETDB_SUCCESS, 1115 | }; 1116 | 1117 | let total_len = size_of::() + b_addrs.len() + b_families.len(); 1118 | let mut buffer = Vec::with_capacity(total_len); 1119 | buffer.extend_from_slice(ai_response_header.as_slice()); 1120 | buffer.extend_from_slice(&b_addrs); 1121 | buffer.extend_from_slice(&b_families); 1122 | buffer.extend_from_slice(&b_canon_name); 1123 | Ok(buffer) 1124 | } else { 1125 | let mut buffer = Vec::with_capacity(size_of::()); 1126 | buffer.extend_from_slice(protocol::AI_RESPONSE_HEADER_NOT_FOUND.as_slice()); 1127 | Ok(buffer) 1128 | } 1129 | } 1130 | 1131 | #[cfg(test)] 1132 | mod test { 1133 | use std::net::{Ipv4Addr, Ipv6Addr}; 1134 | 1135 | use nix::libc::{AF_INET, AF_INET6}; 1136 | 1137 | use super::*; 1138 | 1139 | fn test_logger() -> slog::Logger { 1140 | Logger::root(slog::Discard, slog::o!()) 1141 | } 1142 | 1143 | #[test] 1144 | fn test_handle_request_empty_key() { 1145 | let request = protocol::Request { 1146 | ty: protocol::RequestType::GETPWBYNAME, 1147 | key: &[], 1148 | }; 1149 | 1150 | let result = handle_request(&test_logger(), &Config::default(), &request); 1151 | assert!(result.is_err(), "should error on empty input"); 1152 | } 1153 | 1154 | #[test] 1155 | fn test_handle_request_nul_data() { 1156 | let request = protocol::Request { 1157 | ty: protocol::RequestType::GETPWBYNAME, 1158 | key: &[0x7F, 0x0, 0x0, 0x01], 1159 | }; 1160 | 1161 | let result = handle_request(&test_logger(), &Config::default(), &request); 1162 | assert!(result.is_err(), "should error on garbage input"); 1163 | } 1164 | 1165 | #[test] 1166 | fn test_handle_request_current_user() { 1167 | let current_user = User::from_uid(nix::unistd::geteuid()).unwrap().unwrap(); 1168 | 1169 | let request = protocol::Request { 1170 | ty: protocol::RequestType::GETPWBYNAME, 1171 | key: &CString::new(current_user.name.clone()) 1172 | .unwrap() 1173 | .into_bytes_with_nul(), 1174 | }; 1175 | 1176 | let expected = serialize_user(Some(current_user)) 1177 | .expect("send_user should serialize current user data"); 1178 | let output = handle_request(&test_logger(), &Config::default(), &request) 1179 | .expect("should handle request with no error"); 1180 | assert_eq!(expected, output); 1181 | } 1182 | 1183 | #[test] 1184 | fn test_handle_request_getai() { 1185 | let request = protocol::Request { 1186 | ty: protocol::RequestType::GETAI, 1187 | key: &CString::new("localhost".to_string()) 1188 | .unwrap() 1189 | .into_bytes_with_nul(), 1190 | }; 1191 | 1192 | // The getaddrinfo call can actually return different ordering, or in the case of a 1193 | // IPv4-only host, only return an IPv4 response. 1194 | // Be happy with any of these permutations. 1195 | let gen_ai_resp = |addrs| protocol::AiResponse { 1196 | addrs, 1197 | canon_name: "localhost".to_string(), 1198 | }; 1199 | let ai_resp_1 = gen_ai_resp(vec![ 1200 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1201 | IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1202 | ]); 1203 | let ai_resp_2 = gen_ai_resp(vec![ 1204 | IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1205 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1206 | ]); 1207 | let ai_resp_3 = gen_ai_resp(vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]); 1208 | let expected_1: Vec = serialize_address_info(ai_resp_1) 1209 | .expect("serialize_address_info should serialize correctly"); 1210 | let expected_2: Vec = serialize_address_info(ai_resp_2) 1211 | .expect("serialize_address_info should serialize correctly"); 1212 | let expected_3: Vec = serialize_address_info(ai_resp_3) 1213 | .expect("serialize_address_info should serialize correctly"); 1214 | 1215 | let output = handle_request(&test_logger(), &Config::default(), &request) 1216 | .expect("should handle request with no error"); 1217 | 1218 | assert!( 1219 | expected_1 == output || expected_2 == output || expected_3 == output, 1220 | "\nExpecting \n{:?}\nTo be equal to\n{:?}\nor\n{:?}\nor\n{:?}\n", 1221 | output, 1222 | expected_1, 1223 | expected_2, 1224 | expected_3 1225 | ); 1226 | } 1227 | 1228 | #[test] 1229 | fn test_handle_gethostbyaddr() { 1230 | let request = protocol::Request { 1231 | ty: protocol::RequestType::GETHOSTBYADDR, 1232 | key: &[127, 0, 0, 1], 1233 | }; 1234 | 1235 | let expected = serialize_hostent(Hostent { 1236 | addr_list: vec![IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))], 1237 | name: CString::new(b"localhost".to_vec()).unwrap(), 1238 | addr_type: AF_INET, 1239 | aliases: Vec::new(), 1240 | herrno: 0, 1241 | }) 1242 | .expect("must serialize"); 1243 | 1244 | let output = handle_request(&test_logger(), &Config::default(), &request) 1245 | .expect("should handle request with no error"); 1246 | 1247 | assert_eq!(expected, output) 1248 | } 1249 | 1250 | #[test] 1251 | fn test_handle_gethostbyaddr_invalid_len() { 1252 | let request = protocol::Request { 1253 | ty: protocol::RequestType::GETHOSTBYADDR, 1254 | key: &[127, 0, 0], 1255 | }; 1256 | 1257 | let result = handle_request(&test_logger(), &Config::default(), &request); 1258 | 1259 | assert!(result.is_err(), "should error on invalid length"); 1260 | } 1261 | 1262 | #[test] 1263 | // Fails on CI: depending on the host setup, we might get 1264 | // different or less aliases for localhost. 1265 | #[ignore] 1266 | fn test_handle_gethostbyaddrv6() { 1267 | let request = protocol::Request { 1268 | ty: protocol::RequestType::GETHOSTBYADDRv6, 1269 | key: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 1270 | }; 1271 | 1272 | let expected = serialize_hostent(Hostent { 1273 | addr_list: vec![IpAddr::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))], 1274 | name: CString::new(b"localhost".to_vec()).unwrap(), 1275 | addr_type: AF_INET6, 1276 | aliases: Vec::new(), 1277 | herrno: 0, 1278 | }) 1279 | .expect("must serialize"); 1280 | 1281 | let output = handle_request(&test_logger(), &Config::default(), &request) 1282 | .expect("should handle request with no error"); 1283 | 1284 | assert_eq!(expected, output) 1285 | } 1286 | 1287 | #[test] 1288 | fn test_handle_gethostbyaddrv6_invalid_len() { 1289 | let request = protocol::Request { 1290 | ty: protocol::RequestType::GETHOSTBYADDRv6, 1291 | key: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 1292 | }; 1293 | 1294 | let result = handle_request(&test_logger(), &Config::default(), &request); 1295 | 1296 | assert!(result.is_err(), "should error on invalid length"); 1297 | } 1298 | 1299 | #[test] 1300 | fn test_hostent_serialization() { 1301 | let hostent = serialize_hostent(Hostent { 1302 | name: CString::new(b"trantor.alternativebit.fr".to_vec()).unwrap(), 1303 | aliases: vec![CString::new(b"trantor".to_vec()).unwrap()], 1304 | addr_type: AF_INET6, 1305 | addr_list: vec![IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))], 1306 | herrno: 0, 1307 | }) 1308 | .expect("should serialize"); 1309 | 1310 | // Captured through a mismatched sockburp run 1311 | let expected_bytes: Vec = vec![ 1312 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x01, 0x00, 1313 | 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 1314 | 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6e, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x6c, 1315 | 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x62, 0x69, 0x74, 0x2e, 0x66, 1316 | 0x72, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1317 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x74, 0x72, 0x61, 0x6e, 0x74, 0x6f, 1318 | 0x72, 0x00, 1319 | ]; 1320 | assert_eq!(hostent, expected_bytes) 1321 | } 1322 | 1323 | // Test data for the below harvested using socat between nscd and docker container 1324 | 1325 | #[test] 1326 | fn test_handle_getservbyport_port() { 1327 | // getent service 23 (telnet) 1328 | let request = protocol::Request { 1329 | ty: protocol::RequestType::GETSERVBYPORT, 1330 | key: &[0x35, 0x38, 0x38, 0x38, 0x2f, 0x00], 1331 | }; 1332 | let expected_bytes: Vec = vec![ 1333 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 1334 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x74, 0x65, 0x6c, 0x6e, 1335 | 0x65, 0x74, 0x00, 0x74, 0x63, 0x70, 0x00, 1336 | ]; 1337 | let result = handle_request(&test_logger(), &Config::default(), &request) 1338 | .expect("should handle request with no error"); 1339 | 1340 | assert_eq!(result, expected_bytes); 1341 | } 1342 | 1343 | #[test] 1344 | fn test_handle_getservbyport_port_proto() { 1345 | // getent services 49/udp (tacacs) 1346 | let request = protocol::Request { 1347 | ty: protocol::RequestType::GETSERVBYPORT, 1348 | key: &[0x31, 0x32, 0x35, 0x34, 0x34, 0x2f, 0x75, 0x64, 0x70, 0x00], 1349 | }; 1350 | let expected_bytes: Vec = vec![ 1351 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 1352 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x74, 0x61, 0x63, 0x61, 1353 | 0x63, 0x73, 0x00, 0x75, 0x64, 0x70, 0x00, 1354 | ]; 1355 | let result = handle_request(&test_logger(), &Config::default(), &request) 1356 | .expect("should handle request with no error"); 1357 | assert_eq!(result, expected_bytes); 1358 | } 1359 | 1360 | #[test] 1361 | fn test_handle_getservbyname_name() { 1362 | // getent service domain 1363 | let request = protocol::Request { 1364 | ty: protocol::RequestType::GETSERVBYNAME, 1365 | key: &[ 1366 | 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x75, 0x64, 0x70, 0x00, 1367 | ], 1368 | }; 1369 | let expected_bytes: Vec = vec![ 1370 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 1371 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x64, 0x6f, 0x6d, 0x61, 1372 | 0x69, 0x6e, 0x00, 0x75, 0x64, 0x70, 0x00, 1373 | ]; 1374 | let result = handle_request(&test_logger(), &Config::default(), &request) 1375 | .expect("should handle request with no error"); 1376 | 1377 | assert_eq!(result, expected_bytes); 1378 | } 1379 | #[test] 1380 | fn test_handle_getservbyname_name_proto() { 1381 | // getent service domain/udp 1382 | let request = protocol::Request { 1383 | ty: protocol::RequestType::GETSERVBYNAME, 1384 | key: &[0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x00], 1385 | }; 1386 | let expected_bytes: Vec = vec![ 1387 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 1388 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x64, 0x6f, 0x6d, 0x61, 1389 | 0x69, 0x6e, 0x00, 0x74, 0x63, 0x70, 0x00, 1390 | ]; 1391 | let result = handle_request(&test_logger(), &Config::default(), &request) 1392 | .expect("should handle request with no error"); 1393 | 1394 | assert_eq!(result, expected_bytes); 1395 | } 1396 | 1397 | #[test] 1398 | fn test_handle_getservbyport_port_proto_aliases() { 1399 | // getent service 113/tcp 1400 | // Returns 3 aliases 1401 | // auth 113/tcp authentication tap ident 1402 | let request = protocol::Request { 1403 | ty: protocol::RequestType::GETSERVBYPORT, 1404 | key: &[0x32, 0x38, 0x39, 0x32, 0x38, 0x2f, 0x74, 0x63, 0x70, 0x00], 1405 | }; 1406 | let expected_bytes: Vec = vec![ 1407 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 1408 | 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x61, 0x75, 0x74, 0x68, 1409 | 0x00, 0x74, 0x63, 0x70, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 1410 | 0x00, 0x00, 0x00, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 1411 | 0x69, 0x6f, 0x6e, 0x00, 0x74, 0x61, 0x70, 0x00, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x00, 1412 | ]; 1413 | let result = handle_request(&test_logger(), &Config::default(), &request) 1414 | .expect("should handle request with no error"); 1415 | 1416 | assert_eq!(result, expected_bytes); 1417 | } 1418 | 1419 | // unit tests of netgroup are a bit harder without /etc/netgroup data 1420 | 1421 | #[test] 1422 | fn test_netgroup_serialization() { 1423 | // validate netgroup response serialization 1424 | let netgroupents = serialize_netgroup(vec![ 1425 | Netgroup { 1426 | host: Some(CString::new(b"host1".to_vec()).unwrap()), 1427 | user: Some(CString::new(b"user1".to_vec()).unwrap()), 1428 | domain: Some(CString::new(b"domain1".to_vec()).unwrap()), 1429 | }, 1430 | Netgroup { 1431 | host: Some(CString::new(b"host2".to_vec()).unwrap()), 1432 | user: Some(CString::new(b"user2".to_vec()).unwrap()), 1433 | domain: Some(CString::new(b"domain2".to_vec()).unwrap()), 1434 | }, 1435 | ]) 1436 | .expect("should serialize"); 1437 | 1438 | let expected_bytes: Vec = vec![ 1439 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00, 1440 | 0x00, 0x00, 0x68, 0x6f, 0x73, 0x74, 0x31, 0x00, 0x75, 0x73, 0x65, 0x72, 0x31, 0x00, 1441 | 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x31, 0x00, 0x68, 0x6f, 0x73, 0x74, 0x32, 0x00, 1442 | 0x75, 0x73, 0x65, 0x72, 0x32, 0x00, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x32, 0x00, 1443 | ]; 1444 | assert_eq!(netgroupents, expected_bytes) 1445 | } 1446 | 1447 | #[test] 1448 | fn test_handle_in_netgr_request() { 1449 | // innetgr is the only request with multiple values delimited by NUL 1450 | // ensure from_bytes works 1451 | let in_netgroup_req = InNetGroup::from_bytes(&[ 1452 | 0x6e, 0x65, 0x74, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x00, 0x01, 0x68, 0x6f, 0x73, 0x74, 1453 | 0x31, 0x00, 0x01, 0x75, 0x73, 0x65, 0x72, 0x31, 0x00, 0x01, 0x64, 0x6f, 0x6d, 0x61, 1454 | 0x69, 0x6e, 0x31, 0x00, 1455 | ]) 1456 | .expect("should serialize"); 1457 | let expected = InNetGroup { 1458 | netgroup: String::from("netgroup"), 1459 | host: Some(String::from("host1")), 1460 | user: Some(String::from("user1")), 1461 | domain: Some(String::from("domain1")), 1462 | }; 1463 | 1464 | assert_eq!(in_netgroup_req, expected); 1465 | } 1466 | 1467 | #[test] 1468 | fn test_innetgroup_serialization_in_group() { 1469 | // validate innetgr serialization 1470 | let in_netgroup = serialize_innetgr(true).expect("should serialize"); 1471 | let expected_bytes: Vec = vec![ 1472 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 1473 | ]; 1474 | assert_eq!(in_netgroup, expected_bytes) 1475 | } 1476 | 1477 | #[test] 1478 | fn test_innetgroup_serialization_not_in_group() { 1479 | // validate innetgr serialization 1480 | let in_netgroup = serialize_innetgr(false).expect("should serialize"); 1481 | let expected_bytes: Vec = vec![]; 1482 | assert_eq!(in_netgroup, expected_bytes) 1483 | } 1484 | } 1485 | -------------------------------------------------------------------------------- /src/handlers/nixish.rs: -------------------------------------------------------------------------------- 1 | use nix::libc::{c_int, servent}; 2 | use std::{ 3 | convert::TryFrom, 4 | ffi::{CStr, CString}, 5 | }; 6 | 7 | //nix::unistd does not offer Service or Netgroup functionality 8 | //Duplicate from the group object 9 | //TODO - formally contribute to nix::unistd 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct Service { 12 | pub name: CString, 13 | pub proto: CString, 14 | pub aliases: Vec, 15 | pub port: c_int, 16 | } 17 | 18 | impl TryFrom for Service { 19 | type Error = anyhow::Error; 20 | 21 | fn try_from(serv: servent) -> Result { 22 | if serv.s_name.is_null() || serv.s_proto.is_null() { 23 | anyhow::bail!("Service name or proto are null"); 24 | } 25 | let name = unsafe { CStr::from_ptr(serv.s_name) }.to_owned(); 26 | let proto = unsafe { CStr::from_ptr(serv.s_proto) }.to_owned(); 27 | 28 | let mut alias_ptr = serv.s_aliases; 29 | let mut alias_strings: Vec = vec![]; 30 | 31 | unsafe { 32 | while !(*alias_ptr).is_null() { 33 | alias_strings.push(CStr::from_ptr(*alias_ptr).to_owned()); 34 | alias_ptr = alias_ptr.offset(1); 35 | } 36 | } 37 | 38 | Ok(Service { 39 | name, 40 | proto, 41 | aliases: alias_strings, 42 | port: serv.s_port, 43 | }) 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, PartialEq)] 48 | pub struct Netgroup { 49 | pub host: Option, 50 | pub user: Option, 51 | pub domain: Option, 52 | } 53 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //! `nsncd` is a nscd-compatible daemon that proxies lookups, without caching. 18 | //! 19 | //! `nsncd` stands for "Name Service Non-Caching Daemon." 20 | //! 21 | //! `nsncd` can be used in situations where you want to make an application use 22 | //! nss plugins available to a different libc than the one the application will 23 | //! load. Since most (all?) libc implementations will try to use 24 | //! `/var/run/nscd/socket` if it exists, you can make all lookups on a machine 25 | //! attempt to use the libc that nsncd is running with (and any nss plugins 26 | //! available to it), regardless of the libc used by a particular application. 27 | //! 28 | //! `nsncd` currently does all its lookups directly in its own process, handling 29 | //! each request on a thread. If you have `nss` plugins that behave badly (leak 30 | //! resources, are not thread safe, etc.), this may cause problems. 31 | //! 32 | //! The `unscd` project attempts to solve this by handling lookup requests in 33 | //! child processes instead of directly in the long-lived daemon. This isolates 34 | //! the daemon from problems in the children doing the lookups, but the extra 35 | //! overhead of forking additional child processes seems large. `unscd` can get 36 | //! away with that because it's also caching, but we're not caching right now. 37 | //! We might try forking child processes to handle requests at some point later. 38 | 39 | // TODO: 40 | // - implement other pw and group methods? 41 | // - error handling 42 | // - logging 43 | // - maybe do serde better? 44 | // - test errors in underlying calls 45 | // - daemon/pidfile stuff 46 | 47 | use std::io::prelude::*; 48 | use std::io::ErrorKind; 49 | use std::os::unix::fs::PermissionsExt; 50 | use std::os::unix::net::{UnixListener, UnixStream}; 51 | use std::path::Path; 52 | use std::time::Duration; 53 | 54 | use anyhow::{Context, Result}; 55 | use crossbeam_channel as channel; 56 | use sd_notify::NotifyState; 57 | #[allow(unused_imports)] 58 | use slog::{debug, error, o, Drain}; 59 | use std::env; 60 | 61 | mod config; 62 | mod ffi; 63 | mod handlers; 64 | mod protocol; 65 | mod work_group; 66 | 67 | use config::Config; 68 | use work_group::WorkGroup; 69 | 70 | const DEFAULT_SOCKET_PATH: &str = "/var/run/nscd/socket"; 71 | 72 | fn main() -> Result<()> { 73 | ffi::disable_internal_nscd(); 74 | 75 | let decorator = slog_term::TermDecorator::new().build(); 76 | let drain = slog_term::FullFormat::new(decorator).build().fuse(); 77 | let drain = slog_async::Async::new(drain).build().fuse(); 78 | 79 | let logger = slog::Logger::root(drain, slog::o!()); 80 | 81 | let config = Config::from_env()?; 82 | let socket_path = 83 | env::var("NSNCD_SOCKET_PATH").unwrap_or_else(|_| DEFAULT_SOCKET_PATH.to_string()); 84 | let path = Path::new(&socket_path); 85 | 86 | slog::info!(logger, "started"; 87 | "path" => ?path, 88 | "config" => ?config, 89 | ); 90 | let mut wg = WorkGroup::new(); 91 | let tx = spawn_workers(&mut wg, &logger, config); 92 | 93 | std::fs::create_dir_all(path.parent().expect("socket path has no parent"))?; 94 | std::fs::remove_file(path).ok(); 95 | let listener = UnixListener::bind(path).context("could not bind to socket")?; 96 | std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o777))?; 97 | spawn_acceptor(&mut wg, &logger, listener, tx, config.handoff_timeout); 98 | 99 | let _ = sd_notify::notify(true, &[NotifyState::Ready]); 100 | 101 | let (result, handles) = wg.run(); 102 | if let Err(e) = result { 103 | // if a thread unwound with a panic, just start panicing here. the nss 104 | // modules in this process space are probably very sad, and we'll 105 | // stop serving requests abruptly. 106 | std::panic::resume_unwind(e); 107 | } else { 108 | // something else happened that made a process exit, so try to exit 109 | // gracefully. 110 | slog::info!(logger, "shutting down"); 111 | for handle in handles { 112 | let _ = handle.join(); 113 | } 114 | Ok(()) 115 | } 116 | } 117 | 118 | fn spawn_acceptor( 119 | wg: &mut WorkGroup, 120 | log: &slog::Logger, 121 | listener: UnixListener, 122 | tx: channel::Sender, 123 | handoff_timeout: Duration, 124 | ) { 125 | let log = log.new(o!("thread" => "accept")); 126 | 127 | wg.add(move |ctx| { 128 | for stream in listener.incoming() { 129 | if ctx.is_shutdown() { 130 | break; 131 | } 132 | 133 | match stream { 134 | // if something goes wrong and it's multiple seconds until we 135 | // get a response, kill the process. 136 | // 137 | // the timeout here is set such that nss will fall back to system 138 | // libc before this timeout is hit - clients will already be 139 | // giving up and going elsewhere so crashing the process should 140 | // not make a bad situation worse. 141 | Ok(stream) => match tx.send_timeout(stream, handoff_timeout) { 142 | Err(channel::SendTimeoutError::Timeout(_)) => { 143 | error!(log, "timed out waiting for an available worker"); 144 | break; 145 | } 146 | Err(channel::SendTimeoutError::Disconnected(_)) => { 147 | error!(log, "worker channel is disconnected"); 148 | break; 149 | } 150 | Ok(()) => { /*ok!*/ } 151 | }, 152 | Err(err) => { 153 | error!(log, "error accepting connection"; "err" => %err); 154 | break; 155 | } 156 | } 157 | } 158 | 159 | // at the end of the listener loop, drop tx so that any working threads 160 | // still waiting for a connection have a chance to finish. 161 | // 162 | // this is unnecessary but explicit 163 | std::mem::drop(tx); 164 | }); 165 | } 166 | 167 | fn spawn_workers( 168 | wg: &mut WorkGroup, 169 | log: &slog::Logger, 170 | config: Config, 171 | ) -> channel::Sender { 172 | let (tx, rx) = channel::bounded(0); 173 | 174 | for worker_id in 0..config.worker_count { 175 | let rx = rx.clone(); 176 | let log = log.new(o!("thread" => format!("worker_{}", worker_id))); 177 | 178 | // ctx is ignored - the acceptor thread will close the rx channel if 179 | // the wg is shutdown and it's time to exit. 180 | wg.add(move |_ctx| { 181 | while let Ok(stream) = rx.recv() { 182 | handle_stream(&log, &config, stream); 183 | } 184 | }); 185 | } 186 | 187 | tx 188 | } 189 | 190 | fn handle_stream(log: &slog::Logger, config: &Config, mut stream: UnixStream) { 191 | debug!(log, "accepted connection"; "stream" => ?stream); 192 | let mut buf = [0; 4096]; 193 | let size_read = match stream.read(&mut buf) { 194 | Ok(x) => x, 195 | Err(e) => { 196 | debug!(log, "reading from connection"; "err" => %e); 197 | return; 198 | } 199 | }; 200 | let request = match protocol::Request::parse(&buf[0..size_read]) { 201 | Ok(x) => x, 202 | Err(e) => { 203 | debug!(log, "parsing request"; "err" => %e); 204 | return; 205 | } 206 | }; 207 | let type_str = format!("{:?}", request.ty); 208 | let log = log.new(o!("request_type" => type_str)); 209 | let response = match handlers::handle_request(&log, config, &request) { 210 | Ok(x) => x, 211 | Err(e) => { 212 | error!(log, "error handling request"; "err" => %e); 213 | return; 214 | } 215 | }; 216 | if let Err(e) = stream.write_all(response.as_slice()) { 217 | match e.kind() { 218 | // If we send a response that's too big for the client's buffer, 219 | // the client will disconnect and not read the rest of our 220 | // response, and then come back with a new connection after 221 | // increasing its buffer. There's no need to log that, and 222 | // generally, clients can disappear at any point. 223 | ErrorKind::ConnectionReset | ErrorKind::BrokenPipe => (), 224 | _ => debug!(log, "sending response"; "response_len" => response.len(), "err" => %e), 225 | }; 226 | } 227 | if let Err(e) = stream.shutdown(std::net::Shutdown::Both) { 228 | debug!(log, "shutting down stream"; "err" => %e); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //! The nscd protocol definition (at least, the parts of it we care about). 18 | //! 19 | //! The response structs here only describe the format of the header of the 20 | //! response. For each such response, if the lookup succeeded, there are 21 | //! additional strings we need to send after the header. Those are dealt with in 22 | //! `handlers::send_{user,group}`. For a full picture of the protocol, you will 23 | //! need to read both. 24 | 25 | use std::mem::size_of; 26 | use std::{convert::TryInto, net::IpAddr}; 27 | 28 | use anyhow::{ensure, Context, Result}; 29 | use num_derive::{FromPrimitive, ToPrimitive}; 30 | use num_traits::FromPrimitive; 31 | 32 | use nix::libc::{c_int, gid_t, uid_t}; 33 | 34 | /// This is version 2 of the glibc nscd protocol. The version is passed as part 35 | /// of each message header. 36 | pub const VERSION: i32 = 2; 37 | 38 | /// Errors used in {Ai,Hst}ResponseHeader structs. 39 | /// See NSCD's resolv/netdb.h for the complete list. 40 | pub const H_ERRNO_NETDB_SUCCESS: i32 = 0; 41 | #[allow(dead_code)] 42 | pub const H_ERRNO_TRY_AGAIN: i32 = 2; // Non-Authoritative Host not found 43 | 44 | /// Available services. This enum describes all service types the nscd protocol 45 | /// knows about, though we only implement `GETPW*`, `GETGR*`, and `INITGROUPS`. 46 | #[derive(Clone, Copy, Debug, FromPrimitive, ToPrimitive)] 47 | #[allow(clippy::upper_case_acronyms)] 48 | pub enum RequestType { 49 | GETPWBYNAME, 50 | GETPWBYUID, 51 | GETGRBYNAME, 52 | GETGRBYGID, 53 | GETHOSTBYNAME, 54 | GETHOSTBYNAMEv6, 55 | GETHOSTBYADDR, 56 | GETHOSTBYADDRv6, 57 | /// Shut the server down. 58 | SHUTDOWN, 59 | /// Get the server statistic. 60 | GETSTAT, 61 | /// Invalidate one special cache. 62 | INVALIDATE, 63 | GETFDPW, 64 | GETFDGR, 65 | GETFDHST, 66 | GETAI, 67 | INITGROUPS, 68 | GETSERVBYNAME, 69 | GETSERVBYPORT, 70 | GETFDSERV, 71 | GETNETGRENT, 72 | INNETGR, 73 | GETFDNETGR, 74 | LASTREQ, 75 | } 76 | 77 | /// An incoming request. All requests have a version, a type, and a string key. 78 | /// This struct keeps the type and key, because that's what we need to reply to 79 | /// it, we only handle one version and we validate, but don't retain it. 80 | /// 81 | /// The parsed Request object is valid as long as the buffer it is parsed from 82 | /// (that is, the key is a reference to the bytes in the buffer). 83 | #[derive(Debug)] 84 | pub struct Request<'a> { 85 | pub ty: RequestType, 86 | pub key: &'a [u8], 87 | } 88 | 89 | impl<'a> Request<'a> { 90 | /// Parse a Request from a buffer. 91 | pub fn parse(buf: &'a [u8]) -> Result { 92 | ensure!(buf.len() >= 12, "request body too small: {}", buf.len()); 93 | 94 | let version = buf[0..4].try_into().map(i32::from_ne_bytes)?; 95 | ensure!(version == VERSION, "wrong protocol version {}", version); 96 | 97 | let type_val = buf[4..8].try_into().map(i32::from_ne_bytes)?; 98 | let ty = FromPrimitive::from_i32(type_val) 99 | .with_context(|| format!("invalid enum value {}", type_val))?; 100 | 101 | let key_len = buf[8..12].try_into().map(i32::from_ne_bytes)?; 102 | let key_end = (12 + key_len).try_into()?; 103 | ensure!(buf.len() >= key_end, "request body too small"); 104 | 105 | Ok(Request { 106 | ty, 107 | key: &buf[12..key_end], 108 | }) 109 | } 110 | } 111 | 112 | // the nscd protocol just puts structs onto a socket and hopes they come out 113 | // the same size on the other end. it seems to assume there is no padding 114 | // interpreted by the compiler. 115 | // 116 | // this is pretty sketchy, but we have to match it, so all of the structs 117 | // below use repr(C) and not repr(padded). 118 | 119 | /// Structure sent in reply to password query. Note that this struct is 120 | /// sent also if the service is disabled or there is no record found. 121 | #[repr(C)] 122 | #[derive(Clone, Copy, Default)] 123 | pub struct PwResponseHeader { 124 | pub version: c_int, 125 | pub found: c_int, 126 | pub pw_name_len: c_int, 127 | pub pw_passwd_len: c_int, 128 | pub pw_uid: uid_t, 129 | pub pw_gid: gid_t, 130 | pub pw_gecos_len: c_int, 131 | pub pw_dir_len: c_int, 132 | pub pw_shell_len: c_int, 133 | } 134 | 135 | impl PwResponseHeader { 136 | /// Serialize the header to bytes. 137 | /// 138 | /// The C implementations of nscd just take the address of the struct, so 139 | /// we will too, to make it easy to convince ourselves it's correct. 140 | pub fn as_slice(&self) -> &[u8] { 141 | let p = self as *const _ as *const u8; 142 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 143 | } 144 | } 145 | 146 | /// Structure sent in reply to group query. Note that this struct is 147 | /// sent also if the service is disabled or there is no record found. 148 | #[repr(C)] 149 | #[derive(Clone, Copy, Default)] 150 | pub struct GrResponseHeader { 151 | pub version: c_int, 152 | pub found: c_int, 153 | pub gr_name_len: c_int, 154 | pub gr_passwd_len: c_int, 155 | pub gr_gid: gid_t, 156 | pub gr_mem_cnt: c_int, 157 | } 158 | 159 | impl GrResponseHeader { 160 | /// Serialize the header to bytes. 161 | /// 162 | /// The C implementations of nscd just take the address of the struct, so 163 | /// we will too, to make it easy to convince ourselves it's correct. 164 | pub fn as_slice(&self) -> &[u8] { 165 | let p = self as *const _ as *const u8; 166 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 167 | } 168 | } 169 | 170 | /// Structure sent in reply to initgroups query. Note that this struct is 171 | /// sent also if the service is disabled or there is no record found. 172 | #[repr(C)] 173 | #[derive(Clone, Copy, Default)] 174 | pub struct InitgroupsResponseHeader { 175 | pub version: c_int, 176 | pub found: c_int, 177 | pub ngrps: c_int, 178 | } 179 | 180 | impl InitgroupsResponseHeader { 181 | /// Serialize the header to bytes. 182 | /// 183 | /// The C implementations of nscd just take the address of the struct, so 184 | /// we will too, to make it easy to convince ourselves it's correct. 185 | pub fn as_slice(&self) -> &[u8] { 186 | let p = self as *const _ as *const u8; 187 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 188 | } 189 | } 190 | 191 | /// Structure containing the resulting data of a [RequestType::GETAI] 192 | /// operation. 193 | /// 194 | /// Unlike most of the data types declared in this module, this 195 | /// structure isn't meant to be directly serialized to the wire. 196 | /// Instead, it contains all the necessary informations to to generate 197 | /// a [AiResponseHeader] and its associated payload. 198 | #[derive(Debug, Clone)] 199 | pub struct AiResponse { 200 | pub addrs: Vec, 201 | pub canon_name: String, 202 | } 203 | 204 | /// Response Header derived from the glibc `ai_response_header` 205 | /// structure. 206 | #[repr(C)] 207 | #[derive(Debug, Clone, Copy)] 208 | pub struct AiResponseHeader { 209 | pub version: c_int, 210 | pub found: c_int, 211 | pub naddrs: c_int, 212 | pub addrslen: c_int, 213 | pub canonlen: c_int, 214 | pub error: c_int, 215 | } 216 | 217 | impl AiResponseHeader { 218 | /// Serialize the header to bytes 219 | /// 220 | /// The C implementations of nscd just take the address of the struct, so 221 | /// we will too, to make it easy to convince ourselves it's correct. 222 | pub fn as_slice(&self) -> &[u8] { 223 | let p = self as *const _ as *const u8; 224 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 225 | } 226 | } 227 | 228 | /// Magic address info header returned to the client when an address 229 | /// lookup doesn't yield any matches. See glib's `nscd/aicache.c` file 230 | /// for the original definition. 231 | pub const AI_RESPONSE_HEADER_NOT_FOUND: AiResponseHeader = AiResponseHeader { 232 | version: VERSION, 233 | found: 0, 234 | naddrs: 0, 235 | addrslen: 0, 236 | canonlen: 0, 237 | error: 0, 238 | }; 239 | 240 | /// Structure used to hold the reply header of a 241 | /// gethostbyaddr[v6]/gethostbyname[v6] request. 242 | /// Maps to the hst_response_header struct in nscd. 243 | #[repr(C)] 244 | #[derive(Clone, Copy, Default)] 245 | pub struct HstResponseHeader { 246 | pub version: c_int, 247 | pub found: c_int, // 0 or 1, -1 if disabled 248 | pub h_name_len: c_int, // length of the hostname null-terminated 249 | pub h_aliases_cnt: c_int, // number of aliases (0) 250 | pub h_addrtype: c_int, // AF_INET or AF_INET6 251 | pub h_length: c_int, // length of the address 252 | pub h_addr_list_cnt: c_int, // number of addresses (1) 253 | pub error: c_int, // H_ERRNO_* 254 | } 255 | 256 | impl HstResponseHeader { 257 | /// Serialize the header to bytes. 258 | /// 259 | /// The C implementations of nscd just take the address of the struct, so 260 | /// we will too, to make it easy to convince ourselves it's correct. 261 | pub fn as_slice(&self) -> &[u8] { 262 | let p = self as *const _ as *const u8; 263 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 264 | } 265 | } 266 | 267 | /* Structure send in reply to innetgroup query. Note that this struct is 268 | sent also if the service is disabled or there is no record found. */ 269 | #[repr(C)] 270 | #[derive(Clone, Copy, Default)] 271 | pub struct InNetgroupResponseHeader { 272 | pub version: c_int, 273 | pub found: c_int, 274 | pub result: c_int, 275 | } 276 | 277 | impl InNetgroupResponseHeader { 278 | /// Serialize the header to bytes. 279 | /// 280 | /// The C implementations of nscd just take the address of the struct, so 281 | /// we will too, to make it easy to convince ourselves it's correct. 282 | pub fn as_slice(&self) -> &[u8] { 283 | let p = self as *const _ as *const u8; 284 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 285 | } 286 | } 287 | 288 | /* Structure send in reply to service query. Note that this struct is 289 | sent also if the service is disabled or there is no record found. */ 290 | 291 | #[repr(C)] 292 | #[derive(Clone, Copy, Default)] 293 | pub struct ServResponseHeader { 294 | pub version: c_int, 295 | pub found: c_int, 296 | pub s_name_len: c_int, 297 | pub s_proto_len: c_int, 298 | pub s_aliases_cnt: c_int, 299 | pub s_port: c_int, 300 | } 301 | 302 | /* Structure send in reply to netgroup query. Note that this struct is 303 | sent also if the service is disabled or there is no record found. */ 304 | #[repr(C)] 305 | #[derive(Clone, Copy, Default)] 306 | pub struct NetgroupResponseHeader { 307 | pub version: c_int, 308 | pub found: c_int, 309 | pub nresults: c_int, 310 | pub result_len: c_int, 311 | } 312 | 313 | impl ServResponseHeader { 314 | pub fn as_slice(&self) -> &[u8] { 315 | let p = self as *const _ as *const u8; 316 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 317 | } 318 | } 319 | 320 | impl NetgroupResponseHeader { 321 | pub fn as_slice(&self) -> &[u8] { 322 | let p = self as *const _ as *const u8; 323 | unsafe { std::slice::from_raw_parts(p, size_of::()) } 324 | } 325 | } 326 | 327 | #[cfg(test)] 328 | mod test { 329 | use super::*; 330 | 331 | #[test] 332 | fn test_pw_response_header_as_slice() { 333 | let header = PwResponseHeader { 334 | version: VERSION, 335 | found: 1, 336 | pw_name_len: 5, 337 | pw_passwd_len: 0, 338 | pw_uid: 123, 339 | pw_gid: 456, 340 | pw_gecos_len: 666, 341 | pw_dir_len: 888, 342 | pw_shell_len: 4, 343 | }; 344 | 345 | let mut expected = Vec::with_capacity(4 * 9); 346 | { 347 | expected.extend_from_slice(&VERSION.to_ne_bytes()); 348 | expected.extend_from_slice(&1i32.to_ne_bytes()); 349 | expected.extend_from_slice(&5i32.to_ne_bytes()); 350 | expected.extend_from_slice(&0i32.to_ne_bytes()); 351 | expected.extend_from_slice(&123u32.to_ne_bytes()); 352 | expected.extend_from_slice(&456u32.to_ne_bytes()); 353 | expected.extend_from_slice(&666i32.to_ne_bytes()); 354 | expected.extend_from_slice(&888i32.to_ne_bytes()); 355 | expected.extend_from_slice(&4i32.to_ne_bytes()); 356 | } 357 | 358 | assert_eq!(header.as_slice(), expected); 359 | } 360 | 361 | #[test] 362 | fn gr_response_header_as_slice() { 363 | let header = GrResponseHeader { 364 | version: VERSION, 365 | found: 1, 366 | gr_name_len: 5, 367 | gr_passwd_len: 0, 368 | gr_gid: 420, 369 | gr_mem_cnt: 1, 370 | }; 371 | 372 | let mut expected = Vec::with_capacity(4 * 6); 373 | { 374 | expected.extend_from_slice(&VERSION.to_ne_bytes()); 375 | expected.extend_from_slice(&1i32.to_ne_bytes()); 376 | expected.extend_from_slice(&5i32.to_ne_bytes()); 377 | expected.extend_from_slice(&0i32.to_ne_bytes()); 378 | expected.extend_from_slice(&420u32.to_ne_bytes()); 379 | expected.extend_from_slice(&1i32.to_ne_bytes()); 380 | } 381 | 382 | assert_eq!(header.as_slice(), expected); 383 | } 384 | 385 | #[test] 386 | fn initgroups_response_header_as_slice() { 387 | let header = InitgroupsResponseHeader { 388 | version: VERSION, 389 | found: 1, 390 | ngrps: 10, 391 | }; 392 | 393 | let mut expected = Vec::with_capacity(4 * 3); 394 | { 395 | expected.extend_from_slice(&VERSION.to_ne_bytes()); 396 | expected.extend_from_slice(&1i32.to_ne_bytes()); 397 | expected.extend_from_slice(&10i32.to_ne_bytes()); 398 | } 399 | 400 | assert_eq!(header.as_slice(), expected); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/work_group.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Two Sigma Open Source, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | use std::any::Any; 18 | use std::sync::{Arc, Condvar, Mutex}; 19 | use std::thread::{self, JoinHandle}; 20 | 21 | pub struct WorkGroup { 22 | fs: Vec>, 23 | data: Arc<(Mutex>, Condvar)>, 24 | } 25 | 26 | pub struct Context { 27 | thread: usize, 28 | data: Arc<(Mutex>, Condvar)>, 29 | } 30 | 31 | impl Context { 32 | pub fn is_shutdown(&self) -> bool { 33 | let (m, _) = &*self.data; 34 | m.lock().unwrap().is_some() 35 | } 36 | } 37 | 38 | impl Drop for Context { 39 | fn drop(&mut self) { 40 | let (mtx, cond) = &*self.data; 41 | mtx.lock().unwrap().get_or_insert(self.thread); 42 | 43 | cond.notify_all(); 44 | } 45 | } 46 | 47 | impl WorkGroup { 48 | pub fn new() -> WorkGroup { 49 | let fs = vec![]; 50 | let data = Arc::new((Mutex::new(None), Condvar::new())); 51 | 52 | WorkGroup { fs, data } 53 | } 54 | 55 | pub fn add(&mut self, f: F) 56 | where 57 | F: FnOnce(Context), 58 | F: Send + 'static, 59 | { 60 | self.fs.push(Box::new(f)); 61 | } 62 | 63 | // Box is how the stdlib defines JoinHandle panic 64 | // data, so that's what we're doing too. 65 | #[allow(clippy::type_complexity)] 66 | pub fn run( 67 | self, 68 | ) -> ( 69 | Result<(), Box>, 70 | Vec>, 71 | ) { 72 | let mut handles = vec![]; 73 | 74 | for f in self.fs { 75 | let data = self.data.clone(); 76 | let thread = handles.len(); 77 | handles.push(thread::spawn(move || { 78 | let context = Context { thread, data }; 79 | f(context); 80 | })); 81 | } 82 | 83 | // wait for the first thread to exit. 84 | let (mtx, cond) = &*self.data; 85 | let mut data = mtx.lock().unwrap(); 86 | while !data.is_some() { 87 | data = cond.wait(data).unwrap(); 88 | } 89 | 90 | // pull the result out of the JoinHandle of the thread that exited first 91 | // and then join on the rest so that we wait on them to exit. 92 | let result = handles.remove(data.unwrap()).join(); 93 | (result, handles) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod test { 99 | use super::*; 100 | use std::sync::atomic::{AtomicUsize, Ordering}; 101 | use std::time::Duration; 102 | 103 | #[test] 104 | fn test_run_one() { 105 | let state = Arc::new(AtomicUsize::new(123)); 106 | 107 | let mut wg = WorkGroup::new(); 108 | 109 | let state_ref = state.clone(); 110 | wg.add(move |_| { 111 | state_ref.store(456, Ordering::SeqCst); 112 | }); 113 | 114 | let (result, handles) = wg.run(); 115 | assert!(result.is_ok()); 116 | assert!(handles.is_empty()); 117 | assert_eq!(state.load(Ordering::SeqCst), 456); 118 | } 119 | 120 | #[test] 121 | fn test_run_multiple() { 122 | let state = Arc::new(AtomicUsize::new(0)); 123 | 124 | let mut wg = WorkGroup::new(); 125 | 126 | for _ in 0..10 { 127 | let state_ref = state.clone(); 128 | wg.add(move |_| { 129 | state_ref.fetch_add(1, Ordering::SeqCst); 130 | }); 131 | } 132 | 133 | let (result, handles) = wg.run(); 134 | assert!(result.is_ok()); 135 | assert!(!handles.is_empty()); 136 | for handle in handles { 137 | let _ = handle.join(); 138 | } 139 | assert_eq!(state.load(Ordering::SeqCst), 10); 140 | } 141 | 142 | #[test] 143 | fn test_run_panic() { 144 | let mut wg = WorkGroup::new(); 145 | 146 | for _ in 0..10 { 147 | wg.add(move |_| { 148 | thread::sleep(Duration::from_secs(300)); 149 | }); 150 | } 151 | wg.add(move |_| { 152 | panic!("hello"); 153 | }); 154 | 155 | let (result, _) = wg.run(); 156 | assert!(result.is_err()); 157 | assert_eq!( 158 | result 159 | .err() 160 | .and_then(|data| data.downcast_ref::<&'static str>().map(|s| *s == "hello")), 161 | Some(true) 162 | ); 163 | } 164 | } 165 | --------------------------------------------------------------------------------