├── .cargo └── config.toml ├── .github ├── FUNDING.yml ├── codecov.yml ├── dependabot.yml ├── review-template.md └── workflows │ ├── checks.yaml │ ├── docs.yaml │ ├── nightly.yaml │ ├── packaging-v5.yaml │ ├── packaging.yaml │ └── scorecard.yaml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── clippy.toml ├── config ├── ntp.demobilize.toml ├── nts.client.toml └── nts.server.toml ├── deny.toml ├── docs ├── algorithm │ ├── .gitignore │ ├── algorithm.bib │ ├── algorithm.pdf │ ├── algorithm.tex │ ├── measurement.png │ ├── offset-chrony.dat │ ├── offset-chrony.gnuplot │ ├── offset-chrony.png │ ├── offset-ntpd-rs.dat │ ├── offset-ntpd-rs.gnuplot │ └── offset-ntpd-rs.png ├── audits │ └── report-ntpd-rs-v11-final.pdf ├── development-process.md ├── development │ ├── audits.md │ ├── ca.md │ ├── code-structure.md │ ├── current-dataflow.svg │ ├── flowdiagram.svg │ ├── further-reading.md │ ├── new-dataflow.svg │ └── threat-model.md ├── examples │ └── conf │ │ ├── ntp.toml.default │ │ ├── ntpd-rs-metrics.service │ │ ├── ntpd-rs.preset │ │ └── ntpd-rs.service ├── guide │ ├── exporting-metrics.md │ ├── getting-started.md │ ├── gps-pps.md │ ├── installation.md │ ├── migrating-chrony.md │ ├── migrating-ntpd.md │ ├── migrating-ntpsec.md │ ├── nts.md │ ├── security-guidance.md │ └── server-setup.md ├── includes │ └── glossary.md ├── index.md ├── man │ ├── ntp-ctl.8.md │ ├── ntp-daemon.8.md │ ├── ntp-metrics-exporter.8.md │ └── ntp.toml.5.md └── precompiled │ └── man │ ├── ntp-ctl.8 │ ├── ntp-daemon.8 │ ├── ntp-metrics-exporter.8 │ └── ntp.toml.5 ├── fuzz ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── fuzz_rand_shim │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── fuzz_targets │ ├── cookie_parsing_sound.rs │ ├── duration_from_float.rs │ ├── encrypted_client_parsing.rs │ ├── encrypted_server_parsing.rs │ ├── ipfilter.rs │ ├── key_exchange_result_decoder.rs │ ├── key_exchange_server_decoder.rs │ ├── ntsrecord.rs │ ├── packet_keyset.rs │ ├── packet_parsing_sound.rs │ └── record_encode_decode.rs ├── mkdocs.yml ├── ntp-proto ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── algorithm │ │ ├── kalman │ │ │ ├── combiner.rs │ │ │ ├── config.rs │ │ │ ├── matrix.rs │ │ │ ├── mod.rs │ │ │ ├── select.rs │ │ │ └── source.rs │ │ └── mod.rs │ ├── clock.rs │ ├── config.rs │ ├── cookiestash.rs │ ├── identifiers.rs │ ├── io.rs │ ├── ipfilter.rs │ ├── keyset.rs │ ├── lib.rs │ ├── nts_pool_ke.rs │ ├── nts_record.rs │ ├── packet │ │ ├── crypto.rs │ │ ├── error.rs │ │ ├── extension_fields.rs │ │ ├── mac.rs │ │ ├── mod.rs │ │ └── v5 │ │ │ ├── error.rs │ │ │ ├── extension_fields.rs │ │ │ ├── mod.rs │ │ │ └── server_reference_id.rs │ ├── server.rs │ ├── source.rs │ ├── system.rs │ ├── time_types.rs │ └── tls_utils.rs └── test-keys │ ├── ec_key.pem │ ├── end.fullchain.pem │ ├── end.key │ ├── end.pem │ ├── gen-cert.sh │ ├── pkcs8_key.pem │ ├── rsa_key.pem │ ├── testca.key │ ├── testca.pem │ ├── unsafe.nts.client.toml │ └── unsafe.nts.server.toml ├── ntp.server.toml ├── ntp.toml ├── ntpd ├── CHANGELOG.md ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bin │ ├── ntp-ctl.rs │ ├── ntp-daemon.rs │ └── ntp-metrics-exporter.rs ├── build.rs ├── docs ├── src │ ├── ctl.rs │ ├── daemon │ │ ├── clock.rs │ │ ├── config │ │ │ ├── mod.rs │ │ │ ├── ntp_source.rs │ │ │ ├── server.rs │ │ │ └── subnet.rs │ │ ├── keyexchange.rs │ │ ├── local_ip_provider.rs │ │ ├── mod.rs │ │ ├── ntp_source.rs │ │ ├── nts_key_provider.rs │ │ ├── observer.rs │ │ ├── pps_source.rs │ │ ├── server.rs │ │ ├── sock_source.rs │ │ ├── sockets.rs │ │ ├── spawn │ │ │ ├── mod.rs │ │ │ ├── nts.rs │ │ │ ├── nts_pool.rs │ │ │ ├── pool.rs │ │ │ ├── pps.rs │ │ │ ├── sock.rs │ │ │ └── standard.rs │ │ ├── system.rs │ │ ├── tracing.rs │ │ └── util.rs │ ├── force_sync │ │ ├── algorithm.rs │ │ └── mod.rs │ ├── lib.rs │ └── metrics │ │ ├── exporter.rs │ │ └── mod.rs ├── test-keys ├── testdata │ ├── certificates │ │ ├── nos-nl-chain.pem │ │ └── nos-nl.pem │ └── config │ │ └── invalid.toml └── tests │ └── ctl.rs ├── nts-pool-ke ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bin │ └── nts-pool-ke.rs ├── src │ ├── condcompile │ │ ├── cli.rs │ │ ├── config.rs │ │ └── tracing.rs │ └── lib.rs ├── unsafe.nts.client.toml ├── unsafe.nts.server.toml └── unsafe.pool.toml ├── pkg ├── deb │ ├── COPYRIGHT-debian │ ├── postinst │ ├── postrm │ └── prerm ├── rpm │ └── scriptlets.toml └── test-scripts │ └── test-ntpd-rs.sh └── utils ├── build-release.sh ├── generate-man.sh ├── mkdocs.sh ├── pandoc.sh ├── precompiled-man-diff.sh ├── release.sh └── update-version.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all())'] 2 | rustflags = ["-C", "force-unwind-tables"] 3 | 4 | # runs tests under sudo, useful for certain (hardware) clock manipulation in tests 5 | # [target.x86_64-unknown-linux-gnu] 6 | # runner = 'sudo -E' 7 | 8 | # make `cargo clippy --target ...` just work when cross-compiling 9 | # [target.x86_64-apple-darwin] 10 | # linker = "~/.cargo/bin/cargo-zigbuild zig cc -- -target x86_64-macos-gnu -g" 11 | # 12 | # [target.x86_64-unknown-freebsd] 13 | # linker = "~/.cargo/bin/cargo-zigbuild zig cc -- -target freebsd -g" 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pendulum-project] 4 | custom: ["https://trifectatech.org/support/"] 5 | # patreon: # Replace with a single Patreon username 6 | # open_collective: # Replace with a single Open Collective username 7 | # ko_fi: # Replace with a single Ko-fi username 8 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | # liberapay: # Replace with a single Liberapay username 11 | # issuehunt: # Replace with a single IssueHunt username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # polar: # Replace with a single Polar username 14 | # buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 15 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: off 5 | ignore: 6 | - "test-binaries" 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | groups: 9 | github-actions: 10 | patterns: ["*"] 11 | 12 | - package-ecosystem: cargo 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | versioning-strategy: lockfile-only 17 | open-pull-requests-limit: 10 18 | groups: 19 | cargo: 20 | patterns: ["*"] 21 | -------------------------------------------------------------------------------- /.github/review-template.md: -------------------------------------------------------------------------------- 1 | Checklist review: 2 | - [ ] Checked for security implications from this change? 3 | - [ ] Is the documentation updated? 4 | - [ ] Is the changelog updated? 5 | - [ ] Are relevant tests and fuzzers added? 6 | - [ ] Are changed lines sufficiently covered by tests? 7 | - [ ] Are newly added dependencies really needed? 8 | - [ ] Do the changes call for manual testing? 9 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | permissions: 4 | actions: read 5 | contents: read 6 | pages: write 7 | id-token: write 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout sources 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 24 | 25 | - name: Build site 26 | run: utils/mkdocs.sh --no-bind-port build 27 | 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa 30 | with: 31 | path: target/docs/site 32 | 33 | deploy: 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | runs-on: ubuntu-latest 38 | needs: build 39 | steps: 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e 43 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'release/**' 10 | schedule: 11 | - cron: '0 4 * * *' 12 | workflow_dispatch: {} 13 | 14 | jobs: 15 | test-freebsd: 16 | # see https://github.com/actions/runner/issues/385 17 | # use https://github.com/vmactions/freebsd-vm for now 18 | name: test on freebsd 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 45 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 23 | - name: test on freebsd 24 | uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 25 | with: 26 | usesh: true 27 | mem: 4096 28 | copyback: false 29 | prepare: | 30 | pkg install -y curl cmake llvm-devel 31 | curl https://sh.rustup.rs -sSf --output rustup.sh 32 | sh rustup.sh -y --profile minimal --default-toolchain stable 33 | . "$HOME/.cargo/env" 34 | rustup component add clippy 35 | echo "~~~~ rustc --version ~~~~" 36 | rustc --version 37 | echo "~~~~ freebsd-version ~~~~" 38 | freebsd-version 39 | run: | 40 | . "$HOME/.cargo/env" 41 | cargo clippy --workspace --all-targets -- -D warnings && 42 | cargo build --all-targets && 43 | cargo test 44 | -------------------------------------------------------------------------------- /.github/workflows/packaging-v5.yaml: -------------------------------------------------------------------------------- 1 | name: packaging ntpv5 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'release/**' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | package: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | target: 18 | - aarch64-unknown-linux-gnu 19 | - armv7-unknown-linux-gnueabihf 20 | - x86_64-unknown-linux-gnu 21 | - i686-unknown-linux-gnu 22 | steps: 23 | - name: Setup packaging tools for cross compiled artifacts 24 | uses: awalsh128/cache-apt-pkgs-action@7ca5f46d061ad9aa95863cd9b214dd48edef361d # v1.5.0 25 | with: 26 | packages: musl-tools qemu-user-static crossbuild-essential-armhf crossbuild-essential-arm64 crossbuild-essential-i386 27 | version: 1 28 | 29 | - name: Install toolchain 30 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 31 | with: 32 | toolchain: "stable" 33 | components: "llvm-tools" 34 | 35 | - name: Install cross, cargo-deb and cargo-generate-rpm 36 | uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b 37 | with: 38 | tool: cross, cargo-deb, cargo-generate-rpm@0.14.0 39 | 40 | - name: Checkout sources 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 42 | 43 | - name: Build the release binaries 44 | run: RELEASE_TARGETS="${{ matrix.target }}" RELEASE_FEATURES="unstable_ntpv5" utils/build-release.sh 45 | 46 | - name: Upload artifacts 47 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 48 | with: 49 | name: release-binaries-${{ matrix.target }} 50 | path: target/pkg/ 51 | if-no-files-found: error 52 | -------------------------------------------------------------------------------- /.github/workflows/packaging.yaml: -------------------------------------------------------------------------------- 1 | name: packaging 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'release/**' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | package: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | target: 18 | - aarch64-unknown-linux-gnu 19 | - armv7-unknown-linux-gnueabihf 20 | - x86_64-unknown-linux-gnu 21 | - i686-unknown-linux-gnu 22 | steps: 23 | - name: Setup packaging tools for cross compiled artifacts 24 | uses: awalsh128/cache-apt-pkgs-action@7ca5f46d061ad9aa95863cd9b214dd48edef361d # v1.5.0 25 | with: 26 | packages: musl-tools qemu-user-static crossbuild-essential-armhf crossbuild-essential-arm64 crossbuild-essential-i386 27 | version: 1 28 | 29 | - name: Install toolchain 30 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 31 | with: 32 | toolchain: "stable" 33 | components: "llvm-tools" 34 | 35 | - name: Install cross, cargo-deb and cargo-generate-rpm 36 | uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b 37 | with: 38 | tool: cross, cargo-deb, cargo-generate-rpm@0.14.0 39 | 40 | - name: Checkout sources 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 42 | 43 | - name: Build the release binaries 44 | run: RELEASE_TARGETS="${{ matrix.target }}" utils/build-release.sh 45 | 46 | - name: Upload artifacts 47 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 48 | with: 49 | name: release-binaries-${{ matrix.target }} 50 | path: target/pkg/ 51 | if-no-files-found: error 52 | 53 | gather: 54 | needs: package 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Download artifacts 58 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 59 | with: 60 | pattern: release-binaries-* 61 | path: target/pkg/ 62 | merge-multiple: true 63 | - name: Create a SHA256SUMS file 64 | run: | 65 | cd target/pkg/ 66 | rm -rf SHA256SUMS 67 | sha256sum -b * > SHA256SUMS 68 | - name: Upload artifacts 69 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 70 | with: 71 | name: release-binaries 72 | path: target/pkg/ 73 | if-no-files-found: error 74 | 75 | checks: 76 | uses: './.github/workflows/checks.yaml' 77 | 78 | release: 79 | needs: [gather, checks] 80 | runs-on: ubuntu-latest 81 | if: ${{ startsWith(github.ref, 'refs/heads/release/') }} 82 | permissions: 83 | # This part of the release pipeline needs to create a tag and a release 84 | contents: write 85 | steps: 86 | - name: Checkout sources 87 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 88 | 89 | - name: Download artifacts 90 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 91 | with: 92 | name: release-binaries 93 | path: target/pkg/ 94 | 95 | - name: Install toolchain 96 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 97 | with: 98 | toolchain: "stable" 99 | components: "llvm-tools" 100 | 101 | - name: Check that the release commit is verified 102 | run: | 103 | commit_url="${{ github.api_url }}/repos/${{ github.repository }}/commits/${{ github.sha }}" 104 | json_accept_header="Accept: application/vnd.github+json" 105 | auth_bearer_header="Authorization: Bearer ${{ github.token }}" 106 | test "$(curl -sf -H "$json_accept_header" -H "$auth_bearer_header" "$commit_url" | jq .commit.verification.verified)" == "true" 107 | 108 | - name: Read the version from the manifest file 109 | run: echo "release_version=$(cargo read-manifest --manifest-path ntpd/Cargo.toml | jq -r .version)" >> "$GITHUB_ENV" 110 | 111 | - name: Version in Cargo.toml must match the branch name 112 | run: test "release/$release_version" == "${{ github.ref_name }}" 113 | 114 | - name: Ensure there is not already a released tag with a non-draft release 115 | run: test "$(gh release view "v$release_version" --json isDraft --jq .isDraft 2>/dev/null || echo "true")" == "true" 116 | 117 | - name: Verify that the changelog top most entry concerns this release 118 | run: | 119 | release_notes="$(awk '/^## / && !found { found=1; print; next } /^## / && found { exit } found { print }' CHANGELOG.md)" 120 | release_notes_header="$(echo "$release_notes" | head -1)" 121 | echo "Found release notes for '$release_notes_header'" 122 | release_notes_body="$(echo "$release_notes" | tail +2)" 123 | release_notes_body="${release_notes_body#"${release_notes_body%%[![:space:]]*}"}" 124 | release_notes_body="${release_notes_body%"${release_notes_body##*[![:space:]]}"}" 125 | release_notes_version="$(echo "$release_notes_header" | cut -d' ' -f2 | sed 's/[][]//g')" 126 | echo "Found version '$release_notes_version' in release notes" 127 | test "$release_notes_version" == "${{ env.release_version }}" 128 | { 129 | echo "release_notes_body<> "$GITHUB_ENV" 133 | 134 | - name: Create a draft release 135 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 136 | with: 137 | draft: true 138 | fail_on_unmatched_files: true 139 | tag_name: "v${{ env.release_version }}" 140 | target_commitish: "${{ github.sha }}" 141 | name: "Version ${{ env.release_version }}" 142 | files: target/pkg/* 143 | body: "${{ env.release_notes_body }}" 144 | 145 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yaml: -------------------------------------------------------------------------------- 1 | name: scorecard 2 | on: 3 | # For Branch-Protection check. Only the default branch is supported. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 5 | branch_protection_rule: 6 | # To guarantee Maintained check is occasionally updated. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 8 | schedule: 9 | - cron: '16 17 * * 2' 10 | push: 11 | branches: [ "main" ] 12 | 13 | # Declare default permissions as read only. 14 | permissions: read-all 15 | 16 | jobs: 17 | analysis: 18 | name: Scorecard analysis 19 | runs-on: ubuntu-latest 20 | permissions: 21 | # Needed to upload the results to code-scanning dashboard. 22 | security-events: write 23 | # Needed to publish results and get a badge (see publish_results below). 24 | id-token: write 25 | 26 | steps: 27 | - name: "Checkout code" 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | with: 30 | persist-credentials: false 31 | 32 | - name: "Run analysis" 33 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 34 | with: 35 | results_file: results.sarif 36 | results_format: sarif 37 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 38 | # - you want to enable the Branch-Protection check on a *public* repository, or 39 | # - you are installing Scorecard on a *private* repository 40 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 41 | repo_token: ${{ secrets.SCORECARD_TOKEN }} 42 | 43 | # Public repositories: 44 | # - Publish results to OpenSSF REST API for easy access by consumers 45 | # - Allows the repository to include the Scorecard badge. 46 | # - See https://github.com/ossf/scorecard-action#publishing-results. 47 | # For private repositories: 48 | # - `publish_results` will always be set to `false`, regardless 49 | # of the value entered here. 50 | publish_results: true 51 | 52 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 53 | # format to the repository Actions tab. 54 | - name: "Upload artifact" 55 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 56 | with: 57 | name: SARIF file 58 | path: results.sarif 59 | retention-days: 5 60 | 61 | # Upload the results to GitHub's code scanning dashboard. 62 | - name: "Upload to code-scanning" 63 | uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 64 | with: 65 | sarif_file: results.sarif 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We abide by the [Rust Code of Conduct][coc] and ask that you do as well. 4 | 5 | [coc]: https://www.rust-lang.org/en-US/conduct.html 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | Thank you for taking the time to read this contribution guide. We always welcome 3 | new contributions! 4 | 5 | #### Did you find a bug? 6 | * If your bug is a security issue, make sure to read our [security policy] and 7 | do not report a normal issue in our issue tracker! 8 | * Search the issue tracker on GitHub for existing bugs 9 | * If you were unable to find an existing issue, please open a new issue in our 10 | issue tracker with a clear description of the problem and steps on how to 11 | reproduce it. 12 | * For very small bugs (such as typos), consider directly opening a pull request 13 | with the patched fix instead. 14 | 15 | #### Want to write a patch that fixes a bug? 16 | * If the patch is very small (such as a typo), you may directly open a pull 17 | request with your fix, no accompanying bug report is needed. Please combine 18 | multiple typos into a single pull request as much as possible. 19 | * For other bugs, please open a bug report first, this allows us to discuss the 20 | best way to solve the problem and prevents duplicated effort. 21 | 22 | #### Want to write a new feature? 23 | * Check if there is an existing issue on our issue tracker that already concerns 24 | the feature you would like to add, add your voice in the discussion there to 25 | see if nobody else has started working on it. 26 | * If there is no existing issue, open a new one so we can discuss how to 27 | proceed. 28 | 29 | #### Do you have any questions about ntpd-rs? 30 | * See our [discussions] page instead and avoid the issue tracker 31 | 32 | ## Developing and building 33 | Our project mostly is a standard rust project, so you should be able to use the 34 | normal Rust tooling. One thing to consider though is that the NTP daemon uses 35 | port 123 by default for its server and needs to be able to adjust the clock when 36 | used as a client. You may need root (or the correct Linux capabilities) to do 37 | those two things. 38 | 39 | ## Dependencies and MSRV 40 | As ntpd-rs is intended to be packaged for multiple operating systems, we try to 41 | be conservative in our minimum supported rust version (MSRV) and the versions of 42 | our dependencies. Only add a new dependency if absolutely necessary. Please 43 | refrain from using newer compiler features or using the latest crate features. 44 | If that would however result in lots of duplicated effort, let us know so we can 45 | see if incrementing a crate version or increasing the MSRV is justified. 46 | 47 | ## Documentation 48 | Our end-user documentation is written in mkdocs and can be ran locally using the 49 | `utils/mkdocs.sh` script (this uses docker, so make sure that is available). The 50 | man page source files are additionally converted to the man format using pandoc. 51 | These converted man pages are committed to the repository, you can run 52 | `utils/generate-man.sh` to update them whenever a change was made to the source 53 | man files (also requires docker). 54 | 55 | ## Testing 56 | When adding a contribution we ask that you add tests to validate your work. We 57 | try and keep our code coverage at about the same level or higher than it 58 | currently is. A bot will notify you of the coverage changes that your pull 59 | request resulted in. 60 | 61 | Tests can be written using the standard rust testing framework (ran using 62 | `cargo test`) and should mostly be unit-level tests. If you want to write 63 | integration tests (which would be encouraged) you can do so in the `tests` 64 | folder in the ntpd crate. 65 | 66 | Additionally, we have a few fuzz testing targets. If you can think of any new 67 | targets let us know or add them! 68 | 69 | ## Coding conventions 70 | Every pull request will go through rustfmt and as such we require all 71 | contributions to adhere to this coding standard. It is recommended to run 72 | rustfmt (i.e. using `cargo fmt`) before you create a pull request. For non-Rust 73 | files (such as our documentation) we ask that you follow the conventions from 74 | other files, but we have no strict requirements. 75 | 76 | [security policy]: ./SECURITY.md 77 | [discussions]: https://github.com/pendulum-project/ntpd-rs/discussions 78 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2024 Trifecta Tech Foundation, Tweede Golf, and Contributors 2 | 3 | Except as otherwise noted (below and/or in individual files), ntpd-rs is 4 | licensed under the Apache License, Version 2.0 or 5 | or the MIT license 6 | or , at your option. 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ntp-proto", 4 | "nts-pool-ke", 5 | "ntpd" 6 | ] 7 | exclude = [ ] 8 | 9 | # Without the `-p` flag, cargo ignores `--no-default-features` when you have a 10 | # workspace, and without `resolver = "2"` here, you can't use `-p` like this. 11 | resolver = "2" 12 | 13 | # Global settings for our crates 14 | [workspace.package] 15 | version = "1.5.0" 16 | edition = "2021" 17 | license = "Apache-2.0 OR MIT" 18 | repository = "https://github.com/pendulum-project/ntpd-rs" 19 | homepage = "https://github.com/pendulum-project/ntpd-rs" 20 | readme = "./README.md" 21 | description = "Full-featured implementation of NTP with NTS support" 22 | publish = true 23 | rust-version = "1.71" # MSRV 24 | 25 | # Because of the async runtime, we really want panics to cause an abort, otherwise 26 | # the binary can keep on running as a ghost 27 | [profile.dev] 28 | panic = "abort" 29 | 30 | [profile.release] 31 | lto = true 32 | panic = "abort" 33 | debug = 2 34 | 35 | [workspace.dependencies] 36 | tracing = "0.1.37" 37 | tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "fmt", "ansi"] } 38 | serde = { version = "1.0.145", features = ["derive"] } 39 | serde_json = "1.0" 40 | rand = "0.8.0" 41 | arbitrary = { version = "1.0" } 42 | libc = "0.2.150" 43 | tokio = "1.32" 44 | toml = { version = ">=0.6.0,<0.9.0", default-features = false, features = ["parse"] } 45 | async-trait = "0.1.67" 46 | timestamped-socket = "0.2.2" 47 | clock-steering = "0.2.1" 48 | pps-time = "0.2.3" 49 | 50 | # TLS 51 | rustls23 = { package = "rustls", version = "0.23.16", features = ["logging", "std", "tls12"] } 52 | rustls-platform-verifier = "0.5.0" 53 | tokio-rustls = { version = "0.26.0", features = ["logging", "tls12"] } # testing only 54 | 55 | # crypto 56 | aead = "0.5.0" 57 | aes-siv = "0.7.0" 58 | # Note: md5 is needed to calculate ReferenceIDs for IPv6 addresses per RFC5905 59 | md-5 = "0.10.0" 60 | zeroize = "1.7" 61 | 62 | # our own crates used as dependencies, same version as the workspace version 63 | # NOTE: keep this part at the bottom of the file, do not change this line 64 | ntp-proto = { version = "1.5.0", path = "./ntp-proto", default-features = false, features = ["__internal-api"] } 65 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu" 3 | 4 | [target.armv7-unknown-linux-gnueabihf] 5 | image = "ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:main" 6 | pre-build = [ 7 | "cd /usr/local/bin && curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cargo-bins/cargo-quickinstall/releases/download/bindgen-cli-0.71.1/bindgen-cli-0.71.1-x86_64-unknown-linux-gnu.tar.gz | tar -zxf -" 8 | ] 9 | 10 | [target.aarch64-unknown-linux-gnu] 11 | image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu" 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2024 Trifecta Tech Foundation, Tweede Golf, and Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![checks](https://github.com/pendulum-project/ntpd-rs/actions/workflows/checks.yaml/badge.svg?branch=main) 2 | [![codecov](https://codecov.io/gh/pendulum-project/ntpd-rs/branch/main/graph/badge.svg?token=WES1JIYUJH)](https://codecov.io/gh/pendulum-project/ntpd-rs) 3 | [![Crates.io](https://img.shields.io/crates/v/ntpd.svg)](https://crates.io/crates/ntpd) 4 | [![Docs](https://img.shields.io/badge/ntpd--rs-blue?label=docs)](https://docs.ntpd-rs.pendulum-project.org/) 5 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8054/badge)](https://www.bestpractices.dev/projects/8054) 6 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pendulum-project/ntpd-rs/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pendulum-project/ntpd-rs) 7 | 8 | # ntpd-rs 9 | 10 | ntpd-rs is a tool for synchronizing your computer's clock, implementing the NTP and NTS protocols. It is written in Rust, with a focus on security and stability. It includes both client and server support. 11 | 12 | If a feature you need is missing please let us know by opening an issue. 13 | 14 | ## Documentation 15 | 16 | Be sure to check out the [documentation website] as it includes guides on getting started, installation and migration, as well as a high-level overview of the code structure. 17 | 18 | ## Usage 19 | 20 | You can install the packages from the [releases page]. These packages configure ntpd-rs to synchronize your computers clock to servers from the [NTP pool]. After installation, check the status of the ntpd-rs daemon with 21 | 22 | ```console 23 | $ sudo systemctl status ntpd-rs 24 | ``` 25 | 26 | If ntpd-rs was not started automatically, you can do so now with 27 | 28 | ```console 29 | $ sudo systemctl start ntpd-rs 30 | ``` 31 | 32 | You should now be able to check the synchronization status with 33 | 34 | ```console 35 | $ ntp-ctl status 36 | Synchronization status: 37 | Dispersion: 0.000299s, Delay: 0.007637s 38 | Desired poll interval: 16s 39 | Stratum: 4 40 | 41 | Sources: 42 | ntpd-rs.pool.ntp.org:123/77.171.247.180:123 (1): +0.000024±0.000137(±0.016886)s 43 | poll interval: 16s, missing polls: 0 44 | root dispersion: 0.005905s, root delay:0.016190s 45 | ntpd-rs.pool.ntp.org:123/45.137.101.154:123 (2): +0.000022±0.000081(±0.007414)s 46 | poll interval: 16s, missing polls: 0 47 | root dispersion: 0.004517s, root delay:0.005051s 48 | ntpd-rs.pool.ntp.org:123/178.215.228.24:123 (3): +0.000117±0.000091(±0.009162)s 49 | poll interval: 16s, missing polls: 0 50 | root dispersion: 0.000549s, root delay:0.004318s 51 | ntpd-rs.pool.ntp.org:123/162.159.200.123:123 (4): +0.000111±0.000076(±0.004066)s 52 | poll interval: 16s, missing polls: 0 53 | root dispersion: 0.000351s, root delay:0.003571s 54 | 55 | Servers: 56 | ``` 57 | The top part shows the overall quality of the time synchronization, and the time sources section shows which servers are used as well as offsets and uncertainties of those individual servers. 58 | 59 | For more details on how to install and use ntpd-rs, see our [documentation website]. 60 | 61 | ## Roadmap 62 | 63 | In Q1 2023 we completed our work on NTS. Our implementation is now 64 | full-featured, it supports NTP client and server with NTS. 65 | 66 | Our roadmap for 2024: 67 | 68 | * Q2-Q4 2024: Packaging and industry adoption, maintenance & community work 69 | * Q4 2024: NTS Pool (pending funding) 70 | 71 | We seek sponsorship for features and maintenance to continue our work. Contact 72 | us via pendulum@tweedegolf.com if you are interested! 73 | 74 | ## History 75 | 76 | ### 2022 77 | 78 | The project originates from ISRG's project [Prossimo], as part of their mission 79 | to achieve memory safety for the Internet's most critical infrastructure. 80 | 81 | Prossimo 82 | 83 | Prossimo funded the initial development of the NTP client and server, and NTS 84 | support. The [NTP initiative page] on Prossimo's website tells the story. 85 | 86 | ### 2023 87 | 88 | After completion of the initial development, the project's ownership moved from 89 | Prossimo to Tweede golf in April 2023. See the [NTP announcement] for more 90 | information. 91 | 92 | Tweede golf is the long-term maintainer of ntpd-rs, that is now part of Tweede 93 | golf's [Project Pendulum]. Pendulum is building modern, open-source 94 | implementations of the Network Time Protocol (ntpd-rs) and the Precision Time Protocol (Statime). 95 | 96 | In July of 2023 the [Sovereign Tech Fund] invested in Pendulum, securing ntpd-rs development and maintenance in 2023, and maintenance and adoption work in 2024. 97 | 98 | ![STF](https://tweedegolf.nl/images/logo-stf-blank.png) 99 | 100 | [releases page]: https://github.com/pendulum-project/ntpd-rs/releases 101 | [NTP pool]: https://www.ntppool.org 102 | [documentation website]: https://docs.ntpd-rs.pendulum-project.org/ 103 | [Prossimo]: https://www.memorysafety.org 104 | [NTP initiative page]: https://www.memorysafety.org/initiative/ntp 105 | [NTP announcement]: https://www.memorysafety.org/blog/ntp-and-nts-have-arrived/ 106 | [Project Pendulum]: https://github.com/pendulum-project 107 | [Sovereign Tech Fund]: https://sovereigntechfund.de/en/ 108 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Security policy 2 | =============== 3 | 4 | **Do not report security vulnerabilities through public GitHub issues.** 5 | Instead, you can report security vulnerabilities using [our security page], 6 | or send them by email to security+ntpdrs@tweedegolf.com. 7 | 8 | Please include as much of the following information as possible: 9 | 10 | * Type of issue (e.g. buffer overflow, privilege escalation, etc.) 11 | * The location of the affected source code (tag/branch/commit or direct URL) 12 | * Any special configuration required to reproduce the issue 13 | * If applicable, which platforms are affected 14 | * Step-by-step instructions to reproduce the issue 15 | * Impact of the issue, including how an attacker might exploit the issue 16 | 17 | ## Preferred Languages 18 | 19 | We prefer to receive reports in English. If necessary, we also understand Dutch. 20 | 21 | ## Disclosure Policy 22 | 23 | We adhere to the principle of [coordinated vulnerability disclosure]. 24 | 25 | Security Advisories 26 | =================== 27 | Security advisories will be published on our [github advisories page] and 28 | possibly through other channels. 29 | 30 | [our security page]: https://github.com/pendulum-project/ntpd-rs/security 31 | [coordinated vulnerability disclosure]: https://vuls.cert.org/confluence/display/CVD/Executive+Summary 32 | [github advisories page]: https://github.com/pendulum-project/ntpd-rs/security/advisories 33 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.71" 2 | -------------------------------------------------------------------------------- /config/ntp.demobilize.toml: -------------------------------------------------------------------------------- 1 | # a server that demobilizes on every incomming request 2 | 3 | [observability] 4 | log-level = "info" 5 | 6 | # the server will get its time from the NTP pool 7 | [[source]] 8 | mode = "pool" 9 | address = "pool.ntp.org" 10 | count = 4 11 | 12 | [[server]] 13 | listen = "0.0.0.0:123" 14 | 15 | [server.allowlist] 16 | filter = [] 17 | action = "deny" 18 | 19 | # configure the client with 20 | # [[source]] 21 | # mode = "server" 22 | # address = "0.0.0.0:123" 23 | -------------------------------------------------------------------------------- /config/nts.client.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | observation-path = "/var/run/ntpd-rs/observe" 5 | 6 | # See https://docs.ntpd-rs.pendulum-project.org/man/ntp.toml.5/ on how to set up certificates 7 | [[source]] 8 | mode = "nts" 9 | address = "localhost:4460" 10 | certificate-authority = "path/to/certificate/authority.pem" 11 | 12 | # System parameters used in filtering and steering the clock: 13 | [synchronization] 14 | minimum-agreeing-sources = 1 15 | single-step-panic-threshold = 10 16 | startup-step-panic-threshold = { forward = "inf", backward = 86400 } 17 | -------------------------------------------------------------------------------- /config/nts.server.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | 5 | # the server will get its time from the NTP pool 6 | [[source]] 7 | mode = "pool" 8 | address = "pool.ntp.org" 9 | count = 4 10 | 11 | [[server]] 12 | listen = "0.0.0.0:123" 13 | 14 | # to function as an NTS server, we must also provide key exchange 15 | [[nts-ke-server]] 16 | listen = "0.0.0.0:4460" 17 | certificate-chain-path = "path/to/certificate/chain.pem" 18 | private-key-path = "path/to/private.key" 19 | key-exchange-timeout-ms = 1000 20 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | { triple = "x86_64-unknown-linux-musl" }, 4 | { triple = "x86_64-unknown-linux-gnu" }, 5 | ] 6 | 7 | [licenses] 8 | private = { ignore = true } 9 | allow = [ 10 | "Apache-2.0", 11 | "MIT", 12 | "ISC", 13 | "Unicode-3.0", 14 | "BSD-3-Clause", 15 | "OpenSSL", 16 | "NCSA" 17 | ] 18 | 19 | [[licenses.clarify]] 20 | name = "ring" 21 | expression = "ISC AND MIT AND OpenSSL" 22 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 23 | -------------------------------------------------------------------------------- /docs/algorithm/.gitignore: -------------------------------------------------------------------------------- 1 | *.aux 2 | *.fdb_latexmk 3 | *.fls 4 | *.log 5 | *.blg 6 | *.bbl 7 | *.synctex.gz -------------------------------------------------------------------------------- /docs/algorithm/algorithm.bib: -------------------------------------------------------------------------------- 1 | @article{giada2015, 2 | author = {Giorgi, Giada}, 3 | year = {2015}, 4 | month = {02}, 5 | pages = {449-457}, 6 | title = {An Event-Based Kalman Filter for Clock Synchronization}, 7 | volume = {64}, 8 | journal = {IEEE Transactions on Instrumentation and Measurement}, 9 | doi = {10.1109/TIM.2014.2340631} 10 | } 11 | 12 | @misc{rfc5905, 13 | title={RFC 5905: Network time protocol version 4: Protocol and algorithms specification}, 14 | author={Mills, David and Burbank, J and Kasch, W}, 15 | year={2010}, 16 | publisher={RFC Editor} 17 | } 18 | -------------------------------------------------------------------------------- /docs/algorithm/algorithm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendulum-project/ntpd-rs/ecca90c9e6aa50c9ca7bf46eb682391a6c4e45ff/docs/algorithm/algorithm.pdf -------------------------------------------------------------------------------- /docs/algorithm/measurement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendulum-project/ntpd-rs/ecca90c9e6aa50c9ca7bf46eb682391a6c4e45ff/docs/algorithm/measurement.png -------------------------------------------------------------------------------- /docs/algorithm/offset-chrony.gnuplot: -------------------------------------------------------------------------------- 1 | set yrange [10:70] 2 | set xrange [0:3600] 3 | set xlabel "time (s)" 4 | set ylabel "offset (us)" 5 | plot 'offset-chrony.dat' using ($1/100) pt 7 black title "Offset to server" 6 | -------------------------------------------------------------------------------- /docs/algorithm/offset-chrony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendulum-project/ntpd-rs/ecca90c9e6aa50c9ca7bf46eb682391a6c4e45ff/docs/algorithm/offset-chrony.png -------------------------------------------------------------------------------- /docs/algorithm/offset-ntpd-rs.gnuplot: -------------------------------------------------------------------------------- 1 | set yrange [10:70] 2 | set xrange [0:3600] 3 | set xlabel "time (s)" 4 | set ylabel "offset (us)" 5 | plot 'offset-ntpd-rs.dat' using ($1/100) pt 7 black title "Offset to server" 6 | -------------------------------------------------------------------------------- /docs/algorithm/offset-ntpd-rs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendulum-project/ntpd-rs/ecca90c9e6aa50c9ca7bf46eb682391a6c4e45ff/docs/algorithm/offset-ntpd-rs.png -------------------------------------------------------------------------------- /docs/audits/report-ntpd-rs-v11-final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendulum-project/ntpd-rs/ecca90c9e6aa50c9ca7bf46eb682391a6c4e45ff/docs/audits/report-ntpd-rs-v11-final.pdf -------------------------------------------------------------------------------- /docs/development-process.md: -------------------------------------------------------------------------------- 1 | # Development processes 2 | 3 | ## Releasing 4 | 5 | New releases are created by starting at either the most recent commit on main, 6 | or by backporting fixes on top of an existing tag. Some things to take note of 7 | before starting the release process: 8 | 9 | - Have all dependencies recently been updated using `cargo update`? 10 | - Does the changelog contain all recent additions, removals, changes and fixes? 11 | - Are there still any open issues in any related milestone on GitHub? 12 | 13 | To determine what version to release we keep to semantic versioning: 14 | 15 | - If there are any (major) breaking changes, we always release a new major 16 | version. 17 | - Patch versions are generally only released for tiny fix-only releases. 18 | - Minor versions are released when large fixes or new features are introduced. 19 | - At any time a new dev release can be made, if unknown what version the next 20 | release will be you should always assume a patch version change and only 21 | increase minor or major versions if any new features or breaking changes are 22 | happening respectively. 23 | - Before big releases or if testing is required, a beta or release candidate 24 | can be released. 25 | 26 | ### Checklist 27 | 28 | - [ ] Run `utils/update-version.sh [version]` 29 | - [ ] Update `CHANGELOG.md` with the new version, remove any `Unreleased` 30 | section in the changelog. Make sure the to release version is the top most 31 | section of the changelog. Also make sure the diff link is updated at the 32 | bottom of the document. 33 | - [ ] `git switch -c release/[version]` (the branch name must match this format) 34 | - [ ] `git commit -a -S -m "Release [version]"` (a signed commit is required) 35 | - [ ] `git push -u origin release/[version]` 36 | - [ ] Wait for the github actions pipelines to complete, take special care of 37 | the packaging pipeline 38 | - [ ] Go to the releases page on Github and find the draft release, check if the 39 | binaries have been properly generated. 40 | - [ ] Let somebody review the branch 41 | - [ ] WARNING: only merge the branch to main if it is fully up to date compared 42 | to main, don't let any other branches on the merge queue in the mean time. 43 | You could also store the release on a non-main branch, but make sure to 44 | sync the main branch at a later time in that case to update the changelog 45 | on main. 46 | - [ ] Go to the releases page on GitHub and find the draft release, edit the 47 | draft release and make it public, this should also create a tag on the 48 | repository. 49 | - [ ] On your local computer, checkout the specific commit that was tagged by 50 | GitHub (i.e. `git fetch && git switch --detach v[version]`) 51 | - [ ] Run `utils/release.sh` to publish the crates.io packages 52 | 53 | -------------------------------------------------------------------------------- /docs/development/audits.md: -------------------------------------------------------------------------------- 1 | # Audits 2 | 3 | ## NLnet NGI Review Security Evaluation by Radically Open Security 4 | 5 | Date: 2023-04 \ 6 | Report: [download](../audits/report-ntpd-rs-v11-final.pdf) 7 | -------------------------------------------------------------------------------- /docs/development/further-reading.md: -------------------------------------------------------------------------------- 1 | # Further reading 2 | 3 | - (Link) NTP at the [IETF Datatracker][1] 4 | - (Video) Introduction to the project: [NTP for the modern era @ GOSIM][2] 5 | - (Paper) Clock steering and selection algorithm in ntpd-rs: [docs/algorithm/algorithm.pdf][3] 6 | - (Link) [NTP Pool website][4] 7 | 8 | [1]: https://datatracker.ietf.org/wg/ntp/documents/ 9 | [2]: https://www.youtube.com/watch?v=p_zC5zSRRng 10 | [3]: https://github.com/pendulum-project/ntpd-rs/blob/main/docs/algorithm/algorithm.pdf 11 | [4]: https://www.ntppool.org 12 | -------------------------------------------------------------------------------- /docs/examples/conf/ntp.toml.default: -------------------------------------------------------------------------------- 1 | [observability] 2 | # You can configure ntpd-rs with different output levels of logging information 3 | # Basic values for this are `trace`, `debug`, `info`, `warn` and `error`. 4 | log-level = "info" 5 | ## Using the observe socket you can retrieve statistical information about the 6 | ## daemon while it is running. You can use the `ntp-ctl` or prometheus based 7 | ## `ntp-metrics-exporter` binaries for some default options to read from the 8 | ## observe socket. 9 | observation-path = "/var/run/ntpd-rs/observe" 10 | 11 | ## The sources section allows configuring sources, you may configure multiple of 12 | ## these blocks to add more sources to your configuration. 13 | ## Our default configuration spawns a pool of sources (by default this attempts 14 | ## to discover 4 distinct sources). 15 | [[source]] 16 | mode = "pool" 17 | address = "ntpd-rs.pool.ntp.org" 18 | count = 4 19 | 20 | ## If you have an NTS server, you can configure a source that connects using NTS 21 | ## by adding a configuration such as the one below 22 | #[[source]] 23 | #mode = "nts" 24 | # NTS service from NETNOD: https://www.netnod.se/nts/network-time-security 25 | #address = "nts.netnod.se" 26 | 27 | ## A source in server mode will only create a single source in contrast to the 28 | ## multiple sources of a pool. This is the recommended source mode if you only 29 | ## have an IP address for your source. 30 | #[[source]] 31 | #mode = "server" 32 | #address = "ntpd-rs.pool.ntp.org" 33 | 34 | ## If you want to provide time to other machines, the configuration below 35 | ## enables serving time on port 123 of all network interfaces. 36 | #[[server]] 37 | #listen = "[::]:123" 38 | 39 | ## Below are configured various thresholds beyond which ntpd-rs will not 40 | ## change the system clock. CHANGE THESE TO MATCH YOUR SECURITY NEEDS! 41 | [synchronization] 42 | # The maximum step size (in seconds) of a single step during normal operation 43 | single-step-panic-threshold = 1800 44 | # On startup a larger jump may occur, this sets limits for that initial jump 45 | startup-step-panic-threshold = { forward="inf", backward = 86400 } 46 | # If, during the lifetime of the ntp-daemon the combined time of time jumps 47 | # exceeds this value, then the NTP daemon will stop, this is disabled by default 48 | #accumulated-threshold = 1800 49 | #minimum-agreeing-sources = 3 50 | -------------------------------------------------------------------------------- /docs/examples/conf/ntpd-rs-metrics.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Network Time Service (ntpd-rs) metrics exporter 3 | Documentation=https://github.com/pendulum-project/ntpd-rs 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | ExecStart=/usr/bin/ntp-metrics-exporter 9 | Environment="RUST_LOG=info" 10 | RuntimeDirectory=ntpd-rs-observe 11 | User=ntpd-rs-observe 12 | Group=ntpd-rs-observe 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /docs/examples/conf/ntpd-rs.preset: -------------------------------------------------------------------------------- 1 | enable ntpd-rs.service 2 | disable ntpd-rs-metrics.service 3 | -------------------------------------------------------------------------------- /docs/examples/conf/ntpd-rs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Network Time Service (ntpd-rs) 3 | Documentation=https://github.com/pendulum-project/ntpd-rs 4 | After=network-online.target 5 | Wants=network-online.target 6 | Conflicts=systemd-timesyncd.service ntp.service chrony.service 7 | 8 | [Service] 9 | Type=simple 10 | Restart=no 11 | ExecStart=/usr/bin/ntp-daemon 12 | Environment="RUST_LOG=info" 13 | RuntimeDirectory=ntpd-rs 14 | User=ntpd-rs 15 | Group=ntpd-rs 16 | AmbientCapabilities=CAP_SYS_TIME CAP_NET_BIND_SERVICE 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Ntpd-rs is an implementation of the NTP protocol. It aims to synchronize your system's clock to time received from the internet. It can also, when [configured as server](server-setup.md), provide time to other machines on the network. 4 | 5 | ## Installation 6 | 7 | Installation instructions for your system can be found in the [installation guide](installation.md). For first time users, we strongly recommend using either your OS package repository packages, or the packages we provide. If you have installed ntpd-rs from source, and you have installed files in different locations than the default, you may need to modify the instructions below. 8 | 9 | ## Checking the synchronization. 10 | 11 | The default configuration for ntpd-rs sets it up to synchronize with four servers chosen from [the NTP pool](https://www.ntppool.org). We can check its synchronization status using: 12 | ```sh 13 | ntp-ctl status 14 | ``` 15 | 16 | If everything is installed and working correctly this will display information looking like: 17 | ``` 18 | Synchronization status: 19 | Dispersion: 0.000104s, Delay: 0.005740s 20 | Desired poll interval: 16s 21 | Stratum: 3 22 | 23 | Sources: 24 | ntpd-rs.pool.ntp.org:123/20.101.57.9:123 (1): -0.022944±0.000218(±0.004720)s 25 | poll interval: 16s, missing polls: 0 26 | root dispersion: 0.103531s, root delay:0.001434s 27 | ntpd-rs.pool.ntp.org:123/35.204.193.221:123 (2): +0.000564±0.000138(±0.007323)s 28 | poll interval: 16s, missing polls: 0 29 | root dispersion: 0.000000s, root delay:0.007538s 30 | ntpd-rs.pool.ntp.org:123/94.198.159.15:123 (3): +0.000140±0.000202(±0.005725)s 31 | poll interval: 16s, missing polls: 0 32 | root dispersion: 0.000015s, root delay:0.000015s 33 | ntpd-rs.pool.ntp.org:123/95.211.123.72:123 (4): +0.000052±0.000189(±0.005118)s 34 | poll interval: 16s, missing polls: 0 35 | root dispersion: 0.039536s, root delay:0.021667s 36 | 37 | Servers: 38 | 39 | ``` 40 | 41 | The first section gives some general information on the time synchronization. The dispersion is a measure for how precise it thinks the local time is, and the delay is a measure of how long the communication delay to the best (most precise) server is. Desired poll interval indicates how often it currently wants to know the time from downstream servers, and stratum indicates how many servers are between us and a reference source of time such as an atomic clock or GPS receiver. Stratum will always be at least 2 when configured as a client using only sources from across the internet. 42 | 43 | Next, we get information on each of the time sources, showing the measured offset and the uncertainty on that, as well as (between brackets) the delay to the server. We also show the poll interval used for that particular source. This can be different from the desired poll interval if a server requests us to do fewer queries. Finally, missing polls gives an indication of how many times we have tried to poll the server since last getting a time measurement for it. 44 | 45 | The final section is empty, but if we were running a server, it would show statistics on how often the server is used. 46 | 47 | ## Configuring a custom time source 48 | 49 | The sources ntpd-rs uses to get the current time can be configured in the `/etc/ntpd-rs/ntp.toml` configuration file. Suppose that, in addition to the sources from the NTP pool, we also always want to use two sources from the [time.nl](https://time.nl) pool. To do this, we add the following lines to the configuration file: 50 | ```toml 51 | [[source]] 52 | mode = "pool" 53 | address = "ntp.time.nl" 54 | count = 2 55 | ``` 56 | 57 | After restarting the daemon (using `sudo systemctl restart ntpd-rs` if you are using Linux) and waiting a bit for it to synchronize, the status now looks like 58 | ``` 59 | Synchronization status: 60 | Dispersion: 0.000123s, Delay: 0.005496s 61 | Desired poll interval: 16s 62 | Stratum: 2 63 | 64 | Sources: 65 | ntp.time.nl:123/94.198.159.10:123 (1): +0.000380±0.000249(±0.005496)s 66 | poll interval: 16s, missing polls: 0 67 | root dispersion: 0.000122s, root delay:0.000000s 68 | ntp.time.nl:123/94.198.159.14:123 (2): -0.000046±0.000154(±0.005520)s 69 | poll interval: 16s, missing polls: 0 70 | root dispersion: 0.000122s, root delay:0.000000s 71 | ntpd-rs.pool.ntp.org:123/84.245.9.254:123 (3): -0.000288±0.000698(±0.008572)s 72 | poll interval: 16s, missing polls: 0 73 | root dispersion: 0.000305s, root delay:0.006226s 74 | ntpd-rs.pool.ntp.org:123/83.98.155.30:123 (4): +0.000000±0.000163(±0.005186)s 75 | poll interval: 16s, missing polls: 0 76 | root dispersion: 0.005020s, root delay:0.004898s 77 | ntpd-rs.pool.ntp.org:123/162.159.200.123:123 (5): -0.000380±0.000140(±0.004535)s 78 | poll interval: 16s, missing polls: 0 79 | root dispersion: 0.000259s, root delay:0.003662s 80 | ntpd-rs.pool.ntp.org:123/5.255.99.180:123 (6): +0.000193±0.000203(±0.005414)s 81 | poll interval: 16s, missing polls: 0 82 | root dispersion: 0.008499s, root delay:0.005661s 83 | 84 | Servers: 85 | 86 | ``` 87 | where we see the two servers from `time.nl` added to the list of sources. 88 | 89 | ## Where to go from here 90 | 91 | You now have a working NTP client, can check its status, and if desired modify 92 | the sources it uses for time. There are multiple directions to go from here. 93 | 94 | If you want more certainty around the authenticity of your time sources, you 95 | can take a look at [using NTS](nts.md). 96 | 97 | Setting up your own time server is explained in our [server setup guide](server-setup.md). 98 | 99 | When operating ntpd-rs as part of a larger critical system, you may also be 100 | interested in our [guidance on hardening ntpd-rs](security-guidance.md). 101 | -------------------------------------------------------------------------------- /docs/guide/gps-pps.md: -------------------------------------------------------------------------------- 1 | # GPS / PPS time sources 2 | 3 | ## GPSd time source 4 | Instead of using another NTP server as a time source, it is also possible to use time data from [GPSd](https://gpsd.gitlab.io/gpsd/) as a time source for ntpd-rs. 5 | GPSd is able to interpret GPS signals from a GPS receiver, from which it can derive the current time. 6 | 7 | GPSd can be added as a time source for ntpd-rs by adding the following to the configuration: 8 | ```toml 9 | [[source]] 10 | mode = "sock" 11 | path = "/run/chrony.ttyAMA0.sock" 12 | precision = 1e-3 13 | ``` 14 | The `path` points to the location of the socket that GPSd writes timing data to. This socket was originally meant for chrony, but ntpd-rs also supports this same socket format. Here, `ttyAMA0` is the GPS receiver device used by GPSd. 15 | 16 | The `precision` gives a static estimate of how noisy GPSd's timing data is. Normally with NTP time sources, we would use the network delay as an independent estimate of how noisy the data is. When using GPSd as a time source, we do not have a good estimate of the noise, so we use a static noise estimate instead. The noise estimate should be one standard deviation. 17 | 18 | ### Setting up GPSd 19 | In order for GPSd to connect to ntpd-rs, GPSd must start after ntpd-rs. This is because ntpd-rs needs to create a socket which GPSd will only use if it exists when GPSd starts. 20 | 21 | GPSd can be manually restarted using: 22 | ```sh 23 | sudo systemctl restart gpsd.socket 24 | ``` 25 | 26 | For help with setting up GPSd on e.g. a Raspberry Pi, see for example [this guide](https://n4bfr.com/2020/04/raspberry-pi-with-chrony/2/). 27 | 28 | ## Pulse Per Second (PPS) 29 | Ntpd-rs also supports using PPS timing data via Kernel PPS, based on [RFC 2783](https://datatracker.ietf.org/doc/html/rfc2783). 30 | 31 | Here is an example configuration of a 1 PPS device in ntpd-rs: 32 | ```toml 33 | [[source]] 34 | mode = "pps" 35 | path = "/dev/pps0" 36 | precision = 1e-7 37 | ``` 38 | 39 | By default, PPS sources are treated as 1 PPS sources, which send a pulse every rounded second. For e.g. a 10 PPS device, the source can be configured with a period of 0.1 seconds: 40 | ```toml 41 | [[source]] 42 | mode = "pps" 43 | path = "/dev/pps0" 44 | precision = 1e-7 45 | period = 0.1 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/guide/server-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up an NTP server 2 | 3 | By default, ntpd-rs only acts as an NTP client, and doesn't serve time on any 4 | network interface. To enable ntpd-rs as a server, the following can be added to 5 | the configuration: 6 | ```toml 7 | [[server]] 8 | listen = "0.0.0.0:123" 9 | ``` 10 | This will cause ntpd-rs to listen on all network interfaces on UDP port 123 for 11 | NTP client requests. If you only want to listen on a specific network 12 | interface, change `0.0.0.0` to the IP address of that interface. 13 | 14 | You can now configure a different machine to use your new server by adding to 15 | its configuration: 16 | ```toml 17 | [[source]] 18 | mode = "server" 19 | address = ":123" 20 | ``` 21 | 22 | ## Limiting access 23 | If you only want specific IP addresses to be able to access the server, you can 24 | configure a list of allowed clients through the allowlist mechanism. For this, 25 | edit the server configuration to look like: 26 | ```toml 27 | [[server]] 28 | listen = "0.0.0.0:123" 29 | [server.allowlist] 30 | filter = ["/32", "/32", "/128"] 31 | action = "ignore" 32 | ``` 33 | When configured this way, your server will only respond to the listed IP 34 | addresses. The IP addresses are written in CIDR notation, which means you can 35 | allow entire subnets at a time by specifying the size of the subnet instead of 36 | the 32 or 128 after the slash. For example, `192.168.1.1/24` will allow any IP 37 | address of the form `192.168.1.*`. 38 | 39 | If you want to block certain IP addresses from accessing the server, you can 40 | configure a list of blocked clients as follows: 41 | ```toml 42 | [[server]] 43 | listen = "0.0.0.0:123" 44 | [server.denylist] 45 | filter = ["/32", "/32", "/128"] 46 | action = "deny" 47 | ``` 48 | The deny list uses the same CIDR notion as the allow list, and can also be used 49 | to block subnets. Connections from IP addresses contained in the deny list will 50 | always be blocked, even if they also happen to be in the allow list. 51 | 52 | The allow and deny list configurations are both optional in ntpd-rs. By 53 | default, if a server is configured it will accept traffic from anywhere. When 54 | configuring both allow and deny lists, ntpd-rs will first check if a remote is 55 | on the deny list. Only if this is not the case will the allow list be 56 | considered. 57 | 58 | The `allowlist.action` and `denylist.action` properties can have two values: 59 | 60 | - `ignore` silently ignores the request 61 | - `deny` sends a deny kiss-o'-death packet 62 | 63 | ## Adding your server to the NTP pool 64 | 65 | If your NTP server has a public IP address, you can consider making it 66 | available as part of the [NTP pool](https://www.ntppool.org). Please note that 67 | this can have significant long-term impact in terms of NTP traffic to that 68 | particular IP address. Please read [the join instructions](https://www.ntppool.org/en/join.html) 69 | carefully before joining the pool. 70 | 71 | -------------------------------------------------------------------------------- /docs/includes/glossary.md: -------------------------------------------------------------------------------- 1 | *[NTS-KE]: NTS Key Exchange 2 | *[NTP]: Network Time Protocol 3 | *[NTS]: Network Time Security 4 | *[TLS]: Transport Layer Security 5 | *[TCP]: Transmission Control Protocol 6 | *[UDP]: User Datagram Protocol 7 | *[CA]: Certificate Authority 8 | *[OS]: Operating System 9 | *[DNS]: Domain Name System 10 | *[CIDR]: Classless Inter-Domain Routing 11 | *[GPS]: Global Positioning System 12 | *[PPS]: Pulse Per Second 13 | *[ioctl]: input/output control 14 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ntpd-rs 2 | 3 | Welcome to ntpd-rs, a full-featured NTP server and client implementation, 4 | including NTS support. The documentation is divided into several sections. For 5 | new users of ntpd-rs we recommend to start with the [getting started guide][1]. 6 | If you already have experience with other NTP software, you may want to start 7 | with one of the specific migration guides in the *Guide* section of the 8 | documentation. 9 | 10 | If you are looking for a reference level explanation of functions in ntpd-rs we 11 | recommend looking at the *Man Pages* section. Finally, if you are looking at how 12 | ntpd-rs development is organized, please take a look at the *Development* 13 | section of the documentation. 14 | 15 | [1]: ./guide/getting-started.md 16 | -------------------------------------------------------------------------------- /docs/man/ntp-ctl.8.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # NAME 6 | 7 | `ntp-ctl` - management client for the ntpd-rs ntp-daemon process 8 | 9 | # SYNOPSIS 10 | 11 | `ntp-ctl` validate [`-c` *path*] \ 12 | `ntp-ctl` status [`-f` *format*] [`-c` *path*] \ 13 | `ntp-ctl` force-sync [`-c` *path*] \ 14 | `ntp-ctl` `-h` \ 15 | `ntp-ctl` `-v` 16 | 17 | # DESCRIPTION 18 | 19 | The `ntp-ctl` management client allows management of some aspects of the 20 | ntpd-rs daemon. Currently the management client only allows displaying the 21 | current status of the daemon and validating a configuration file for usage 22 | with the daemon. 23 | 24 | # OPTIONS 25 | 26 | `-c` *path*, `--config`=*path* 27 | : Path to the configuration file from which the observation socket address 28 | will be retrieved. If not specified this defaults to 29 | `/etc/ntpd-rs/ntp.toml`. 30 | 31 | `-f` *format*, `--format`=*format* 32 | : The output format for the status command. If not specified this defaults to 33 | *plain*. Alternatively the format *prometheus* is available to display the 34 | output in an OpenMetrics/Prometheus compatible format. 35 | 36 | `-h`, `--help` 37 | : Display usage instructions. 38 | 39 | `-v`, `--version` 40 | : Display version information. 41 | 42 | # COMMANDS 43 | 44 | `validate` 45 | : Checks if the configuration specified (or `/etc/ntpd-rs/ntp.toml` by 46 | default) is valid. 47 | 48 | `status` 49 | : Returns status information about the current state of the ntp-daemon that 50 | the client connects to. 51 | 52 | `force-sync` 53 | : Interactively run a single synchronization of your clock. This command can 54 | be used to do a one-off synchronization to the time sources configured in 55 | your configuration file. This command should never be used without any 56 | validation by a human operator. 57 | 58 | # SEE ALSO 59 | 60 | [ntp-daemon(8)](ntp-daemon.8.md), 61 | [ntp-metrics-exporter(8)](ntp-metrics-exporter.8.md), 62 | [ntp.toml(5)](ntp.toml.5.md) 63 | -------------------------------------------------------------------------------- /docs/man/ntp-daemon.8.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # NAME 6 | 7 | `ntp-daemon` - ntpd-rs Network Time Protocol service daemon 8 | 9 | # SYNOPSIS 10 | 11 | `ntp-daemon` [`-c` *path*] [`-l` *loglevel*] \ 12 | `ntp-daemon` `-h` \ 13 | `ntp-daemon` `-v` 14 | 15 | # DESCRIPTION 16 | 17 | `ntp-daemon` is the Network Time Protocol (NTP) service daemon for ntpd-rs, an 18 | NTP implementation with a focus on security and stability. The `ntp-deamon` can 19 | be configured as both an NTP client and an NTP server. The daemon also works 20 | with the Network Time Security (NTS) protocol. Details of the configuration 21 | of the daemon and implementation details can be found in ntp.toml(5), where 22 | several concepts of the ntp-daemon are also explained. 23 | 24 | # OPTIONS 25 | 26 | `-c` *path*, `--config`=*path* 27 | : The configuration file path for the ntp-daemon where settings for the 28 | configuration of ntpd-rs are stored. If not specified the default 29 | configuration file is `/etc/ntpd-rs/ntp.toml`. 30 | 31 | `-h`, `--help` 32 | : Display usage instructions. 33 | 34 | `-l` *loglevel*, `--log-level`=*loglevel* 35 | : Change which log messages are logged to stdout. Available log levels are 36 | *trace*, *debug*, *info*, *warn* and *error* (from lower to higher 37 | priority). Only messages with the given priority and higher will be 38 | displayed. The default log level is *info*. 39 | 40 | `-v`, `--version` 41 | : Display version information. 42 | 43 | # SEE ALSO 44 | 45 | [ntp-ctl(8)](ntp-ctl.8.md), 46 | [ntp-metrics-exporter(8)](ntp-metrics-exporter.8.md), 47 | [ntp.toml(5)](ntp.toml.5.md) 48 | -------------------------------------------------------------------------------- /docs/man/ntp-metrics-exporter.8.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # NAME 6 | 7 | `ntp-metrics-exporter` - Prometheus/OpenMetrics exporter for the ntpd-rs daemon 8 | 9 | # SYNOPSIS 10 | 11 | `ntp-metrics-exporter` [`-c` *path*] \ 12 | `ntp-metrics-exporter` `-h` \ 13 | `ntp-metrics-exporter` `-v` 14 | 15 | # DESCRIPTION 16 | 17 | Exports the status metrics from the ntpd-rs daemon as Prometheus/OpenMetrics 18 | via an HTTP socket. 19 | 20 | # OPTIONS 21 | 22 | `-c` *path*, `--config`=*path* 23 | : Path to the configuration file where the observation socket path for 24 | connecting with the ntp-daemon is specified. This defaults to 25 | `/etc/ntpd-rs/ntp.toml` if not specified. 26 | 27 | `-h`, `--help` 28 | : Display usage instructions. 29 | 30 | `-v`, `--version` 31 | : Display version information. 32 | 33 | # SEE ALSO 34 | 35 | [ntp-daemon(8)](ntp-daemon.8.md), [ntp-ctl(8)](ntp-ctl.8.md), 36 | [ntp.toml(5)](ntp.toml.5.md) 37 | -------------------------------------------------------------------------------- /docs/precompiled/man/ntp-ctl.8: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.1.1 2 | .\" 3 | .\" Define V font for inline verbatim, using C font in formats 4 | .\" that render this, and otherwise B font. 5 | .ie "\f[CB]x\f[]"x" \{\ 6 | . ftr V B 7 | . ftr VI BI 8 | . ftr VB B 9 | . ftr VBI BI 10 | .\} 11 | .el \{\ 12 | . ftr V CR 13 | . ftr VI CI 14 | . ftr VB CB 15 | . ftr VBI CBI 16 | .\} 17 | .TH "NTP-CTL" "8" "" "ntpd-rs 1.5.0" "ntpd-rs" 18 | .hy 19 | .SH NAME 20 | .PP 21 | \f[V]ntp-ctl\f[R] - management client for the ntpd-rs ntp-daemon process 22 | .SH SYNOPSIS 23 | .PP 24 | \f[V]ntp-ctl\f[R] validate [\f[V]-c\f[R] \f[I]path\f[R]] 25 | .PD 0 26 | .P 27 | .PD 28 | \f[V]ntp-ctl\f[R] status [\f[V]-f\f[R] \f[I]format\f[R]] [\f[V]-c\f[R] 29 | \f[I]path\f[R]] 30 | .PD 0 31 | .P 32 | .PD 33 | \f[V]ntp-ctl\f[R] force-sync [\f[V]-c\f[R] \f[I]path\f[R]] 34 | .PD 0 35 | .P 36 | .PD 37 | \f[V]ntp-ctl\f[R] \f[V]-h\f[R] 38 | .PD 0 39 | .P 40 | .PD 41 | \f[V]ntp-ctl\f[R] \f[V]-v\f[R] 42 | .SH DESCRIPTION 43 | .PP 44 | The \f[V]ntp-ctl\f[R] management client allows management of some 45 | aspects of the ntpd-rs daemon. 46 | Currently the management client only allows displaying the current 47 | status of the daemon and validating a configuration file for usage with 48 | the daemon. 49 | .SH OPTIONS 50 | .TP 51 | \f[V]-c\f[R] \f[I]path\f[R], \f[V]--config\f[R]=\f[I]path\f[R] 52 | Path to the configuration file from which the observation socket address 53 | will be retrieved. 54 | If not specified this defaults to \f[V]/etc/ntpd-rs/ntp.toml\f[R]. 55 | .TP 56 | \f[V]-f\f[R] \f[I]format\f[R], \f[V]--format\f[R]=\f[I]format\f[R] 57 | The output format for the status command. 58 | If not specified this defaults to \f[I]plain\f[R]. 59 | Alternatively the format \f[I]prometheus\f[R] is available to display 60 | the output in an OpenMetrics/Prometheus compatible format. 61 | .TP 62 | \f[V]-h\f[R], \f[V]--help\f[R] 63 | Display usage instructions. 64 | .TP 65 | \f[V]-v\f[R], \f[V]--version\f[R] 66 | Display version information. 67 | .SH COMMANDS 68 | .TP 69 | \f[V]validate\f[R] 70 | Checks if the configuration specified (or 71 | \f[V]/etc/ntpd-rs/ntp.toml\f[R] by default) is valid. 72 | .TP 73 | \f[V]status\f[R] 74 | Returns status information about the current state of the ntp-daemon 75 | that the client connects to. 76 | .TP 77 | \f[V]force-sync\f[R] 78 | Interactively run a single synchronization of your clock. 79 | This command can be used to do a one-off synchronization to the time 80 | sources configured in your configuration file. 81 | This command should never be used without any validation by a human 82 | operator. 83 | .SH SEE ALSO 84 | .PP 85 | ntp-daemon(8), ntp-metrics-exporter(8), ntp.toml(5) 86 | -------------------------------------------------------------------------------- /docs/precompiled/man/ntp-daemon.8: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.1.1 2 | .\" 3 | .\" Define V font for inline verbatim, using C font in formats 4 | .\" that render this, and otherwise B font. 5 | .ie "\f[CB]x\f[]"x" \{\ 6 | . ftr V B 7 | . ftr VI BI 8 | . ftr VB B 9 | . ftr VBI BI 10 | .\} 11 | .el \{\ 12 | . ftr V CR 13 | . ftr VI CI 14 | . ftr VB CB 15 | . ftr VBI CBI 16 | .\} 17 | .TH "NTP-DAEMON" "8" "" "ntpd-rs 1.5.0" "ntpd-rs" 18 | .hy 19 | .SH NAME 20 | .PP 21 | \f[V]ntp-daemon\f[R] - ntpd-rs Network Time Protocol service daemon 22 | .SH SYNOPSIS 23 | .PP 24 | \f[V]ntp-daemon\f[R] [\f[V]-c\f[R] \f[I]path\f[R]] [\f[V]-l\f[R] 25 | \f[I]loglevel\f[R]] 26 | .PD 0 27 | .P 28 | .PD 29 | \f[V]ntp-daemon\f[R] \f[V]-h\f[R] 30 | .PD 0 31 | .P 32 | .PD 33 | \f[V]ntp-daemon\f[R] \f[V]-v\f[R] 34 | .SH DESCRIPTION 35 | .PP 36 | \f[V]ntp-daemon\f[R] is the Network Time Protocol (NTP) service daemon 37 | for ntpd-rs, an NTP implementation with a focus on security and 38 | stability. 39 | The \f[V]ntp-deamon\f[R] can be configured as both an NTP client and an 40 | NTP server. 41 | The daemon also works with the Network Time Security (NTS) protocol. 42 | Details of the configuration of the daemon and implementation details 43 | can be found in ntp.toml(5), where several concepts of the ntp-daemon 44 | are also explained. 45 | .SH OPTIONS 46 | .TP 47 | \f[V]-c\f[R] \f[I]path\f[R], \f[V]--config\f[R]=\f[I]path\f[R] 48 | The configuration file path for the ntp-daemon where settings for the 49 | configuration of ntpd-rs are stored. 50 | If not specified the default configuration file is 51 | \f[V]/etc/ntpd-rs/ntp.toml\f[R]. 52 | .TP 53 | \f[V]-h\f[R], \f[V]--help\f[R] 54 | Display usage instructions. 55 | .TP 56 | \f[V]-l\f[R] \f[I]loglevel\f[R], \f[V]--log-level\f[R]=\f[I]loglevel\f[R] 57 | Change which log messages are logged to stdout. 58 | Available log levels are \f[I]trace\f[R], \f[I]debug\f[R], 59 | \f[I]info\f[R], \f[I]warn\f[R] and \f[I]error\f[R] (from lower to higher 60 | priority). 61 | Only messages with the given priority and higher will be displayed. 62 | The default log level is \f[I]info\f[R]. 63 | .TP 64 | \f[V]-v\f[R], \f[V]--version\f[R] 65 | Display version information. 66 | .SH SEE ALSO 67 | .PP 68 | ntp-ctl(8), ntp-metrics-exporter(8), ntp.toml(5) 69 | -------------------------------------------------------------------------------- /docs/precompiled/man/ntp-metrics-exporter.8: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.1.1 2 | .\" 3 | .\" Define V font for inline verbatim, using C font in formats 4 | .\" that render this, and otherwise B font. 5 | .ie "\f[CB]x\f[]"x" \{\ 6 | . ftr V B 7 | . ftr VI BI 8 | . ftr VB B 9 | . ftr VBI BI 10 | .\} 11 | .el \{\ 12 | . ftr V CR 13 | . ftr VI CI 14 | . ftr VB CB 15 | . ftr VBI CBI 16 | .\} 17 | .TH "NTP-METRICS-EXPORTER" "8" "" "ntpd-rs 1.5.0" "ntpd-rs" 18 | .hy 19 | .SH NAME 20 | .PP 21 | \f[V]ntp-metrics-exporter\f[R] - Prometheus/OpenMetrics exporter for the 22 | ntpd-rs daemon 23 | .SH SYNOPSIS 24 | .PP 25 | \f[V]ntp-metrics-exporter\f[R] [\f[V]-c\f[R] \f[I]path\f[R]] 26 | .PD 0 27 | .P 28 | .PD 29 | \f[V]ntp-metrics-exporter\f[R] \f[V]-h\f[R] 30 | .PD 0 31 | .P 32 | .PD 33 | \f[V]ntp-metrics-exporter\f[R] \f[V]-v\f[R] 34 | .SH DESCRIPTION 35 | .PP 36 | Exports the status metrics from the ntpd-rs daemon as 37 | Prometheus/OpenMetrics via an HTTP socket. 38 | .SH OPTIONS 39 | .TP 40 | \f[V]-c\f[R] \f[I]path\f[R], \f[V]--config\f[R]=\f[I]path\f[R] 41 | Path to the configuration file where the observation socket path for 42 | connecting with the ntp-daemon is specified. 43 | This defaults to \f[V]/etc/ntpd-rs/ntp.toml\f[R] if not specified. 44 | .TP 45 | \f[V]-h\f[R], \f[V]--help\f[R] 46 | Display usage instructions. 47 | .TP 48 | \f[V]-v\f[R], \f[V]--version\f[R] 49 | Display version information. 50 | .SH SEE ALSO 51 | .PP 52 | ntp-daemon(8), ntp-ctl(8), ntp.toml(5) 53 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ntp-proto-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | edition = "2018" 6 | publish = false 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [features] 12 | ntpv5 = ["ntp-proto/ntpv5"] 13 | 14 | [dependencies] 15 | rand = "0.8.5" 16 | 17 | [dependencies.libfuzzer-sys] 18 | version = "0.4" 19 | features = ["arbitrary-derive"] 20 | 21 | [dependencies.ntp-proto] 22 | path = "../ntp-proto" 23 | features = ["__internal-fuzz"] 24 | 25 | # Prevent this from interfering with workspaces 26 | [workspace] 27 | members = ["."] 28 | 29 | [[bin]] 30 | name = "packet_parsing_sound" 31 | path = "fuzz_targets/packet_parsing_sound.rs" 32 | test = false 33 | doc = false 34 | 35 | [[bin]] 36 | name = "cookie_parsing_sound" 37 | path = "fuzz_targets/cookie_parsing_sound.rs" 38 | test = false 39 | doc = false 40 | 41 | [[bin]] 42 | name = "packet_keyset" 43 | path = "fuzz_targets/packet_keyset.rs" 44 | test = false 45 | doc = false 46 | 47 | [[bin]] 48 | name = "encrypted_server_parsing" 49 | path = "fuzz_targets/encrypted_server_parsing.rs" 50 | test = false 51 | doc = false 52 | 53 | [[bin]] 54 | name = "duration_from_float" 55 | path = "fuzz_targets/duration_from_float.rs" 56 | test = false 57 | doc = false 58 | 59 | [[bin]] 60 | name = "ipfilter" 61 | path = "fuzz_targets/ipfilter.rs" 62 | test = false 63 | doc = false 64 | 65 | [[bin]] 66 | name = "record_encode_decode" 67 | path = "fuzz_targets/record_encode_decode.rs" 68 | test = false 69 | doc = false 70 | 71 | [[bin]] 72 | name = "ntsrecord" 73 | path = "fuzz_targets/ntsrecord.rs" 74 | test = false 75 | doc = false 76 | 77 | [[bin]] 78 | name = "key_exchange_result_decoder" 79 | path = "fuzz_targets/key_exchange_result_decoder.rs" 80 | test = false 81 | doc = false 82 | 83 | [[bin]] 84 | name = "key_exchange_server_decoder" 85 | path = "fuzz_targets/key_exchange_server_decoder.rs" 86 | test = false 87 | doc = false 88 | 89 | [[bin]] 90 | name = "encrypted_client_parsing" 91 | path = "fuzz_targets/encrypted_client_parsing.rs" 92 | test = false 93 | doc = false 94 | 95 | [patch.crates-io] 96 | rand = { path="./fuzz_rand_shim" } 97 | rand_core = { git="https://github.com/rust-random/rand.git", tag="0.8.5" } 98 | -------------------------------------------------------------------------------- /fuzz/fuzz_rand_shim/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /fuzz/fuzz_rand_shim/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 = "cfg-if" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 10 | 11 | [[package]] 12 | name = "getrandom" 13 | version = "0.2.9" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 16 | dependencies = [ 17 | "cfg-if", 18 | "libc", 19 | "wasi", 20 | ] 21 | 22 | [[package]] 23 | name = "libc" 24 | version = "0.2.144" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 27 | 28 | [[package]] 29 | name = "ppv-lite86" 30 | version = "0.2.17" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 33 | 34 | [[package]] 35 | name = "rand" 36 | version = "0.8.5" 37 | dependencies = [ 38 | "rand 0.8.5 (git+https://github.com/rust-random/rand.git?tag=0.8.5)", 39 | ] 40 | 41 | [[package]] 42 | name = "rand" 43 | version = "0.8.5" 44 | source = "git+https://github.com/rust-random/rand.git?tag=0.8.5#937320cbfeebd4352a23086d9c6e68f067f74644" 45 | dependencies = [ 46 | "libc", 47 | "rand_chacha", 48 | "rand_core", 49 | ] 50 | 51 | [[package]] 52 | name = "rand_chacha" 53 | version = "0.3.1" 54 | source = "git+https://github.com/rust-random/rand.git?tag=0.8.5#937320cbfeebd4352a23086d9c6e68f067f74644" 55 | dependencies = [ 56 | "ppv-lite86", 57 | "rand_core", 58 | ] 59 | 60 | [[package]] 61 | name = "rand_core" 62 | version = "0.6.4" 63 | source = "git+https://github.com/rust-random/rand.git?tag=0.8.5#937320cbfeebd4352a23086d9c6e68f067f74644" 64 | dependencies = [ 65 | "getrandom", 66 | ] 67 | 68 | [[package]] 69 | name = "wasi" 70 | version = "0.11.0+wasi-snapshot-preview1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 73 | -------------------------------------------------------------------------------- /fuzz/fuzz_rand_shim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rand" 3 | version = "0.8.5" 4 | edition = "2021" 5 | publish = false 6 | 7 | [workspace] 8 | members = ["."] 9 | 10 | [dependencies] 11 | real_rand = { git = "https://github.com/rust-random/rand.git", tag="0.8.5", package="rand" } 12 | -------------------------------------------------------------------------------- /fuzz/fuzz_rand_shim/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate is a shim for the rand crate allowing deterministic testing for code otherwise using thread_rng 2 | //! Note that it will only work properly for single-threaded tests, although multiple tests can be run in parallel 3 | //! Also, for obvious reasons this is not safe to use in production. 4 | 5 | use std::cell::RefCell; 6 | use std::ops::DerefMut; 7 | 8 | use real_rand::rngs::StdRng; 9 | 10 | pub use real_rand::distributions; 11 | pub use real_rand::prelude; 12 | pub use real_rand::rngs; 13 | pub use real_rand::seq; 14 | 15 | pub use real_rand::{random, CryptoRng, Error, Fill, Rng, RngCore, SeedableRng}; 16 | 17 | thread_local!( 18 | static THREAD_RNG: RefCell = { 19 | RefCell::new(StdRng::seed_from_u64(0)) 20 | } 21 | ); 22 | 23 | pub fn thread_rng() -> real_rand::rngs::StdRng { 24 | THREAD_RNG 25 | .with(|t| StdRng::from_rng(t.borrow_mut().deref_mut())) 26 | .unwrap() 27 | } 28 | 29 | pub fn set_thread_rng(rng: StdRng) { 30 | THREAD_RNG.with(|t| *t.borrow_mut() = rng); 31 | } 32 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/cookie_parsing_sound.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use ntp_proto::KeySetProvider; 5 | 6 | fuzz_target!(|data: Vec| { 7 | let provider = KeySetProvider::dangerous_new_deterministic(1); 8 | 9 | let keyset = provider.get(); 10 | 11 | let _ = keyset.decode_cookie_pub(&data); 12 | }); 13 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/duration_from_float.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use ntp_proto::fuzz_duration_from_seconds; 4 | 5 | fuzz_target!(|v: f64| { 6 | fuzz_duration_from_seconds(v); 7 | }); 8 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/encrypted_client_parsing.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::{Cursor, Write}; 4 | 5 | use libfuzzer_sys::fuzz_target; 6 | use ntp_proto::{test_cookie, EncryptResult, NtpPacket}; 7 | use rand::{rngs::StdRng, set_thread_rng, SeedableRng}; 8 | 9 | const fn next_multiple_of(lhs: u16, rhs: u16) -> u16 { 10 | match lhs % rhs { 11 | 0 => lhs, 12 | r => lhs + (rhs - r), 13 | } 14 | } 15 | 16 | fuzz_target!(|parts: (u64, Vec, Vec, Vec)| { 17 | set_thread_rng(StdRng::seed_from_u64(parts.0)); 18 | 19 | // Build packet 20 | let mut cursor = Cursor::new([0u8; 8192]); 21 | let _ = cursor.write_all(&parts.1); 22 | let cookie = test_cookie(); 23 | 24 | let mut ciphertext = parts.2.clone(); 25 | ciphertext.resize(ciphertext.len() + 32, 0); 26 | let EncryptResult { 27 | nonce_length, 28 | ciphertext_length, 29 | } = cookie 30 | .c2s 31 | .encrypt( 32 | &mut ciphertext, 33 | parts.2.len(), 34 | &cursor.get_ref()[..cursor.position() as usize], 35 | ) 36 | .unwrap(); 37 | 38 | let _ = cursor.write_all(&0x404u16.to_be_bytes()); 39 | let _ = cursor.write_all( 40 | &(8 + next_multiple_of((nonce_length + ciphertext_length) as u16, 4)).to_be_bytes(), 41 | ); 42 | let _ = cursor.write_all(&(nonce_length as u16).to_be_bytes()); 43 | let _ = cursor.write_all(&(ciphertext_length as u16).to_be_bytes()); 44 | let _ = cursor.write_all(&ciphertext); 45 | let _ = cursor.write_all(&parts.3); 46 | 47 | let _ = NtpPacket::deserialize(cursor.get_ref(), &Some(cookie.s2c.as_ref())); 48 | }); 49 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/encrypted_server_parsing.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::{ 4 | borrow::Cow, 5 | io::{Cursor, Write}, 6 | }; 7 | 8 | use libfuzzer_sys::fuzz_target; 9 | use ntp_proto::{ 10 | test_cookie, EncryptResult, ExtensionField, ExtensionHeaderVersion, KeySetProvider, NtpPacket, 11 | }; 12 | use rand::{rngs::StdRng, set_thread_rng, SeedableRng}; 13 | 14 | const fn next_multiple_of(lhs: u16, rhs: u16) -> u16 { 15 | match lhs % rhs { 16 | 0 => lhs, 17 | r => lhs + (rhs - r), 18 | } 19 | } 20 | 21 | fuzz_target!(|parts: ( 22 | Vec, 23 | Vec, 24 | Vec, 25 | Vec, 26 | u64, 27 | ExtensionHeaderVersion 28 | )| { 29 | set_thread_rng(StdRng::seed_from_u64(parts.4)); 30 | 31 | // Can't test reencoding because of the keyset 32 | let provider = KeySetProvider::dangerous_new_deterministic(1); 33 | 34 | let keyset = provider.get(); 35 | 36 | // Build packet 37 | let mut cursor = Cursor::new([0u8; 8192]); 38 | let _ = cursor.write_all(&parts.0); 39 | let cookie = test_cookie(); 40 | let enc_cookie = keyset.encode_cookie_pub(&cookie); 41 | let _ = ExtensionField::NtsCookie(Cow::Borrowed(&enc_cookie)).serialize_pub( 42 | &mut cursor, 43 | 4, 44 | parts.5, 45 | ); 46 | let _ = cursor.write_all(&parts.1); 47 | 48 | let mut ciphertext = parts.2.clone(); 49 | ciphertext.resize(ciphertext.len() + 32, 0); 50 | let EncryptResult { 51 | nonce_length, 52 | ciphertext_length, 53 | } = cookie 54 | .c2s 55 | .encrypt( 56 | &mut ciphertext, 57 | parts.2.len(), 58 | &cursor.get_ref()[..cursor.position() as usize], 59 | ) 60 | .unwrap(); 61 | 62 | let _ = cursor.write_all(&0x404u16.to_be_bytes()); 63 | let _ = cursor.write_all( 64 | &(8 + next_multiple_of((nonce_length + ciphertext_length) as u16, 4)).to_be_bytes(), 65 | ); 66 | let _ = cursor.write_all(&(nonce_length as u16).to_be_bytes()); 67 | let _ = cursor.write_all(&(ciphertext_length as u16).to_be_bytes()); 68 | let _ = cursor.write_all(&ciphertext); 69 | let _ = cursor.write_all(&parts.3); 70 | 71 | let _ = NtpPacket::deserialize(cursor.get_ref(), keyset.as_ref()); 72 | }); 73 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/ipfilter.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::{ 3 | arbitrary::{ 4 | size_hint::{and, or}, 5 | Arbitrary, 6 | }, 7 | fuzz_target, 8 | }; 9 | use ntp_proto::{fuzz_ipfilter, IpSubnet}; 10 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | struct ASubnet(IpSubnet); 14 | 15 | impl<'a> Arbitrary<'a> for ASubnet { 16 | fn arbitrary( 17 | u: &mut libfuzzer_sys::arbitrary::Unstructured<'a>, 18 | ) -> libfuzzer_sys::arbitrary::Result { 19 | let ipv4: bool = u.arbitrary()?; 20 | if ipv4 { 21 | let mask: u8 = u.int_in_range(0..=32)?; 22 | let addr = IpAddr::V4(Ipv4Addr::from(u.arbitrary::<[u8; 4]>()?)); 23 | Ok(ASubnet(IpSubnet { mask, addr })) 24 | } else { 25 | let mask: u8 = u.int_in_range(0..=128)?; 26 | let addr = IpAddr::V6(Ipv6Addr::from(u.arbitrary::<[u8; 16]>()?)); 27 | Ok(ASubnet(IpSubnet { mask, addr })) 28 | } 29 | } 30 | 31 | fn size_hint(depth: usize) -> (usize, Option) { 32 | and( 33 | >::size_hint(depth), 34 | or( 35 | <[u8; 4] as Arbitrary<'a>>::size_hint(depth), 36 | <[u8; 16] as Arbitrary<'a>>::size_hint(depth), 37 | ), 38 | ) 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq, Eq)] 43 | struct AIp(IpAddr); 44 | 45 | impl<'a> Arbitrary<'a> for AIp { 46 | fn arbitrary( 47 | u: &mut libfuzzer_sys::arbitrary::Unstructured<'a>, 48 | ) -> libfuzzer_sys::arbitrary::Result { 49 | let ipv4: bool = u.arbitrary()?; 50 | if ipv4 { 51 | Ok(AIp(IpAddr::V4(Ipv4Addr::from(u.arbitrary::<[u8; 4]>()?)))) 52 | } else { 53 | Ok(AIp(IpAddr::V6(Ipv6Addr::from(u.arbitrary::<[u8; 16]>()?)))) 54 | } 55 | } 56 | } 57 | 58 | fuzz_target!(|spec: (Vec, Vec)| { 59 | let subnets: Vec<_> = spec.0.into_iter().map(|v| v.0).collect(); 60 | let addrs: Vec<_> = spec.1.into_iter().map(|v| v.0).collect(); 61 | fuzz_ipfilter(&subnets, &addrs); 62 | }); 63 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/key_exchange_result_decoder.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use ntp_proto::fuzz_key_exchange_result_decoder; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | fuzz_key_exchange_result_decoder(data); 7 | }); 8 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/key_exchange_server_decoder.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use ntp_proto::fuzz_key_exchange_server_decoder; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | fuzz_key_exchange_server_decoder(data); 7 | }); 8 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/ntsrecord.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use ntp_proto::NtsRecord; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | let mut decoder = NtsRecord::decoder(); 7 | decoder.extend(data.iter().copied()); 8 | 9 | // mimic test_decode_nts_time_nl_response() which has 11 steps 10 | let _ = decoder.step(); 11 | let _ = decoder.step(); 12 | let _ = decoder.step(); 13 | let _ = decoder.step(); 14 | let _ = decoder.step(); 15 | let _ = decoder.step(); 16 | let _ = decoder.step(); 17 | let _ = decoder.step(); 18 | let _ = decoder.step(); 19 | let _ = decoder.step(); 20 | }); 21 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/packet_keyset.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use ntp_proto::{KeySetProvider, NtpPacket}; 5 | 6 | fuzz_target!(|data: Vec| { 7 | // Can't test reencoding because of the keyset 8 | let provider = KeySetProvider::dangerous_new_deterministic(1); 9 | 10 | let keyset = provider.get(); 11 | 12 | let _ = NtpPacket::deserialize(&data, keyset.as_ref()); 13 | }); 14 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/packet_parsing_sound.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::Cursor; 4 | 5 | use libfuzzer_sys::fuzz_target; 6 | use ntp_proto::{NoCipher, NtpPacket}; 7 | 8 | fuzz_target!(|data: Vec| { 9 | // packets expand by a factor of at most 4 on re-encode 10 | let mut buf = [0u8; 4096 * 4]; 11 | let mut buf2 = [0u8; 4096 * 4]; 12 | // We test here without ciphers, as that is required to make reencoding work. 13 | if let Ok((a, _)) = NtpPacket::deserialize(&data, &NoCipher) { 14 | let mut cursor = Cursor::new(buf.as_mut_slice()); 15 | a.serialize(&mut cursor, &NoCipher, None).unwrap(); 16 | let used = cursor.position(); 17 | let slice = &buf[..used as usize]; 18 | let b = NtpPacket::deserialize(&data, &NoCipher).unwrap().0; 19 | let mut cursor = Cursor::new(buf2.as_mut_slice()); 20 | b.serialize(&mut cursor, &NoCipher, None).unwrap(); 21 | let used = cursor.position(); 22 | let slice2 = &buf2[..used as usize]; 23 | assert_eq!(slice, slice2); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/record_encode_decode.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use ntp_proto::NtsRecord; 4 | use std::io::Cursor; 5 | 6 | fuzz_target!(|record: NtsRecord| { 7 | // fuzz inputs are at most 4096 bytes long (by default) 8 | let mut buffer = Cursor::new([0u8; 4096]); 9 | record.write(&mut buffer).unwrap(); 10 | 11 | buffer.set_position(0); 12 | NtsRecord::read(&mut buffer).unwrap(); 13 | }); 14 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | theme: 2 | name: material 3 | features: 4 | - content.tooltips 5 | - content.code.copy 6 | - content.code.select 7 | - navigation.sections 8 | docs_dir: docs 9 | site_name: ntpd-rs documentation 10 | site_dir: target/docs/site 11 | exclude_docs: | 12 | /precompiled # Precompiled assets 13 | /includes # Only included indirectly 14 | markdown_extensions: 15 | - def_list 16 | - abbr 17 | - attr_list 18 | - admonition 19 | - pymdownx.snippets: 20 | auto_append: [./docs/includes/glossary.md] 21 | - pymdownx.escapeall: 22 | hardbreak: true 23 | - pymdownx.highlight: 24 | anchor_linenums: true 25 | line_spans: __span 26 | pygments_lang_class: true 27 | - pymdownx.inlinehilite 28 | - pymdownx.details 29 | - pymdownx.superfences 30 | nav: 31 | - Home: index.md 32 | - Guide: 33 | - guide/getting-started.md 34 | - guide/installation.md 35 | - guide/server-setup.md 36 | - guide/gps-pps.md 37 | - guide/exporting-metrics.md 38 | - guide/nts.md 39 | - guide/migrating-chrony.md 40 | - guide/migrating-ntpd.md 41 | - guide/migrating-ntpsec.md 42 | - guide/security-guidance.md 43 | - Man Pages: 44 | - ntp-daemon(8): man/ntp-daemon.8.md 45 | - ntp.toml(5): man/ntp.toml.5.md 46 | - ntp-ctl(8): man/ntp-ctl.8.md 47 | - ntp-metrics-exporter(8): man/ntp-metrics-exporter.8.md 48 | - Development: 49 | - development/code-structure.md 50 | - development/threat-model.md 51 | - development/ca.md 52 | - development/audits.md 53 | - development/further-reading.md 54 | -------------------------------------------------------------------------------- /ntp-proto/COPYRIGHT: -------------------------------------------------------------------------------- 1 | ../COPYRIGHT -------------------------------------------------------------------------------- /ntp-proto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ntp-proto" 3 | description = "ntpd-rs packet parsing and algorithms" 4 | readme = "README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [features] 16 | default = [] 17 | __internal-fuzz = ["arbitrary", "__internal-api"] 18 | __internal-test = ["__internal-api"] 19 | __internal-api = [] 20 | ntpv5 = [] 21 | nts-pool = [] 22 | 23 | [dependencies] 24 | # Note: md5 is needed to calculate ReferenceIDs for IPv6 addresses per RFC5905 25 | md-5.workspace = true 26 | rand.workspace = true 27 | tracing.workspace = true 28 | serde.workspace = true 29 | rustls23.workspace = true 30 | rustls-platform-verifier.workspace = true 31 | arbitrary = { workspace = true, optional = true } 32 | aead.workspace = true 33 | aes-siv.workspace = true 34 | zeroize.workspace = true 35 | 36 | [dev-dependencies] 37 | serde_json.workspace = true 38 | -------------------------------------------------------------------------------- /ntp-proto/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /ntp-proto/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /ntp-proto/README.md: -------------------------------------------------------------------------------- 1 | # ntp-proto 2 | This crate contains packet parsing and algorithm code for ntpd-rs and is not 3 | intended as a public interface at this time. It follows the same version as the 4 | main ntpd-rs crate, but that version is not intended to give any stability 5 | guarantee. Use at your own risk. 6 | 7 | Please visit the [ntpd-rs](https://github.com/pendulum-project/ntpd-rs) project 8 | for more information. 9 | -------------------------------------------------------------------------------- /ntp-proto/src/algorithm/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, time::Duration}; 2 | 3 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | clock::NtpClock, 7 | config::{SourceConfig, SynchronizationConfig}, 8 | source::Measurement, 9 | system::TimeSnapshot, 10 | time_types::{NtpDuration, NtpTimestamp}, 11 | PollInterval, 12 | }; 13 | 14 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 15 | pub struct ObservableSourceTimedata { 16 | pub offset: NtpDuration, 17 | pub uncertainty: NtpDuration, 18 | pub delay: NtpDuration, 19 | 20 | pub remote_delay: NtpDuration, 21 | pub remote_uncertainty: NtpDuration, 22 | 23 | pub last_update: NtpTimestamp, 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct StateUpdate { 28 | // Message for all sources, if any 29 | pub source_message: Option, 30 | // Update to the time snapshot, if any 31 | pub time_snapshot: Option, 32 | // Update to the used sources, if any 33 | pub used_sources: Option>, 34 | // Requested timestamp for next non-measurement update 35 | pub next_update: Option, 36 | } 37 | 38 | // Note: this default implementation is necessary since the 39 | // derive only works if SourceId is Default (which it isn't 40 | // necessarily) 41 | impl Default for StateUpdate { 42 | fn default() -> Self { 43 | Self { 44 | source_message: None, 45 | time_snapshot: None, 46 | used_sources: None, 47 | next_update: None, 48 | } 49 | } 50 | } 51 | 52 | pub trait TimeSyncController: Sized + Send + 'static { 53 | type Clock: NtpClock; 54 | type SourceId; 55 | type AlgorithmConfig: Debug + Copy + DeserializeOwned + Send; 56 | type ControllerMessage: Debug + Clone + Send + 'static; 57 | type SourceMessage: Debug + Clone + Send + 'static; 58 | type NtpSourceController: SourceController< 59 | ControllerMessage = Self::ControllerMessage, 60 | SourceMessage = Self::SourceMessage, 61 | MeasurementDelay = NtpDuration, 62 | >; 63 | type OneWaySourceController: SourceController< 64 | ControllerMessage = Self::ControllerMessage, 65 | SourceMessage = Self::SourceMessage, 66 | MeasurementDelay = (), 67 | >; 68 | 69 | /// Create a new clock controller controlling the given clock 70 | fn new( 71 | clock: Self::Clock, 72 | synchronization_config: SynchronizationConfig, 73 | algorithm_config: Self::AlgorithmConfig, 74 | ) -> Result::Error>; 75 | 76 | /// Take control of the clock (should not be done in new!) 77 | fn take_control(&mut self) -> Result<(), ::Error>; 78 | 79 | /// Create a new source with given identity 80 | fn add_source( 81 | &mut self, 82 | id: Self::SourceId, 83 | source_config: SourceConfig, 84 | ) -> Self::NtpSourceController; 85 | /// Create a new one way source with given identity (used e.g. with GPS sock sources) 86 | fn add_one_way_source( 87 | &mut self, 88 | id: Self::SourceId, 89 | source_config: SourceConfig, 90 | measurement_noise_estimate: f64, 91 | period: Option, 92 | ) -> Self::OneWaySourceController; 93 | /// Notify the controller that a previous source has gone 94 | fn remove_source(&mut self, id: Self::SourceId); 95 | /// Notify the controller that the status of a source (whether 96 | /// or not it is usable for synchronization) has changed. 97 | fn source_update(&mut self, id: Self::SourceId, usable: bool); 98 | /// Notify the controller of a new measurement from a source. 99 | /// The list of SourceIds is used for loop detection, with the 100 | /// first SourceId given considered the primary source used. 101 | fn source_message( 102 | &mut self, 103 | id: Self::SourceId, 104 | message: Self::SourceMessage, 105 | ) -> StateUpdate; 106 | /// Non-message driven update (queued via next_update) 107 | fn time_update(&mut self) -> StateUpdate; 108 | } 109 | 110 | pub trait SourceController: Sized + Send + 'static { 111 | type ControllerMessage: Debug + Clone + Send + 'static; 112 | type SourceMessage: Debug + Clone + Send + 'static; 113 | type MeasurementDelay: Debug + Copy + Clone; 114 | 115 | fn handle_message(&mut self, message: Self::ControllerMessage); 116 | 117 | fn handle_measurement( 118 | &mut self, 119 | measurement: Measurement, 120 | ) -> Option; 121 | 122 | fn desired_poll_interval(&self) -> PollInterval; 123 | 124 | fn observe(&self) -> ObservableSourceTimedata; 125 | } 126 | 127 | mod kalman; 128 | 129 | pub use kalman::{ 130 | config::AlgorithmConfig, KalmanClockController, KalmanControllerMessage, 131 | KalmanSourceController, KalmanSourceMessage, TwoWayKalmanSourceController, 132 | }; 133 | -------------------------------------------------------------------------------- /ntp-proto/src/clock.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | packet::NtpLeapIndicator, 3 | time_types::{NtpDuration, NtpTimestamp}, 4 | }; 5 | 6 | /// Interface for a clock settable by the ntp implementation. 7 | /// This needs to be a trait as a single system can have multiple clocks 8 | /// which need different implementation for steering and/or now. 9 | pub trait NtpClock: Clone + Send + 'static { 10 | type Error: std::error::Error + Send + Sync; 11 | 12 | // Get current time 13 | fn now(&self) -> Result; 14 | 15 | // Change the frequency of the clock, returning the time 16 | // at which the change was applied. 17 | fn set_frequency(&self, freq: f64) -> Result; 18 | 19 | // Get the frequency of the clock 20 | fn get_frequency(&self) -> Result; 21 | 22 | // Change the current time of the clock by offset. Returns 23 | // the time at which the change was applied. 24 | fn step_clock(&self, offset: NtpDuration) -> Result; 25 | 26 | // A clock can have a built in NTP clock discipline algorithm 27 | // that does more processing on the offsets it receives. This 28 | // functions disables that discipline. 29 | fn disable_ntp_algorithm(&self) -> Result<(), Self::Error>; 30 | 31 | // Provide the system with our current best estimates for 32 | // the statistical error of the clock (est_error), and 33 | // the maximum deviation due to frequency error and 34 | // distance to the root clock. 35 | fn error_estimate_update( 36 | &self, 37 | est_error: NtpDuration, 38 | max_error: NtpDuration, 39 | ) -> Result<(), Self::Error>; 40 | // Change the indicators for upcoming leap seconds and 41 | // the clocks synchronization status. 42 | fn status_update(&self, leap_status: NtpLeapIndicator) -> Result<(), Self::Error>; 43 | } 44 | -------------------------------------------------------------------------------- /ntp-proto/src/cookiestash.rs: -------------------------------------------------------------------------------- 1 | //! Datastructure for managing cookies. It keeps the following 2 | //! invariants: 3 | //! - Each cookie is yielded at most once 4 | //! - The oldest cookie is always yielded first 5 | //! 6 | //! Note that as a consequence, this type is not Clone! 7 | 8 | pub const MAX_COOKIES: usize = 8; 9 | 10 | #[derive(Default, PartialEq, Eq)] 11 | pub(crate) struct CookieStash { 12 | cookies: [Vec; MAX_COOKIES], 13 | read: usize, 14 | valid: usize, 15 | } 16 | 17 | impl std::fmt::Debug for CookieStash { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | f.debug_struct("CookieStash") 20 | .field("cookies", &self.cookies.len()) 21 | .field("read", &self.read) 22 | .field("valid", &self.valid) 23 | .finish() 24 | } 25 | } 26 | 27 | impl CookieStash { 28 | /// Store a new cookie 29 | pub fn store(&mut self, cookie: Vec) { 30 | let wpos = (self.read + self.valid) % self.cookies.len(); 31 | self.cookies[wpos] = cookie; 32 | if self.valid < self.cookies.len() { 33 | self.valid += 1; 34 | } else { 35 | debug_assert!(self.valid == self.cookies.len()); 36 | // No place for extra cookies, but it is still 37 | // newer so just keep the newest cookies. 38 | self.read = (self.read + 1) % self.cookies.len(); 39 | } 40 | } 41 | 42 | /// Get oldest cookie 43 | pub fn get(&mut self) -> Option> { 44 | if self.valid == 0 { 45 | None 46 | } else { 47 | // takes the cookie, puts `vec![]` in its place 48 | let result = std::mem::take(&mut self.cookies[self.read]); 49 | self.read = (self.read + 1) % self.cookies.len(); 50 | self.valid -= 1; 51 | Some(result) 52 | } 53 | } 54 | 55 | /// Number of cookies missing from the stash 56 | pub fn gap(&self) -> u8 { 57 | // This never overflows or underflows since cookies.len will 58 | // fit in a u8 and 0 <= self.valid <= self.cookies.len() 59 | (self.cookies.len() - self.valid) as u8 60 | } 61 | 62 | pub fn len(&self) -> usize { 63 | self.valid 64 | } 65 | 66 | pub fn is_empty(&self) -> bool { 67 | self.valid == 0 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | 75 | #[test] 76 | fn test_empty_read() { 77 | let mut stash = CookieStash::default(); 78 | assert_eq!(stash.get(), None); 79 | } 80 | 81 | #[test] 82 | fn test_overfill() { 83 | let mut stash = CookieStash::default(); 84 | for i in 0..10_u8 { 85 | stash.store(vec![i]); 86 | } 87 | assert_eq!(stash.get(), Some(vec![2])); 88 | assert_eq!(stash.get(), Some(vec![3])); 89 | } 90 | 91 | #[test] 92 | fn test_normal_op() { 93 | let mut stash = CookieStash::default(); 94 | for i in 0..8_u8 { 95 | stash.store(vec![i]); 96 | assert_eq!(stash.gap(), 7 - i); 97 | } 98 | 99 | for i in 8_u8..32_u8 { 100 | assert_eq!(stash.get(), Some(vec![i - 8])); 101 | assert_eq!(stash.gap(), 1); 102 | stash.store(vec![i]); 103 | assert_eq!(stash.gap(), 0); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ntp-proto/src/identifiers.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use md5::{Digest, Md5}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | pub struct ReferenceId(u32); 8 | 9 | impl ReferenceId { 10 | // Note: Names chosen to match the identifiers given in rfc5905 11 | pub const KISS_DENY: ReferenceId = ReferenceId(u32::from_be_bytes(*b"DENY")); 12 | pub const KISS_RATE: ReferenceId = ReferenceId(u32::from_be_bytes(*b"RATE")); 13 | pub const KISS_RSTR: ReferenceId = ReferenceId(u32::from_be_bytes(*b"RSTR")); 14 | pub const NONE: ReferenceId = ReferenceId(u32::from_be_bytes(*b"XNON")); 15 | pub const SOCK: ReferenceId = ReferenceId(u32::from_be_bytes(*b"SOCK")); 16 | pub const PPS: ReferenceId = ReferenceId(u32::from_be_bytes(*b"PPS\0")); 17 | 18 | // Network Time Security (NTS) negative-acknowledgment (NAK), from rfc8915 19 | pub const KISS_NTSN: ReferenceId = ReferenceId(u32::from_be_bytes(*b"NTSN")); 20 | 21 | pub fn from_ip(addr: IpAddr) -> ReferenceId { 22 | match addr { 23 | IpAddr::V4(addr) => ReferenceId(u32::from_be_bytes(addr.octets())), 24 | IpAddr::V6(addr) => ReferenceId(u32::from_be_bytes( 25 | Md5::digest(addr.octets())[0..4].try_into().unwrap(), 26 | )), 27 | } 28 | } 29 | 30 | pub(crate) const fn from_int(value: u32) -> ReferenceId { 31 | ReferenceId(value) 32 | } 33 | 34 | pub(crate) fn is_deny(&self) -> bool { 35 | *self == Self::KISS_DENY 36 | } 37 | 38 | pub(crate) fn is_rate(&self) -> bool { 39 | *self == Self::KISS_RATE 40 | } 41 | 42 | pub(crate) fn is_rstr(&self) -> bool { 43 | *self == Self::KISS_RSTR 44 | } 45 | 46 | pub(crate) fn is_ntsn(&self) -> bool { 47 | *self == Self::KISS_NTSN 48 | } 49 | 50 | pub(crate) fn to_bytes(self) -> [u8; 4] { 51 | self.0.to_be_bytes() 52 | } 53 | 54 | pub(crate) fn from_bytes(bits: [u8; 4]) -> ReferenceId { 55 | ReferenceId(u32::from_be_bytes(bits)) 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn referenceid_serialization_roundtrip() { 65 | let a = [12, 34, 56, 78]; 66 | let b = ReferenceId::from_bytes(a); 67 | let c = b.to_bytes(); 68 | let d = ReferenceId::from_bytes(c); 69 | assert_eq!(a, c); 70 | assert_eq!(b, d); 71 | } 72 | 73 | #[test] 74 | fn referenceid_kiss_codes() { 75 | let a = [b'R', b'A', b'T', b'E']; 76 | let b = ReferenceId::from_bytes(a); 77 | assert!(b.is_rate()); 78 | 79 | let a = [b'R', b'S', b'T', b'R']; 80 | let b = ReferenceId::from_bytes(a); 81 | assert!(b.is_rstr()); 82 | 83 | let a = [b'D', b'E', b'N', b'Y']; 84 | let b = ReferenceId::from_bytes(a); 85 | assert!(b.is_deny()); 86 | } 87 | 88 | #[test] 89 | fn referenceid_from_ipv4() { 90 | let ip: IpAddr = "12.34.56.78".parse().unwrap(); 91 | let rep = [12, 34, 56, 78]; 92 | let a = ReferenceId::from_ip(ip); 93 | let b = ReferenceId::from_bytes(rep); 94 | assert_eq!(a, b); 95 | 96 | // TODO: Generate and add a testcase for ipv6 addresses once 97 | // we have access to an ipv6 network. 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ntp-proto/src/io.rs: -------------------------------------------------------------------------------- 1 | /// Write trait for structs that implement std::io::Write without doing blocking io 2 | pub trait NonBlockingWrite: std::io::Write {} 3 | 4 | impl NonBlockingWrite for std::io::Cursor where std::io::Cursor: std::io::Write {} 5 | impl NonBlockingWrite for Vec {} 6 | impl NonBlockingWrite for &mut [u8] {} 7 | impl NonBlockingWrite for std::collections::VecDeque {} 8 | impl NonBlockingWrite for Box where W: NonBlockingWrite {} 9 | impl NonBlockingWrite for &mut W where W: NonBlockingWrite {} 10 | 11 | pub trait NonBlockingRead: std::io::Read {} 12 | 13 | impl NonBlockingRead for std::io::Cursor where std::io::Cursor: std::io::Read {} 14 | impl NonBlockingRead for &[u8] {} 15 | impl NonBlockingRead for std::collections::VecDeque {} 16 | impl NonBlockingRead for Box where R: NonBlockingRead {} 17 | impl NonBlockingRead for &mut R where R: NonBlockingRead {} 18 | -------------------------------------------------------------------------------- /ntp-proto/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains packet parsing and algorithm code for ntpd-rs and is not 2 | //! intended as a public interface at this time. It follows the same version as the 3 | //! main ntpd-rs crate, but that version is not intended to give any stability 4 | //! guarantee. Use at your own risk. 5 | //! 6 | //! Please visit the [ntpd-rs](https://github.com/pendulum-project/ntpd-rs) project 7 | //! for more information. 8 | #![forbid(unsafe_code)] 9 | #![cfg_attr(not(feature = "__internal-api"), allow(unused))] 10 | 11 | mod algorithm; 12 | mod clock; 13 | mod config; 14 | mod cookiestash; 15 | mod identifiers; 16 | mod io; 17 | mod ipfilter; 18 | mod keyset; 19 | mod nts_record; 20 | mod packet; 21 | mod server; 22 | mod source; 23 | mod system; 24 | mod time_types; 25 | 26 | #[cfg(feature = "nts-pool")] 27 | mod nts_pool_ke; 28 | pub mod tls_utils; 29 | 30 | pub(crate) mod exitcode { 31 | /// An internal software error has been detected. This 32 | /// should be limited to non-operating system related 33 | /// errors as possible. 34 | #[cfg(not(test))] 35 | pub const SOFTWARE: i32 = 70; 36 | } 37 | 38 | mod exports { 39 | pub use super::algorithm::{ 40 | AlgorithmConfig, KalmanClockController, KalmanControllerMessage, KalmanSourceController, 41 | KalmanSourceMessage, ObservableSourceTimedata, SourceController, StateUpdate, 42 | TimeSyncController, TwoWayKalmanSourceController, 43 | }; 44 | pub use super::clock::NtpClock; 45 | pub use super::config::{SourceConfig, StepThreshold, SynchronizationConfig}; 46 | pub use super::identifiers::ReferenceId; 47 | #[cfg(feature = "__internal-fuzz")] 48 | pub use super::ipfilter::fuzz::fuzz_ipfilter; 49 | pub use super::keyset::{DecodedServerCookie, KeySet, KeySetProvider}; 50 | 51 | #[cfg(feature = "__internal-fuzz")] 52 | pub use super::keyset::test_cookie; 53 | #[cfg(feature = "__internal-fuzz")] 54 | pub use super::packet::ExtensionField; 55 | pub use super::packet::{ 56 | Cipher, CipherProvider, EncryptResult, ExtensionHeaderVersion, NoCipher, 57 | NtpAssociationMode, NtpLeapIndicator, NtpPacket, PacketParsingError, 58 | }; 59 | pub use super::server::{ 60 | FilterAction, FilterList, IpSubnet, Server, ServerAction, ServerConfig, ServerReason, 61 | ServerResponse, ServerStatHandler, SubnetParseError, 62 | }; 63 | #[cfg(feature = "__internal-test")] 64 | pub use super::source::source_snapshot; 65 | pub use super::source::{ 66 | AcceptSynchronizationError, Measurement, NtpSource, NtpSourceAction, 67 | NtpSourceActionIterator, NtpSourceSnapshot, NtpSourceUpdate, ObservableSourceState, 68 | OneWaySource, OneWaySourceSnapshot, OneWaySourceUpdate, ProtocolVersion, Reach, 69 | SourceNtsData, 70 | }; 71 | pub use super::system::{ 72 | System, SystemAction, SystemActionIterator, SystemSnapshot, SystemSourceUpdate, 73 | TimeSnapshot, 74 | }; 75 | 76 | #[cfg(feature = "__internal-fuzz")] 77 | pub use super::time_types::fuzz_duration_from_seconds; 78 | pub use super::time_types::{ 79 | FrequencyTolerance, NtpDuration, NtpInstant, NtpTimestamp, PollInterval, PollIntervalLimits, 80 | }; 81 | 82 | #[cfg(feature = "__internal-fuzz")] 83 | pub use super::nts_record::fuzz_key_exchange_result_decoder; 84 | #[cfg(feature = "__internal-fuzz")] 85 | pub use super::nts_record::fuzz_key_exchange_server_decoder; 86 | pub use super::nts_record::{ 87 | KeyExchangeClient, KeyExchangeError, KeyExchangeResult, KeyExchangeServer, NtpVersion, 88 | NtsRecord, NtsRecordDecoder, WriteError, 89 | }; 90 | 91 | pub use super::cookiestash::MAX_COOKIES; 92 | 93 | #[cfg(feature = "ntpv5")] 94 | pub mod v5 { 95 | pub use crate::packet::v5::server_reference_id::{BloomFilter, ServerId}; 96 | } 97 | 98 | #[cfg(feature = "nts-pool")] 99 | pub use super::nts_record::AeadAlgorithm; 100 | 101 | #[cfg(feature = "nts-pool")] 102 | pub use super::nts_pool_ke::{ 103 | ClientToPoolData, ClientToPoolDecoder, PoolToServerData, PoolToServerDecoder, 104 | SupportedAlgorithmsDecoder, 105 | }; 106 | } 107 | 108 | #[cfg(feature = "__internal-api")] 109 | pub use exports::*; 110 | 111 | #[cfg(not(feature = "__internal-api"))] 112 | pub(crate) use exports::*; 113 | -------------------------------------------------------------------------------- /ntp-proto/src/packet/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use super::NtpPacket; 4 | 5 | #[derive(Debug)] 6 | pub enum ParsingError { 7 | InvalidVersion(u8), 8 | IncorrectLength, 9 | MalformedNtsExtensionFields, 10 | MalformedNonce, 11 | MalformedCookiePlaceholder, 12 | DecryptError(T), 13 | #[cfg(feature = "ntpv5")] 14 | V5(super::v5::V5Error), 15 | } 16 | 17 | impl ParsingError { 18 | pub(super) fn get_decrypt_error(self) -> Result> { 19 | use ParsingError::*; 20 | 21 | match self { 22 | InvalidVersion(v) => Err(InvalidVersion(v)), 23 | IncorrectLength => Err(IncorrectLength), 24 | MalformedNtsExtensionFields => Err(MalformedNtsExtensionFields), 25 | MalformedNonce => Err(MalformedNonce), 26 | MalformedCookiePlaceholder => Err(MalformedCookiePlaceholder), 27 | DecryptError(decrypt_error) => Ok(decrypt_error), 28 | #[cfg(feature = "ntpv5")] 29 | V5(e) => Err(V5(e)), 30 | } 31 | } 32 | } 33 | 34 | impl ParsingError { 35 | pub(super) fn generalize(self) -> ParsingError { 36 | use ParsingError::*; 37 | 38 | match self { 39 | InvalidVersion(v) => InvalidVersion(v), 40 | IncorrectLength => IncorrectLength, 41 | MalformedNtsExtensionFields => MalformedNtsExtensionFields, 42 | MalformedNonce => MalformedNonce, 43 | MalformedCookiePlaceholder => MalformedCookiePlaceholder, 44 | DecryptError(decrypt_error) => match decrypt_error {}, 45 | #[cfg(feature = "ntpv5")] 46 | V5(e) => V5(e), 47 | } 48 | } 49 | } 50 | 51 | pub type PacketParsingError<'a> = ParsingError>; 52 | 53 | impl Display for ParsingError { 54 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 55 | match self { 56 | Self::InvalidVersion(version) => f.write_fmt(format_args!("Invalid version {version}")), 57 | Self::IncorrectLength => f.write_str("Incorrect packet length"), 58 | Self::MalformedNtsExtensionFields => f.write_str("Malformed nts extension fields"), 59 | Self::MalformedNonce => f.write_str("Malformed nonce (likely invalid length)"), 60 | Self::MalformedCookiePlaceholder => f.write_str("Malformed cookie placeholder"), 61 | Self::DecryptError(_) => f.write_str("Failed to decrypt NTS extension fields"), 62 | #[cfg(feature = "ntpv5")] 63 | Self::V5(e) => Display::fmt(e, f), 64 | } 65 | } 66 | } 67 | 68 | impl std::error::Error for ParsingError {} 69 | -------------------------------------------------------------------------------- /ntp-proto/src/packet/mac.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::io::NonBlockingWrite; 4 | 5 | use super::error::ParsingError; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub(super) struct Mac<'a> { 9 | keyid: u32, 10 | mac: Cow<'a, [u8]>, 11 | } 12 | 13 | impl<'a> Mac<'a> { 14 | // As per RFC7822: 15 | // If a MAC is used, it resides at the end of the packet. This field 16 | // can be either 24 octets long, 20 octets long, or a 4-octet 17 | // crypto-NAK. 18 | pub(super) const MAXIMUM_SIZE: usize = 24; 19 | 20 | pub(super) fn into_owned(self) -> Mac<'static> { 21 | Mac { 22 | keyid: self.keyid, 23 | mac: Cow::Owned(self.mac.into_owned()), 24 | } 25 | } 26 | 27 | pub(super) fn serialize(&self, mut w: impl NonBlockingWrite) -> std::io::Result<()> { 28 | w.write_all(&self.keyid.to_be_bytes())?; 29 | w.write_all(&self.mac) 30 | } 31 | 32 | pub(super) fn deserialize( 33 | data: &'a [u8], 34 | ) -> Result, ParsingError> { 35 | if data.len() < 4 || data.len() >= Self::MAXIMUM_SIZE { 36 | return Err(ParsingError::IncorrectLength); 37 | } 38 | 39 | Ok(Mac { 40 | keyid: u32::from_be_bytes(data[0..4].try_into().unwrap()), 41 | mac: Cow::Borrowed(&data[4..]), 42 | }) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | 50 | #[test] 51 | fn roundtrip() { 52 | let input = Mac { 53 | keyid: 42, 54 | mac: Cow::Borrowed(&[1, 2, 3, 4, 5, 6, 7, 8]), 55 | }; 56 | 57 | let input = input.to_owned(); 58 | 59 | let mut w = Vec::new(); 60 | input.serialize(&mut w).unwrap(); 61 | 62 | let output = Mac::deserialize(&w).unwrap(); 63 | 64 | assert_eq!(input, output); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ntp-proto/src/packet/v5/error.rs: -------------------------------------------------------------------------------- 1 | use crate::packet::error::ParsingError; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | #[derive(Debug)] 5 | pub enum V5Error { 6 | InvalidDraftIdentification, 7 | MalformedTimescale, 8 | MalformedMode, 9 | InvalidFlags, 10 | } 11 | 12 | impl V5Error { 13 | /// `const` alternative to `.into()` 14 | pub const fn into_parse_err(self) -> ParsingError { 15 | ParsingError::V5(self) 16 | } 17 | } 18 | 19 | impl Display for V5Error { 20 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | Self::InvalidDraftIdentification => f.write_str("Draft Identification invalid"), 23 | Self::MalformedTimescale => f.write_str("Malformed timescale"), 24 | Self::MalformedMode => f.write_str("Malformed mode"), 25 | Self::InvalidFlags => f.write_str("Invalid flags specified"), 26 | } 27 | } 28 | } 29 | 30 | impl From for crate::packet::error::ParsingError { 31 | fn from(value: V5Error) -> Self { 32 | Self::V5(value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/ec_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIB+uHkwPd9WSCTR9m1ITVFwL8UPGaKWnreDdtMBsk8c7oAoGCCqGSM49 3 | AwEHoUQDQgAEW9lR99aS5JMx8ZI5FsJPLOhfSggg+vngirYItXGB8F2y8CblgQfw 4 | PTYuxatX/a49ea2ENluguEDKcDaL2+6iHw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/end.fullchain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDsTCCApmgAwIBAgIUaNuir1ru01VEHIHC8baug66nkbQwDQYJKoZIhvcNAQEL 3 | BQAwVzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAeFw0y 5 | NTAyMjcwOTA3MTJaFw0yNjAyMjcwOTA3MTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYD 6 | VQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBM 7 | dGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDd0nhS+SWqtQMyIMuX 8 | /WMopeUH6u+WljlUEXpxZQKa3KL0+3r4byo51D8R6OF05zG3ANw4NCSMKfRBpK/+ 9 | wb/QvVFi4Ib8wmrNJI019UE/gzbnDfg5QaMvztusAHZF2wkELRjgX/DdVBWXkQ1W 10 | jA9052XyPZs+zTbVg6Am6n3GOmKoCI2n0TIY9sKG+ZCJFfE+TzQU0z7r9OnvnT0Z 11 | BZ4poOgG8GbxTCr3dRfM+tLh8MLLMOZ3CKunGaMHD4zdeDm4l7fYZ6/jXVoZrbio 12 | 2/uj9+KjhhKi+xTIBc8zug91piotN0uCuxdawgt0EtjaO4czQoLl3D/WNNSez4rl 13 | HLeVAgMBAAGjgYYwgYMwHwYDVR0jBBgwFoAUd1WulbCt/dtDVY+7a5EIrbXIbaUw 14 | CQYDVR0TBAIwADALBgNVHQ8EBAMCA6gwEwYDVR0lBAwwCgYIKwYBBQUHAwEwFAYD 15 | VR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBT18h+AQlYckl0KMVOGK14oPlsM 16 | NDANBgkqhkiG9w0BAQsFAAOCAQEAm6ZnJcT3IfF+1Y8P0NjkqG/U5CyPmEstwyFX 17 | zxnRP9ILmtQ2l+9V10KQ+STNKqyQWsfJ/+F71nKjOBzkS8cHDidVDZ1Y/3JoLByf 18 | sIbdA6XQVMLCNKDgLCJ33h8r0yDiucu1RRKQUIdMyLHnhNbPQtMpdFq8B2CjOi36 19 | tD36Ah/nAzVDCxUg2wvcc/aLqmMyZVmFqjjDtOqLlPQIJ4iiUT2MYw7avqVWq8br 20 | t0+3ezAGkMuTqjsBiTM/ov6l7OS12hFL/u+kA4lG38UuCkLoDjjG5QEu9d4q347l 21 | Y3C1v7K1YDXkXmX28sRQgAqzKVfTIRcbcvKgu4jx4EIhSjaNug== 22 | -----END CERTIFICATE----- 23 | -----BEGIN CERTIFICATE----- 24 | MIIDkTCCAnmgAwIBAgIUSJ4RLbU532cpXBrIPM0dgLjFoRowDQYJKoZIhvcNAQEL 25 | BQAwVzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 26 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAgFw0y 27 | MzAxMjAwOTQzMzdaGA80NzYwMTIxNzA5NDMzN1owVzELMAkGA1UEBhMCQVUxEzAR 28 | BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 29 | IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 30 | AQoCggEBALzqkvECUcCFlg4cldjWKD1/X2e+FPrMBesmUCDExAtGYIjJy2YFovFL 31 | 20eNFa4K3QK61MfsmnbhC97Q3Nrm2tFiDXdM1XjnnbGk/GKtTH/cS/v5FQt+8kbj 32 | YPKkxfwo02Nhgf8r0Ttsg439tuT+qpw3CymVzEZDllhYFL0EDq5JHAx9Sz5RiXm4 33 | 1+4E0ahWpWbTagiG/Ldgk/sXCTZvxsCw7gbULKSVEbaN+cW+pXqkD3YSvrnYCPtk 34 | /8OK7llBCtDC9puDIntrd5z6tIxCbj3jnfb9Ek/Pb/AmK04NF5OPw+eUgEwteSde 35 | lNInFgNnlEPikNrkDAmBydLuEX7yCO8CAwEAAaNTMFEwHQYDVR0OBBYEFHdVrpWw 36 | rf3bQ1WPu2uRCK21yG2lMB8GA1UdIwQYMBaAFHdVrpWwrf3bQ1WPu2uRCK21yG2l 37 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFHWNTDdy9BbCoX5 38 | RRvP0S4V0g8HcaWohYuI7uNsDwW/xvOsJ7u+1rjv/Hx3lOCtnEHCAS5peJQenf6Y 39 | uQSXbt2BVX7U01TzGKC9y47yxgovpdKJDiJodWSGs6sZP/4x3M5AbGmhmdfSBFAZ 40 | /fchAzZPWd5FdYBEaT5J1nnXDCe3G5Aa43zvZzN8i/YCJ376yB7Vt6qUW8L70o9X 41 | ++snpnom2bvIKwkO4Z9jBY6njrpYjE212N1OY+eYRLknOdJlFuy6kGO2ipEoPKt/ 42 | +vur95a6fTo8WiU2kYQc649XiPNW53v1epWNFJCRoOFietIVrKANWuqQB7xVYuIG 43 | Yo0A3Sw= 44 | -----END CERTIFICATE----- 45 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/end.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDd0nhS+SWqtQMy 3 | IMuX/WMopeUH6u+WljlUEXpxZQKa3KL0+3r4byo51D8R6OF05zG3ANw4NCSMKfRB 4 | pK/+wb/QvVFi4Ib8wmrNJI019UE/gzbnDfg5QaMvztusAHZF2wkELRjgX/DdVBWX 5 | kQ1WjA9052XyPZs+zTbVg6Am6n3GOmKoCI2n0TIY9sKG+ZCJFfE+TzQU0z7r9Onv 6 | nT0ZBZ4poOgG8GbxTCr3dRfM+tLh8MLLMOZ3CKunGaMHD4zdeDm4l7fYZ6/jXVoZ 7 | rbio2/uj9+KjhhKi+xTIBc8zug91piotN0uCuxdawgt0EtjaO4czQoLl3D/WNNSe 8 | z4rlHLeVAgMBAAECggEAQ3Y8sONkEMA/ahHuSVm/PAAEIT3SwuYKJmawaecx/R4o 9 | E0CeXAsW+QJzcgN0+gRMKt+AmjlFejlSN1qaSezr5NSG+X7Wnu2T5LL+nU/rGaFS 10 | 479sZCFxu1r6lRuI3OLqIZKDk82p5+4oKPHs8ArlsoSjjSIuYlGwIQyIev1q5guP 11 | sfzi2cYAHc6rySDBCtFmP9yIxwdhkYRIrDqGWGbtrGIsguyuOUjQHbkidX5FaWjs 12 | wj4ApcrYsFQcS/aeEWKqn0wjYTbm+MXXjfao5mAe+1jrPYXyvYs0GpkC3pt+rjS9 13 | XO4pohuFEnAvrxZ9JX0DEsnvUoi5C38TB2A/LRboewKBgQD0Z2ZTvTwk+fUb9IRD 14 | MRNCWuB8+eiHNZwTRx4v9ZHJQ9blfXirDV0thzV2loYvW1uI91iL4SIWLH1d/O5Z 15 | WtwrrTBtzKjiQaeIqopzJH/a1enm8vSTDuazb/n2nmfe49ZBkfgdCnBEEGu4HuZZ 16 | viPAdLTWLjimsiXOrBtzJBCFXwKBgQDoWMkoj+Lkx9ZGe3x5KqPMHI8UkEolsLXd 17 | dyC8qLLtQJKIGbJj+uk2I0cYnyTfVNi+nOu+Rt2EK187wkMid1encnxqUyDsKoBJ 18 | XGiSs9mqyc32DvGOH9X2heQ0FrnbNlaYozdW5jjXafFlV8dnpoX7wQnjMg329qDB 19 | 2zVSainTiwKBgBNSPU+vbRrLO+pa2T3qmkgroQWgSBawUUdg3u0Rr9XGbC22TpzP 20 | MKeRwdM/MRp7UXAxhamBQc2Y9MxCW6Fqwm8dgO+dN1izsgfm240gvI7TTGt6l4Us 21 | r2ZOGue5PCLtxhlm7cN1+MwYtDtZDgLYOkFTuJwaCVZ8TOraxkzC9B9nAoGATPbk 22 | I4COKzSbIRvUnppmSb2IE8q8FQIVLDhC6tuC8Z47K8Q/WGkMCXfkHB7TavtDFNkM 23 | Kip1REvNrxDphig8K+Z7mgjRVgm6FxL6POZAixdwFzrZ/zdCe/fcIPkKNbgpNUST 24 | l0CJwamBYg2Sqx35Mey+5rh08cK+e5iucA9krYMCgYBe+4fUD9GC+GUjnsyzkjDT 25 | oNKbsFMcYrjLld9nQdeX2oViWudqU7cGlk3mwpWyX81UkEc1LRcnE3/FSdd4Pj1t 26 | xMUjq+fMXF2Au4w1NTZ4Exp6Vk8mz9gmsNuFfLKOKnr8x/MT89kYr7+959WlB0jE 27 | 0ayKuXuHxFpY1rDFSJabaw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/end.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDsTCCApmgAwIBAgIUaNuir1ru01VEHIHC8baug66nkbQwDQYJKoZIhvcNAQEL 3 | BQAwVzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAeFw0y 5 | NTAyMjcwOTA3MTJaFw0yNjAyMjcwOTA3MTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYD 6 | VQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBM 7 | dGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDd0nhS+SWqtQMyIMuX 8 | /WMopeUH6u+WljlUEXpxZQKa3KL0+3r4byo51D8R6OF05zG3ANw4NCSMKfRBpK/+ 9 | wb/QvVFi4Ib8wmrNJI019UE/gzbnDfg5QaMvztusAHZF2wkELRjgX/DdVBWXkQ1W 10 | jA9052XyPZs+zTbVg6Am6n3GOmKoCI2n0TIY9sKG+ZCJFfE+TzQU0z7r9OnvnT0Z 11 | BZ4poOgG8GbxTCr3dRfM+tLh8MLLMOZ3CKunGaMHD4zdeDm4l7fYZ6/jXVoZrbio 12 | 2/uj9+KjhhKi+xTIBc8zug91piotN0uCuxdawgt0EtjaO4czQoLl3D/WNNSez4rl 13 | HLeVAgMBAAGjgYYwgYMwHwYDVR0jBBgwFoAUd1WulbCt/dtDVY+7a5EIrbXIbaUw 14 | CQYDVR0TBAIwADALBgNVHQ8EBAMCA6gwEwYDVR0lBAwwCgYIKwYBBQUHAwEwFAYD 15 | VR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBT18h+AQlYckl0KMVOGK14oPlsM 16 | NDANBgkqhkiG9w0BAQsFAAOCAQEAm6ZnJcT3IfF+1Y8P0NjkqG/U5CyPmEstwyFX 17 | zxnRP9ILmtQ2l+9V10KQ+STNKqyQWsfJ/+F71nKjOBzkS8cHDidVDZ1Y/3JoLByf 18 | sIbdA6XQVMLCNKDgLCJ33h8r0yDiucu1RRKQUIdMyLHnhNbPQtMpdFq8B2CjOi36 19 | tD36Ah/nAzVDCxUg2wvcc/aLqmMyZVmFqjjDtOqLlPQIJ4iiUT2MYw7avqVWq8br 20 | t0+3ezAGkMuTqjsBiTM/ov6l7OS12hFL/u+kA4lG38UuCkLoDjjG5QEu9d4q347l 21 | Y3C1v7K1YDXkXmX28sRQgAqzKVfTIRcbcvKgu4jx4EIhSjaNug== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/gen-cert.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # This script generates a private key/certificate for a server, and signs it with the provided CA key 4 | # based on https://docs.ntpd-rs.pendulum-project.org/development/ca/ 5 | 6 | # Because this script generate keys without passwords set, they should only be used in a development setting. 7 | 8 | if [ -z "$1" ]; then 9 | echo "usage: gen-cert.sh name-of-server [ca-name] [filename]" 10 | echo 11 | echo "This will generate a name-of-server.key, name-of-server.pem and name-of-server.chain.pem file" 12 | echo "containing the private key, public certificate, and full certificate chain (respectively)" 13 | echo 14 | echo "The second argument denotes the name of the CA be used (found in the files ca-name.key and ca-name.pem)" 15 | echo "If this is omitted, the name 'testca' will be used." 16 | exit 17 | fi 18 | 19 | NAME="${1:-ntpd-rs.test}" 20 | CA="${2:-testca}" 21 | FILENAME="${3:-$NAME}" 22 | 23 | # generate a key 24 | openssl genrsa -out "$FILENAME".key 2048 25 | 26 | # generate a certificate signing request 27 | openssl req -batch -new -key "$FILENAME".key -out "$FILENAME".csr 28 | 29 | # generate an ext file 30 | cat >> "$FILENAME".ext < "$FILENAME".fullchain.pem 46 | 47 | # cleanup 48 | rm "$FILENAME".csr 49 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/pkcs8_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDi/ejEuJATtM3Y1u 3 | zzdOIYXvP0FoKUDD2b0dJD+A1PChRANCAAQVage65def6DD2jTzZ7hu+sNaw9zeQ 4 | SbSlApUWht98YHRhVM/hyN3lJ0or0qVyjcW49uSzHyuDm2BtwlcLQjOh 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/rsa_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC1Dt8tFmGS76ciuNXvk/QRrV8wCcArWxvl7Ku0aSQXgcFBAav6 3 | P5RD8b+dC9DihSu/r+6OOfjsAZ6oKCq3OTUfmoUhLpoBomxPczJgLyyLD+nQkp5q 4 | B1Q3WB6ACL/HJRRjJEIn7lc5u1FVBGbiCAHKMiaP4BDSym8oqimKC6uiaQIDAQAB 5 | AoGAGKmY7sxQqDIqwwkIYyT1Jv9FqwZ4/a7gYvZVATMdLnKHP3KZ2XGVoZepcRvt 6 | 7R0Us3ykcw0kgglKcj9eaizJtnSuoDPPwt53mDypPN2sU3hZgyk2tPgr49DB3MIp 7 | fjoqw4RL/p60ksgGXbDEqBuXqOtH5i61khWlMj+BWL9VDq0CQQDaELWPQGjgs+7X 8 | /QyWMJwOF4FXE4jecH/CcPVDB9K1ukllyC1HqTNe44Sp2bIDuSXXWb8yEixrEWBE 9 | ci2CSSjXAkEA1I4W9IzwEmAeLtL6VBip9ks52O0JKu373/Xv1F2GYdhnQaFw7IC6 10 | 1lSzcYMKGTmDuM8Cj26caldyv19Q0SPmvwJAdRHjZzS9GWWAJJTF3Rvbq/USix0B 11 | renXrRvXkFTy2n1YSjxdkstTuO2Mm2M0HquXlTWpX8hB8HkzpYtmwztjoQJAECKl 12 | LXVReCOhxu4vIJkqtc6qGoSL8J1WRH8X8KgU3nKeDAZkWx++jyyo3pIS/y01iZ71 13 | U8wSxaPTyyFCMk4mYwJBALjg7g8yDy1Lg9GFfOZvAVzPjqD28jZh/VJsDz9IhYoG 14 | z89iHWHkllOisbOm+SeynVC8CoFXmJPc26U65GcjI18= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/testca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC86pLxAlHAhZYO 3 | HJXY1ig9f19nvhT6zAXrJlAgxMQLRmCIyctmBaLxS9tHjRWuCt0CutTH7Jp24Qve 4 | 0Nza5trRYg13TNV4552xpPxirUx/3Ev7+RULfvJG42DypMX8KNNjYYH/K9E7bION 5 | /bbk/qqcNwsplcxGQ5ZYWBS9BA6uSRwMfUs+UYl5uNfuBNGoVqVm02oIhvy3YJP7 6 | Fwk2b8bAsO4G1CyklRG2jfnFvqV6pA92Er652Aj7ZP/Diu5ZQQrQwvabgyJ7a3ec 7 | +rSMQm494532/RJPz2/wJitODReTj8PnlIBMLXknXpTSJxYDZ5RD4pDa5AwJgcnS 8 | 7hF+8gjvAgMBAAECggEAAZrFvgbSoSHLqN7lSP7ZLtfkTwpuA7RZeIUQNQmgGW0P 9 | 3BFQZA0v8kaImiM8gdb2TC7dKJSGBKImQTW4CXmejxSX7l1H7bsYWHBgHKsYifQw 10 | q95QccSuZHJ0zYIGtcMA8e2Zk4Qa/GVzbT7+0QMb1IKuh+mRrbN9hLWsXJTTuYvf 11 | GppDVqMdDPy5NibudiZPKdpnMyDCJ/Wxl1+1PX18anifzBHw/G8ZPnLU3OKDqL2T 12 | OtEivvk9ZFDiRKKEsHksr+aLcUGhXFswk0zEQJwMj6rFwcDEExTQkMar+xaxshpf 13 | qo6AC88SDT9qEffSHHGJzTi73NIGgLNPO1aON4/pwQKBgQDUPo+ZJymo9IunaXWi 14 | HywqLLVZJSvqo2x9SrlqqYe3Yz0bROGBoHSMaGQzxiDApeOabdyg24wrU1P24jrC 15 | jPt94TWdu8bZKAkZAGOUPvdSGA/5yQkxVSMUK5zZwQxyLWfb77+B+WSvzhxI17Bt 16 | bX6od5pcdFSC5OczJ64DjLeHlQKBgQDj3NjsbLnxFu88A121kPD4AdpoMAtgrA5R 17 | AWwc7mWzKvL1RZlZCn861QMaRoUThQW4+dxTdoOoL68PXK3L8zuU3imKOBOe33lh 18 | j7B+M0gjdWnkcTag5q56qk1VA4YZ0R30LhUw44JxFHXhtuTR00CattI1pOQr6OdK 19 | By3kj4NdcwKBgQChOxko1eg+0e6Y8XMMAjQxoZ7tpmAzMYxDrZUm4rwXYsrTwUKx 20 | jyuaUd70uai90AcTlCuLAtz7OKTLIlZS3nhZytBJD5Fh+5jVpkb/IcoNUfwo20Ah 21 | erRYKT1Q6ebDgZypJfpMCSEksCUqbLc4mXojDiBz5WchvDOp15XIWog89QKBgE3c 22 | Vxtig58IETNWixzRrCVyrKjRUfH0mOfBLqosJAA2+tIouB+e4J6/ztGZqztiRvRQ 23 | HKNAafh8YrtDFfgM4x0ZVORwCPROtHFL4ikdaNcE9ewja2FLse8kZkxYaehEdpHL 24 | dV5BP39YWHeKQWIZZ4f2VJoUAAupB+9ZyKrDB0ZVAoGBALJ0KzHlAizbZyXRtfk+ 25 | ThnegTgjbTd6drMTsRlyHdL1Zet0tdx2nhn2keMQVSDep5KEwTvm+Wy41s9EmzZx 26 | RyehNaq9hMljLGR6mtr4Em5RtxtkPTwoJcOttHXQXnTgplDbePb8zQ8N084fScek 27 | 0dIjCbVBt5X7akmgHaaizIDl 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/testca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkTCCAnmgAwIBAgIUSJ4RLbU532cpXBrIPM0dgLjFoRowDQYJKoZIhvcNAQEL 3 | BQAwVzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAgFw0y 5 | MzAxMjAwOTQzMzdaGA80NzYwMTIxNzA5NDMzN1owVzELMAkGA1UEBhMCQVUxEzAR 6 | BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 7 | IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 8 | AQoCggEBALzqkvECUcCFlg4cldjWKD1/X2e+FPrMBesmUCDExAtGYIjJy2YFovFL 9 | 20eNFa4K3QK61MfsmnbhC97Q3Nrm2tFiDXdM1XjnnbGk/GKtTH/cS/v5FQt+8kbj 10 | YPKkxfwo02Nhgf8r0Ttsg439tuT+qpw3CymVzEZDllhYFL0EDq5JHAx9Sz5RiXm4 11 | 1+4E0ahWpWbTagiG/Ldgk/sXCTZvxsCw7gbULKSVEbaN+cW+pXqkD3YSvrnYCPtk 12 | /8OK7llBCtDC9puDIntrd5z6tIxCbj3jnfb9Ek/Pb/AmK04NF5OPw+eUgEwteSde 13 | lNInFgNnlEPikNrkDAmBydLuEX7yCO8CAwEAAaNTMFEwHQYDVR0OBBYEFHdVrpWw 14 | rf3bQ1WPu2uRCK21yG2lMB8GA1UdIwQYMBaAFHdVrpWwrf3bQ1WPu2uRCK21yG2l 15 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFHWNTDdy9BbCoX5 16 | RRvP0S4V0g8HcaWohYuI7uNsDwW/xvOsJ7u+1rjv/Hx3lOCtnEHCAS5peJQenf6Y 17 | uQSXbt2BVX7U01TzGKC9y47yxgovpdKJDiJodWSGs6sZP/4x3M5AbGmhmdfSBFAZ 18 | /fchAzZPWd5FdYBEaT5J1nnXDCe3G5Aa43zvZzN8i/YCJ376yB7Vt6qUW8L70o9X 19 | ++snpnom2bvIKwkO4Z9jBY6njrpYjE212N1OY+eYRLknOdJlFuy6kGO2ipEoPKt/ 20 | +vur95a6fTo8WiU2kYQc649XiPNW53v1epWNFJCRoOFietIVrKANWuqQB7xVYuIG 21 | Yo0A3Sw= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/unsafe.nts.client.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | observation-path = "/var/run/ntpd-rs/observe" 5 | 6 | # uses an unsecure certificate! 7 | [[source]] 8 | mode = "nts" 9 | address = "localhost:4460" 10 | certificate-authority = "ntp-proto/test-keys/testca.pem" 11 | 12 | # System parameters used in filtering and steering the clock: 13 | [synchronization] 14 | minimum-agreeing-sources = 1 15 | single-step-panic-threshold = 10 16 | startup-step-panic-threshold = { forward = "inf", backward = 86400 } 17 | -------------------------------------------------------------------------------- /ntp-proto/test-keys/unsafe.nts.server.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | observation-path = "/var/run/ntpd-rs/observe" 5 | 6 | # the server will get its time from the NTP pool 7 | [[source]] 8 | mode = "pool" 9 | address = "pool.ntp.org" 10 | count = 4 11 | 12 | [[server]] 13 | listen = "0.0.0.0:123" 14 | 15 | # System parameters used in filtering and steering the clock: 16 | [synchronization] 17 | minimum-agreeing-sources = 1 18 | single-step-panic-threshold = 10 19 | startup-step-panic-threshold = { forward = 0, backward = 86400 } 20 | 21 | # to function as an NTS server, we must also provide key exchange 22 | # uses an unsecure certificate chain! 23 | [[nts-ke-server]] 24 | listen = "0.0.0.0:4460" 25 | certificate-chain-path = "ntp-proto/test-keys/end.fullchain.pem" 26 | private-key-path = "ntp-proto/test-keys/end.key" 27 | key-exchange-timeout-ms = 1000 28 | -------------------------------------------------------------------------------- /ntp.server.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | observation-path = "/var/run/ntpd-rs/observe" 5 | 6 | # Pool servers from ntppool.org. See http://www.pool.ntp.org/join.html 7 | # for more information 8 | [[source]] 9 | mode = "pool" 10 | address = "ntpd-rs.pool.ntp.org" 11 | count = 4 12 | 13 | # Alternative configuration for IPv6 only machines 14 | #[[source]] 15 | #mode = "pool" 16 | #address = "2.pool.ntp.org" 17 | #count = 4 18 | 19 | # Serve NTP on any interface (requires permissions to use udp port 123) 20 | [[server]] 21 | listen = "[::]:123" 22 | 23 | # Below are configured various thresholds beyond which ntpd-rs will not 24 | # change the system clock. CHANGE THESE TO MATCH YOUR SECURITY NEEDS! 25 | # For guidance, see OPERATIONAL_CONSIDERATIONS.md 26 | [synchronization] 27 | single-step-panic-threshold = 1800 28 | startup-step-panic-threshold = { forward="inf", backward = 1800 } 29 | #accumulated-threshold = 1800 30 | #minimum-agreeing-sources = 3 31 | 32 | [keyset] 33 | key-storage-path="/path/to/store/key/material" 34 | -------------------------------------------------------------------------------- /ntp.toml: -------------------------------------------------------------------------------- 1 | [observability] 2 | # Other values include trace, debug, warn and error 3 | log-level = "info" 4 | observation-path = "/var/run/ntpd-rs/observe" 5 | 6 | # Pool servers from ntppool.org. See http://www.pool.ntp.org/join.html 7 | # for more information 8 | [[source]] 9 | mode = "pool" 10 | address = "ntpd-rs.pool.ntp.org" 11 | count = 16 12 | 13 | # Alternative configuration for IPv6 only machines 14 | #[[source]] 15 | #mode = "pool" 16 | #address = "2.pool.ntp.org" 17 | #count = 4 18 | 19 | # Below are configured various thresholds beyond which ntpd-rs will not 20 | # change the system clock. CHANGE THESE TO MATCH YOUR SECURITY NEEDS! 21 | # For guidance, see OPERATIONAL_CONSIDERATIONS.md 22 | [synchronization] 23 | single-step-panic-threshold = 1800 24 | startup-step-panic-threshold = { forward="inf", backward = 1800 } 25 | #accumulated-step-panic-threshold = 1800 26 | #minimum-agreeing-sources = 3 27 | -------------------------------------------------------------------------------- /ntpd/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /ntpd/COPYRIGHT: -------------------------------------------------------------------------------- 1 | ../COPYRIGHT -------------------------------------------------------------------------------- /ntpd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ntpd" 3 | readme = "README.md" 4 | description.workspace = true 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | build = "build.rs" 13 | 14 | [dependencies] 15 | ntp-proto.workspace = true 16 | 17 | tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "io-std", "fs", "sync", "net", "macros"] } 18 | tracing.workspace = true 19 | tracing-subscriber.workspace = true 20 | toml.workspace = true 21 | rand.workspace = true 22 | libc.workspace = true 23 | async-trait.workspace = true 24 | timestamped-socket.workspace = true 25 | clock-steering.workspace = true 26 | pps-time = { workspace = true, optional = true } 27 | 28 | serde.workspace = true 29 | serde_json.workspace = true 30 | 31 | rustls23.workspace = true 32 | 33 | [dev-dependencies] 34 | ntp-proto = { workspace = true, features = ["__internal-test",] } 35 | tokio-rustls.workspace = true 36 | 37 | [features] 38 | default = [ "pps" ] 39 | hardware-timestamping = [] 40 | unstable_ntpv5 = ["ntp-proto/ntpv5"] 41 | unstable_nts-pool = ["ntp-proto/nts-pool"] 42 | pps = [ "dep:pps-time" ] 43 | 44 | [lib] 45 | name = "ntpd" 46 | path = "src/lib.rs" 47 | 48 | [[bin]] 49 | name = "ntp-daemon" 50 | path = "bin/ntp-daemon.rs" 51 | 52 | [[bin]] 53 | name = "ntp-ctl" 54 | path = "bin/ntp-ctl.rs" 55 | 56 | [[bin]] 57 | name = "ntp-metrics-exporter" 58 | path = "bin/ntp-metrics-exporter.rs" 59 | 60 | [package.metadata.deb] 61 | name = "ntpd-rs" 62 | priority = "optional" 63 | section = "net" 64 | copyright = "Copyright (c) 2022-2024 Trifecta Tech Foundation, Tweede Golf, and Contributors" 65 | license-file = "../pkg/deb/COPYRIGHT-debian" 66 | maintainer = "NTPD-rs Maintainers " 67 | maintainer-scripts = "../pkg/deb/" 68 | assets = [ 69 | ["target/release/ntp-daemon", "/usr/bin/ntp-daemon", "755"], 70 | ["target/release/ntp-ctl", "/usr/bin/ntp-ctl", "755"], 71 | ["target/release/ntp-metrics-exporter", "/usr/bin/ntp-metrics-exporter", "755"], 72 | ["docs/precompiled/man/ntp-ctl.8", "/usr/share/man/man8/ntp-ctl.8", "644"], 73 | ["docs/precompiled/man/ntp-daemon.8", "/usr/share/man/man8/ntp-daemon.8", "644"], 74 | ["docs/precompiled/man/ntp-metrics-exporter.8", "/usr/share/man/man8/ntp-metrics-exporter.8", "644"], 75 | ["docs/precompiled/man/ntp.toml.5", "/usr/share/man/man5/ntp.toml.5", "644"], 76 | ["docs/examples/conf/ntp.toml.default", "/usr/share/doc/ntpd-rs/ntp.toml.default", "644"], 77 | ["docs/examples/conf/ntp.toml.default", "/etc/ntpd-rs/ntp.toml", "644"], 78 | ["docs/examples/conf/ntpd-rs.preset", "/lib/systemd/system-preset/50-ntpd-rs.preset", "644"], 79 | ["docs/examples/conf/ntpd-rs.service", "/lib/systemd/system/ntpd-rs.service", "644"], 80 | ["docs/examples/conf/ntpd-rs-metrics.service", "/lib/systemd/system/ntpd-rs-metrics.service", "644"], 81 | ["../COPYRIGHT", "/usr/share/doc/ntpd-rs/COPYRIGHT", "644"], 82 | ["../LICENSE-APACHE", "/usr/share/doc/ntpd-rs/LICENSE-APACHE", "644"], 83 | ["../LICENSE-MIT", "/usr/share/doc/ntpd-rs/LICENSE-MIT", "644"], 84 | ["../CHANGELOG.md", "/usr/share/doc/ntpd-rs/CHANGELOG.md", "644"], 85 | ["../README.md", "/usr/share/doc/ntpd-rs/README.md", "644"], 86 | ] 87 | conf-files = [ 88 | "/etc/ntpd-rs/ntp.toml", 89 | ] 90 | provides = "time-daemon" 91 | conflicts = "time-daemon" 92 | replaces = "time-daemon" 93 | 94 | [package.metadata.generate-rpm] 95 | name = "ntpd-rs" 96 | # See: https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses 97 | license = "MIT or ASL 2.0" 98 | assets = [ 99 | { source = "target/release/ntp-daemon", dest = "/usr/bin/ntp-daemon", mode = "755" }, 100 | { source = "target/release/ntp-ctl", dest = "/usr/bin/ntp-ctl", mode = "755" }, 101 | { source = "target/release/ntp-metrics-exporter", dest = "/usr/bin/ntp-metrics-exporter", mode = "755" }, 102 | { source = "docs/precompiled/man/ntp-ctl.8", dest = "/usr/share/man/man8/ntp-ctl.8", mode = "644", doc = true }, 103 | { source = "docs/precompiled/man/ntp-daemon.8", dest = "/usr/share/man/man8/ntp-daemon.8", mode = "644", doc = true }, 104 | { source = "docs/precompiled/man/ntp-metrics-exporter.8", dest = "/usr/share/man/man8/ntp-metrics-exporter.8", mode = "644", doc = true }, 105 | { source = "docs/precompiled/man/ntp.toml.5", dest = "/usr/share/man/man5/ntp-toml.5", mode = "644", doc = true }, 106 | { source = "docs/examples/conf/ntp.toml.default", dest = "/usr/share/doc/ntpd-rs/ntp.toml.default", mode = "644", doc = true }, 107 | { source = "docs/examples/conf/ntp.toml.default", dest = "/etc/ntpd-rs/ntp.toml", mode = "644", config = true }, 108 | { source = "docs/examples/conf/ntpd-rs.service", dest = "/lib/systemd/system/ntpd-rs.service", mode = "644" }, 109 | { source = "docs/examples/conf/ntpd-rs-metrics.service", dest = "/lib/systemd/system/ntpd-rs-metrics.service", mode = "644" }, 110 | { source = "docs/examples/conf/ntpd-rs.preset", dest = "/lib/systemd/system-preset/50-ntpd-rs.preset", mode = "644" }, 111 | { source = "../COPYRIGHT", dest = "/usr/share/doc/ntpd-rs/COPYRIGHT", mode = "644", doc = true }, 112 | { source = "../LICENSE-APACHE", dest = "/usr/share/doc/ntpd-rs/LICENSE-APACHE", mode = "644", doc = true }, 113 | { source = "../LICENSE-MIT", dest = "/usr/share/doc/ntpd-rs/LICENSE-MIT", mode = "644", doc = true }, 114 | { source = "../CHANGELOG.md", dest = "/usr/share/doc/ntpd-rs/CHANGELOG.md", mode = "644", doc = true }, 115 | { source = "../README.md", dest = "/usr/share/doc/ntpd-rs/README.md", mode = "644", doc = true }, 116 | ] 117 | -------------------------------------------------------------------------------- /ntpd/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /ntpd/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /ntpd/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /ntpd/bin/ntp-ctl.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | fn main() -> std::io::Result { 4 | ntpd::ctl_main() 5 | } 6 | -------------------------------------------------------------------------------- /ntpd/bin/ntp-daemon.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use std::process; 4 | 5 | fn main() { 6 | let result = ntpd::daemon_main(); 7 | process::exit(if result.is_ok() { 0 } else { 1 }); 8 | } 9 | -------------------------------------------------------------------------------- /ntpd/bin/ntp-metrics-exporter.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | fn main() -> Result<(), Box> { 4 | ntpd::metrics_exporter_main() 5 | } 6 | -------------------------------------------------------------------------------- /ntpd/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, ExitStatus}; 2 | 3 | fn main() { 4 | // check if the repository is dirty (if there is any) 5 | let is_dirty = if let Ok(status) = Command::new("git") 6 | .args(["diff-index", "--quiet", "HEAD", "--"]) 7 | .status() 8 | { 9 | !status.success() 10 | } else { 11 | false 12 | }; 13 | 14 | // use environment variable for the git commit rev if set 15 | let git_rev = std::env::var("NTPD_RS_GIT_REV").ok(); 16 | 17 | // allow usage of the GITHUB_SHA environment variable during CI 18 | let git_rev = if let Some(gr) = git_rev { 19 | Some(gr) 20 | } else { 21 | std::env::var("GITHUB_SHA").ok() 22 | }; 23 | 24 | // determine the git commit (if there is any) 25 | let git_rev = if let Some(gr) = git_rev { 26 | Some(gr) 27 | } else { 28 | run_command_out("git", &["rev-parse", "HEAD"]) 29 | .ok() 30 | .map(|rev| { 31 | if is_dirty { 32 | format!("{}-dirty", rev) 33 | } else { 34 | rev 35 | } 36 | }) 37 | }; 38 | 39 | // use environment variable for the git commit date if set 40 | let git_date = std::env::var("NTPD_RS_GIT_DATE").ok(); 41 | 42 | // determine the date of the git commit (if there is any) 43 | let git_date = if let Some(gd) = git_date { 44 | Some(gd) 45 | } else if let Some(hash) = &git_rev { 46 | if is_dirty { 47 | run_command_out("date", &["-u", "+%Y-%m-%d"]).ok() 48 | } else { 49 | run_command_out( 50 | "git", 51 | &[ 52 | "show", 53 | "-s", 54 | "--date=format:%Y-%m-%d", 55 | "--format=%cd", 56 | hash, 57 | "--", 58 | ], 59 | ) 60 | .ok() 61 | } 62 | } else { 63 | None 64 | }; 65 | 66 | println!( 67 | "cargo:rustc-env=NTPD_RS_GIT_REV={}", 68 | git_rev.unwrap_or("-".to_owned()) 69 | ); 70 | println!( 71 | "cargo:rustc-env=NTPD_RS_GIT_DATE={}", 72 | git_date.unwrap_or("-".to_owned()) 73 | ); 74 | println!("cargo:rustc-rerun-if-changed=.git/HEAD"); 75 | } 76 | 77 | fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(String, ExitStatus)> { 78 | let res = Command::new(cmd).args(args).output()?; 79 | match String::from_utf8(res.stdout) { 80 | Ok(data) => Ok((data.trim().to_owned(), res.status)), 81 | Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)), 82 | } 83 | } 84 | 85 | fn run_command_out(cmd: &str, args: &[&str]) -> std::io::Result { 86 | run_command(cmd, args).map(|(out, _)| out) 87 | } 88 | -------------------------------------------------------------------------------- /ntpd/docs: -------------------------------------------------------------------------------- 1 | ../docs -------------------------------------------------------------------------------- /ntpd/src/daemon/clock.rs: -------------------------------------------------------------------------------- 1 | use clock_steering::{unix::UnixClock, Clock, TimeOffset}; 2 | use ntp_proto::NtpClock; 3 | 4 | use super::util::convert_clock_timestamp; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct NtpClockWrapper(UnixClock); 8 | 9 | impl NtpClockWrapper { 10 | pub fn new(clock: UnixClock) -> Self { 11 | NtpClockWrapper(clock) 12 | } 13 | } 14 | 15 | impl Default for NtpClockWrapper { 16 | fn default() -> Self { 17 | NtpClockWrapper(UnixClock::CLOCK_REALTIME) 18 | } 19 | } 20 | 21 | impl NtpClock for NtpClockWrapper { 22 | type Error = ::Error; 23 | 24 | fn now(&self) -> Result { 25 | self.0.now().map(convert_clock_timestamp) 26 | } 27 | 28 | fn set_frequency(&self, freq: f64) -> Result { 29 | self.0 30 | .set_frequency(freq * 1e6) 31 | .map(convert_clock_timestamp) 32 | } 33 | 34 | fn get_frequency(&self) -> Result { 35 | self.0.get_frequency().map(|v| v * 1e-6) 36 | } 37 | 38 | fn step_clock( 39 | &self, 40 | offset: ntp_proto::NtpDuration, 41 | ) -> Result { 42 | let (seconds, nanos) = offset.as_seconds_nanos(); 43 | self.0 44 | .step_clock(TimeOffset { 45 | seconds: seconds as _, 46 | nanos, 47 | }) 48 | .map(convert_clock_timestamp) 49 | } 50 | 51 | fn disable_ntp_algorithm(&self) -> Result<(), Self::Error> { 52 | self.0.disable_kernel_ntp_algorithm() 53 | } 54 | 55 | fn error_estimate_update( 56 | &self, 57 | est_error: ntp_proto::NtpDuration, 58 | max_error: ntp_proto::NtpDuration, 59 | ) -> Result<(), Self::Error> { 60 | self.0.error_estimate_update( 61 | core::time::Duration::from_secs_f64(est_error.to_seconds()), 62 | core::time::Duration::from_secs_f64(max_error.to_seconds()), 63 | ) 64 | } 65 | 66 | fn status_update(&self, leap_status: ntp_proto::NtpLeapIndicator) -> Result<(), Self::Error> { 67 | self.0.set_leap_seconds(match leap_status { 68 | ntp_proto::NtpLeapIndicator::NoWarning => clock_steering::LeapIndicator::NoWarning, 69 | ntp_proto::NtpLeapIndicator::Leap61 => clock_steering::LeapIndicator::Leap61, 70 | ntp_proto::NtpLeapIndicator::Leap59 => clock_steering::LeapIndicator::Leap59, 71 | ntp_proto::NtpLeapIndicator::Unknown => clock_steering::LeapIndicator::Unknown, 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ntpd/src/daemon/config/subnet.rs: -------------------------------------------------------------------------------- 1 | use serde::{de, Deserialize, Deserializer}; 2 | use std::{ 3 | fmt::Display, 4 | net::{AddrParseError, IpAddr}, 5 | }; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub struct IpSubnet { 9 | pub addr: IpAddr, 10 | pub mask: u8, 11 | } 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub enum SubnetParseError { 15 | Subnet, 16 | Ip(AddrParseError), 17 | Mask, 18 | } 19 | 20 | impl std::error::Error for SubnetParseError {} 21 | 22 | impl Display for SubnetParseError { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | match self { 25 | Self::Subnet => write!(f, "Invalid subnet syntax"), 26 | Self::Ip(e) => write!(f, "{e} in subnet"), 27 | Self::Mask => write!(f, "Invalid subnet mask"), 28 | } 29 | } 30 | } 31 | 32 | impl From for SubnetParseError { 33 | fn from(value: AddrParseError) -> Self { 34 | Self::Ip(value) 35 | } 36 | } 37 | 38 | impl std::str::FromStr for IpSubnet { 39 | type Err = SubnetParseError; 40 | 41 | fn from_str(s: &str) -> Result { 42 | let (addr, mask) = s.split_once('/').ok_or(SubnetParseError::Subnet)?; 43 | let addr: IpAddr = addr.parse()?; 44 | let mask: u8 = mask.parse().map_err(|_| SubnetParseError::Mask)?; 45 | let max_mask = match addr { 46 | IpAddr::V4(_) => 32, 47 | IpAddr::V6(_) => 128, 48 | }; 49 | if mask > max_mask { 50 | return Err(SubnetParseError::Mask); 51 | } 52 | Ok(IpSubnet { addr, mask }) 53 | } 54 | } 55 | 56 | impl<'de> Deserialize<'de> for IpSubnet { 57 | fn deserialize(deserializer: D) -> Result 58 | where 59 | D: Deserializer<'de>, 60 | { 61 | let s = String::deserialize(deserializer)?; 62 | std::str::FromStr::from_str(&s).map_err(de::Error::custom) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn test_subnet_parsing() { 72 | let a = "0.0.0.0/0".parse::().unwrap(); 73 | assert_eq!(a.mask, 0); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ntpd/src/daemon/local_ip_provider.rs: -------------------------------------------------------------------------------- 1 | use std::{net::IpAddr, sync::Arc}; 2 | 3 | use timestamped_socket::interface::{interfaces, ChangeDetector}; 4 | use tokio::sync::watch; 5 | 6 | pub fn spawn() -> std::io::Result>> { 7 | let mut change_listener = ChangeDetector::new()?; 8 | let local_ips: Arc<[IpAddr]> = interfaces()? 9 | .iter() 10 | .flat_map(|(_, interface)| interface.ips()) 11 | .collect(); 12 | 13 | let (writer, reader) = watch::channel(local_ips); 14 | 15 | tokio::spawn(async move { 16 | loop { 17 | change_listener.wait_for_change().await; 18 | match interfaces() { 19 | Ok(interfaces) => { 20 | let _ = writer.send( 21 | interfaces 22 | .iter() 23 | .flat_map(|(_, interface)| interface.ips()) 24 | .collect(), 25 | ); 26 | } 27 | Err(e) => { 28 | tracing::warn!("Could not get new list of which ip addresses the interfaces in the system have: {}", e); 29 | } 30 | } 31 | } 32 | }); 33 | 34 | Ok(reader) 35 | } 36 | -------------------------------------------------------------------------------- /ntpd/src/daemon/mod.rs: -------------------------------------------------------------------------------- 1 | mod clock; 2 | pub mod config; 3 | pub mod keyexchange; 4 | mod local_ip_provider; 5 | mod ntp_source; 6 | pub mod nts_key_provider; 7 | pub mod observer; 8 | #[cfg(feature = "pps")] 9 | mod pps_source; 10 | mod server; 11 | mod sock_source; 12 | pub mod sockets; 13 | pub mod spawn; 14 | mod system; 15 | pub mod tracing; 16 | mod util; 17 | 18 | use std::{error::Error, path::PathBuf}; 19 | 20 | use ::tracing::info; 21 | pub use config::Config; 22 | use ntp_proto::KalmanClockController; 23 | pub use observer::ObservableState; 24 | pub use system::spawn; 25 | use tokio::runtime::Builder; 26 | use tracing_subscriber::util::SubscriberInitExt; 27 | 28 | use config::NtpDaemonOptions; 29 | 30 | use self::tracing::LogLevel; 31 | 32 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 33 | 34 | pub fn main() -> Result<(), Box> { 35 | let options = NtpDaemonOptions::try_parse_from(std::env::args())?; 36 | 37 | match options.action { 38 | config::NtpDaemonAction::Help => { 39 | println!("{}", config::long_help_message()); 40 | } 41 | config::NtpDaemonAction::Version => { 42 | eprintln!("ntp-daemon {VERSION}"); 43 | } 44 | config::NtpDaemonAction::Run => run(options)?, 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | // initializes the logger so that logs during config parsing are reported. Then it overrides the 51 | // log level based on the config if required. 52 | pub(crate) fn initialize_logging_parse_config( 53 | initial_log_level: Option, 54 | config_path: Option, 55 | ) -> Config { 56 | let mut log_level = initial_log_level.unwrap_or_default(); 57 | 58 | let config_tracing = crate::daemon::tracing::tracing_init(log_level, true); 59 | let config = ::tracing::subscriber::with_default(config_tracing, || { 60 | match Config::from_args(config_path, vec![], vec![]) { 61 | Ok(c) => c, 62 | Err(e) => { 63 | // print to stderr because tracing is not yet setup 64 | eprintln!("There was an error loading the config: {e}"); 65 | std::process::exit(exitcode::CONFIG); 66 | } 67 | } 68 | }); 69 | 70 | if let Some(config_log_level) = config.observability.log_level { 71 | if initial_log_level.is_none() { 72 | log_level = config_log_level; 73 | } 74 | } 75 | 76 | // set a default global subscriber from now on 77 | let tracing_inst = self::tracing::tracing_init(log_level, config.observability.ansi_colors); 78 | tracing_inst.init(); 79 | 80 | config 81 | } 82 | 83 | fn run(options: NtpDaemonOptions) -> Result<(), Box> { 84 | let config = initialize_logging_parse_config(options.log_level, options.config); 85 | 86 | let runtime = if config.servers.is_empty() && config.nts_ke.is_empty() { 87 | Builder::new_current_thread().enable_all().build()? 88 | } else { 89 | Builder::new_multi_thread().enable_all().build()? 90 | }; 91 | 92 | runtime.block_on(async { 93 | // give the user a warning that we use the command line option 94 | if config.observability.log_level.is_some() && options.log_level.is_some() { 95 | info!("Log level override from command line arguments is active"); 96 | } 97 | 98 | // Warn/error if the config is unreasonable. We do this after finishing 99 | // tracing setup to ensure logging is fully configured. 100 | config.check(); 101 | 102 | // we always generate the keyset (even if NTS is not used) 103 | let keyset = nts_key_provider::spawn(config.keyset).await; 104 | 105 | #[cfg(feature = "hardware-timestamping")] 106 | let clock_config = config.clock; 107 | 108 | #[cfg(not(feature = "hardware-timestamping"))] 109 | let clock_config = config::ClockConfig::default(); 110 | 111 | ::tracing::debug!("Configuration loaded, spawning daemon jobs"); 112 | let (main_loop_handle, channels) = spawn::>( 113 | config.synchronization.synchronization_base, 114 | config.synchronization.algorithm, 115 | config.source_defaults, 116 | clock_config, 117 | &config.sources, 118 | &config.servers, 119 | keyset.clone(), 120 | ) 121 | .await?; 122 | 123 | for nts_ke_config in config.nts_ke { 124 | let _join_handle = keyexchange::spawn(nts_ke_config, keyset.clone()); 125 | } 126 | 127 | observer::spawn( 128 | &config.observability, 129 | channels.source_snapshots, 130 | channels.server_data_receiver, 131 | channels.system_snapshot_receiver, 132 | ); 133 | 134 | Ok(main_loop_handle.await??) 135 | }) 136 | } 137 | 138 | pub(crate) mod exitcode { 139 | /// An internal software error has been detected. This 140 | /// should be limited to non-operating system related 141 | /// errors as possible. 142 | pub const SOFTWARE: i32 = 70; 143 | 144 | /// You did not have sufficient permission to perform 145 | /// the operation. This is not intended for file system 146 | /// problems, which should use `NOINPUT` or `CANTCREAT`, 147 | /// but rather for higher level permissions. 148 | pub const NOPERM: i32 = 77; 149 | 150 | /// Something was found in an unconfigured or misconfigured state. 151 | pub const CONFIG: i32 = 78; 152 | } 153 | -------------------------------------------------------------------------------- /ntpd/src/daemon/nts_key_provider.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{File, OpenOptions}, 3 | os::unix::prelude::{OpenOptionsExt, PermissionsExt}, 4 | sync::Arc, 5 | }; 6 | 7 | use ntp_proto::{KeySet, KeySetProvider}; 8 | use tokio::sync::watch; 9 | use tracing::{instrument, warn, Span}; 10 | 11 | use super::config::KeysetConfig; 12 | 13 | #[instrument(level = tracing::Level::ERROR, name = "KeySet Provider", skip_all, fields(path = debug(config.key_storage_path.clone())))] 14 | pub async fn spawn(config: KeysetConfig) -> watch::Receiver> { 15 | let (mut provider, mut next_interval) = match &config.key_storage_path { 16 | Some(path) => { 17 | let path = path.to_owned(); 18 | 19 | if let Ok(meta) = std::fs::metadata(&path) { 20 | let perm = meta.permissions(); 21 | 22 | if perm.mode() as libc::mode_t & (libc::S_IWOTH | libc::S_IROTH | libc::S_IXOTH) 23 | != 0 24 | { 25 | warn!("Keyset file permissions: Others can interact with it. This is a potential security issue."); 26 | } 27 | } 28 | 29 | let (provider, time) = tokio::task::spawn_blocking( 30 | move || -> std::io::Result<(KeySetProvider, std::time::SystemTime)> { 31 | let mut input = File::open(path)?; 32 | KeySetProvider::load(&mut input, config.stale_key_count) 33 | }, 34 | ) 35 | .await 36 | .unwrap_or_else(|e| Err(std::io::Error::new(std::io::ErrorKind::Other, e))) 37 | .unwrap_or_else(|e| { 38 | warn!(error = ?e, "Could not load nts server keys, starting with new set"); 39 | ( 40 | KeySetProvider::new(config.stale_key_count), 41 | std::time::SystemTime::now(), 42 | ) 43 | }); 44 | ( 45 | provider, 46 | std::time::Duration::from_secs(config.key_rotation_interval as _).saturating_sub( 47 | std::time::SystemTime::now() 48 | .duration_since(time) 49 | .unwrap_or(std::time::Duration::from_secs(0)), 50 | ), 51 | ) 52 | } 53 | None => ( 54 | KeySetProvider::new(config.stale_key_count), 55 | std::time::Duration::from_secs(config.key_rotation_interval as _), 56 | ), 57 | }; 58 | let (tx, rx) = watch::channel(provider.get()); 59 | let span = Span::current(); 60 | tokio::task::spawn_blocking(move || { 61 | let _enter = span.enter(); 62 | loop { 63 | // First save, then sleep. Ensures new sets created at boot are also saved. 64 | if let Some(path) = &config.key_storage_path { 65 | if let Err(e) = (|| -> std::io::Result<()> { 66 | let mut output = OpenOptions::new() 67 | .create(true) 68 | .truncate(true) 69 | .write(true) 70 | .mode(0o600) 71 | .open(path)?; 72 | provider.store(&mut output) 73 | })() { 74 | if e.kind() == std::io::ErrorKind::NotFound 75 | || e.kind() == std::io::ErrorKind::PermissionDenied 76 | { 77 | warn!(error = ?e, "Could not store nts server keys, parent directory does not exist or has insufficient permissions"); 78 | } else { 79 | warn!(error = ?e, "Could not store nts server keys"); 80 | } 81 | } 82 | } 83 | if tx.send(provider.get()).is_err() { 84 | break; 85 | } 86 | std::thread::sleep(next_interval); 87 | next_interval = std::time::Duration::from_secs(config.key_rotation_interval as _); 88 | provider.rotate(); 89 | } 90 | }); 91 | rx 92 | } 93 | -------------------------------------------------------------------------------- /ntpd/src/daemon/sockets.rs: -------------------------------------------------------------------------------- 1 | use std::fs::Permissions; 2 | use std::path::Path; 3 | 4 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; 5 | 6 | pub async fn write_json(stream: &mut (impl AsyncWrite + Unpin), value: &T) -> std::io::Result<()> 7 | where 8 | T: serde::Serialize, 9 | { 10 | let bytes = serde_json::to_vec(value).unwrap(); 11 | stream.write_u64(bytes.len() as u64).await?; 12 | stream.write_all(&bytes).await 13 | } 14 | 15 | pub async fn read_json<'a, T>( 16 | stream: &mut (impl AsyncRead + Unpin), 17 | buffer: &'a mut Vec, 18 | ) -> std::io::Result 19 | where 20 | T: serde::Deserialize<'a>, 21 | { 22 | buffer.clear(); 23 | let msg_size = stream.read_u64().await? as usize; 24 | buffer.resize(msg_size, 0); 25 | stream.read_exact(buffer).await?; 26 | serde_json::from_slice(buffer) 27 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) 28 | } 29 | 30 | fn other_error(msg: String) -> std::io::Result { 31 | use std::io::{Error, ErrorKind}; 32 | Err(Error::new(ErrorKind::Other, msg)) 33 | } 34 | 35 | pub fn create_unix_socket_with_permissions( 36 | path: &Path, 37 | permissions: Permissions, 38 | ) -> std::io::Result { 39 | let listener = create_unix_socket(path)?; 40 | 41 | std::fs::set_permissions(path, permissions)?; 42 | 43 | Ok(listener) 44 | } 45 | 46 | fn create_unix_socket(path: &Path) -> std::io::Result { 47 | // must unlink path before the bind below (otherwise we get "address already in use") 48 | if path.exists() { 49 | use std::os::unix::fs::FileTypeExt; 50 | 51 | let meta = std::fs::metadata(path)?; 52 | if !meta.file_type().is_socket() { 53 | return other_error(format!("path {path:?} exists but is not a socket")); 54 | } 55 | 56 | std::fs::remove_file(path)?; 57 | } 58 | 59 | // OS errors are terrible; let's try to do better 60 | let error = match tokio::net::UnixListener::bind(path) { 61 | Ok(listener) => return Ok(listener), 62 | Err(e) => e, 63 | }; 64 | 65 | // we don create parent directories 66 | if let Some(parent) = path.parent() { 67 | if !parent.exists() { 68 | let msg = format!( 69 | r"Could not create observe socket at {:?} because its parent directory does not exist", 70 | &path 71 | ); 72 | return other_error(msg); 73 | } 74 | } 75 | 76 | // otherwise, just forward the OS error 77 | let msg = format!( 78 | "Could not create observe socket at {:?}: {:?}", 79 | &path, error 80 | ); 81 | 82 | other_error(msg) 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use tokio::net::{UnixListener, UnixStream}; 88 | 89 | use crate::test::alloc_port; 90 | 91 | use super::*; 92 | 93 | #[tokio::test] 94 | async fn write_then_read_is_identity() { 95 | // be careful with copying: tests run concurrently and should use a unique socket name! 96 | let path = std::env::temp_dir().join(format!("ntp-test-stream-{}", alloc_port())); 97 | if path.exists() { 98 | std::fs::remove_file(&path).unwrap(); 99 | } 100 | let listener = UnixListener::bind(&path).unwrap(); 101 | let mut writer = UnixStream::connect(&path).await.unwrap(); 102 | 103 | let (mut reader, _) = listener.accept().await.unwrap(); 104 | 105 | let object = vec![10u64; 1_000]; 106 | 107 | write_json(&mut writer, &object).await.unwrap(); 108 | 109 | let mut buf = Vec::new(); 110 | let output = read_json::>(&mut reader, &mut buf).await.unwrap(); 111 | 112 | assert_eq!(object, output); 113 | 114 | // the logic will automatically grow the buffer to the required size 115 | assert!(!buf.is_empty()); 116 | } 117 | 118 | #[tokio::test] 119 | async fn invalid_input_is_io_error() { 120 | // be careful with copying: tests run concurrently and should use a unique socket name! 121 | let path = std::env::temp_dir().join(format!("ntp-test-stream-{}", alloc_port())); 122 | if path.exists() { 123 | std::fs::remove_file(&path).unwrap(); 124 | } 125 | let listener = UnixListener::bind(&path).unwrap(); 126 | let mut writer = UnixStream::connect(&path).await.unwrap(); 127 | 128 | let (mut reader, _) = listener.accept().await.unwrap(); 129 | 130 | // write data that cannot be parsed 131 | let data = [0; 24]; 132 | writer.write_u64(data.len() as u64).await.unwrap(); 133 | writer.write_all(&data).await.unwrap(); 134 | 135 | let mut buf = Vec::new(); 136 | let output = read_json::>(&mut reader, &mut buf) 137 | .await 138 | .unwrap_err(); 139 | 140 | assert_eq!(output.kind(), std::io::ErrorKind::InvalidInput); 141 | 142 | // the logic will automatically grow the buffer to the required size 143 | assert!(!buf.is_empty()); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /ntpd/src/daemon/spawn/nts.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::net::SocketAddr; 3 | use std::ops::Deref; 4 | 5 | use ntp_proto::SourceConfig; 6 | use tokio::sync::mpsc; 7 | use tracing::warn; 8 | 9 | use super::super::{config::NtsSourceConfig, keyexchange::key_exchange_client}; 10 | 11 | use super::{SourceId, SourceRemovedEvent, SpawnAction, SpawnEvent, Spawner, SpawnerId}; 12 | 13 | pub struct NtsSpawner { 14 | config: NtsSourceConfig, 15 | source_config: SourceConfig, 16 | id: SpawnerId, 17 | has_spawned: bool, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum NtsSpawnError { 22 | SendError(mpsc::error::SendError), 23 | } 24 | 25 | impl std::error::Error for NtsSpawnError {} 26 | 27 | impl Display for NtsSpawnError { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | match self { 30 | Self::SendError(e) => write!(f, "Channel send error: {e}"), 31 | } 32 | } 33 | } 34 | 35 | impl From> for NtsSpawnError { 36 | fn from(value: mpsc::error::SendError) -> Self { 37 | Self::SendError(value) 38 | } 39 | } 40 | 41 | pub(super) async fn resolve_addr(address: (&str, u16)) -> Option { 42 | match tokio::net::lookup_host(address).await { 43 | Ok(mut addresses) => match addresses.next() { 44 | Some(address) => Some(address), 45 | None => { 46 | warn!("received unknown domain name from NTS-ke"); 47 | None 48 | } 49 | }, 50 | Err(e) => { 51 | warn!(error = ?e, "error while resolving source address, retrying"); 52 | None 53 | } 54 | } 55 | } 56 | 57 | impl NtsSpawner { 58 | pub fn new(config: NtsSourceConfig, source_config: SourceConfig) -> NtsSpawner { 59 | NtsSpawner { 60 | config, 61 | source_config, 62 | id: Default::default(), 63 | has_spawned: false, 64 | } 65 | } 66 | } 67 | 68 | #[async_trait::async_trait] 69 | impl Spawner for NtsSpawner { 70 | type Error = NtsSpawnError; 71 | 72 | async fn try_spawn( 73 | &mut self, 74 | action_tx: &mpsc::Sender, 75 | ) -> Result<(), NtsSpawnError> { 76 | match key_exchange_client( 77 | self.config.address.server_name.clone(), 78 | self.config.address.port, 79 | &self.config.certificate_authorities, 80 | #[cfg(feature = "unstable_ntpv5")] 81 | self.config.ntp_version, 82 | #[cfg(not(feature = "unstable_ntpv5"))] 83 | None, 84 | ) 85 | .await 86 | { 87 | Ok(ke) => { 88 | if let Some(address) = resolve_addr((ke.remote.as_str(), ke.port)).await { 89 | action_tx 90 | .send(SpawnEvent::new( 91 | self.id, 92 | SpawnAction::create_ntp( 93 | SourceId::new(), 94 | address, 95 | self.config.address.deref().clone(), 96 | ke.protocol_version, 97 | self.source_config, 98 | Some(ke.nts), 99 | ), 100 | )) 101 | .await?; 102 | self.has_spawned = true; 103 | } 104 | } 105 | Err(e) => { 106 | warn!(error = ?e, "error while attempting key exchange"); 107 | } 108 | } 109 | 110 | Ok(()) 111 | } 112 | 113 | fn is_complete(&self) -> bool { 114 | self.has_spawned 115 | } 116 | 117 | async fn handle_source_removed( 118 | &mut self, 119 | _removed_source: SourceRemovedEvent, 120 | ) -> Result<(), NtsSpawnError> { 121 | self.has_spawned = false; 122 | Ok(()) 123 | } 124 | 125 | fn get_id(&self) -> SpawnerId { 126 | self.id 127 | } 128 | 129 | fn get_addr_description(&self) -> String { 130 | self.config.address.to_string() 131 | } 132 | 133 | fn get_description(&self) -> &str { 134 | "nts" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /ntpd/src/daemon/spawn/nts_pool.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::ops::Deref; 3 | 4 | use tokio::sync::mpsc; 5 | use tracing::warn; 6 | 7 | use ntp_proto::SourceConfig; 8 | 9 | use super::super::{ 10 | config::NtsPoolSourceConfig, keyexchange::key_exchange_client_with_denied_servers, 11 | }; 12 | 13 | use super::{SourceId, SourceRemovedEvent, SpawnAction, SpawnEvent, Spawner, SpawnerId}; 14 | 15 | use super::nts::resolve_addr; 16 | 17 | struct PoolSource { 18 | id: SourceId, 19 | remote: String, 20 | } 21 | 22 | pub struct NtsPoolSpawner { 23 | config: NtsPoolSourceConfig, 24 | source_config: SourceConfig, 25 | id: SpawnerId, 26 | current_sources: Vec, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub enum NtsPoolSpawnError { 31 | SendError(mpsc::error::SendError), 32 | } 33 | 34 | impl std::error::Error for NtsPoolSpawnError {} 35 | 36 | impl Display for NtsPoolSpawnError { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | match self { 39 | Self::SendError(e) => write!(f, "Channel send error: {e}"), 40 | } 41 | } 42 | } 43 | 44 | impl From> for NtsPoolSpawnError { 45 | fn from(value: mpsc::error::SendError) -> Self { 46 | Self::SendError(value) 47 | } 48 | } 49 | 50 | impl NtsPoolSpawner { 51 | pub fn new(config: NtsPoolSourceConfig, source_config: SourceConfig) -> NtsPoolSpawner { 52 | NtsPoolSpawner { 53 | config, 54 | source_config, 55 | id: Default::default(), 56 | current_sources: Default::default(), 57 | } 58 | } 59 | 60 | fn contains_source(&self, domain: &str) -> bool { 61 | self.current_sources 62 | .iter() 63 | .any(|source| source.remote == domain) 64 | } 65 | } 66 | 67 | #[async_trait::async_trait] 68 | impl Spawner for NtsPoolSpawner { 69 | type Error = NtsPoolSpawnError; 70 | 71 | async fn try_spawn( 72 | &mut self, 73 | action_tx: &mpsc::Sender, 74 | ) -> Result<(), NtsPoolSpawnError> { 75 | for _ in 0..self.config.count.saturating_sub(self.current_sources.len()) { 76 | match key_exchange_client_with_denied_servers( 77 | self.config.addr.server_name.clone(), 78 | self.config.addr.port, 79 | &self.config.certificate_authorities, 80 | #[cfg(feature = "unstable_ntpv5")] 81 | self.config.ntp_version, 82 | #[cfg(not(feature = "unstable_ntpv5"))] 83 | None, 84 | self.current_sources 85 | .iter() 86 | .map(|source| source.remote.clone()), 87 | ) 88 | .await 89 | { 90 | Ok(ke) if !self.contains_source(&ke.remote) => { 91 | if let Some(address) = resolve_addr((ke.remote.as_str(), ke.port)).await { 92 | let id = SourceId::new(); 93 | self.current_sources.push(PoolSource { 94 | id, 95 | remote: ke.remote, 96 | }); 97 | action_tx 98 | .send(SpawnEvent::new( 99 | self.id, 100 | SpawnAction::create_ntp( 101 | id, 102 | address, 103 | self.config.addr.deref().clone(), 104 | ke.protocol_version, 105 | self.source_config, 106 | Some(ke.nts), 107 | ), 108 | )) 109 | .await?; 110 | } 111 | } 112 | Ok(_) => { 113 | warn!("received an address from pool-ke that we already had, ignoring"); 114 | continue; 115 | } 116 | Err(e) => { 117 | warn!(error = ?e, "error while attempting key exchange"); 118 | break; 119 | } 120 | }; 121 | } 122 | 123 | Ok(()) 124 | } 125 | 126 | fn is_complete(&self) -> bool { 127 | self.current_sources.len() >= self.config.count 128 | } 129 | 130 | async fn handle_source_removed( 131 | &mut self, 132 | removed_source: SourceRemovedEvent, 133 | ) -> Result<(), NtsPoolSpawnError> { 134 | self.current_sources.retain(|p| p.id != removed_source.id); 135 | Ok(()) 136 | } 137 | 138 | fn get_id(&self) -> SpawnerId { 139 | self.id 140 | } 141 | 142 | fn get_addr_description(&self) -> String { 143 | format!("{} ({})", self.config.addr.deref(), self.config.count) 144 | } 145 | 146 | fn get_description(&self) -> &str { 147 | "nts-pool" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /ntpd/src/daemon/spawn/pps.rs: -------------------------------------------------------------------------------- 1 | use ntp_proto::SourceConfig; 2 | use tokio::sync::mpsc; 3 | 4 | use crate::daemon::config::PpsSourceConfig; 5 | 6 | use super::{ 7 | standard::StandardSpawnError, PpsSourceCreateParameters, SourceCreateParameters, SourceId, 8 | SourceRemovalReason, SourceRemovedEvent, SpawnAction, SpawnEvent, Spawner, SpawnerId, 9 | }; 10 | 11 | pub struct PpsSpawner { 12 | config: PpsSourceConfig, 13 | source_config: SourceConfig, 14 | id: SpawnerId, 15 | has_spawned: bool, 16 | } 17 | 18 | impl PpsSpawner { 19 | pub fn new(config: PpsSourceConfig, source_config: SourceConfig) -> PpsSpawner { 20 | PpsSpawner { 21 | config, 22 | source_config, 23 | id: Default::default(), 24 | has_spawned: false, 25 | } 26 | } 27 | } 28 | 29 | #[async_trait::async_trait] 30 | impl Spawner for PpsSpawner { 31 | type Error = StandardSpawnError; 32 | 33 | async fn try_spawn( 34 | &mut self, 35 | action_tx: &mpsc::Sender, 36 | ) -> Result<(), StandardSpawnError> { 37 | action_tx 38 | .send(SpawnEvent::new( 39 | self.id, 40 | SpawnAction::Create(SourceCreateParameters::Pps(PpsSourceCreateParameters { 41 | id: SourceId::new(), 42 | path: self.config.path.clone(), 43 | config: self.source_config, 44 | noise_estimate: self.config.precision.powi(2), 45 | period: self.config.period, 46 | })), 47 | )) 48 | .await?; 49 | self.has_spawned = true; 50 | Ok(()) 51 | } 52 | 53 | fn is_complete(&self) -> bool { 54 | self.has_spawned 55 | } 56 | 57 | async fn handle_source_removed( 58 | &mut self, 59 | removed_source: SourceRemovedEvent, 60 | ) -> Result<(), StandardSpawnError> { 61 | if removed_source.reason != SourceRemovalReason::Demobilized { 62 | self.has_spawned = false; 63 | } 64 | Ok(()) 65 | } 66 | 67 | fn get_id(&self) -> SpawnerId { 68 | self.id 69 | } 70 | 71 | fn get_addr_description(&self) -> String { 72 | self.config.path.display().to_string() 73 | } 74 | 75 | fn get_description(&self) -> &str { 76 | "PPS" 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use ntp_proto::SourceConfig; 83 | use tokio::sync::mpsc; 84 | 85 | use crate::{ 86 | daemon::{ 87 | config::PpsSourceConfig, 88 | spawn::{pps::PpsSpawner, SourceCreateParameters, SpawnAction, Spawner}, 89 | system::MESSAGE_BUFFER_SIZE, 90 | }, 91 | test::alloc_port, 92 | }; 93 | 94 | #[tokio::test] 95 | async fn creates_a_source() { 96 | let socket_path = std::env::temp_dir().join(format!("ntp-test-stream-{}", alloc_port())); 97 | let precision = 1e-3; 98 | let mut spawner = PpsSpawner::new( 99 | PpsSourceConfig { 100 | path: socket_path.clone(), 101 | precision, 102 | period: 1., 103 | }, 104 | SourceConfig::default(), 105 | ); 106 | let spawner_id = spawner.get_id(); 107 | let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); 108 | 109 | assert!(!spawner.is_complete()); 110 | spawner.try_spawn(&action_tx).await.unwrap(); 111 | let res = action_rx.try_recv().unwrap(); 112 | assert_eq!(res.id, spawner_id); 113 | 114 | let SpawnAction::Create(create_params) = res.action; 115 | assert_eq!(create_params.get_addr(), socket_path.display().to_string()); 116 | 117 | let SourceCreateParameters::Pps(params) = create_params else { 118 | panic!("did not receive PPS source create parameters!"); 119 | }; 120 | assert_eq!(params.path, socket_path); 121 | assert!((params.noise_estimate - precision.powi(2)).abs() < 1e-9); 122 | 123 | // Should be complete after spawning 124 | assert!(spawner.is_complete()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ntpd/src/daemon/spawn/sock.rs: -------------------------------------------------------------------------------- 1 | use ntp_proto::SourceConfig; 2 | use tokio::sync::mpsc; 3 | 4 | use crate::daemon::config::SockSourceConfig; 5 | 6 | use super::{ 7 | standard::StandardSpawnError, SockSourceCreateParameters, SourceCreateParameters, SourceId, 8 | SourceRemovalReason, SourceRemovedEvent, SpawnAction, SpawnEvent, Spawner, SpawnerId, 9 | }; 10 | 11 | pub struct SockSpawner { 12 | config: SockSourceConfig, 13 | source_config: SourceConfig, 14 | id: SpawnerId, 15 | has_spawned: bool, 16 | } 17 | 18 | impl SockSpawner { 19 | pub fn new(config: SockSourceConfig, source_config: SourceConfig) -> SockSpawner { 20 | SockSpawner { 21 | config, 22 | source_config, 23 | id: Default::default(), 24 | has_spawned: false, 25 | } 26 | } 27 | } 28 | 29 | #[async_trait::async_trait] 30 | impl Spawner for SockSpawner { 31 | type Error = StandardSpawnError; 32 | 33 | async fn try_spawn( 34 | &mut self, 35 | action_tx: &mpsc::Sender, 36 | ) -> Result<(), StandardSpawnError> { 37 | action_tx 38 | .send(SpawnEvent::new( 39 | self.id, 40 | SpawnAction::Create(SourceCreateParameters::Sock(SockSourceCreateParameters { 41 | id: SourceId::new(), 42 | path: self.config.path.clone(), 43 | config: self.source_config, 44 | noise_estimate: self.config.precision.powi(2), 45 | })), 46 | )) 47 | .await?; 48 | self.has_spawned = true; 49 | Ok(()) 50 | } 51 | 52 | fn is_complete(&self) -> bool { 53 | self.has_spawned 54 | } 55 | 56 | async fn handle_source_removed( 57 | &mut self, 58 | removed_source: SourceRemovedEvent, 59 | ) -> Result<(), StandardSpawnError> { 60 | if removed_source.reason != SourceRemovalReason::Demobilized { 61 | self.has_spawned = false; 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn get_id(&self) -> SpawnerId { 67 | self.id 68 | } 69 | 70 | fn get_addr_description(&self) -> String { 71 | self.config.path.display().to_string() 72 | } 73 | 74 | fn get_description(&self) -> &str { 75 | "sock" 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use ntp_proto::SourceConfig; 82 | use tokio::sync::mpsc; 83 | 84 | use crate::{ 85 | daemon::{ 86 | config::SockSourceConfig, 87 | spawn::{sock::SockSpawner, SourceCreateParameters, SpawnAction, Spawner}, 88 | system::MESSAGE_BUFFER_SIZE, 89 | }, 90 | test::alloc_port, 91 | }; 92 | 93 | #[tokio::test] 94 | async fn creates_a_source() { 95 | let socket_path = std::env::temp_dir().join(format!("ntp-test-stream-{}", alloc_port())); 96 | let precision = 1e-3; 97 | let mut spawner = SockSpawner::new( 98 | SockSourceConfig { 99 | path: socket_path.clone(), 100 | precision, 101 | }, 102 | SourceConfig::default(), 103 | ); 104 | let spawner_id = spawner.get_id(); 105 | let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); 106 | 107 | assert!(!spawner.is_complete()); 108 | spawner.try_spawn(&action_tx).await.unwrap(); 109 | let res = action_rx.try_recv().unwrap(); 110 | assert_eq!(res.id, spawner_id); 111 | 112 | let SpawnAction::Create(create_params) = res.action; 113 | assert_eq!(create_params.get_addr(), socket_path.display().to_string()); 114 | 115 | let SourceCreateParameters::Sock(params) = create_params else { 116 | panic!("did not receive sock source create parameters!"); 117 | }; 118 | assert_eq!(params.path, socket_path); 119 | assert!((params.noise_estimate - precision.powi(2)).abs() < 1e-9); 120 | 121 | // Should be complete after spawning 122 | assert!(spawner.is_complete()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ntpd/src/daemon/tracing.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde::Deserialize; 4 | use tracing::metadata::LevelFilter; 5 | 6 | #[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum LogLevel { 9 | /// The "trace" level. 10 | /// 11 | /// Designates very low priority, often extremely verbose, information. 12 | Trace = 0, 13 | /// The "debug" level. 14 | /// 15 | /// Designates lower priority information. 16 | Debug = 1, 17 | /// The "info" level. 18 | /// 19 | /// Designates useful information. 20 | #[default] 21 | Info = 2, 22 | /// The "warn" level. 23 | /// 24 | /// Designates hazardous situations. 25 | Warn = 3, 26 | /// The "error" level. 27 | /// 28 | /// Designates very serious errors. 29 | Error = 4, 30 | } 31 | 32 | pub struct UnknownLogLevel; 33 | 34 | impl FromStr for LogLevel { 35 | type Err = UnknownLogLevel; 36 | 37 | fn from_str(s: &str) -> Result { 38 | match s { 39 | "trace" => Ok(LogLevel::Trace), 40 | "debug" => Ok(LogLevel::Debug), 41 | "info" => Ok(LogLevel::Info), 42 | "warn" => Ok(LogLevel::Warn), 43 | "error" => Ok(LogLevel::Error), 44 | _ => Err(UnknownLogLevel), 45 | } 46 | } 47 | } 48 | 49 | impl From for tracing::Level { 50 | fn from(value: LogLevel) -> Self { 51 | match value { 52 | LogLevel::Trace => tracing::Level::TRACE, 53 | LogLevel::Debug => tracing::Level::DEBUG, 54 | LogLevel::Info => tracing::Level::INFO, 55 | LogLevel::Warn => tracing::Level::WARN, 56 | LogLevel::Error => tracing::Level::ERROR, 57 | } 58 | } 59 | } 60 | 61 | impl From for LevelFilter { 62 | fn from(value: LogLevel) -> Self { 63 | LevelFilter::from_level(value.into()) 64 | } 65 | } 66 | 67 | pub fn tracing_init( 68 | level: impl Into, 69 | ansi_colors: bool, 70 | ) -> tracing_subscriber::fmt::Subscriber { 71 | tracing_subscriber::fmt() 72 | .with_max_level(level) 73 | .with_ansi(ansi_colors) 74 | .finish() 75 | } 76 | -------------------------------------------------------------------------------- /ntpd/src/daemon/util.rs: -------------------------------------------------------------------------------- 1 | use ntp_proto::NtpTimestamp; 2 | 3 | // Epoch offset between NTP and UNIX timescales 4 | pub(crate) const EPOCH_OFFSET: u32 = (70 * 365 + 17) * 86400; 5 | 6 | pub(crate) fn convert_net_timestamp(ts: timestamped_socket::socket::Timestamp) -> NtpTimestamp { 7 | NtpTimestamp::from_seconds_nanos_since_ntp_era( 8 | EPOCH_OFFSET.wrapping_add(ts.seconds as _), 9 | ts.nanos, 10 | ) 11 | } 12 | 13 | pub(crate) fn convert_clock_timestamp(ts: clock_steering::Timestamp) -> NtpTimestamp { 14 | NtpTimestamp::from_seconds_nanos_since_ntp_era( 15 | EPOCH_OFFSET.wrapping_add(ts.seconds as _), 16 | ts.nanos, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /ntpd/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | mod ctl; 4 | mod daemon; 5 | mod force_sync; 6 | mod metrics; 7 | 8 | pub use ctl::main as ctl_main; 9 | pub use daemon::main as daemon_main; 10 | pub use metrics::exporter::main as metrics_exporter_main; 11 | 12 | #[cfg(test)] 13 | mod test { 14 | use std::sync::atomic::{AtomicU16, Ordering}; 15 | 16 | pub fn alloc_port() -> u16 { 17 | static PORT: AtomicU16 = AtomicU16::new(5000); 18 | PORT.fetch_add(1, Ordering::Relaxed) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ntpd/test-keys: -------------------------------------------------------------------------------- 1 | ../ntp-proto/test-keys -------------------------------------------------------------------------------- /ntpd/testdata/certificates/nos-nl-chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHCTCCBfGgAwIBAgIMTdpv32aw4nga60ZTMA0GCSqGSIb3DQEBCwUAMGIxCzAJ 3 | BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTgwNgYDVQQDEy9H 4 | bG9iYWxTaWduIEV4dGVuZGVkIFZhbGlkYXRpb24gQ0EgLSBTSEEyNTYgLSBHMzAe 5 | Fw0yMjA1MTMwODMwMjFaFw0yMzA2MTQwODMwMjBaMIHSMR0wGwYDVQQPDBRQcml2 6 | YXRlIE9yZ2FuaXphdGlvbjERMA8GA1UEBRMIMzIxNDIxNTcxEzARBgsrBgEEAYI3 7 | PAIBAxMCTkwxCzAJBgNVBAYTAk5MMRYwFAYDVQQIDA1Ob29yZC1Ib2xsYW5kMRIw 8 | EAYDVQQHDAlIaWx2ZXJzdW0xGDAWBgNVBAkTD0pvdXJuYWFscGxlaW4gMTElMCMG 9 | A1UECgwcTmVkZXJsYW5kc2UgT21yb2VwIFN0aWNodGluZzEPMA0GA1UEAwwGbm9z 10 | Lm5sMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuTH3TymFYol1moZf 11 | mpd+aU5BvXhu0/upYNfG/Pdeb+hDhNqh6PBy4GpjFUlUM9ZTmaLBrCcw1h8UfqUX 12 | lMK8CFMZ2WTPvLn7ye5Lz5h573fY9weB3zQXpW76XWChUs9pkutFYS8ib6pJJ4Ry 13 | 81hMpORi3eXkA4KcSKZjzoMYj/7qXr0zg5D3HvaVwr5bO60gp8+kTmV05SAgm1Si 14 | mi8j9L5hvzCu76VoOxe2YQ62vGnc/gbFFri5tb2BQzOi/0bqdGp/3CSmFpWSBFgp 15 | tkNBIoNIZkA2TSrvDLI+nWb8tKbQcInWsxpjNkI2R1d3H7YEQ2D4PslaIp4R2pOL 16 | XhjGgwIDAQABo4IDTDCCA0gwDgYDVR0PAQH/BAQDAgWgMIGWBggrBgEFBQcBAQSB 17 | iTCBhjBHBggrBgEFBQcwAoY7aHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9j 18 | YWNlcnQvZ3NleHRlbmR2YWxzaGEyZzNyMy5jcnQwOwYIKwYBBQUHMAGGL2h0dHA6 19 | Ly9vY3NwMi5nbG9iYWxzaWduLmNvbS9nc2V4dGVuZHZhbHNoYTJnM3IzMFUGA1Ud 20 | IAROMEwwQQYJKwYBBAGgMgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmds 21 | b2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMAcGBWeBDAEBMAkGA1UdEwQCMAAwRQYD 22 | VR0fBD4wPDA6oDigNoY0aHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9ncy9nc2V4 23 | dGVuZHZhbHNoYTJnM3IzLmNybDARBgNVHREECjAIggZub3MubmwwHQYDVR0lBBYw 24 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMB8GA1UdIwQYMBaAFN2z522oLujFTm7PdOZ1 25 | PJQVzugdMB0GA1UdDgQWBBRwAeiFlr/vJwBoNAcRwcfv6fGcjDCCAYAGCisGAQQB 26 | 1nkCBAIEggFwBIIBbAFqAHcA6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0G 27 | vW4AAAGAvIpOYwAABAMASDBGAiEA34Pweig2mO0zRSShk8wOiNhZjXZeIYN+RoGp 28 | 3cIZ9uICIQDG6+LKsYyDYJ/a5y++PWvB4naUG9I7rLk+nutTAc2qdAB3AG9Tdqwx 29 | 8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kTAAABgLyKTk8AAAQDAEgwRgIhAOy9 30 | TFYU2Ha00+55nikglOYb5EwIV+Bo8gwWw6v0KEqOAiEAv9dfDzlsroqmwzWBHZ9c 31 | zk5khSjX7f7uOtmKJPzx+k0AdgCzc3cH4YRQ+GOG1gWp3BEJSnktsWcMC4fc8AMO 32 | eTalmgAAAYC8ik56AAAEAwBHMEUCIQCcW8UV1T6fubzmyf/GsudPVmVG/vMUDL1m 33 | Qsr5BX3fwwIgBfAo4On+NVAeTocK4YpTBq4U5ZYM03AnhSbLkl1vGpUwDQYJKoZI 34 | hvcNAQELBQADggEBAHVqGqbSpa6rnWZY/zJrfe4EGiMA+U6fQS+e1FR04KJPEEVl 35 | D2w00a7yeqKdklxWnABhQBKL9k7cn7MBIXwzcApJIrAhk1WV1thlia3+5oP+5H4i 36 | sc6bshjYd9O2aQQ4bAA7CFIgioew9pU8oNKR/ocNQsWKAfJHszgngS3sD0Wzn0Kz 37 | mM+KfxwKlxfv8Ruf/H8j0GVMN3PIisvaynjTSBA2EKrvkBCMA3hn6kpdykrN6H9p 38 | 5ZGAJz2+jAxpIxRTnknC9IL/pDZ1dHz9fknyJihqv+9M701J2GQVtSzvZksE6zJA 39 | wzlqekvHQpgxHT42k+gVSk6tNYstvAJuugxiz6g= 40 | -----END CERTIFICATE----- 41 | -----BEGIN CERTIFICATE----- 42 | MIIEYTCCA0mgAwIBAgIOSKQC3SeSDaIINJ3RmXswDQYJKoZIhvcNAQELBQAwTDEg 43 | MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2Jh 44 | bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTYwOTIxMDAwMDAwWhcNMjYw 45 | OTIxMDAwMDAwWjBiMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBu 46 | di1zYTE4MDYGA1UEAxMvR2xvYmFsU2lnbiBFeHRlbmRlZCBWYWxpZGF0aW9uIENB 47 | IC0gU0hBMjU2IC0gRzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr 48 | awNnVNXcEfvFohPBjBkn3BB04mGDPfqO24+lD+SpvkY/Ar5EpAkcJjOfR0iBFYhW 49 | N80HzpXYy2tIA7mbXpKu2JpmYdU1xcoQpQK0ujE/we+vEDyjyjmtf76LLqbOfuq3 50 | xZbSqUqAY+MOvA67nnpdawvkHgJBFVPnxui45XH4BwTwbtDucx+Mo7EK4mS0Ti+P 51 | 1NzARxFNCUFM8Wxc32wxXKff6WU4TbqUx/UJm485ttkFqu0Ox4wTUUbn0uuzK7yV 52 | 3Y986EtGzhKBraMH36MekSYlE473GqHetRi9qbNG5pM++Sa+WjR9E1e0Yws16CGq 53 | smVKwAqg4uc43eBTFUhVAgMBAAGjggEpMIIBJTAOBgNVHQ8BAf8EBAMCAQYwEgYD 54 | VR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU3bPnbagu6MVObs905nU8lBXO6B0w 55 | HwYDVR0jBBgwFoAUj/BLf6guRSSuTVD6Y5qL3uLdG7wwPgYIKwYBBQUHAQEEMjAw 56 | MC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIuZ2xvYmFsc2lnbi5jb20vcm9vdHIz 57 | MDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9v 58 | dC1yMy5jcmwwRwYDVR0gBEAwPjA8BgRVHSAAMDQwMgYIKwYBBQUHAgEWJmh0dHBz 59 | Oi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 60 | A4IBAQBVaJzl0J/i0zUV38iMXIQ+Q/yht+JZZ5DW1otGL5OYV0LZ6ZE6xh+WuvWJ 61 | J4hrDbhfo6khUEaFtRUnurqzutvVyWgW8msnoP0gtMZO11cwPUMUuUV8iGyIOuIB 62 | 0flo6G+XbV74SZuR5v5RAgqgGXucYUPZWvv9AfzMMQhRQkr/MO/WR2XSdiBrXHoD 63 | L2xk4DmjA4K6iPI+1+qMhyrkUM/2ZEdA8ldqwl8nQDkKS7vq6sUZ5LPVdfpxJZZu 64 | 5JBj4y7FNFTVW1OMlCUvwt5H8aFgBMLFik9xqK6JFHpYxYmf4t2sLLxN0LlCthJE 65 | abvp10ZlOtfu8hL5gCXcxnwGxzSb 66 | -----END CERTIFICATE----- 67 | -----BEGIN CERTIFICATE----- 68 | MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G 69 | A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp 70 | Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 71 | MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG 72 | A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI 73 | hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 74 | RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT 75 | gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm 76 | KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd 77 | QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ 78 | XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw 79 | DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o 80 | LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU 81 | RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp 82 | jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 83 | 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX 84 | mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs 85 | Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH 86 | WD9f 87 | -----END CERTIFICATE----- 88 | -------------------------------------------------------------------------------- /ntpd/testdata/certificates/nos-nl.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEYTCCA0mgAwIBAgIOSKQC3SeSDaIINJ3RmXswDQYJKoZIhvcNAQELBQAwTDEg 3 | MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2Jh 4 | bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTYwOTIxMDAwMDAwWhcNMjYw 5 | OTIxMDAwMDAwWjBiMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBu 6 | di1zYTE4MDYGA1UEAxMvR2xvYmFsU2lnbiBFeHRlbmRlZCBWYWxpZGF0aW9uIENB 7 | IC0gU0hBMjU2IC0gRzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr 8 | awNnVNXcEfvFohPBjBkn3BB04mGDPfqO24+lD+SpvkY/Ar5EpAkcJjOfR0iBFYhW 9 | N80HzpXYy2tIA7mbXpKu2JpmYdU1xcoQpQK0ujE/we+vEDyjyjmtf76LLqbOfuq3 10 | xZbSqUqAY+MOvA67nnpdawvkHgJBFVPnxui45XH4BwTwbtDucx+Mo7EK4mS0Ti+P 11 | 1NzARxFNCUFM8Wxc32wxXKff6WU4TbqUx/UJm485ttkFqu0Ox4wTUUbn0uuzK7yV 12 | 3Y986EtGzhKBraMH36MekSYlE473GqHetRi9qbNG5pM++Sa+WjR9E1e0Yws16CGq 13 | smVKwAqg4uc43eBTFUhVAgMBAAGjggEpMIIBJTAOBgNVHQ8BAf8EBAMCAQYwEgYD 14 | VR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU3bPnbagu6MVObs905nU8lBXO6B0w 15 | HwYDVR0jBBgwFoAUj/BLf6guRSSuTVD6Y5qL3uLdG7wwPgYIKwYBBQUHAQEEMjAw 16 | MC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIuZ2xvYmFsc2lnbi5jb20vcm9vdHIz 17 | MDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9v 18 | dC1yMy5jcmwwRwYDVR0gBEAwPjA8BgRVHSAAMDQwMgYIKwYBBQUHAgEWJmh0dHBz 19 | Oi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 20 | A4IBAQBVaJzl0J/i0zUV38iMXIQ+Q/yht+JZZ5DW1otGL5OYV0LZ6ZE6xh+WuvWJ 21 | J4hrDbhfo6khUEaFtRUnurqzutvVyWgW8msnoP0gtMZO11cwPUMUuUV8iGyIOuIB 22 | 0flo6G+XbV74SZuR5v5RAgqgGXucYUPZWvv9AfzMMQhRQkr/MO/WR2XSdiBrXHoD 23 | L2xk4DmjA4K6iPI+1+qMhyrkUM/2ZEdA8ldqwl8nQDkKS7vq6sUZ5LPVdfpxJZZu 24 | 5JBj4y7FNFTVW1OMlCUvwt5H8aFgBMLFik9xqK6JFHpYxYmf4t2sLLxN0LlCthJE 25 | abvp10ZlOtfu8hL5gCXcxnwGxzSb 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /ntpd/testdata/config/invalid.toml: -------------------------------------------------------------------------------- 1 | [[source]] 2 | mode = "server" 3 | address = "example.com" 4 | 5 | [synchronization] 6 | does-not-exist = 5 7 | minimum-agreeing-sources = 2 8 | -------------------------------------------------------------------------------- /nts-pool-ke/COPYRIGHT: -------------------------------------------------------------------------------- 1 | ../COPYRIGHT -------------------------------------------------------------------------------- /nts-pool-ke/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nts-pool-ke" 3 | version.workspace = true 4 | edition.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | homepage.workspace = true 8 | readme.workspace = true 9 | description.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | 13 | [package.metadata.cargo-udeps.ignore] 14 | normal = [ "ntp-proto", "rustls-platform-verifier", "rustls-pemfile2", "rustls23", "serde", "tokio-rustls", "toml", "tracing", "tracing-subscriber" ] 15 | 16 | [dependencies] 17 | tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "fs", "net", "macros", "time" ] } 18 | toml.workspace = true 19 | tracing.workspace = true 20 | tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "fmt", "ansi"] } 21 | rustls23.workspace = true 22 | rustls-platform-verifier.workspace = true 23 | serde.workspace = true 24 | ntp-proto = { workspace = true } 25 | tokio-rustls.workspace = true 26 | 27 | [features] 28 | default = [] 29 | unstable_nts-pool = [ "ntp-proto/nts-pool" ] 30 | 31 | [[bin]] 32 | name = "nts-pool-ke" 33 | path = "bin/nts-pool-ke.rs" 34 | -------------------------------------------------------------------------------- /nts-pool-ke/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /nts-pool-ke/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /nts-pool-ke/README.md: -------------------------------------------------------------------------------- 1 | # NTS pool KE 2 | 3 | The NTS pool KE experimental feature provides an analogue to the [ntp pool](https://www.ntppool.org/en/) that supports Network Time Security (NTS). 4 | 5 | The technical details are described in the [draft RFC](https://github.com/pendulum-project/nts-pool-draft). 6 | 7 | ## Building 8 | 9 | The NTS pool KE feature is behind a feature flag that is disabled by default. Enabling this feature requires a from-source build. 10 | 11 | ```sh 12 | > cargo build --release --features "unstable_nts-pool" 13 | 14 | > ls target/release/ 15 | ntp-daemon 16 | nts-pool-ke 17 | ... 18 | ``` 19 | 20 | The command builds a version of `ntp-daemon` that accepts configuration for being an NTS pool client and can be an NTS pool KE server. The `nts-pool-ke` binary is the actual pool. 21 | 22 | ## Example 23 | 24 | An (insecure!) example setup is given by the `unsafe.*.toml` files in this directory. Purely for testing, this example can be run on a single machine as follows. In three different terminals, run 25 | 26 | ``` 27 | sudo target/release/ntp-daemon -c nts-pool-ke/unsafe.nts.server.toml 28 | target/release/nts-pool-ke -c nts-pool-ke/unsafe.pool.toml 29 | sudo target/release/ntp-daemon -c nts-pool-ke/unsafe.nts.client.toml 30 | ``` 31 | 32 | The server should show something like this 33 | 34 | ``` 35 | > sudo target/release/ntp-daemon -c nts-pool-ke/unsafe.nts.server.toml 36 | 37 | 2023-12-21T10:49:12.702642Z INFO ntpd::daemon::system: new source source_id=SourceId(1) addr=213.109.127.82:123 spawner=SpawnerId(1) 38 | 2023-12-21T10:49:12.702693Z INFO ntpd::daemon::system: new source source_id=SourceId(2) addr=94.198.159.16:123 spawner=SpawnerId(1) 39 | 2023-12-21T10:49:12.702706Z INFO ntpd::daemon::system: new source source_id=SourceId(3) addr=154.51.12.215:123 spawner=SpawnerId(1) 40 | 2023-12-21T10:49:12.702719Z INFO ntpd::daemon::system: new source source_id=SourceId(4) addr=45.138.55.60:123 spawner=SpawnerId(1) 41 | 2023-12-21T10:49:12.709153Z INFO ntp_proto::algorithm::kalman: Offset: 2.2252593194659007+-70.88905127356101ms, frequency: 0+-10000000ppm 42 | 2023-12-21T10:49:12.709438Z INFO ntp_proto::algorithm::kalman: Offset: 1.6675194083763052+-50.70180376962068ms, frequency: 0+-7071067.811865476ppm 43 | 2023-12-21T10:49:12.711005Z INFO ntp_proto::algorithm::kalman: Offset: 1.3434683112795662+-43.24338657667398ms, frequency: 0+-5773502.691896257ppm 44 | ``` 45 | 46 | The NTS pool KE should show 47 | 48 | ``` 49 | > target/release/nts-pool-ke -c nts-pool-ke/unsafe.pool.toml 50 | 51 | 2023-12-21T10:49:34.628308Z INFO nts_pool_ke: listening on 'Ok(0.0.0.0:4460)' 52 | 2023-12-21T10:49:37.765321Z INFO nts_pool_ke: received records from the client 53 | 2023-12-21T10:49:37.766481Z INFO nts_pool_ke: checking supported algorithms for 'localhost:8081' 54 | 2023-12-21T10:49:37.767102Z INFO nts_pool_ke: checking supported algorithms for 'localhost:8080' 55 | 2023-12-21T10:49:37.769478Z INFO nts_pool_ke: established connection to the server 56 | 2023-12-21T10:49:37.769717Z INFO nts_pool_ke: received supported algorithms from the NTS KE server 57 | 2023-12-21T10:49:37.770599Z INFO nts_pool_ke: fetching cookies from the NTS KE server 58 | 2023-12-21T10:49:37.770834Z INFO nts_pool_ke: received cookies from the NTS KE server 59 | 2023-12-21T10:49:37.770868Z INFO nts_pool_ke: wrote records for client 60 | ``` 61 | 62 | Finally the client should show 63 | 64 | ``` 65 | > sudo target/release/ntp-daemon -c nts-pool-ke/unsafe.nts.client.toml 66 | 67 | 2023-12-21T13:11:03.577635Z INFO ntpd::daemon::system: new source source_id=SourceId(1) addr=127.0.0.1:123 spawner=SpawnerId(1) 68 | 2023-12-21T13:11:03.580097Z INFO ntp_proto::algorithm::kalman: Offset: -0.043484615172139515+-44.92576728378405ms, frequency: 0+-10000000ppm 69 | ``` 70 | 71 | > NOTE: the client may need a while to synchronize in this scenario. So long as no warnings are printed things should eventually settle and start to synchronize. 72 | 73 | ## Setup 74 | 75 | An (insecure!) example setup is given by the `unsafe.*.toml` files in this directory. The important bits are highlighted here. 76 | 77 | ### Client 78 | 79 | Because this is a build with the NTS pool KE enabled, the client accepts a source of type `"nts-pool"`, for instance: 80 | 81 | ```toml 82 | # client.toml 83 | 84 | [[source]] 85 | mode = "nts-pool" 86 | address = "custom.nts.pool:4460" 87 | certificate-authority = "ca.pem" 88 | count = 2 89 | ``` 90 | 91 | Configuration for the client is otherwise unchanged. 92 | 93 | ### Pool 94 | 95 | The pool is configured with a custom `pool.toml` configuration file. Because the pool behaves like both a server and a client, it needs certificate information that combines the configuration that we normally see for NTS clients and servers. 96 | 97 | ```toml 98 | # pool.toml 99 | 100 | [nts-pool-ke-server] 101 | listen = "0.0.0.0:4460" 102 | certificate-authority-path = "ca.pem" 103 | certificate-chain-path = "end.fullchain.pem" 104 | private-key-path = "end.key" 105 | key-exchange-servers = [ 106 | { domain = "nts.server.1", port = 8081 }, 107 | { domain = "nts.server.2", port = 8080 }, 108 | ] 109 | ``` 110 | 111 | ### Server 112 | 113 | Server configuration is mostly standard, but the certificate for an NTS pool must be specifically allow-listed by specifying them using `authorized-pool-server-certificates`. The files listed there must consist of just a single certificate, *not* a certificate chain. 114 | 115 | ```toml 116 | # server.toml 117 | 118 | [[nts-ke-server]] 119 | listen = "0.0.0.0:8080" 120 | certificate-chain-path = "end.fullchain.pem" 121 | private-key-path = "end.key" 122 | authorized-pool-server-certificates = ["end.pem"] 123 | key-exchange-timeout-ms = 1000 124 | ``` 125 | 126 | ### Generating Certificates 127 | 128 | The NTS pool KE requires a relatively complex certificate setup. Documentation for generating a certificate authority can be found [in our docs](https://docs.ntpd-rs.pendulum-project.org/development/ca/). The `test-keys/gen-cert.sh` script generates a certificate and private key for a server. 129 | -------------------------------------------------------------------------------- /nts-pool-ke/bin/nts-pool-ke.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() -> ! { 3 | let result = nts_pool_ke::nts_pool_ke_main().await; 4 | std::process::exit(if result.is_ok() { 0 } else { 1 }); 5 | } 6 | -------------------------------------------------------------------------------- /nts-pool-ke/src/condcompile/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | net::SocketAddr, 4 | os::unix::fs::PermissionsExt, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use serde::Deserialize; 9 | use tracing::{info, warn}; 10 | 11 | #[derive(Deserialize, Debug)] 12 | #[serde(rename_all = "kebab-case", deny_unknown_fields)] 13 | pub struct Config { 14 | pub nts_pool_ke_server: NtsPoolKeConfig, 15 | #[serde(default)] 16 | pub observability: ObservabilityConfig, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum ConfigError { 21 | Io(std::io::Error), 22 | Toml(toml::de::Error), 23 | } 24 | 25 | impl From for ConfigError { 26 | fn from(value: std::io::Error) -> Self { 27 | Self::Io(value) 28 | } 29 | } 30 | 31 | impl From for ConfigError { 32 | fn from(value: toml::de::Error) -> Self { 33 | Self::Toml(value) 34 | } 35 | } 36 | 37 | impl Display for ConfigError { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | Self::Io(e) => write!(f, "io error while reading config: {e}"), 41 | Self::Toml(e) => write!(f, "config toml parsing error: {e}"), 42 | } 43 | } 44 | } 45 | 46 | impl std::error::Error for ConfigError {} 47 | 48 | impl Config { 49 | pub fn check(&self) -> bool { 50 | true 51 | } 52 | 53 | async fn from_file(file: impl AsRef) -> Result { 54 | let meta = std::fs::metadata(&file)?; 55 | let perm = meta.permissions(); 56 | 57 | const S_IWOTH: u32 = 2; 58 | if perm.mode() & S_IWOTH != 0 { 59 | warn!("Unrestricted config file permissions: Others can write."); 60 | } 61 | 62 | let contents = tokio::fs::read_to_string(file).await?; 63 | Ok(toml::de::from_str(&contents)?) 64 | } 65 | 66 | pub async fn from_args(file: impl AsRef) -> Result { 67 | let path = file.as_ref(); 68 | info!(?path, "using config file"); 69 | 70 | let config = Config::from_file(path).await?; 71 | 72 | Ok(config) 73 | } 74 | } 75 | 76 | #[derive(Deserialize, Debug, Clone, Default)] 77 | #[serde(rename_all = "kebab-case", deny_unknown_fields)] 78 | pub struct ObservabilityConfig { 79 | #[serde(default)] 80 | pub log_level: Option, 81 | } 82 | 83 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] 84 | #[serde(rename_all = "kebab-case", deny_unknown_fields)] 85 | pub struct NtsPoolKeConfig { 86 | pub certificate_authority_path: PathBuf, 87 | pub certificate_chain_path: PathBuf, 88 | pub private_key_path: PathBuf, 89 | #[serde(default = "default_nts_ke_timeout")] 90 | pub key_exchange_timeout_ms: u64, 91 | pub listen: SocketAddr, 92 | pub key_exchange_servers: Vec, 93 | } 94 | 95 | fn default_nts_ke_timeout() -> u64 { 96 | 1000 97 | } 98 | 99 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] 100 | #[serde(rename_all = "kebab-case", deny_unknown_fields)] 101 | pub struct KeyExchangeServer { 102 | pub domain: String, 103 | pub port: u16, 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | 110 | #[test] 111 | fn test_deserialize_nts_pool_ke() { 112 | let test: Config = toml::from_str( 113 | r#" 114 | [nts-pool-ke-server] 115 | listen = "0.0.0.0:4460" 116 | certificate-authority-path = "/foo/bar/ca.pem" 117 | certificate-chain-path = "/foo/bar/baz.pem" 118 | private-key-path = "spam.der" 119 | key-exchange-servers = [ 120 | { domain = "foo.bar", port = 1234 }, 121 | { domain = "bar.foo", port = 4321 }, 122 | ] 123 | "#, 124 | ) 125 | .unwrap(); 126 | 127 | let ca = PathBuf::from("/foo/bar/ca.pem"); 128 | assert_eq!(test.nts_pool_ke_server.certificate_authority_path, ca); 129 | 130 | let chain = PathBuf::from("/foo/bar/baz.pem"); 131 | assert_eq!(test.nts_pool_ke_server.certificate_chain_path, chain); 132 | 133 | let private_key = PathBuf::from("spam.der"); 134 | assert_eq!(test.nts_pool_ke_server.private_key_path, private_key); 135 | 136 | assert_eq!(test.nts_pool_ke_server.key_exchange_timeout_ms, 1000,); 137 | assert_eq!( 138 | test.nts_pool_ke_server.listen, 139 | "0.0.0.0:4460".parse().unwrap(), 140 | ); 141 | 142 | assert_eq!( 143 | test.nts_pool_ke_server.key_exchange_servers, 144 | vec![ 145 | KeyExchangeServer { 146 | domain: String::from("foo.bar"), 147 | port: 1234 148 | }, 149 | KeyExchangeServer { 150 | domain: String::from("bar.foo"), 151 | port: 4321 152 | }, 153 | ] 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /nts-pool-ke/src/condcompile/tracing.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde::Deserialize; 4 | use tracing::metadata::LevelFilter; 5 | 6 | #[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum LogLevel { 9 | /// The "trace" level. 10 | /// 11 | /// Designates very low priority, often extremely verbose, information. 12 | Trace = 0, 13 | /// The "debug" level. 14 | /// 15 | /// Designates lower priority information. 16 | Debug = 1, 17 | /// The "info" level. 18 | /// 19 | /// Designates useful information. 20 | #[default] 21 | Info = 2, 22 | /// The "warn" level. 23 | /// 24 | /// Designates hazardous situations. 25 | Warn = 3, 26 | /// The "error" level. 27 | /// 28 | /// Designates very serious errors. 29 | Error = 4, 30 | } 31 | 32 | pub struct UnknownLogLevel; 33 | 34 | impl FromStr for LogLevel { 35 | type Err = UnknownLogLevel; 36 | 37 | fn from_str(s: &str) -> Result { 38 | match s { 39 | "trace" => Ok(LogLevel::Trace), 40 | "debug" => Ok(LogLevel::Debug), 41 | "info" => Ok(LogLevel::Info), 42 | "warn" => Ok(LogLevel::Warn), 43 | "error" => Ok(LogLevel::Error), 44 | _ => Err(UnknownLogLevel), 45 | } 46 | } 47 | } 48 | 49 | impl From for tracing::Level { 50 | fn from(value: LogLevel) -> Self { 51 | match value { 52 | LogLevel::Trace => tracing::Level::TRACE, 53 | LogLevel::Debug => tracing::Level::DEBUG, 54 | LogLevel::Info => tracing::Level::INFO, 55 | LogLevel::Warn => tracing::Level::WARN, 56 | LogLevel::Error => tracing::Level::ERROR, 57 | } 58 | } 59 | } 60 | 61 | impl From for LevelFilter { 62 | fn from(value: LogLevel) -> Self { 63 | LevelFilter::from_level(value.into()) 64 | } 65 | } 66 | 67 | pub fn tracing_init(level: impl Into) -> tracing_subscriber::fmt::Subscriber { 68 | tracing_subscriber::fmt().with_max_level(level).finish() 69 | } 70 | -------------------------------------------------------------------------------- /nts-pool-ke/unsafe.nts.client.toml: -------------------------------------------------------------------------------- 1 | # part of the test setup for the NTS pool KE. Do not use in production! 2 | # (the private key of the certificate is public!) 3 | 4 | [observability] 5 | # Other values include trace, debug, warn and error 6 | log-level = "info" 7 | observation-path = "/var/run/ntpd-rs/observe" 8 | 9 | # See https://docs.ntpd-rs.pendulum-project.org/man/ntp.toml.5/ on how to set up certificates 10 | [[source]] 11 | mode = "nts-pool" 12 | address = "localhost:4460" 13 | certificate-authority = "test-keys/testca.pem" 14 | count = 1 15 | 16 | # System parameters used in filtering and steering the clock: 17 | [synchronization] 18 | minimum-agreeing-sources = 1 19 | single-step-panic-threshold = 10 20 | startup-step-panic-threshold = { forward = "inf", backward = 86400 } 21 | -------------------------------------------------------------------------------- /nts-pool-ke/unsafe.nts.server.toml: -------------------------------------------------------------------------------- 1 | # part of the test setup for the NTS pool KE. Do not use in production! 2 | # (the private key of the certificate is public!) 3 | 4 | [observability] 5 | # Other values include trace, debug, warn and error 6 | log-level = "info" 7 | observation-path = "/var/run/ntpd-rs/observe" 8 | 9 | # the server will get its time from the NTP pool 10 | [[source]] 11 | mode = "pool" 12 | address = "pool.ntp.org" 13 | count = 4 14 | 15 | [[server]] 16 | listen = "0.0.0.0:123" 17 | 18 | # System parameters used in filtering and steering the clock: 19 | [synchronization] 20 | minimum-agreeing-sources = 1 21 | single-step-panic-threshold = 10 22 | startup-step-panic-threshold = { forward = 0, backward = 86400 } 23 | 24 | # to function as an NTS server, we must also provide key exchange 25 | # uses an unsecure certificate chain! 26 | [[nts-ke-server]] 27 | listen = "0.0.0.0:8080" 28 | certificate-chain-path = "test-keys/end.fullchain.pem" 29 | private-key-path = "test-keys/end.key" 30 | authorized-pool-server-certificates = ["test-keys/end.pem"] 31 | key-exchange-timeout-ms = 1000 32 | -------------------------------------------------------------------------------- /nts-pool-ke/unsafe.pool.toml: -------------------------------------------------------------------------------- 1 | # part of the test setup for the NTS pool KE. Do not use in production! 2 | # (the private key of the certificate is public!) 3 | [nts-pool-ke-server] 4 | listen = "0.0.0.0:4460" 5 | certificate-authority-path = "test-keys/testca.pem" 6 | certificate-chain-path = "test-keys/end.fullchain.pem" 7 | private-key-path = "test-keys/end.key" 8 | key-exchange-servers = [ 9 | { domain = "localhost", port = 8081 }, 10 | { domain = "localhost", port = 8080 }, 11 | ] 12 | -------------------------------------------------------------------------------- /pkg/deb/COPYRIGHT-debian: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2024 Trifecta Tech Foundation, Tweede Golf, and Contributors 2 | 3 | Except as otherwise noted (below and/or in individual files), ntpd-rs is 4 | licensed under the Apache License, Version 2.0 or 5 | or 6 | or the MIT license or 7 | , at your option. 8 | -------------------------------------------------------------------------------- /pkg/deb/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | NTPDRS_CONF_SRC="/usr/share/doc/ntpd-rs/ntp.toml.default" 6 | NTPDRS_CONF_DIR="/etc/ntpd-rs" 7 | NTPDRS_CONF="${NTPDRS_CONF_DIR}/ntp.toml" 8 | NTPDRS_CONF_PERMS=644 9 | NTPDRS_HOME="/var/lib/ntpd-rs/" 10 | NTPDRS_USER="ntpd-rs" 11 | NTPDRS_OBSERVE_HOME="/var/lib/ntpd-rs-observe/" 12 | NTPDRS_OBSERVE_USER="ntpd-rs-observe" 13 | 14 | create_user() { 15 | if ! id ${NTPDRS_USER} > /dev/null 2>&1; then 16 | adduser --system --home "${NTPDRS_HOME}" --group ${NTPDRS_USER} 17 | fi 18 | if ! id ${NTPDRS_OBSERVE_USER} > /dev/null 2>&1; then 19 | adduser --system --home "${NTPDRS_OBSERVE_HOME}" --group ${NTPDRS_OBSERVE_USER} 20 | fi 21 | } 22 | 23 | case "$1" in 24 | configure) 25 | create_user 26 | ;; 27 | esac 28 | 29 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 30 | # This will only remove masks created by d-s-h on package removal. 31 | deb-systemd-helper unmask ntpd-rs.service >/dev/null || true 32 | 33 | # was-enabled defaults to true, so new installations run enable. 34 | if deb-systemd-helper --quiet was-enabled ntpd-rs.service; then 35 | # Enables the unit on first installation, creates new 36 | # symlinks on upgrades if the unit file has changed. 37 | deb-systemd-helper enable ntpd-rs.service >/dev/null || true 38 | else 39 | # Update the statefile to add new symlinks (if any), which need to be 40 | # cleaned up on purge. Also remove old symlinks. 41 | deb-systemd-helper update-state ntpd-rs.service >/dev/null || true 42 | fi 43 | fi 44 | 45 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 46 | if deb-systemd-helper debian-installed ntpd-rs-metrics.service; then 47 | # This will only remove masks created by d-s-h on package removal. 48 | deb-systemd-helper unmask ntpd-rs-metrics.service >/dev/null || true 49 | 50 | if deb-systemd-helper --quiet was-enabled ntpd-rs-metrics.service; then 51 | # Create new symlinks, if any. 52 | deb-systemd-helper enable ntpd-rs-metrics.service >/dev/null || true 53 | fi 54 | fi 55 | 56 | # Update the statefile to add new symlinks (if any), which need to be cleaned 57 | # up on purge. Also remove old symlinks. 58 | deb-systemd-helper update-state ntpd-rs-metrics.service >/dev/null || true 59 | fi 60 | 61 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 62 | if [ -d /run/systemd/system ]; then 63 | systemctl --system daemon-reload >/dev/null || true 64 | if [ -n "$2" ]; then 65 | _dh_action=restart 66 | else 67 | _dh_action=start 68 | fi 69 | deb-systemd-invoke $_dh_action ntpd-rs.service ntpd-rs-metrics.service >/dev/null || true 70 | fi 71 | fi 72 | 73 | #DEBHELPER# 74 | -------------------------------------------------------------------------------- /pkg/deb/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | NTPDRS_CONF_DIR="/etc/ntpd-rs" 5 | 6 | case "$1" in 7 | purge) 8 | # Per https://www.debian.org/doc/debian-policy/ch-files.html#behavior 9 | # "configuration files must be preserved when the package is removed, and 10 | # only deleted when the package is purged." 11 | if [ -d ${NTPDRS_CONF_DIR} ]; then 12 | rm -r ${NTPDRS_CONF_DIR} 13 | fi 14 | ;; 15 | esac 16 | 17 | if [ -d /run/systemd/system ]; then 18 | systemctl --system daemon-reload >/dev/null || true 19 | fi 20 | 21 | if [ "$1" = "remove" ]; then 22 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 23 | deb-systemd-helper mask ntpd-rs.service >/dev/null || true 24 | deb-systemd-helper mask ntpd-rs-metrics.service >/dev/null || true 25 | fi 26 | fi 27 | 28 | if [ "$1" = "purge" ]; then 29 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 30 | deb-systemd-helper purge ntpd-rs.service >/dev/null || true 31 | deb-systemd-helper unmask ntpd-rs.service >/dev/null || true 32 | 33 | deb-systemd-helper purge ntpd-rs-metrics.service >/dev/null || true 34 | deb-systemd-helper unmask ntpd-rs-metrics.service >/dev/null || true 35 | fi 36 | fi 37 | 38 | #DEBHELPER# 39 | -------------------------------------------------------------------------------- /pkg/deb/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d /run/systemd/system ] && [ "$1" = remove ]; then 5 | deb-systemd-invoke stop ntpd-rs.service >/dev/null || true 6 | deb-systemd-invoke stop ntpd-rs-metrics.service >/dev/null || true 7 | fi 8 | -------------------------------------------------------------------------------- /pkg/rpm/scriptlets.toml: -------------------------------------------------------------------------------- 1 | post_install_script = ''' 2 | #!/bin/bash -e 3 | #RPM_SYSTEMD_MACROS# 4 | 5 | if [ $EUID -ne 0 ]; then 6 | echo >&2 "ERROR: ntpd-rs postinst script must be run as root" 7 | exit 1 8 | fi 9 | 10 | NTPDRS_USER=ntpd-rs 11 | NTPDRS_OBSERVE_USER=ntpd-rs-observe 12 | NTPDRS_HOME_DIR="/var/lib/ntpd-rs" 13 | NTPDRS_OBSERVE_HOME_DIR="/var/lib/ntpd-rs-observe" 14 | NTPDRS_HOME_DIR_PERMS=700 15 | NTPDRS_CONF_SRC="/usr/share/doc/ntpd-rs/ntp.toml.default" 16 | NTPDRS_CONF_DIR="/etc/ntpd-rs" 17 | NTPDRS_CONF="${NTPDRS_CONF_DIR}/ntp.toml" 18 | NTPDRS_CONF_PERMS=644 19 | 20 | create_user() { 21 | if ! id ${NTPDRS_USER} > /dev/null 2>&1; then 22 | # According to the CentOS 7 useradd man page: 23 | # --user-group causes a group by the same name as the user to be created 24 | # --create-home should force creation of a home dir even for a system account. 25 | useradd --home-dir ${NTPDRS_HOME_DIR} --system --create-home --user-group ${NTPDRS_USER} 26 | fi 27 | if ! id ${NTPDRS_OBSERVE_USER} > /dev/null 2>&1; then 28 | useradd --home-dir ${NTPDRS_OBSERVE_HOME_DIR} --system --create-home --user-group ${NTPDRS_OBSERVE_USER} 29 | fi 30 | # Ensure that the home directory has the correct ownership 31 | chown -R ${NTPDRS_USER}:${NTPDRS_USER} ${NTPDRS_HOME_DIR} 32 | chown -R ${NTPDRS_OBSERVE_USER}:${NTPDRS_OBSERVE_USER} ${NTPDRS_OBSERVE_HOME_DIR} 33 | # Ensure that the home directory has the correct permissions 34 | chmod ${NTPDRS_HOME_DIR_PERMS} ${NTPDRS_HOME_DIR} 35 | chmod ${NTPDRS_HOME_DIR_PERMS} ${NTPDRS_OBSERVE_HOME_DIR} 36 | } 37 | 38 | init_systemd_service() { 39 | systemd_post ntpd-rs.service 40 | systemd_post ntpd-rs-metrics.service 41 | systemd_triggers 42 | } 43 | 44 | link_man_page() { 45 | if [ ! -f "/usr/share/man/man5/ntp.toml.5" ]; then 46 | (cd "/usr/share/man/man5" && ln -s "ntp-toml.5" "ntp.toml.5") 47 | fi 48 | } 49 | 50 | if [ $1 -eq 1 ] ; then 51 | # Initial installation 52 | create_user 53 | link_man_page 54 | init_systemd_service 55 | fi 56 | ''' 57 | 58 | pre_uninstall_script = ''' 59 | #!/bin/bash -e 60 | #RPM_SYSTEMD_MACROS# 61 | 62 | if [ $1 -eq 0 ] ; then 63 | # Package removal, not upgrade 64 | # Run commands equivalent to what the RPM systemd macros would do 65 | systemd_preun ntpd-rs.service 66 | systemd_preun ntpd-rs-metrics.service 67 | systemd_triggers 68 | fi 69 | ''' 70 | 71 | post_uninstall_script = ''' 72 | #!/bin/bash -e 73 | #RPM_SYSTEMD_MACROS# 74 | 75 | if [ $1 -ge 1 ] ; then 76 | # Run commands equivalent to what the RPM systemd macros would do 77 | systemd_postun_with_restart ntpd-rs.service 78 | systemd_postun_with_restart ntpd-rs-metrics.service 79 | systemd_triggers 80 | fi 81 | ''' 82 | -------------------------------------------------------------------------------- /pkg/test-scripts/test-ntpd-rs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | set -x 5 | 6 | case $1 in 7 | post-install|post-upgrade) 8 | # Ensure users are created 9 | id ntpd-rs 10 | id ntpd-rs-observe 11 | 12 | # Ensure deamon and ctl client are present 13 | # and configuration validates. 14 | echo -e "\nNTPD-RS HELP OUTPUT:" 15 | /usr/bin/ntp-daemon --help 16 | /usr/bin/ntp-metrics-exporter --help 17 | /usr/bin/ntp-ctl validate 18 | 19 | # # Ensure that the systemd service is running 20 | # systemctl is-active ntpd-rs.service --quiet 21 | 22 | # # Ensure that the metrics systemd service is not running 23 | # ! systemctl is-active ntpd-rs-metrics.service --quiet 24 | ;; 25 | esac 26 | -------------------------------------------------------------------------------- /utils/build-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | : "${RELEASE_TARGETS:=aarch64-unknown-linux-gnu,armv7-unknown-linux-gnueabihf,x86_64-unknown-linux-gnu,i686-unknown-linux-gnu}" 6 | IFS=',' read -r -a targets <<< "$RELEASE_TARGETS" 7 | 8 | target_dir="target/pkg" 9 | 10 | rm -rf "$target_dir" 11 | mkdir -p "$target_dir" 12 | 13 | package_version=$(cargo read-manifest --manifest-path ntpd/Cargo.toml | jq -r .version) 14 | toolchain=$(rustup show active-toolchain | cut -d' ' -f1) 15 | host_target=$(echo "$toolchain" | cut -d'-' -f2-) 16 | sysroot=$(rustc --print sysroot) 17 | llvm_tools_path="$sysroot/lib/rustlib/$host_target/bin" 18 | 19 | echo "--- Running on toolchain '${toolchain}', make sure the llvm-tools component is installed" 20 | echo "--- Host target is '$host_target'" 21 | 22 | for target in "${targets[@]}"; do 23 | dbg_sym_tar="ntpd-rs_dbg_$package_version-$target.tar.gz" 24 | 25 | echo "--- Calling cross for building ntpd package for target '$target'" 26 | cross build --target "$target" --package ntpd --release --features "${RELEASE_FEATURES:-}" 27 | 28 | echo "--- Creating separate debug symbol files for target '$target'" 29 | ( 30 | cd "target/$target/release" 31 | find . -maxdepth 1 -type f -executable -print0 | while IFS= read -r -d '' file; do 32 | echo "--- Writing debug symbols from '$file' to '$file.dbg'" 33 | "$llvm_tools_path/llvm-strip" --only-keep-debug -o "$file.dbg" "$file" 34 | chmod -x "$file.dbg" 35 | echo "--- Removing all symbols from binary '$file'" 36 | "$llvm_tools_path/llvm-strip" -s "$file" 37 | done 38 | ); 39 | 40 | echo "--- Create tar for debug symbols" 41 | ( 42 | cd "target/$target/release" 43 | rm -f "$dbg_sym_tar" 44 | find . -maxdepth 1 -type f -name '*.dbg' -exec tar uvf "$dbg_sym_tar" {} + 45 | ); 46 | 47 | echo "--- Creating deb package" 48 | cargo deb --no-build --no-strip --target "$target" --compress-type xz --package ntpd 49 | 50 | echo "--- Creating rpm package" 51 | cargo generate-rpm --payload-compress xz --package ntpd --target "$target" --target-dir target 52 | 53 | echo "--- Copying output files to target" 54 | cp "target/$target/release/$dbg_sym_tar" "$target_dir/" 55 | find "target/$target/debian" -maxdepth 1 -type f -name '*.deb' -exec cp "{}" "$target_dir/" \; 56 | find "target/$target/generate-rpm" -maxdepth 1 -type f -name '*.rpm' -exec cp "{}" "$target_dir/" \; 57 | done 58 | 59 | echo "--- Generating SHA256SUMS file" 60 | ( 61 | cd $target_dir 62 | sha256sum -b * > SHA256SUMS 63 | ) 64 | 65 | echo "--- Done, output is in $target_dir" 66 | 67 | 68 | -------------------------------------------------------------------------------- /utils/generate-man.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | docs_dir="docs/man" 6 | output_dir="${1:-"docs/precompiled/man"}" 7 | files=("ntp-ctl.8" "ntp-daemon.8" "ntp-metrics-exporter.8" "ntp.toml.5") 8 | 9 | mkdir -p "$output_dir" 10 | 11 | for f in "${files[@]}"; do 12 | origin_file="$docs_dir/$f.md" 13 | tmp_file="$output_dir/$f.md" 14 | target_file="$output_dir/$f" 15 | 16 | echo "Generating man page for $f from '$origin_file' to '$target_file'" 17 | sed '//s/--- -->/---/' > "$tmp_file" 18 | utils/pandoc.sh -s -t man "$tmp_file" -o "$target_file" 19 | rm "$tmp_file" 20 | done 21 | -------------------------------------------------------------------------------- /utils/mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bind_port_arg=(-p "127.0.0.1:8000:8000") 4 | if [ "$#" -gt 0 ]; then 5 | if [ "$1" == "--no-bind-port" ]; then 6 | shift 7 | bind_port_arg=() 8 | fi 9 | fi 10 | 11 | exec docker run --rm "${bind_port_arg[@]}" -v "${PWD}:/docs" -u "$(id -u):$(id -g)" squidfunk/mkdocs-material "$@" 12 | -------------------------------------------------------------------------------- /utils/pandoc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PANDOC_VERSION="3.1.1" 4 | 5 | exec docker run --rm -v "$(pwd):/data" -u "$(id -u):$(id -g)" "pandoc/core:$PANDOC_VERSION" "$@" 6 | -------------------------------------------------------------------------------- /utils/precompiled-man-diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | rm -rf target/docs/man 6 | utils/generate-man.sh target/docs/man 7 | 8 | exec diff -r -s --color "docs/precompiled/man" "target/docs/man" 9 | -------------------------------------------------------------------------------- /utils/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo publish -p ntp-proto 4 | cargo publish -p ntpd 5 | -------------------------------------------------------------------------------- /utils/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$#" -lt 1 ]; then 4 | echo "Missing new version specifier" 5 | exit 1 6 | fi 7 | 8 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) 9 | PROJECT_DIR=$(dirname "$SCRIPT_DIR") 10 | NEW_VERSION="$1" 11 | NOTICE_LINE="# NOTE: keep this part at the bottom of the file, do not change this line" 12 | 13 | echo "Updating version in Cargo.toml" 14 | sed -i 's/^version\s*=\s*".*"/version = "'"$NEW_VERSION"'"/' "$PROJECT_DIR/Cargo.toml" 15 | 16 | echo "Updating workspace crate versions in Cargo.toml" 17 | tmp_file="$PROJECT_DIR/Cargo.toml.tmp" 18 | replace_flag=0 19 | while IFS= read -r line; do 20 | if [ $replace_flag -eq 1 ]; then 21 | line=$(echo "$line" | sed 's/version\s*=\s*"[^"]*"/version = "'"$NEW_VERSION"'"/g') 22 | fi 23 | 24 | if [ "$line" = "$NOTICE_LINE" ]; then 25 | replace_flag=1 26 | fi 27 | 28 | echo "$line" >> "$tmp_file" 29 | done < "$PROJECT_DIR/Cargo.toml" 30 | mv "$tmp_file" "$PROJECT_DIR/Cargo.toml" 31 | 32 | echo "Updating version in man pages" 33 | sed -i 's/^title: NTP-CTL(8) ntpd-rs .*/title: NTP-CTL(8) ntpd-rs '"$NEW_VERSION"' | ntpd-rs/' "$PROJECT_DIR"/docs/man/ntp-ctl.8.md 34 | sed -i 's/^title: NTP-DAEMON(8) ntpd-rs .*/title: NTP-DAEMON(8) ntpd-rs '"$NEW_VERSION"' | ntpd-rs/' "$PROJECT_DIR"/docs/man/ntp-daemon.8.md 35 | sed -i 's/^title: NTP-METRICS-EXPORTER(8) ntpd-rs .*/title: NTP-METRICS-EXPORTER(8) ntpd-rs '"$NEW_VERSION"' | ntpd-rs/' "$PROJECT_DIR"/docs/man/ntp-metrics-exporter.8.md 36 | sed -i 's/^title: NTP.TOML(5) ntpd-rs .*/title: NTP.TOML(5) ntpd-rs '"$NEW_VERSION"' | ntpd-rs/' "$PROJECT_DIR"/docs/man/ntp.toml.5.md 37 | 38 | echo "Rebuilding precompiled man pages" 39 | utils/generate-man.sh 40 | 41 | echo "Rebuilding project" 42 | (cd $PROJECT_DIR && cargo clean && cargo build --release) 43 | 44 | echo "!!! Version changes complete, make sure that the changelog is synchronized" 45 | --------------------------------------------------------------------------------