[
  {
    "path": ".github/actions/setup/action.yaml",
    "content": "name: Shared Setup\n\ndescription: \"Install Rust, checkout code and install dependencies.\"\n\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Checkout Code\n      uses: actions/checkout@v3\n\n    - name: Cache Cargo Dependencies\n      uses: actions/cache@v3\n      with:\n        path: |\n          ~/.cargo\n          target\n        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n        restore-keys: |\n          ${{ runner.os }}-cargo-\n\n    - name: Install Rust\n      uses: actions-rs/toolchain@v1\n      with:\n        toolchain: 1.87\n        profile: minimal\n        components: clippy, rustfmt\n        override: true\n\n"
  },
  {
    "path": ".github/workflows/build-artifacts.yml",
    "content": "# This workflow builds the artifacts for the release.\n# It creates the gzips and uploads them as artifacts.\n# The artifacts can then be used to create a release on Github.\n\nname: Build Artifacts\n\non:\n  # This workflow is triggered every time a push is made to the main branch.\n  # Aka when a PR is merged.\n  push:\n    branches: [master]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      # Setup the environment\n      - name: Log in to GitHub Container Registry # Where we store the docker images with the cross compile toolchain for Apple\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - uses: actions/checkout@v4\n      - name: Set up Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n      - name: Install cross # Tool to cross-compile the binaries\n        run: cargo install cross --git https://github.com/cross-rs/cross --rev 51f46f296253d8122c927c5bb933e3c4f27cc317\n\n\n      # Build artifacts\n      - name: Build Artifacts\n        run: .scripts/build-artifacts.sh\n\n      # Upload artifacts to Github\n      - name: Gzip artifacts\n        run: (cd target && tar -czf github-release.tar.gz github-release)\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: pkdns-artifacts.tar.gz\n          path: target/github-release.tar.gz\n          if-no-files-found: error\n          overwrite: true\n\n"
  },
  {
    "path": ".github/workflows/rust.yaml",
    "content": "name: Pipeline\n\non:\n  pull_request:\n    branches:\n      - \"*\"\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup environment\n        id: setup\n        uses: ./.github/actions/setup\n      \n      - name: Build Project\n        run: cargo build --verbose\n\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup environment\n        id: setup\n        uses: ./.github/actions/setup\n      - name: Run Tests\n        run: cargo test --verbose\n\n\n  format:\n    name: Check Format\n    runs-on: ubuntu-latest\n    needs: build\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup environment\n        id: setup\n        uses: ./.github/actions/setup\n      - name: Run Rustfmt (Code Formatting)\n        run: cargo fmt --all -- --check\n\n\n  clippy:\n    name: Check Clippy\n    runs-on: ubuntu-latest\n    needs: build\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup environment\n        id: setup\n        uses: ./.github/actions/setup\n\n      - name: Run Clippy (Lint)\n        run: cargo clippy --all-targets --all-features -- -D warnings"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# MSVC Windows builds of rustc generate these, which store debugging information\n*.pdb\n\n# Mac pollution\n.DS_STORE\ncli/7fmjpc\n\n.vscode\n"
  },
  {
    "path": ".scripts/build-artifacts.sh",
    "content": "#!/bin/bash\n\n# -------------------------------------------------------------------------------------------------\n# This script prepares the artifacts for the current project.\n# It builds all the binaries and prepares them for upload as a Github Release.\n# The end result will be a target/github-release directory with the following structure:\n#\n# target/github-release/\n# ├── pkdns-v0.5.0-rc.0-linux-arm64.tar.gz\n# ├── pkdns-v0.5.0-rc.0-linux-amd64.tar.gz\n# ├── pkdns-v0.5.0-rc.0-windows-amd64.tar.gz\n# ├── pkdns-v0.5.0-rc.0-osx-arm64.tar.gz\n# ├── pkdns-v0.5.0-rc.0-osx-amd64.tar.gz\n# └── ...\n#\n# Make sure you installed https://github.com/cross-rs/cross for cross-compilation.\n# To build macos, you need access to the [Pubky Github Packages](https://github.com/orgs/pubky/packages).\n# Use [docker login](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic)\n# to authenticate yourself.\n# -------------------------------------------------------------------------------------------------\n\n\nset -e # fail the script if any command fails\nset -u # fail the script if any variable is not set\nset -o pipefail # fail the script if any pipe command fails\n\n\n# Check if cross is installed\nif ! command -v cross &> /dev/null\nthen\n    echo \"cross executable could not be found. It is required to cross-compile the binaries. Please install it from https://github.com/cross-rs/cross\"\n    exit 1\nfi\n\n# Read the version from the homeserver\nVERSION=$(cargo pkgid -p pkdns | cut -d@ -f2)\necho \"Preparing release executables for version $VERSION...\"\nTARGETS=(\n# target, nickname\n\"aarch64-apple-darwin,osx-arm64\" \n\"x86_64-apple-darwin,osx-amd64\"\n\"aarch64-unknown-linux-musl,linux-arm64\"\n\"x86_64-unknown-linux-musl,linux-amd64\"\n\"x86_64-pc-windows-gnu,windows-amd64\"\n)\n\n# List of binaries to build.\nARTIFACTS=(\"pkdns\" \"pkdns-cli\")\n\necho \"Create the github-release directory...\"\nrm -rf target/github-release\nmkdir -p target/github-release\n\n# Helper function to build an artifact for one specific target.\nbuild_target() {\n    local TARGET=$1\n    local NICKNAME=$2\n    echo \"Build $NICKNAME with $TARGET\"\n    FOLDER=\"pkdns-v$VERSION-$NICKNAME\"\n    DICT=\"target/github-release/$FOLDER\"\n    mkdir -p $DICT\n    for ARTIFACT in \"${ARTIFACTS[@]}\"; do\n        echo \"- Build $ARTIFACT with $TARGET\"\n        cross build -p $ARTIFACT --release --target $TARGET\n        if [[ $TARGET == *\"windows\"* ]]; then\n            cp target/$TARGET/release/$ARTIFACT.exe $DICT/\n        else\n            cp target/$TARGET/release/$ARTIFACT $DICT/\n        fi\n        echo \"[Done] Artifact $ARTIFACT built for $TARGET\"\n    done;\n    (cd target/github-release && tar -czf $FOLDER.tar.gz $FOLDER && rm -rf $FOLDER)\n}\n\n# Build the binaries\necho \"Build all the binaries for version $VERSION...\"\nfor ELEMENT in \"${TARGETS[@]}\"; do\n    # Split tuple by comma\n    IFS=',' read -r TARGET NICKNAME <<< \"$ELEMENT\"\n\n    build_target $TARGET $NICKNAME\ndone\n\ntree target/github-release\n(cd target/github-release && pwd)"
  },
  {
    "path": ".scripts/build_docker.sh",
    "content": "#!/bin/bash\n\nVERSION=$(cargo pkgid -p pkdns | cut -d@ -f2)\ndocker build --platform linux/amd64 -t synonymsoft/pkdns:$VERSION -t synonymsoft/pkdns:latest .\n\n\necho\nread -p \"Do you want to publish synonymsoft/pkdns:$VERSION to Docker Hub? [yN] \" response\necho\ncase \"$response\" in\n    [yY]|[yY][eE][sS])  # Accepts y, Y, yes, YES\n\n        docker push synonymsoft/pkdns:$VERSION\n        ;;\n    *)\n        echo \"Dont publish synonymsoft/pkdns:$VERSION.\"\n        exit 1\n        ;;\nesac\n\necho\nread -p \"Do you want to publish synonymsoft/pkdns:latest to Docker Hub? [yN] \" response\necho\ncase \"$response\" in\n    [yY]|[yY][eE][sS])  # Accepts y, Y, yes, YES\n\n        docker push synonymsoft/pkdns:latest\n        ;;\n    *)\n        echo \"Dont publish synonymsoft/pkdns:latest.\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n  \"server\", \n  \"cli\"\n]\n\n\n\n# See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352\nresolver = \"2\"\n"
  },
  {
    "path": "Cross.toml",
    "content": "# --------------------------------------------------------------------------------\n# Costum defined Apple cross-compile images.\n# They should not be public because of Apple licencing restrictions.\n# https://github.com/cross-rs/cross/\n# See https://github.com/cross-rs/cross-toolchains?tab=readme-ov-file#apple-targets\n# SDKs can be downloaded here: https://github.com/joseluisq/macosx-sdks\n# Images are uploaded to the Github Registry. Docker login required.\n# https://github.com/orgs/pubky/packages\n# @Sev May 2025\n# --------------------------------------------------------------------------------\n\n\n[target.aarch64-apple-darwin]\nimage = \"ghcr.io/pubky/aarch64-apple-darwin-cross:latest\"\n\n[target.x86_64-apple-darwin]\nimage = \"ghcr.io/pubky/x86_64-apple-darwin-cross:latest\""
  },
  {
    "path": "Dockerfile",
    "content": "# ========================\n# Build Image\n# ========================\nFROM rust:1.86.0-alpine3.20 AS builder\n\n# Install build dependencies\nRUN apk add --no-cache \\\n    gcc \\\n    libc-dev\n\n# Set the working directory\nWORKDIR /usr/src/app\n\n# Copy over Cargo.toml and Cargo.lock for dependency caching\nCOPY Cargo.toml Cargo.lock ./\n\n# Copy over all the source code\nCOPY . .\n\n# Build the project in release mode\nRUN cargo build --release\n\n# ========================\n# Runtime Image\n# ========================\nFROM alpine:3.20\n\nARG TARGETARCH=x86_64\n\n# Install runtime dependencies (only ca-certificates)\nRUN apk add --no-cache ca-certificates\n\n# Copy the compiled binary from the builder stage\nCOPY --from=builder /usr/src/app/target/release/pkdns /usr/local/bin\nCOPY --from=builder /usr/src/app/target/release/pkdns-cli /usr/local/bin\n\n# Expose the DNS port\nEXPOSE 53\n\n# Set the default command to run the binary\nCMD [\"pkdns\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (Severin Alexander Bühler) 2023 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# pkdns\n\n[![GitHub Release](https://img.shields.io/github/v/release/pubky/pkdns)](https://github.com/pubky/pkdns/releases/latest/)\n[![Demo](https://img.shields.io/badge/Demo-7fmjpc-green)](http://pkdns-demo.pubky.app/)\n[![Docker](https://img.shields.io/badge/Image-Docker-red)](https://hub.docker.com/r/synonymsoft/pkdns)\n[![Telegram Chat Group](https://img.shields.io/badge/Chat-Telegram-violet)](https://t.me/pubkycore)\n\nA DNS server providing self-sovereign and censorship-resistant domain names. It resolves records hosted on the [Mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT), the biggest DHT on the planet with ~10M nodes that services torrents since 15 years.\n\n\n## Getting Started\n\n### Hosted DNS\n\nUse one of the [hosted DNS servers](./servers.txt) to try out pkdns quickly.\n\n- [Verify](#verify-pkdns-is-working) the server is working.\n- Configure your [browser](#use-dns-over-https-in-your-browser) or [system dns](#change-your-system-dns).\n- [Browse](#browse-the-self-sovereign-web) the self-sovereign web.\n\n\n### Pre-Built Binaries\n\n1. Download the [latest release](https://github.com/pubky/pkdns/releases/latest/) for your plattform.\n2. Extract the tar file. Should be something like `tar -xvf tarfile.tar.gz`.\n3. Run `pkdns --verbose`.\n4. [Verify](#verify-pkdns-is-working) the server is working. Your dns server ip is `127.0.0.1`.\n5. Configure your [browser](#use-dns-over-https-in-your-browser) or [system dns](#change-your-system-dns).\n6. [Browse](#browse-the-self-sovereign-web) the self-sovereign web.\n\n\n### Build It Yourself\n\nMake sure you have the [Rust toolchain](https://rustup.rs/) installed.\n\n1. Clone repository `git clone https://github.com/pubky/pkdns.git`.\n2. Switch directory `cd pkdns`.\n3. Run `cargo run --package=pkdns`.\n4. [Verify](#verify-pkdns-is-working) the server is working. Your server ip is `127.0.0.1`.\n6. Configure your [browser](#use-dns-over-https-in-your-browser) or [system dns](#change-your-system-dns).\n7. [Browse](#browse-the-self-sovereign-web) the self-sovereign web.\n\n\n### Use Docker Compose\n\nSee [compose.yaml](./compose.yaml).\n\n## Guides\n\n### Use DNS-over-HTTPS in your Browser\n\n1. Pick a DNS-over-HTTPS URL from our public [servers.txt](./servers.txt) list.\n2. Configure your browser. See [this guide](https://support.privadovpn.com/kb/article/848-how-to-enable-doh-on-your-browser/).\n\n\nVerify your server with this domain [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/).\n\n### Change your System DNS\n\nUpgrade your whole machine to pkdns by setting it as your primary system DNS.\n\nFollow one of the guides to change your DNS server on your system:\n- [MacOS guide](https://support.apple.com/en-gb/guide/mac-help/mh14127)\n- [Ubuntu guide](https://www.ionos.com/digitalguide/server/configuration/change-dns-server-on-ubuntu/)\n- [Windows guide](https://www.windowscentral.com/how-change-your-pcs-dns-settings-windows-10)\n\n\nVerify your server with this domain [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/).\n\n### Verify pkdns is working\n\n#### PKDNS Domains\nVerify the server resolves pkdns domains. Replace `PKDNS_SERVER_IP` with your pkdns server IP address.\n\n```bash \nnslookup 7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy PKDNS_SERVER_IP\n```\n\n> *Troubleshooting* If this does not work then the pkdns server is likely not running or misconfigured.\n\n\n#### ICANN Domains\n\nVerify it resolves regular ICANN domains. Replace `PKDNS_SERVER_IP` with your pkdns server IP address.\n\n```bash\nnslookup example.com PKDNS_SERVER_IP\n```\n\n> *Troubleshooting* If this does not work then you need to change your ICANN fallback server with\n> `pkdns -f REGULAR_DNS_SERVER_IP`. Or use the Google DNS server: `pkdns -f 8.8.8.8`.\n\n### Browse the Self-Sovereign Web\n\nHere are some example pkdns domains:\n\n- [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/)\n- [http://pkdns.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://pkdns.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/)\n\n\nHint: Always add a `./` to the end of a pkarr domain. Otherwise browsers will search instead of resolve the website.\n\n### Address already in use\n\nOther services might occupy the port 53 already. For example, [Docker Desktop](https://github.com/docker/for-mac/issues/7008) uses the port 53 on MacOS. [systemd-resolved](https://www.linuxuprising.com/2020/07/ubuntu-how-to-free-up-port-53-used-by.html) is using it on Ubuntu. Make sure to free those.\n\n## Configuration\n\n### Options\n\n```\nUsage: pkdns [OPTIONS]\n\nOptions:\n  -f, --forward <FORWARD>      ICANN fallback DNS server. Format: IP:Port. [default: 8.8.8.8:53]\n  -v, --verbose                Show verbose output. [default: false]\n  -c, --config <CONFIG>        The path to pkdns configuration file. This will override the pkdns-dir config path\n  -p, --pkdns-dir <PKDNS_DIR>  The base directory that contains pkdns's data, configuration file, etc [default: ~/.pkdns]\n  -h, --help                   Print help\n  -V, --version                Print version\n```\n\n### Config File\n\n`~/.pkdns/pkdns.toml` is used for all extended configurations. An example can be found in [config.sample.toml](./server/config.sample.toml).\n\n\n## FAQs\n\n- [How Censorship-Resistant is Mainline DHT?](https://medium.com/pubky/mainline-dht-censorship-explained-b62763db39cb)\n- [How Censorship-Resistant are Public Key Domains](https://medium.com/pubky/public-key-domains-censorship-resistance-explained-33d0333e6123)\n- [How to publish a Public Key Domain Website?](https://medium.com/pubky/how-to-host-a-public-key-domain-website-v0-6-0-ubuntu-24-04-57e6f2cb6f77)\n- [How can I run my own DNS over HTTPS endpoint?](./docs/dns-over-https.md)\n- [How to configure DynDNS?](./docs/dyn-dns.md)\n\n## Related Tools\n\n- [pkarr zone explorer](https://pkdns.net/)\n- [pkdns-vanity](https://github.com/jphastings/pkdns-vanity)\n- [awesome-pubky](https://github.com/aljazceru/awesome-pubky)\n\n### Record Types\n\nCurrently, pkdns only supports `A`, `AAAA`, `TXT`, `CNAME`, and `MX` records. For any other types, use bind9.\n\n\n---\n\nMay the power ⚡ be with you. Powered by [pkarr](https://github.com/pubky/pkarr).\n"
  },
  {
    "path": "cli/Cargo.toml",
    "content": "[package]\nname = \"pkdns-cli\"\nversion = \"0.7.1\"\nauthors = [\"SeverinAlexB <severin@synonym.to>\"]\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nanyhow = \"1.0.82\"\ntokio = { version = \"1.37.0\", features = [\"full\"] }\nthiserror = \"1.0.49\"\nserde = { version = \"1.0.199\", features = [\"derive\"] }\nclap = { version = \"4.5.1\", features = [\"derive\"] }\npkarr = { version = \"3.8.0\"}\ndomain = {version = \"0.11.0\", features = [\"zonefile\", \"bytes\"]}\nbytes = \"1.7.1\"\nchrono = \"0.4.38\"\nshellexpand = \"3.1.0\"\nzbase32 = \"0.1.2\"\nctrlc = \"3.4.4\"\nreqwest = { version=\"0.12.12\", default-features = false, features = [\"json\", \"rustls-tls\", \"http2\"]}\nrand = {version = \"0.8\"}\nhex = \"0.4.3\"\n\n[dev-dependencies]\ntempfile = \"3.20.0\"\n"
  },
  {
    "path": "cli/sample/README.md",
    "content": "## pkdns-cli sample\n\nThis is an example on how to announce your own records on the mainline DHT.\n\n- `seed.txt` contains a 64 character hex encoded seed that the records are published under. You can generate one with `./pkdns-cli generate > seed.txt`.\n- `pkarr.zone` is a dns zone file without the SOA record.\n\nPublish the records by pointing to the seed and zone files.\n\n> ⚠️ pkdns caches DHT packets for at least 60 seconds to improve latency. Run your own instance with `pkdns --max-ttl 0` to disable caching.\n\n```bash\n$ ./pkdns-cli publish seed.txt pkarr.zone\n\nPacket 8qhdp5s8jjmxmqam3bpg9kzeg7x8teztuwrfgxw5ikn9z5bt15uy\nName                 TTL     Type   Data\n@                    60      A      127.0.0.1                \ndynv4                60      A      213.55.243.129           \ndynv6                60      AAAA   2a04:ee41:0:819d:1905:9907:f695:41f3\ntext                 60      TXT    hero=satoshi2  \n\nAnnounce every 60min. Stop with Ctrl-C...\n2024-08-05 14:30:03.612747 +02:00 Successfully announced.\n```\n\n## Verify the Public Key Domain\n\n- Lookup the domain on pkdns.net: https://pkdns.net/?id=8qhdp5s8jjmxmqam3bpg9kzeg7x8teztuwrfgxw5ikn9z5bt15uy\n- Lookup the domain with nslookup: `nslookup 8qhdp5s8jjmxmqam3bpg9kzeg7x8teztuwrfgxw5ikn9z5bt15uy 34.65.109.99`. 34.65.109.99 is the IP of our hosted pkdns server."
  },
  {
    "path": "cli/sample/pkarr.zone",
    "content": "$TTL 60\n\n@      IN    A      127.0.0.1\ndynv4  IN    A      {external_ipv4}\ndynv6  IN    AAAA   {external_ipv6}\n\ntext   IN    TXT    hero=satoshi2\n"
  },
  {
    "path": "cli/sample/seed.txt",
    "content": "292bbfb6a5bc1ec3f6d01a7c8e3fac1782d364ca41a1847497e9e3bc949dbb9a\n"
  },
  {
    "path": "cli/src/cli.rs",
    "content": "use crate::commands::{cli_publickey, generate::cli_generate_seed, publish::cli_publish, resolve::cli_resolve};\n\n/**\n * Main cli entry function.\n */\npub async fn run_cli() {\n    const VERSION: &str = env!(\"CARGO_PKG_VERSION\");\n\n    let cmd = clap::Command::new(\"pkdns-cli\")\n        .version(VERSION)\n        .arg_required_else_help(true)\n        .subcommand(\n            clap::Command::new(\"publish\")\n                .about(\"Publish pkarr dns records.\")\n                .arg(\n                    clap::Arg::new(\"seed\")\n                        .help(\"File path to the seed file.\")\n                        .default_value(\"./seed.txt\"),\n                )\n                .arg(\n                    clap::Arg::new(\"zonefile\")\n                        .help(\"File path to the dns zone file.\")\n                        .default_value(\"./pkarr.zone\"),\n                ),\n        )\n        .subcommand(\n            clap::Command::new(\"resolve\")\n                .about(\"Resolve a public key domain on the DHT.\")\n                .arg_required_else_help(true)\n                .arg(clap::Arg::new(\"pubkey\").required(false).help(\"Public Key Domain\")),\n        )\n        .subcommand(clap::Command::new(\"generate\").about(\"Generate a new seed\"))\n        .subcommand(\n            clap::Command::new(\"publickey\")\n                .about(\"Derive the public key from the seed.\")\n                .arg(\n                    clap::Arg::new(\"seed\")\n                        .required(false)\n                        .help(\"File path to the pkarr seed file.\")\n                        .default_value(\"./seed.txt\"),\n                ),\n        );\n    let matches = cmd.get_matches();\n\n    match matches.subcommand() {\n        Some((\"resolve\", matches)) => {\n            cli_resolve(matches).await;\n        }\n        Some((\"publish\", matches)) => {\n            cli_publish(matches).await;\n        }\n        Some((\"generate\", matches)) => {\n            cli_generate_seed(matches).await;\n        }\n        Some((\"publickey\", matches)) => {\n            cli_publickey(matches).await;\n        }\n        _ => {\n            unimplemented!(\"command not implemented\")\n        }\n    };\n}\n"
  },
  {
    "path": "cli/src/commands/generate.rs",
    "content": "use clap::ArgMatches;\nuse pkarr::Keypair;\n\npub async fn cli_generate_seed(_matches: &ArgMatches) {\n    let keypair = Keypair::random();\n    let encoded = hex::encode(keypair.secret_key());\n    println!(\"{encoded}\");\n}\n"
  },
  {
    "path": "cli/src/commands/mod.rs",
    "content": "pub mod generate;\nmod publickey;\npub mod publish;\npub mod resolve;\n\npub use publickey::cli_publickey;\n"
  },
  {
    "path": "cli/src/commands/publickey.rs",
    "content": "use clap::ArgMatches;\nuse pkarr::Keypair;\nuse std::{\n    fs::read_to_string,\n    path::{Path, PathBuf},\n};\n\nconst SECRET_KEY_LENGTH: usize = 32;\n\nfn read_seed_file(matches: &ArgMatches) -> Keypair {\n    let unexpanded_path: &String = matches.get_one(\"seed\").unwrap();\n    let expanded_path: String = shellexpand::full(unexpanded_path).expect(\"Valid shell path.\").into();\n    let path = Path::new(&expanded_path);\n    let path = PathBuf::from(path);\n\n    let seed = read_to_string(path);\n    if let Err(e) = seed {\n        eprintln!(\"Failed to read seed at {expanded_path}. {e}\");\n        std::process::exit(1);\n    };\n    let seed = seed.unwrap();\n    parse_seed(&seed)\n}\n\nfn parse_seed(seed: &str) -> Keypair {\n    let seed = seed.trim();\n    let decode_result = zbase32::decode_full_bytes_str(seed);\n    if let Err(e) = decode_result {\n        eprintln!(\"Failed to parse the seed file. {e} {seed}\");\n        std::process::exit(1);\n    };\n\n    let plain_secret = decode_result.unwrap();\n\n    let slice: &[u8; SECRET_KEY_LENGTH] = &plain_secret[0..SECRET_KEY_LENGTH].try_into().unwrap();\n\n    Keypair::from_secret_key(slice)\n}\n\npub async fn cli_publickey(matches: &ArgMatches) {\n    let keypair = read_seed_file(matches);\n    let pubkey = keypair.to_z32();\n\n    println!(\"{pubkey}\");\n}\n"
  },
  {
    "path": "cli/src/commands/publish.rs",
    "content": "use std::io::Write;\nuse std::{\n    fs::read_to_string,\n    path::{Path, PathBuf},\n};\n\nuse anyhow::anyhow;\n\nuse clap::ArgMatches;\nuse pkarr::{Keypair, SignedPacket, Timestamp};\n\nuse crate::external_ip::{resolve_ipv4, resolve_ipv6};\nuse crate::helpers::nts_to_chrono;\nuse crate::{helpers::construct_pkarr_client, simple_zone::SimpleZone};\n\nconst SECRET_KEY_LENGTH: usize = 32;\n\n/// Replaces {externl_ipv4} and {external_ipv6} variables in the zone file\n/// with the according ips.\n/// Errors if ips can't be resolved.\nasync fn fill_dyndns_variables(zone: &mut String) -> Result<(), anyhow::Error> {\n    if zone.contains(\"{external_ipv4}\") {\n        let ip_result = resolve_ipv4().await;\n        if let Err(e) = ip_result {\n            return Err(anyhow!(e));\n        }\n        let (external_ipv4, _) = ip_result.unwrap();\n        *zone = zone.replace(\"{external_ipv4}\", &external_ipv4.to_string());\n    };\n\n    if zone.contains(\"{external_ipv6}\") {\n        let ip_result = resolve_ipv6().await;\n        if let Err(e) = ip_result {\n            return Err(anyhow!(e));\n        }\n        let (external_ipv6, _) = ip_result.unwrap();\n        *zone = zone.replace(\"{external_ipv6}\", &external_ipv6.to_string());\n    };\n\n    Ok(())\n}\n\nasync fn read_zone_file(zone_file_path: &str, pubkey: &str) -> SimpleZone {\n    let csv_path_str: String = shellexpand::full(zone_file_path).expect(\"Valid shell path.\").into();\n    let path = Path::new(&csv_path_str);\n    let path = PathBuf::from(path);\n\n    let zone = read_to_string(path);\n    if let Err(e) = zone {\n        eprintln!(\"Failed to read zone at {csv_path_str}. {e}\");\n        std::process::exit(1);\n    };\n    let mut zone = zone.unwrap();\n    if let Err(e) = fill_dyndns_variables(&mut zone).await {\n        panic!(\"Failed to fetch external ips. {e}\");\n    };\n\n    let zone = SimpleZone::read(zone, pubkey);\n    if let Err(e) = zone {\n        eprintln!(\"Failed to parse zone file. {e}\");\n        std::process::exit(1);\n    };\n    zone.unwrap()\n}\n\nfn read_seed_file(seed_file_path: &str) -> Keypair {\n    let expanded_path: String = shellexpand::full(seed_file_path).expect(\"Valid shell path.\").into();\n    let path = Path::new(&expanded_path);\n    let path = PathBuf::from(path);\n\n    let seed = std::fs::read_to_string(path);\n    if let Err(e) = seed {\n        eprintln!(\"Failed to read seed at {expanded_path}. {e}\");\n        std::process::exit(1);\n    };\n    let seed = seed.unwrap();\n    match parse_seed(&seed) {\n        Ok(keypair) => keypair,\n        Err(e) => {\n            eprintln!(\"Failed to parse the seed file. {e} {seed}\");\n            std::process::exit(1);\n        }\n    }\n}\n\n/// Parse a seed file into a keypair.\n/// Tries to parse as hex first, then as zbase32.\n/// Errors if the seed is not valid.\nfn parse_seed(seed: &str) -> anyhow::Result<Keypair> {\n    let seed = seed.trim();\n\n    if seed.len() == 52 {\n        return parse_seed_zbase32(seed);\n    }\n\n    parse_seed_hex(seed)\n}\n\n/// Parse a hex seed into a keypair.\n/// The seed is expected to be 64 characters long.\n/// This is the new format of the seed.\nfn parse_seed_hex(seed: &str) -> anyhow::Result<Keypair> {\n    let decode_result = hex::decode(seed)?;\n    let slice: &[u8; SECRET_KEY_LENGTH] = &decode_result[0..SECRET_KEY_LENGTH].try_into()?;\n    Ok(Keypair::from_secret_key(slice))\n}\n\n/// Parse a zbase32 seed into a keypair.\n/// The seed is expected to be 52 characters long.\n/// This is the old format of the seed.\nfn parse_seed_zbase32(seed: &str) -> anyhow::Result<Keypair> {\n    let decode_result = match zbase32::decode_full_bytes_str(seed) {\n        Ok(bytes) => bytes,\n        Err(_) => return Err(anyhow!(\"Invalid zbase32 seed\")),\n    };\n    let slice: &[u8; SECRET_KEY_LENGTH] = &decode_result[0..SECRET_KEY_LENGTH].try_into()?;\n    Ok(Keypair::from_secret_key(slice))\n}\n\npub async fn cli_publish(matches: &ArgMatches) {\n    let seed_file_path: &String = matches.get_one(\"seed\").expect(\"--seed file path is required\");\n    let zone_file_path: &String = matches.get_one(\"zonefile\").expect(\"--zonefile file path is required\");\n\n    let keypair = read_seed_file(seed_file_path.as_str());\n    let pubkey = keypair.to_z32();\n    let client = construct_pkarr_client();\n\n    let zone = read_zone_file(zone_file_path.as_str(), &pubkey).await;\n    println!(\"{}\", zone.packet);\n    let packet = zone.packet.parsed();\n    let packet = SignedPacket::new(&keypair, &packet.answers, Timestamp::now());\n    if let Err(e) = packet {\n        eprintln!(\"Failed to sign the pkarr packet. {e}\");\n        std::process::exit(1);\n    }\n    let packet = packet.unwrap();\n\n    print!(\"Hang on... {}\", nts_to_chrono(packet.timestamp()));\n    std::io::stdout().flush().unwrap();\n    let result = client.publish(&packet, None).await;\n    print!(\"\\r\");\n    match result {\n        Ok(_) => {\n            println!(\"{} Successfully announced.\", nts_to_chrono(packet.timestamp()))\n        }\n        Err(e) => {\n            println!(\"Error {}\", e)\n        }\n    };\n}\n"
  },
  {
    "path": "cli/src/commands/resolve.rs",
    "content": "use chrono::{DateTime, Utc};\nuse clap::ArgMatches;\nuse pkarr::PublicKey;\n\nuse crate::{\n    helpers::{construct_pkarr_client, nts_to_chrono},\n    pkarr_packet::PkarrPacket,\n};\n\nasync fn resolve_pkarr(uri: &str) -> (PkarrPacket, DateTime<Utc>) {\n    let client = construct_pkarr_client();\n    let pubkey: PublicKey = uri.try_into().expect(\"Should be valid pkarr public key.\");\n    let res = client.resolve_most_recent(&pubkey).await;\n    if res.is_none() {\n        println!(\"Failed to find the packet.\");\n        return (PkarrPacket::empty(), DateTime::<Utc>::MIN_UTC);\n    };\n    let signed_packet = res.unwrap();\n    let timestamp = nts_to_chrono(signed_packet.timestamp());\n\n    let data = signed_packet.encoded_packet();\n\n    (PkarrPacket::by_data(data.to_vec()), timestamp)\n}\n\nfn get_arg_pubkey(matches: &ArgMatches) -> Option<PublicKey> {\n    let uri_arg: &String = matches.get_one(\"pubkey\").unwrap();\n    let trying: Result<PublicKey, _> = uri_arg.as_str().try_into();\n    trying.ok()\n}\n\npub async fn cli_resolve(matches: &ArgMatches) {\n    let pubkey_opt = get_arg_pubkey(matches);\n\n    if pubkey_opt.is_none() {\n        eprintln!(\"pubkey is not a valid pkarr public key.\");\n        std::process::exit(1);\n    };\n    let pubkey = pubkey_opt.unwrap();\n    let uri = pubkey.to_uri_string();\n\n    println!(\"Resolve dns records of {}\", uri);\n    let (packet, timestamp) = resolve_pkarr(&uri).await;\n\n    println!(\"{packet}\");\n    if !packet.is_emtpy() {\n        println!(\"Last updated at: {timestamp}\");\n    };\n}\n"
  },
  {
    "path": "cli/src/external_ip/mod.rs",
    "content": "mod providers;\nmod resolver;\n\npub use resolver::{resolve_ipv4, resolve_ipv6};\n"
  },
  {
    "path": "cli/src/external_ip/providers/external_ip_resolver.rs",
    "content": "use std::{\n    future::Future,\n    net::{AddrParseError, Ipv4Addr, Ipv6Addr},\n    pin::Pin,\n};\n\nuse reqwest::IntoUrl;\n\ntype ExternalIpResult<T> = Result<T, ExternalIpResolverError>;\n\ntype IpFuture<T> = Pin<Box<dyn Future<Output = ExternalIpResult<T>>>>;\n\ntype IpResolver<T> = Pin<Box<dyn Fn() -> IpFuture<T>>>;\n\npub struct ProviderResolver {\n    pub name: String,\n    ipv4: IpResolver<Ipv4Addr>,\n    ipv6: IpResolver<Ipv6Addr>,\n}\n\nimpl ProviderResolver {\n    pub fn new(name: String, ipv4: IpResolver<Ipv4Addr>, ipv6: IpResolver<Ipv6Addr>) -> Self {\n        Self { name, ipv4, ipv6 }\n    }\n\n    /// Resolve this computers external ipv4 address.\n    pub async fn ipv4(&self) -> Result<Ipv4Addr, ExternalIpResolverError> {\n        let func = &self.ipv4;\n        func().await\n    }\n\n    /// Resolve this computers external ipv6 address.\n    pub async fn ipv6(&self) -> Result<Ipv6Addr, ExternalIpResolverError> {\n        let func = &self.ipv6;\n        func().await\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum ExternalIpResolverError {\n    #[error(transparent)]\n    IO(#[from] reqwest::Error),\n\n    #[error(transparent)]\n    IpParse(#[from] AddrParseError),\n}\n\n/// Resolves a url return the Ipv4 in it's response.\npub async fn resolve_ipv4_with_url<T: IntoUrl>(url: T) -> Result<Ipv4Addr, ExternalIpResolverError> {\n    let response = reqwest::get(url).await?;\n    let text = response.text().await?;\n    let text = text.trim();\n    let ip: Ipv4Addr = text.parse()?;\n    Ok(ip)\n}\n\n/// Resolves a url return the Ipv6 in it's response.\npub async fn resolve_ipv6_with_url<T: IntoUrl>(url: T) -> Result<Ipv6Addr, ExternalIpResolverError> {\n    let response = reqwest::get(url).await?;\n    let text = response.text().await?;\n    let text = text.trim();\n    let ip: Ipv6Addr = text.parse()?;\n    Ok(ip)\n}\n"
  },
  {
    "path": "cli/src/external_ip/providers/icanhazip.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\n\nuse super::external_ip_resolver::{\n    resolve_ipv4_with_url, resolve_ipv6_with_url, ExternalIpResolverError, ProviderResolver,\n};\n\npub async fn resolve_ipv4() -> Result<Ipv4Addr, ExternalIpResolverError> {\n    resolve_ipv4_with_url(\"https://ipv4.icanhazip.com\").await\n}\n\npub async fn resolve_ipv6() -> Result<Ipv6Addr, ExternalIpResolverError> {\n    resolve_ipv6_with_url(\"https://ipv6.icanhazip.com\").await\n}\n\npub fn get_resolver() -> ProviderResolver {\n    ProviderResolver::new(\n        \"icanhazip.com\".to_string(),\n        Box::pin(move || Box::pin(resolve_ipv4())),\n        Box::pin(move || Box::pin(resolve_ipv6())),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        assert!(ip.is_ok());\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        assert!(ip.is_ok());\n    }\n}\n"
  },
  {
    "path": "cli/src/external_ip/providers/identme.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\n\nuse super::external_ip_resolver::{\n    resolve_ipv4_with_url, resolve_ipv6_with_url, ExternalIpResolverError, ProviderResolver,\n};\n\npub async fn resolve_ipv4() -> Result<Ipv4Addr, ExternalIpResolverError> {\n    resolve_ipv4_with_url(\"https://v4.ident.me\").await\n}\n\npub async fn resolve_ipv6() -> Result<Ipv6Addr, ExternalIpResolverError> {\n    resolve_ipv6_with_url(\"https://v6.ident.me\").await\n}\n\npub fn get_resolver() -> ProviderResolver {\n    ProviderResolver::new(\n        \"ident.me\".to_string(),\n        Box::pin(move || Box::pin(resolve_ipv4())),\n        Box::pin(move || Box::pin(resolve_ipv6())),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        assert!(ip.is_ok());\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        assert!(ip.is_ok());\n    }\n}\n"
  },
  {
    "path": "cli/src/external_ip/providers/ipifyorg.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\n\nuse super::external_ip_resolver::{\n    resolve_ipv4_with_url, resolve_ipv6_with_url, ExternalIpResolverError, ProviderResolver,\n};\n\npub async fn resolve_ipv4() -> Result<Ipv4Addr, ExternalIpResolverError> {\n    resolve_ipv4_with_url(\"https://api.ipify.org\").await\n}\n\npub async fn resolve_ipv6() -> Result<Ipv6Addr, ExternalIpResolverError> {\n    resolve_ipv6_with_url(\"https://api6.ipify.org\").await\n}\n\npub fn get_resolver() -> ProviderResolver {\n    ProviderResolver::new(\n        \"ipify.org\".to_string(),\n        Box::pin(move || Box::pin(resolve_ipv4())),\n        Box::pin(move || Box::pin(resolve_ipv6())),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        assert!(ip.is_ok());\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        assert!(ip.is_ok());\n    }\n}\n"
  },
  {
    "path": "cli/src/external_ip/providers/ipinfoio.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\n\nuse serde::Deserialize;\n\nuse super::external_ip_resolver::{ExternalIpResolverError, ProviderResolver};\n\n#[derive(Debug, Deserialize)]\nstruct IpResponse {\n    pub ip: String,\n}\n\npub async fn resolve_ipv4() -> Result<Ipv4Addr, ExternalIpResolverError> {\n    let response = reqwest::get(\"https://ipinfo.io\").await?;\n    let text = response.json::<IpResponse>().await?;\n    let ip: Ipv4Addr = text.ip.parse()?;\n    Ok(ip)\n}\n\npub async fn resolve_ipv6() -> Result<Ipv6Addr, ExternalIpResolverError> {\n    let response = reqwest::get(\"https://v6.ipinfo.io\").await?;\n    let text = response.json::<IpResponse>().await?;\n    let ip: Ipv6Addr = text.ip.parse()?;\n    Ok(ip)\n}\n\npub fn get_resolver() -> ProviderResolver {\n    ProviderResolver::new(\n        \"ipinfo.io\".to_string(),\n        Box::pin(move || Box::pin(resolve_ipv4())),\n        Box::pin(move || Box::pin(resolve_ipv6())),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        assert!(ip.is_ok());\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        assert!(ip.is_ok());\n    }\n}\n"
  },
  {
    "path": "cli/src/external_ip/providers/mod.rs",
    "content": "pub mod icanhazip;\npub mod identme;\npub mod ipifyorg;\npub mod ipinfoio;\npub mod myip;\n\nmod external_ip_resolver;\npub use external_ip_resolver::ProviderResolver;\n"
  },
  {
    "path": "cli/src/external_ip/providers/myip.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\n\nuse serde::Deserialize;\n\nuse super::external_ip_resolver::{ExternalIpResolverError, ProviderResolver};\n\n#[derive(Debug, Deserialize)]\nstruct IpResponse {\n    pub ip: String,\n}\n\npub async fn resolve_ipv4() -> Result<Ipv4Addr, ExternalIpResolverError> {\n    let response = reqwest::get(\"https://4.myip.is/\").await?;\n    let text = response.json::<IpResponse>().await?;\n    let ip: Ipv4Addr = text.ip.parse()?;\n    Ok(ip)\n}\n\npub async fn resolve_ipv6() -> Result<Ipv6Addr, ExternalIpResolverError> {\n    let response = reqwest::get(\"https://6.myip.is/\").await?;\n    let text = response.json::<IpResponse>().await?;\n    let ip: Ipv6Addr = text.ip.parse()?;\n    Ok(ip)\n}\n\npub fn get_resolver() -> ProviderResolver {\n    ProviderResolver::new(\n        \"myip.is\".to_string(),\n        Box::pin(move || Box::pin(resolve_ipv4())),\n        Box::pin(move || Box::pin(resolve_ipv6())),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        assert!(ip.is_ok());\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        assert!(ip.is_ok());\n    }\n}\n"
  },
  {
    "path": "cli/src/external_ip/resolver.rs",
    "content": "use rand::seq::SliceRandom;\nuse rand::thread_rng;\nuse std::net::{Ipv4Addr, Ipv6Addr};\n\nuse super::providers::ProviderResolver;\nuse super::providers::{icanhazip, identme, ipifyorg, ipinfoio, myip};\n\n/// Resolves the external IPv4 address randomly from a list of 5 service providers.\n/// Returns IP and the name of the service provider.\npub async fn resolve_ipv4() -> Result<(Ipv4Addr, String), &'static str> {\n    let mut providers: Vec<ProviderResolver> = vec![\n        icanhazip::get_resolver(),\n        identme::get_resolver(),\n        ipifyorg::get_resolver(),\n        ipinfoio::get_resolver(),\n        myip::get_resolver(),\n    ];\n\n    let mut rng = thread_rng();\n    providers.shuffle(&mut rng);\n\n    for provider in providers {\n        match provider.ipv4().await {\n            Ok(ip) => return Ok((ip, provider.name.clone())),\n            Err(e) => {\n                println!(\"Failed to fetch ip from {}. {e}\", provider.name);\n            }\n        }\n    }\n\n    Err(\"All ip providers failed to return the external ip.\")\n}\n\n/// Resolves the external IPv6 address randomly from a list of 5 service providers.\n/// Returns IP and the name of the service provider.\npub async fn resolve_ipv6() -> Result<(Ipv6Addr, String), &'static str> {\n    let mut providers: Vec<ProviderResolver> = vec![\n        icanhazip::get_resolver(),\n        identme::get_resolver(),\n        ipifyorg::get_resolver(),\n        ipinfoio::get_resolver(),\n        myip::get_resolver(),\n    ];\n\n    let mut rng = thread_rng();\n    providers.shuffle(&mut rng);\n\n    for provider in providers {\n        match provider.ipv6().await {\n            Ok(ip) => return Ok((ip, provider.name.clone())),\n            Err(e) => {\n                println!(\"Failed to fetch ip from {}. {e}\", provider.name);\n            }\n        }\n    }\n\n    Err(\"All ip providers failed to return the external ip.\")\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn test_ipv4() {\n        let ip = resolve_ipv4().await;\n        println!(\"{:?}\", ip.expect(\"Valid ipv4\"));\n    }\n\n    #[ignore = \"Github runners don't support ipv6 request.\"]\n    #[tokio::test]\n    async fn test_ipv6() {\n        let ip = resolve_ipv6().await;\n        println!(\"{:?}\", ip.expect(\"Valid ipv6\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/helpers.rs",
    "content": "use pkarr::{Client, Timestamp};\n\n/// Construct new pkarr client with no cache, no resolver, only DHT\npub fn construct_pkarr_client() -> Client {\n    let client = Client::builder().maximum_ttl(0).build().unwrap();\n    client\n}\n\n// Turns a pkarr ntimestamp into a chrono timestamp\npub fn nts_to_chrono(ntc: Timestamp) -> chrono::DateTime<chrono::Utc> {\n    chrono::DateTime::from_timestamp((ntc.as_u64() / 1000000).try_into().unwrap(), 0).unwrap()\n}\n"
  },
  {
    "path": "cli/src/main.rs",
    "content": "use cli::run_cli;\n\nmod cli;\n\nmod commands;\nmod external_ip;\nmod helpers;\nmod pkarr_packet;\nmod simple_zone;\n\n#[tokio::main]\nasync fn main() -> Result<(), anyhow::Error> {\n    run_cli().await;\n    Ok(())\n}\n"
  },
  {
    "path": "cli/src/pkarr_packet.rs",
    "content": "use core::fmt;\nuse std::net::{Ipv4Addr, Ipv6Addr};\n\nuse anyhow::anyhow;\nuse pkarr::dns::{rdata::RData, Name, Packet, ResourceRecord};\n\n/**\n * Full pkarr dns packet. All data is saved in the answers.\n */\n#[derive(Debug)]\npub struct PkarrPacket {\n    pub data: Vec<u8>,\n}\n\nimpl PkarrPacket {\n    pub fn empty() -> Self {\n        let packet = Packet::new_reply(0);\n        let data = packet.build_bytes_vec_compressed().unwrap();\n        Self { data }\n    }\n\n    pub fn by_data(data: Vec<u8>) -> Self {\n        Self { data }\n    }\n\n    pub fn parsed(&self) -> Packet {\n        Packet::parse(&self.data).unwrap()\n    }\n\n    pub fn to_records(&self) -> Vec<PkarrRecord> {\n        self.parsed()\n            .answers\n            .iter()\n            .map(|answer| PkarrRecord::by_resource_record(answer))\n            .collect()\n    }\n\n    pub fn answers_len(&self) -> usize {\n        self.parsed().answers.len()\n    }\n\n    pub fn is_emtpy(&self) -> bool {\n        self.answers_len() == 0\n    }\n}\n\nimpl fmt::Display for PkarrPacket {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        if self.answers_len() == 0 {\n            writeln!(f, \"Packet is empty.\")?;\n            return Ok(());\n        };\n        let records = self.to_records();\n        writeln!(f, \"Packet {}\", records.first().unwrap().pubkey())?;\n        writeln!(f, \"Name TTL Type Data\")?;\n        for record in self.to_records() {\n            writeln!(f, \"{record}\")?;\n        }\n        Ok(())\n    }\n}\n\n/**\n * Simple Pkarr record\n */\npub struct PkarrRecord {\n    pub data: Vec<u8>,\n}\n\nimpl PkarrRecord {\n    #[allow(dead_code)]\n    pub fn by_data(data: Vec<u8>) -> Result<Self, anyhow::Error> {\n        let result = Packet::parse(&data);\n        if let Err(e) = result {\n            return Err(e.into());\n        }\n        let packet = result.unwrap();\n        if packet.answers.len() != 1 {\n            return Err(anyhow!(\"packet data must contain 1 answer.\"));\n        }\n\n        Ok(Self { data })\n    }\n\n    pub fn by_resource_record(rr: &ResourceRecord) -> Self {\n        let rr = rr.clone();\n        let mut packet = Packet::new_reply(0);\n        packet.answers.push(rr);\n        Self {\n            data: packet.build_bytes_vec_compressed().unwrap(),\n        }\n    }\n\n    pub fn get_resource_record(&self) -> ResourceRecord {\n        let packet = Packet::parse(&self.data).unwrap();\n        packet.answers[0].clone()\n    }\n\n    pub fn pubkey(&self) -> String {\n        let rr = self.get_resource_record();\n        let pubkey = rr.name.get_labels().last().unwrap();\n        pubkey.to_string()\n    }\n\n    pub fn name(&self) -> String {\n        let pubkey = self.pubkey();\n\n        let name = Name::try_from(pubkey.as_str()).unwrap();\n        let rr = self.get_resource_record();\n        let name = rr.name.without(&name);\n        match name {\n            Some(n) => n.to_string(),\n            None => \"@\".to_string(),\n        }\n    }\n\n    pub fn ttl(&self) -> u32 {\n        let rr = self.get_resource_record();\n        rr.ttl\n    }\n\n    pub fn data_as_strings(&self) -> (&str, String) {\n        let (record_type, data) = match self.get_resource_record().rdata {\n            RData::A(a) => {\n                let ipv4 = Ipv4Addr::from(a.address);\n                (\"A\", ipv4.to_string())\n            }\n            RData::AAAA(val) => {\n                let ipv6 = Ipv6Addr::from(val.address);\n                (\"AAAA\", ipv6.to_string())\n            }\n            RData::CNAME(val) => {\n                let data = val.to_string();\n                (\"CNAME\", data)\n            }\n            RData::MX(val) => {\n                let data = format!(\"{} - {}\", val.preference, val.exchange);\n                (\"MX\", data)\n            }\n            RData::TXT(val) => {\n                let data = val\n                    .attributes()\n                    .iter()\n                    .map(|(key, val)| {\n                        if val.is_some() {\n                            format!(\"{}={}\", key, val.clone().unwrap())\n                        } else {\n                            format!(\"{}=\", key)\n                        }\n                    })\n                    .collect::<Vec<String>>()\n                    .join(\", \");\n                (\"TXT\", data)\n            }\n            RData::NS(val) => {\n                let data = val.to_string();\n                (\"NS\", data)\n            }\n            _ => (\"Unknown\", \"Unknown\".to_string()),\n        };\n        (record_type, data)\n    }\n}\n\nimpl fmt::Display for PkarrRecord {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let name = self.name();\n        let ttl = self.ttl();\n        let data = self.data_as_strings();\n        write!(f, \"{0: <20} {1: <7} {2: <6} {3: <25}\", name, ttl, data.0, data.1)\n    }\n}\n"
  },
  {
    "path": "cli/src/simple_zone.rs",
    "content": "use anyhow::anyhow;\nuse domain::{\n    base::Rtype,\n    zonefile::inplace::{Entry, Zonefile},\n};\nuse pkarr::dns::{Name, Packet, ResourceRecord};\nuse std::io::Cursor;\n\nuse crate::pkarr_packet::PkarrPacket;\n\n/**\n * Reads a dns zone file without the otherwise necessary SOA entry.\n */\n#[derive(Debug)]\npub struct SimpleZone {\n    pub packet: PkarrPacket,\n}\n\nimpl SimpleZone {\n    /**\n     * Read the zone data. Pkarr pubkey must be provided in zbase32 format.\n     */\n    pub fn read(simplified_zone: String, pubkey: &str) -> Result<Self, anyhow::Error> {\n        let entries = Self::parse_simplified_zone(simplified_zone, pubkey)?;\n        let packet = Self::entries_to_simple_dns_packet(entries)?;\n        Ok(Self {\n            packet: PkarrPacket { data: packet },\n        })\n    }\n\n    /**\n     * Generate a fake soa entry to simplify the\n     * zone file the user needs to write.\n     */\n    fn generate_soa(pubkey: &str) -> String {\n        let formatted = format!(\n            \"$ORIGIN {pubkey}. \n$TTL 300\n@\tIN\tSOA\t127.0.0.1.\thostmaster.example.com. (\n\t\t\t2001062501 ; serial                     \n\t\t\t21600      ; refresh after 6 hours                     \n\t\t\t3600       ; retry after 1 hour                     \n\t\t\t604800     ; expire after 1 week                     \n\t\t\t300 )    ; minimum TTL of 1 day  \n            \"\n        );\n        formatted\n    }\n\n    /**\n     * Parses the zone data. Returns domain::Entry list.\n     */\n    fn parse_simplified_zone(simplified_zone: String, pubkey: &str) -> Result<Vec<Entry>, anyhow::Error> {\n        let raw_soa = SimpleZone::generate_soa(pubkey);\n        let zone = format!(\"{raw_soa}\\n{simplified_zone}\\n\");\n\n        let byte_data = zone.into_bytes();\n        let mut cursor = Cursor::new(byte_data);\n        let zone = Zonefile::load(&mut cursor)?;\n\n        let mut entries: Vec<Entry> = vec![];\n        for entry_res in zone.into_iter() {\n            let entry = entry_res?;\n\n            let should_include: bool = match entry.clone() {\n                Entry::Record(val) => val.rtype() != Rtype::SOA,\n                _ => false,\n            };\n            if should_include {\n                entries.push(entry);\n            }\n        }\n        Ok(entries)\n    }\n\n    /**\n     * Converts domain::Entry to simple-dns packet bytes.\n     */\n    fn entries_to_simple_dns_packet(entries: Vec<Entry>) -> Result<Vec<u8>, anyhow::Error> {\n        let mut packets = vec![];\n        for entry in entries.iter() {\n            let entry = entry.clone();\n            let packet = match entry {\n                Entry::Include { .. } => continue,\n                Entry::Record(val) => {\n                    let ttl = val.ttl().as_secs();\n                    let (name, data) = val.clone().into_owner_and_data();\n                    let simple_name_str = name.to_string();\n                    let simple_name = Name::try_from(simple_name_str.as_str())?;\n                    let simple_data = match data {\n                        domain::rdata::ZoneRecordData::A(val) => {\n                            let rdata: pkarr::dns::rdata::RData = pkarr::dns::rdata::RData::A(pkarr::dns::rdata::A {\n                                address: val.addr().into(),\n                            });\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n                        domain::rdata::ZoneRecordData::Aaaa(val) => {\n                            let rdata: pkarr::dns::rdata::RData =\n                                pkarr::dns::rdata::RData::AAAA(pkarr::dns::rdata::AAAA {\n                                    address: val.addr().into(),\n                                });\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n                        domain::rdata::ZoneRecordData::Ns(val) => {\n                            let ns_name = val.to_string();\n                            let rdata: pkarr::dns::rdata::RData =\n                                pkarr::dns::rdata::RData::NS(pkarr::dns::rdata::NS(Name::try_from(ns_name.as_str())?));\n\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n\n                        domain::rdata::ZoneRecordData::Txt(val) => {\n                            let mut txt = pkarr::dns::rdata::TXT::new();\n\n                            for bytes in val.iter() {\n                                let ascii = std::str::from_utf8(bytes).unwrap();\n                                txt.add_string(ascii)?;\n                            }\n                            let rdata: pkarr::dns::rdata::RData = pkarr::dns::rdata::RData::TXT(txt);\n\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n                        domain::rdata::ZoneRecordData::Mx(val) => {\n                            let exchange = val.exchange().to_string();\n                            let mx = pkarr::dns::rdata::MX {\n                                preference: val.preference(),\n                                exchange: Name::try_from(exchange.as_str())?,\n                            };\n\n                            let rdata: pkarr::dns::rdata::RData = pkarr::dns::rdata::RData::MX(mx);\n\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n                        domain::rdata::ZoneRecordData::Cname(val) => {\n                            let value = val.to_string();\n                            let value = Name::try_from(value.as_str()).unwrap();\n                            let rdata: pkarr::dns::rdata::RData =\n                                pkarr::dns::rdata::RData::CNAME(pkarr::dns::rdata::CNAME(value));\n                            let rr = ResourceRecord::new(simple_name, pkarr::dns::CLASS::IN, ttl, rdata);\n                            let mut packet = pkarr::dns::Packet::new_reply(0);\n                            packet.answers.push(rr);\n                            packet.build_bytes_vec_compressed()?\n                        }\n                        _ => return Err(anyhow!(\"Not support record type.\")),\n                    };\n                    simple_data\n                }\n            };\n            packets.push(packet);\n        }\n        let mut final_packet = Packet::new_reply(0);\n        for packet in packets.iter() {\n            let parsed = Packet::parse(packet)?;\n            for answer in parsed.answers {\n                final_packet.answers.push(answer)\n            }\n        }\n        Ok(final_packet.build_bytes_vec_compressed()?)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    // Note this useful idiom: importing names from outer (for mod tests) scope.\n    use super::*;\n\n    fn simplified_zone() -> String {\n        String::from(\n            \"\n@\tIN\tNS\tdns1.example.com. \n@ 400\tIN\tNS\tdns2.example.com.        \n\t\n\t\n@ 301\tIN\tMX\t10\tmail.example.com.       \n@\tIN\tMX\t20\tmail2.example.com.   \n\n@   IN  A 127.0.0.1\ntest   IN  A 127.0.0.1\n\n\t\ndns1\tIN\tA\t10.0.1.1\ndns2\tIN\tA\t10.0.1.2\n\ntext    IN  TXT  hero=satoshi \n\",\n        )\n    }\n\n    #[test]\n    fn test_create_entries() {\n        let simplified_zone = simplified_zone();\n        let zone = SimpleZone::read(simplified_zone, \"123456\");\n        let zone = zone.unwrap();\n        assert_eq!(zone.packet.parsed().answers.len(), 9);\n\n        println!(\"{}\", zone.packet);\n    }\n\n    #[test]\n    fn test_transform() {\n        let simplified_zone = simplified_zone();\n        let zone = SimpleZone::read(simplified_zone, \"123456\").unwrap();\n        let packet = zone.packet.parsed();\n\n        println!(\"{:#?}\", packet.answers);\n    }\n\n    #[test]\n    fn test_pkarr_records() {\n        let records = \"\n@\t\t\t\tIN \t\tA\t\t37.27.13.182\npknames.p2p\t\tIN \t\tA\t\t37.27.13.182\nwww.pknames.p2p\tIN\t\tCNAME\tpknames.p2p.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.\nsub\t\t\t\tIN\t\tNS\t\tns.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.\nns\t\t\t\tIN\t\tA\t\t95.217.214.181\ncname\t\t\tIN\t\tCNAME\texample.com.\n_text\t\t\tIN\t\tTXT\t\thero=satoshi\";\n\n        let zone = SimpleZone::read(records.to_string(), \"123456\").unwrap();\n        let packet = zone.packet.parsed();\n\n        println!(\"{:#?}\", packet.answers);\n    }\n\n    #[test]\n    fn test_read_zone_txt1() {\n        let raw_records = \"foo   IN  TXT   \\\"key1=1\\\" \\\"key2=2\\\"\";\n\n        let zone = SimpleZone::read(raw_records.to_string(), \"123456\").unwrap();\n        let packet = zone.packet.parsed();\n\n        let entry = packet.answers.first().unwrap();\n\n        match &entry.rdata {\n            pkarr::dns::rdata::RData::TXT(txt) => {\n                let value1 = txt.clone().attributes().get(\"key1\").unwrap().clone().unwrap();\n                assert_eq!(value1, \"1\");\n                let value2 = txt.clone().attributes().get(\"key2\").unwrap().clone().unwrap();\n                assert_eq!(value2, \"2\");\n            }\n            _ => panic!(\"Expected TXT record, got {:?}\", entry.rdata),\n        }\n    }\n\n    #[test]\n    fn test_read_zone_txt2() {\n        let raw_records = \"foo   IN  TXT   key=value\";\n\n        let zone = SimpleZone::read(raw_records.to_string(), \"123456\").unwrap();\n        let packet = zone.packet.parsed();\n        let entry = packet.answers.last().unwrap();\n\n        match &entry.rdata {\n            pkarr::dns::rdata::RData::TXT(txt) => {\n                let value = txt.clone().attributes().get(\"key\").unwrap().clone().unwrap();\n                assert_eq!(value, \"value\");\n            }\n            _ => panic!(\"Expected TXT record, got {:?}\", entry.rdata),\n        }\n    }\n\n    #[test]\n    fn test_read_zone_txt3() {\n        let raw_records = \"foo   IN  TXT   \\\"key=value\\\"\";\n\n        let zone = SimpleZone::read(raw_records.to_string(), \"123456\").unwrap();\n        let packet = zone.packet.parsed();\n        let entry = packet.answers.last().unwrap();\n\n        match &entry.rdata {\n            pkarr::dns::rdata::RData::TXT(txt) => {\n                let value = txt.clone().attributes().get(\"key\").unwrap().clone().unwrap();\n                assert_eq!(value, \"value\");\n            }\n            _ => panic!(\"Expected TXT record, got {:?}\", entry.rdata),\n        }\n    }\n}\n"
  },
  {
    "path": "compose.yaml",
    "content": "services:\n  pkdns:\n    image: \"synonymsoft/pkdns:latest\"\n    container_name: pkdns\n    ports: \n      - \"53:53/udp\"\n    restart: unless-stopped\n    command: [\"pkdns\"]\n    # Uncomment the volumes to persist the pkdns cache and config file on your disk permanently.\n    # Change `/my/pkdns/folder/location` to a path on your machine where you want the data saved in.\n    # volumes:\n    #  - /my/pkdns/folder/location:/root/.pkdns\n"
  },
  {
    "path": "docs/dns-over-https.md",
    "content": "# DNS over HTTPS (DoH)\n\n## What is DoH?\n\nDNS over HTTPS (DoH) encrypts DNS queries by sending them over HTTPS instead of plain UDP or TCP. This enhances privacy and security by preventing eavesdropping and tampering of DNS traffic.\n\nPopular web browsers like Firefox, Chrome, Brave, and Edge support DoH out of the box. It is a convenient way to enable Public Key Domains (PKD) in your browser without changing your system dns.\n\n## Use A Hosted DoH Server In Your Browser\n\n1. Pick a DNS-over-HTTPS URL from our public [servers.txt](../servers.txt) list.\n2. Configure your browser. See [this guide](https://support.privadovpn.com/kb/article/848-how-to-enable-doh-on-your-browser/).\n3. Test if everything is working with [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/).\n\n\n\n## Enable DoH In PKDNS\n\npkdns supports [RFC8484](https://datatracker.ietf.org/doc/html/rfc8484).\n\n1. Start pkdns with `dns_over_http_socket = \"127.0.0.1:3000\"` in pkdns.toml. This makes pkdns listen for HTTP (not HTTPS) requests on http://127.0.0.1/dns-query.\n2. Use a reverse proxy like NGINX to add HTTPS to the DoH socket. See this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04).\n3. Forward the nginx requests to pkdns. Example configuration:\n\n```\nlocation / {\n\tproxy_set_header X-Forwarded-For $remote_addr;\n\tproxy_pass http://127.0.0.1:3000;\n}\n```\n4. Configure your browser with your new doh url.\n5. Test if everything is working with [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/).\n\n\n\n"
  },
  {
    "path": "docs/dyn-dns.md",
    "content": "# DynDNS\n\nDynDNS (Dynamic Domain Name System) is a service that allows users to assign a fixed domain name to a device with a dynamic IP address. This is particularly useful for home networks, servers, or remote devices that do not have a static IP from their Internet Service Provider (ISP).\n\nKey Purposes:\n- Remote Access – Enables access to home or office networks remotely using a consistent domain name.\n- Hosting Services – Supports hosting websites, game servers, or other services from a dynamic IP.\n- Simplified Configuration – Automatically updates DNS records when the IP address changes, avoiding manual reconfiguration.\n\nEssentially, DynDNS bridges the gap between dynamic IP addresses and the need for reliable remote access.\n\n## How does PKDNS Enable DynDNS?\n\nWhile publishing the `pkarr.zone` with `pkdns-cli`, the cli replaces the variables `{external_ipv4}` and `{external_ipv6}` with your actual external IP address. In combination with publishing your Public Key Domain (PKD)\nevery 60 minutes, your PKD keeps pointing to the correct IP address.\n\n\nSee [cli/sample/pkarr.zone](../cli/sample/pkarr.zone) as an example to use your external IPv4 address and [this guide](https://medium.com/pubky/how-to-host-a-public-key-domain-website-v0-6-0-ubuntu-24-04-57e6f2cb6f77) on how to publish it."
  },
  {
    "path": "docs/logging.md",
    "content": "# Logging\n\nBy default pkdns stays silent. Use `--verbose` to make pkdns log all queries.\n\n## Advanced\n\nThe log output can be adjusted with the environment variable `RUST_LOG`. This is either done by setting the log level directly (`RUST_LOG=debug`) or by setting the log level for specific modules.\n\nExamples:\n\n- `RUST_LOG=pkdns=trace` will make pkdns very chatty.\n- `RUST_LOG=mainline=debug` will display mainline DHT logs.\n\nThese can also be combined: `RUST_LOG=pkdns=trace,mainline=trace`.\n\n### Interesting Logs\n\n- `RUST_LOG=pkdns=trace` Investigate pkdns.\n- `RUST_LOG=pkdns=debug,pkarr=debug,mainline=debug` Investigate the mainline DHT.\n\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "max_width = 120"
  },
  {
    "path": "server/Cargo.toml",
    "content": "[package]\nname = \"pkdns\"\nversion = \"0.7.1\"\nauthors = [\"SeverinAlexB <severin@synonym.to>\"]\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nctrlc = \"3.4.2\"\npkarr = { version = \"3.8.0\"}\nzbase32 = \"0.1.2\"\nclap = { version = \"4.4.18\", features = [\"derive\"] }\nchrono = \"0.4.33\"\ntokio = { version = \"1.36.0\", features = [\"full\"] }\nasync-trait = \"0.1.77\"\nanyhow = \"1.0.79\"\ntracing = \"0.1.40\"\ntracing-subscriber = {version = \"0.3.18\", features = [\"smallvec\", \"fmt\", \"ansi\", \"tracing-log\", \"std\", \"env-filter\"]}\nrustdns = \"0.4.0\"\nmoka = { version = \"0.12.8\", features = [\"future\"] }\n\ndyn-clone = \"1.0.16\"\nthiserror = \"1.0.56\"\ngovernor = \"0.7.0\"\naxum = { version = \"0.7.9\", features = [\"tokio\"]}\naxum-extra = { version = \"0.9.6\", features = [\"typed-header\"] }\ntower-http = { version = \"0.6.2\", features = [\"cors\"] }\nserde = {version = \"1.0.216\", features = [\"derive\"]}\nbase64 = \"0.22.1\"\ntoml = \"0.8.19\"\ndirs = \"5.0.1\"\nonce_cell = \"1.20.2\"\nrand = \"0.8\"\nself_cell = \"1.1.0\"\ntempfile = \"3.20.0\"\n\n\n[dev-dependencies]\naxum-test = \"16.4.1\"\ntracing-test = \"0.2.5\"\n\n"
  },
  {
    "path": "server/config.sample.toml",
    "content": "# PKDNS configuration file\n# More information on https://github.com/pubky/pkdns/server/sample-config.toml\n\n[general]\n# DNS UDP socket that pkdns is listening on.\nsocket = \"0.0.0.0:53\"\n\n# DNS server that pkdns is falling back to for regular ICANN queries.\nforward = \"8.8.8.8:53\"\n\n# [EXPERIMENTAL] Enables DNS over HTTP on the given socket. Default: Disabled. More info https://github.com/pubky/pkdns/blob/master/docs/dns-over-https.md\ndns_over_http_socket = \"127.0.0.1:3000\"\n\n# Verbose logging. See https://github.com/pubky/pkdns/blob/master/docs/logging.md\nverbose = false\n\n[dns]\n# Minimum number of seconds a value is cached for before being refreshed.\nmin_ttl = 60\n\n# Maximum number of seconds before a cached value gets auto-refreshed. Set to 0 to prevent caching.\nmax_ttl = 86400\n\n# Maximum number of queries per second one IP address can make before it is rate limited. 0 is disabled.\nquery_rate_limit = 100\n\n# Short term burst size of the query-rate-limit. 0 is disabled.\nquery_rate_limit_burst = 200\n\n# Disables ANY queries by silently dropping them. This is used to protect against DNS amplification attacks.\ndisable_any_queries = false\n\n# ICANN response cache size in megabytes.\nicann_cache_mb = 100\n\n# Maximum recursion depth\nmax_recursion_depth = 15\n\n# [dht]\n# Maximum size of the pkarr packet cache in megabytes.\ndht_cache_mb = 100\n\n# Maximum number of queries per second one IP address can make to the DHT before it is rate limited. 0 is disabled.\ndht_query_rate_limit = 5\n\n# Short term burst size of the dht-rate-limit. 0 is disabled.\ndht_query_rate_limit_burst = 25\n\n# Optional Top Level Domain for public key domains. Set to \"\" to disable.\ntop_level_domain = \"key\"\n"
  },
  {
    "path": "server/pkdns.service",
    "content": "# https://github.com/pubky/pkdns/blob/master/server/pkdns.service\n[Unit]\nDescription=pkdns - Self-Sovereign And Censorship-Resistant Domain Names\nAfter=network-online.target\n\n[Service]\n# Update the binary path. Add --verbose to the command if you want to have more insights.\nExecStart=/usr/local/bin/pkdns\nEnvironment=\"RUST_BACKTRACE=full\"\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "server/src/app_context.rs",
    "content": "use crate::config::{ConfigToml, DataDir};\n\n#[derive(Debug, Clone, Default)]\npub struct AppContext {\n    pub config: ConfigToml,\n}\n\nimpl AppContext {\n    pub fn from_data_dir(data_dir: impl DataDir) -> Result<Self, anyhow::Error> {\n        data_dir.ensure_data_dir_exists_and_is_writable()?;\n        let config = data_dir.read_or_create_config_file()?;\n        Ok(Self { config })\n    }\n\n    #[cfg(test)]\n    pub fn test() -> Self {\n        Self {\n            config: ConfigToml::test(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::config::MockDataDir;\n\n    use super::*;\n\n    #[test]\n    fn test_from_data_dir() {\n        let data_dir = MockDataDir::test();\n        let _ = AppContext::from_data_dir(data_dir).unwrap();\n    }\n}\n"
  },
  {
    "path": "server/src/config/config_file.rs",
    "content": "use serde::{Deserialize, Deserializer, Serialize};\nuse std::{fs, net::SocketAddr, num::NonZeroU64, path::Path};\n\nuse crate::config::TopLevelDomain;\n\n/// Error that can occur when reading a configuration file.\n#[derive(Debug, thiserror::Error)]\npub enum ConfigReadError {\n    /// The file did not exist or could not be read.\n    #[error(\"config file not found: {0}\")]\n    NotFound(#[from] std::io::Error),\n    /// The TOML was syntactically invalid.\n    #[error(\"config file is not valid TOML: {0}\")]\n    NotValid(#[from] toml::de::Error),\n}\n\n/// Example configuration file\npub const SAMPLE_CONFIG: &str = include_str!(\"../../config.sample.toml\");\n\n#[derive(Debug, Deserialize, Clone, Default)]\npub struct ConfigToml {\n    #[serde(default)]\n    pub general: General,\n    #[serde(default)]\n    pub dns: Dns,\n    #[serde(default)]\n    pub dht: Dht,\n}\n\nimpl ConfigToml {\n    /// Example configuration file as a string.\n    pub fn sample() -> String {\n        SAMPLE_CONFIG.to_string()\n    }\n\n    /// Render the embedded sample config but comment out every value,\n    /// producing a handy template for end-users.\n    pub fn commented_out_sample() -> String {\n        SAMPLE_CONFIG\n            .lines()\n            .map(|line| {\n                let trimmed = line.trim_start();\n                let is_comment = trimmed.starts_with('#');\n                if !is_comment && !trimmed.is_empty() {\n                    format!(\"# {}\", line)\n                } else {\n                    line.to_string()\n                }\n            })\n            .collect::<Vec<String>>()\n            .join(\"\\n\")\n    }\n\n    /// Read and parse a configuration file.\n    ///\n    /// # Arguments\n    /// * `path` - The path to the TOML configuration file\n    ///\n    /// # Returns\n    /// * `Result<ConfigToml>` - The parsed configuration or an error if reading/parsing fails\n    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadError> {\n        let raw = fs::read_to_string(path)?;\n        let config: ConfigToml = toml::from_str(&raw)?;\n        Ok(config)\n    }\n\n    #[cfg(test)]\n    pub fn test() -> Self {\n        let mut config = Self::default();\n        config.general.dns_over_http_socket = None;\n        config.general.socket = \"0.0.0.0:0\".parse().expect(\"Is always be a valid socket address\");\n        config\n    }\n}\n\nimpl TryFrom<&str> for ConfigToml {\n    type Error = ConfigReadError;\n    fn try_from(value: &str) -> Result<Self, Self::Error> {\n        let config: ConfigToml = toml::from_str(value)?;\n        Ok(config)\n    }\n}\n\n#[derive(Debug, Deserialize, Serialize, Clone)]\npub struct General {\n    #[serde(default = \"default_socket\")]\n    pub socket: SocketAddr,\n\n    #[serde(default = \"default_forward\")]\n    pub forward: SocketAddr,\n\n    #[serde(default = \"default_none\")]\n    pub dns_over_http_socket: Option<SocketAddr>,\n\n    #[serde(default = \"default_false\")]\n    pub verbose: bool,\n}\n\nimpl Default for General {\n    fn default() -> Self {\n        Self {\n            socket: default_socket(),\n            forward: default_forward(),\n            verbose: default_false(),\n            dns_over_http_socket: default_none(),\n        }\n    }\n}\n\nfn default_socket() -> SocketAddr {\n    \"0.0.0.0:53\".parse().unwrap()\n}\n\nfn default_forward() -> SocketAddr {\n    \"8.8.8.8:53\".parse().unwrap()\n}\n\nfn default_false() -> bool {\n    false\n}\n\nfn default_none() -> Option<SocketAddr> {\n    None\n}\n\n#[derive(Debug, Deserialize, Serialize, Clone)]\npub struct Dns {\n    #[serde(default = \"default_min_ttl\")]\n    pub min_ttl: u64,\n\n    #[serde(default = \"default_max_ttl\")]\n    pub max_ttl: u64,\n\n    #[serde(default = \"default_query_rate_limit\")]\n    pub query_rate_limit: u32,\n\n    #[serde(default = \"default_query_rate_limit_burst\")]\n    pub query_rate_limit_burst: u32,\n\n    #[serde(default = \"default_false\")]\n    pub disable_any_queries: bool,\n\n    #[serde(default = \"default_icann_cache_mb\")]\n    pub icann_cache_mb: u64,\n\n    #[serde(default = \"default_max_recursion_depth\")]\n    pub max_recursion_depth: u8,\n}\n\nimpl Default for Dns {\n    fn default() -> Self {\n        Self {\n            min_ttl: default_min_ttl(),\n            max_ttl: default_max_ttl(),\n            query_rate_limit: default_query_rate_limit(),\n            query_rate_limit_burst: default_query_rate_limit_burst(),\n            disable_any_queries: default_false(),\n            icann_cache_mb: default_icann_cache_mb(),\n            max_recursion_depth: default_max_recursion_depth(),\n        }\n    }\n}\n\nfn default_min_ttl() -> u64 {\n    60\n}\n\nfn default_max_ttl() -> u64 {\n    86400\n}\n\nfn default_query_rate_limit() -> u32 {\n    100\n}\n\nfn default_query_rate_limit_burst() -> u32 {\n    200\n}\n\nfn default_icann_cache_mb() -> u64 {\n    100\n}\n\nfn default_max_recursion_depth() -> u8 {\n    15\n}\n\n#[derive(Debug, Deserialize, Clone)]\npub struct Dht {\n    #[serde(default = \"default_cache_mb\")]\n    pub dht_cache_mb: NonZeroU64,\n    #[serde(default = \"default_dht_rate_limit\")]\n    pub dht_query_rate_limit: u32,\n    #[serde(default = \"default_dht_rate_limit_burst\")]\n    pub dht_query_rate_limit_burst: u32,\n    #[serde(\n        default = \"default_top_level_domain\",\n        deserialize_with = \"deserialize_top_level_domain\"\n    )]\n    pub top_level_domain: Option<TopLevelDomain>,\n}\n\nfn deserialize_top_level_domain<'de, D>(deserializer: D) -> Result<Option<TopLevelDomain>, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let value = Option::<String>::deserialize(deserializer)?;\n    if let Some(tld) = &value {\n        if tld.is_empty() {\n            return Ok(None);\n        }\n    }\n    Ok(value.map(TopLevelDomain::new))\n}\n\nfn default_cache_mb() -> NonZeroU64 {\n    NonZeroU64::new(100).expect(\"100 is a valid non-zero u64\")\n}\n\nfn default_dht_rate_limit() -> u32 {\n    5\n}\n\nfn default_dht_rate_limit_burst() -> u32 {\n    25\n}\n\nfn default_top_level_domain() -> Option<TopLevelDomain> {\n    Some(TopLevelDomain::new(\"key\".to_string()))\n}\n\nimpl Default for Dht {\n    fn default() -> Self {\n        Self {\n            dht_cache_mb: default_cache_mb(),\n            dht_query_rate_limit: default_dht_rate_limit(),\n            dht_query_rate_limit_burst: default_dht_rate_limit_burst(),\n            top_level_domain: default_top_level_domain(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_sample_config() {\n        let _: ConfigToml = toml::from_str(SAMPLE_CONFIG).expect(\"Sample config must be parseble\");\n    }\n\n    #[test]\n    fn test_commented_out_sample() {\n        let commented_out = ConfigToml::commented_out_sample();\n        println!(\"{commented_out}\");\n    }\n\n    #[test]\n    fn test_default_config_top_level_domain() {\n        let config_str = \"[dht]\\ntop_level_domain = \\\"\\\"\";\n        let config = ConfigToml::try_from(config_str).unwrap();\n        assert!(config.dht.top_level_domain.is_none());\n\n        let config_str = \"[dht]\\ntop_level_domain = \\\"test\\\"\";\n        let config = ConfigToml::try_from(config_str).unwrap();\n        assert_eq!(config.dht.top_level_domain.unwrap().0, \"test\".to_string());\n    }\n}\n"
  },
  {
    "path": "server/src/config/data_dir.rs",
    "content": "use dyn_clone::DynClone;\nuse std::path::Path;\n\nuse crate::config::ConfigToml;\n\n/// A trait for the data directory.\n/// Used to abstract the data directory from the rest of the code.\n///\n/// To create a real dir and a test dir.\npub trait DataDir: std::fmt::Debug + DynClone + Send + Sync {\n    /// Returns the path to the data directory.\n    fn path(&self) -> &Path;\n    /// Makes sure the data directory exists.\n    /// Create the directory if it doesn't exist.\n    fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()>;\n\n    /// Reads the config file from the data directory.\n    /// Creates a default config file if it doesn't exist.\n    fn read_or_create_config_file(&self) -> anyhow::Result<ConfigToml>;\n}\n\ndyn_clone::clone_trait_object!(DataDir);\n"
  },
  {
    "path": "server/src/config/mock_data_dir.rs",
    "content": "use std::path::Path;\n\nuse super::DataDir;\n\n/// Mock data directory for testing.\n///\n/// It uses a temporary directory to store all data in. The data is removed as soon as the object is dropped.\n///\n\n#[derive(Debug, Clone)]\npub struct MockDataDir {\n    pub(crate) temp_dir: std::sync::Arc<tempfile::TempDir>,\n    /// The configuration for the homeserver.\n    pub config_toml: super::ConfigToml,\n}\n\nimpl MockDataDir {\n    /// Create a new DataDirMock with a temporary directory.\n    ///\n    /// If keypair is not provided, a new one will be generated.\n    pub fn new(config_toml: super::ConfigToml) -> anyhow::Result<Self> {\n        Ok(Self {\n            temp_dir: std::sync::Arc::new(tempfile::TempDir::new()?),\n            config_toml,\n        })\n    }\n\n    /// Creates a mock data directory with a config and keypair appropriate for testing.\n    pub fn test() -> Self {\n        let config = super::ConfigToml::test();\n        Self::new(config).expect(\"failed to create MockDataDir\")\n    }\n}\n\nimpl Default for MockDataDir {\n    fn default() -> Self {\n        Self::test()\n    }\n}\n\nimpl DataDir for MockDataDir {\n    fn path(&self) -> &Path {\n        self.temp_dir.path()\n    }\n\n    fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()> {\n        Ok(()) // Always ok because this is validated by the tempfile crate.\n    }\n\n    fn read_or_create_config_file(&self) -> anyhow::Result<super::ConfigToml> {\n        Ok(self.config_toml.clone())\n    }\n}\n"
  },
  {
    "path": "server/src/config/mod.rs",
    "content": "mod config_file;\nmod data_dir;\n#[cfg(test)]\nmod mock_data_dir;\nmod persistent_data_dir;\nmod top_level_domain;\n\npub use config_file::ConfigToml;\npub use data_dir::DataDir;\n#[cfg(test)]\npub use mock_data_dir::MockDataDir;\npub use persistent_data_dir::PersistentDataDir;\npub use top_level_domain::TopLevelDomain;\n"
  },
  {
    "path": "server/src/config/persistent_data_dir.rs",
    "content": "use super::{data_dir::DataDir, ConfigToml};\nuse std::{\n    io::Write,\n    path::{Path, PathBuf},\n};\n\n/// The data directory for the homeserver.\n///\n/// This is the directory that will store the homeservers data.\n///\n#[derive(Debug, Clone)]\npub struct PersistentDataDir {\n    expanded_path: PathBuf,\n}\n\nimpl PersistentDataDir {\n    /// Creates a new data directory.\n    /// `path` will be expanded to the home directory if it starts with \"~\".\n    pub fn new(path: PathBuf) -> Self {\n        Self {\n            expanded_path: Self::expand_home_dir(path),\n        }\n    }\n\n    /// Expands the data directory to the home directory if it starts with \"~\".\n    /// Return the full path to the data directory.\n    fn expand_home_dir(path: PathBuf) -> PathBuf {\n        let path = match path.to_str() {\n            Some(path) => path,\n            None => {\n                // Path not valid utf-8 so we can't expand it.\n                return path;\n            }\n        };\n\n        if path.starts_with(\"~/\") {\n            if let Some(home) = dirs::home_dir() {\n                let without_home = path.strip_prefix(\"~/\").expect(\"Invalid ~ prefix\");\n                let joined = home.join(without_home);\n                return joined;\n            }\n        }\n        PathBuf::from(path)\n    }\n\n    /// Returns the config file path in this directory.\n    pub fn get_config_file_path(&self) -> PathBuf {\n        self.expanded_path.join(\"config.toml\")\n    }\n\n    fn write_sample_config_file(&self) -> anyhow::Result<()> {\n        let config_string = ConfigToml::commented_out_sample();\n        let config_file_path = self.get_config_file_path();\n        let mut config_file = std::fs::File::create(config_file_path)?;\n        config_file.write_all(config_string.as_bytes())?;\n        Ok(())\n    }\n}\n\nimpl Default for PersistentDataDir {\n    fn default() -> Self {\n        Self::new(PathBuf::from(\"~/.pubky\"))\n    }\n}\n\nimpl DataDir for PersistentDataDir {\n    /// Returns the full path to the data directory.\n    fn path(&self) -> &Path {\n        &self.expanded_path\n    }\n\n    /// Makes sure the data directory exists.\n    /// Create the directory if it doesn't exist.\n    fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()> {\n        std::fs::create_dir_all(&self.expanded_path)?;\n\n        // Check if we can write to the data directory\n        let test_file_path = self.expanded_path.join(\"test_write_f2d560932f9b437fa9ef430ba436d611\"); // random file name to not conflict with anything\n        std::fs::write(test_file_path.clone(), b\"test\")\n            .map_err(|err| anyhow::anyhow!(\"Failed to write to data directory: {}\", err))?;\n        std::fs::remove_file(test_file_path)\n            .map_err(|err| anyhow::anyhow!(\"Failed to write to data directory: {}\", err))?;\n        Ok(())\n    }\n\n    /// Reads the config file from the data directory.\n    /// Creates a default config file if it doesn't exist.\n    fn read_or_create_config_file(&self) -> anyhow::Result<ConfigToml> {\n        let config_file_path = self.get_config_file_path();\n        if !config_file_path.exists() {\n            self.write_sample_config_file()?;\n        }\n        let config = ConfigToml::from_file(config_file_path)?;\n        Ok(config)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::io::Write;\n\n    use super::*;\n    use tempfile::TempDir;\n\n    /// Test that the home directory is expanded correctly.\n    #[test]\n    pub fn test_expand_home_dir() {\n        let data_dir = PersistentDataDir::new(PathBuf::from(\"~/.pkdns\"));\n        let homedir = dirs::home_dir().unwrap();\n        let expanded_path = homedir.join(\".pkdns\");\n        assert_eq!(data_dir.expanded_path, expanded_path);\n    }\n\n    /// Test that the data directory is created if it doesn't exist.\n    #[test]\n    pub fn test_ensure_data_dir_exists_and_is_accessible() {\n        let temp_dir = TempDir::new().unwrap();\n        let test_path = temp_dir.path().join(\".pkdns\");\n        let data_dir = PersistentDataDir::new(test_path.clone());\n\n        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();\n        assert!(test_path.exists());\n        data_dir.read_or_create_config_file().unwrap();\n        assert!(data_dir.get_config_file_path().exists());\n        // temp_dir will be automatically cleaned up when it goes out of scope\n    }\n\n    #[test]\n    pub fn test_get_default_config_file_path_exists() {\n        let temp_dir = TempDir::new().unwrap();\n        let test_path = temp_dir.path().join(\".pkdns\");\n        let data_dir = PersistentDataDir::new(test_path.clone());\n        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();\n        let config_file_path = data_dir.get_config_file_path();\n        assert!(!config_file_path.exists()); // Should not exist yet\n\n        let mut config_file = std::fs::File::create(config_file_path.clone()).unwrap();\n        config_file.write_all(b\"test\").unwrap();\n        assert!(config_file_path.exists()); // Should exist now\n                                            // temp_dir will be automatically cleaned up when it goes out of scope\n    }\n\n    #[test]\n    pub fn test_read_or_create_config_file() {\n        let temp_dir = TempDir::new().unwrap();\n        let test_path = temp_dir.path().join(\".pkdns\");\n        let data_dir = PersistentDataDir::new(test_path.clone());\n        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();\n        let _ = data_dir.read_or_create_config_file().unwrap(); // Should create a default config file\n        assert!(data_dir.get_config_file_path().exists());\n\n        let _ = data_dir.read_or_create_config_file().unwrap(); // Should read the existing file\n        assert!(data_dir.get_config_file_path().exists());\n    }\n\n    #[test]\n    pub fn test_read_or_create_config_file_dont_override_existing_file() {\n        let temp_dir = TempDir::new().unwrap();\n        let test_path = temp_dir.path().join(\".pkdns\");\n        let data_dir = PersistentDataDir::new(test_path.clone());\n        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();\n\n        // Write a broken config file\n        let config_file_path = data_dir.get_config_file_path();\n        std::fs::write(config_file_path.clone(), b\"test\").unwrap();\n        assert!(config_file_path.exists()); // Should exist now\n\n        // Try to read the config file and fail because config is broken\n        let read_result = data_dir.read_or_create_config_file();\n        assert!(read_result.is_err());\n\n        // Make sure the broken config file is still there\n        let content = std::fs::read_to_string(config_file_path).unwrap();\n        assert_eq!(content, \"test\");\n    }\n}\n"
  },
  {
    "path": "server/src/config/top_level_domain.rs",
    "content": "use pkarr::dns::{Name, Packet, Question, ResourceRecord};\n\n/// Top Level Domain like .pkd with the capability\n/// to remove and add the top level domain in queries/replies.\n#[derive(Clone, Debug)]\npub struct TopLevelDomain(pub String);\n\nimpl TopLevelDomain {\n    pub fn new(tld: String) -> Self {\n        Self(tld)\n    }\n\n    pub fn label(&self) -> &str {\n        &self.0\n    }\n\n    /// Checks if the query or reply contains a question that ends with a public key and the tld.\n    pub fn question_ends_with_pubkey_tld(&self, packet: &Packet<'_>) -> bool {\n        let question = packet.questions.first();\n        if question.is_none() {\n            return false;\n        }\n        let question = question.unwrap();\n        self.name_ends_with_pubkey_tld(&question.qname)\n    }\n\n    /// Removes the top level domain from the query if it exists.\n    /// Returns the new query and a flag if the tld has been removed.\n    pub fn remove(&self, packet: &mut Packet<'_>) {\n        let question = packet\n            .questions\n            .first()\n            .expect(\"No question in query in pkarr_resolver.\");\n        let labels = question.qname.get_labels();\n\n        let question_tld = labels\n            .last()\n            .expect(\"Question labels with no domain in pkarr_resolver\")\n            .to_string();\n\n        if question_tld != self.0 {\n            panic!(\n                \"Question tld {question_tld} does not match the given tld .{}\",\n                self.label()\n            );\n        }\n\n        // let second_label = labels.get(labels.len() - 2).expect(\"Question should have 2 labels\");\n        // let parse_res: pkarr::PublicKey = parse_pkarr_uri(&second_label.to_string()).expect(\"Second label must be a pkarr public key\");\n\n        let slice = &labels[0..labels.len() - 1];\n        let new_domain = slice\n            .iter()\n            .map(|label| label.to_string())\n            .collect::<Vec<String>>()\n            .join(\".\");\n\n        let name = Name::new(&new_domain).unwrap().into_owned();\n        let new_question = Question::new(name, question.qtype, question.qclass, question.unicast_response).into_owned();\n        packet.questions = vec![new_question];\n    }\n\n    /// Checks if the name ends with a public key domain and the tld.\n    pub fn name_ends_with_pubkey_tld(&self, name: &Name<'_>) -> bool {\n        let labels = name.get_labels();\n        if labels.len() < 2 {\n            // Needs at least 2 labels. First: tld, second: publickey\n            return false;\n        }\n\n        let question_tld = labels.last().unwrap().to_string();\n\n        if question_tld != self.0 {\n            return false;\n        };\n\n        let second_label = labels.get(labels.len() - 2).unwrap().to_string();\n        let res: Result<pkarr::PublicKey, _> = second_label.try_into();\n        res.is_ok()\n    }\n\n    /// Checks if the name ends with a public key domain\n    pub fn name_ends_with_pubkey(&self, name: &Name<'_>) -> bool {\n        let labels = name.get_labels();\n        if labels.is_empty() {\n            // Needs at least 2 labels. First: tld, second: publickey\n            return false;\n        }\n\n        let question_tld = labels.last().unwrap().to_string();\n        let res: Result<pkarr::PublicKey, _> = question_tld.try_into();\n        res.is_ok()\n    }\n\n    /// Append the top level domain to the reply. Zones are stored without a tld on Mainline\n    /// so we need to add it again here.\n    pub fn add(&self, reply: &mut Packet<'_>) {\n        // Append questions\n        let mut new_questions = vec![];\n        for question in reply.questions.iter() {\n            if !self.name_ends_with_pubkey(&question.qname) {\n                // Other question. Don't change.\n                new_questions.push(question.clone());\n                continue;\n            };\n            let original_domain = question.qname.to_string();\n            let new_domain = format!(\"{original_domain}.{}\", self.0);\n            let new_name = Name::new(&new_domain).unwrap();\n            let new_question =\n                Question::new(new_name, question.qtype, question.qclass, question.unicast_response).into_owned();\n            new_questions.push(new_question);\n        }\n        reply.questions = new_questions;\n        // Append answers\n        let mut new_answers = vec![];\n        for answer in reply.answers.iter() {\n            if !self.name_ends_with_pubkey(&answer.name) {\n                // Other answer. Don't change.\n                new_answers.push(answer.clone());\n                continue;\n            };\n            let original_domain = answer.name.to_string();\n            let new_domain = format!(\"{original_domain}.{}\", self.0);\n            let new_name = Name::new(&new_domain).unwrap();\n            let new_answer = ResourceRecord::new(new_name, answer.class, answer.ttl, answer.rdata.clone()).into_owned();\n            new_answers.push(new_answer);\n        }\n        reply.answers = new_answers;\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use pkarr::dns::rdata::A;\n\n    fn create_query_with_domain(domain: &str) -> Vec<u8> {\n        let name = Name::new(domain).unwrap();\n        let mut query = Packet::new_query(0);\n        let question = Question::new(\n            name.clone(),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            true,\n        );\n        query.questions.push(question);\n        query.build_bytes_vec().unwrap()\n    }\n\n    fn create_reply_with_domain(domain: &str) -> Vec<u8> {\n        let query = create_query_with_domain(domain);\n        let mut packet = Packet::parse(&query).unwrap().into_reply();\n\n        let rdata = pkarr::dns::rdata::RData::A(A { address: 0 });\n        let answer1 = ResourceRecord::new(Name::new(domain).unwrap(), pkarr::dns::CLASS::IN, 60, rdata.clone());\n        packet.answers.push(answer1);\n\n        let answer2 = ResourceRecord::new(Name::new(\"example.com\").unwrap(), pkarr::dns::CLASS::IN, 60, rdata);\n        packet.answers.push(answer2);\n\n        packet.build_bytes_vec().unwrap()\n    }\n\n    #[tokio::test]\n    async fn is_pkarr_with_tld_valid_2_label() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_query_with_domain(\"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\");\n        let packet = Packet::parse(&domain).unwrap();\n        assert!(tld.question_ends_with_pubkey_tld(&packet));\n    }\n\n    #[tokio::test]\n    async fn is_pkarr_with_tld_valid_3_label() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_query_with_domain(\"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\");\n        let packet = Packet::parse(&domain).unwrap();\n        assert!(tld.question_ends_with_pubkey_tld(&packet));\n    }\n\n    #[tokio::test]\n    async fn is_pkarr_with_tld_fail_1_label() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_query_with_domain(\"pkd\");\n        let packet = Packet::parse(&domain).unwrap();\n        assert!(!tld.question_ends_with_pubkey_tld(&packet));\n    }\n\n    #[tokio::test]\n    async fn is_pkarr_with_tld_fail_2_label_no_pubkey() {\n        let tld = TopLevelDomain::new(\"nopubkey.pkd\".to_string());\n        let domain = create_query_with_domain(\"pkd\");\n        let packet = Packet::parse(&domain).unwrap();\n        assert!(!tld.question_ends_with_pubkey_tld(&packet));\n    }\n\n    #[tokio::test]\n    async fn is_pkarr_with_tld_fail_2_label_wrong_tld() {\n        let tld = TopLevelDomain::new(\"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.wrongpkd\".to_string());\n        let domain = create_query_with_domain(\"pkd\");\n        let packet = Packet::parse(&domain).unwrap();\n        assert!(!tld.question_ends_with_pubkey_tld(&packet));\n    }\n\n    #[tokio::test]\n    async fn remove_tld_success_2_labels() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_query_with_domain(\"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\");\n        let mut packet = Packet::parse(&domain).unwrap();\n        tld.remove(&mut packet);\n        // Rebuild packet from scratch\n        let removed_query = packet.build_bytes_vec().unwrap();\n        let packet = Packet::parse(&removed_query).unwrap();\n        let question_domain = packet.questions.first().unwrap().qname.to_string();\n        assert_eq!(question_domain, \"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\")\n    }\n\n    #[tokio::test]\n    async fn remove_tld_success_3_labels() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_query_with_domain(\"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\");\n        let mut packet = Packet::parse(&domain).unwrap();\n        tld.remove(&mut packet);\n        // Rebuild packet from scratch\n        let removed_query = packet.build_bytes_vec().unwrap();\n        let packet = Packet::parse(&removed_query).unwrap();\n        let question_domain = packet.questions.first().unwrap().qname.to_string();\n        assert_eq!(\n            question_domain,\n            \"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\"\n        )\n    }\n\n    #[tokio::test]\n    async fn add_success_1_label() {\n        let tld = TopLevelDomain::new(\"pkd\".to_string());\n        let domain = create_reply_with_domain(\"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\");\n        let mut packet = Packet::parse(&domain).unwrap();\n        tld.add(&mut packet);\n        // Rebuild packet from scratch\n        let removed_query = packet.build_bytes_vec().unwrap();\n        let packet = Packet::parse(&removed_query).unwrap();\n\n        let question_domain = packet.questions.first().unwrap().qname.to_string();\n        assert_eq!(\n            question_domain,\n            \"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\"\n        );\n\n        let answer1_domain = packet.answers.first().unwrap().name.to_string();\n        assert_eq!(\n            answer1_domain,\n            \"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\"\n        );\n        let answer2_domain = packet.answers.get(1).unwrap().name.to_string();\n        assert_eq!(answer2_domain, \"example.com\");\n    }\n\n    #[tokio::test]\n    async fn add_success_2_label() {\n        let tld = TopLevelDomain(\"pkd\".to_string());\n        let domain = create_reply_with_domain(\"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\");\n        let mut packet = Packet::parse(&domain).unwrap();\n        tld.add(&mut packet);\n        // Rebuild packet from scratch\n        let removed_query = packet.build_bytes_vec().unwrap();\n        let packet = Packet::parse(&removed_query).unwrap();\n\n        let question_domain = packet.questions.first().unwrap().qname.to_string();\n        assert_eq!(\n            question_domain,\n            \"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\"\n        );\n\n        let answer1_domain = packet.answers.first().unwrap().name.to_string();\n        assert_eq!(\n            answer1_domain,\n            \"test.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy.pkd\"\n        );\n        let answer2_domain = packet.answers.get(1).unwrap().name.to_string();\n        assert_eq!(answer2_domain, \"example.com\");\n    }\n}\n"
  },
  {
    "path": "server/src/dns_over_https/mod.rs",
    "content": "mod server;\n\npub use server::run_doh_server;\n"
  },
  {
    "path": "server/src/dns_over_https/server.rs",
    "content": "//! RFC8484 Dns-over-http wireformat\n//! https://datatracker.ietf.org/doc/html/rfc8484\n//! The implementation works but could implement the standard more accurately,\n//! especially when it comes to cache-control.\n\nuse crate::resolution::DnsSocket;\nuse axum::{\n    body::Body,\n    extract::{ConnectInfo, Query, State},\n    http::{header, HeaderMap, Method, Response, StatusCode},\n    response::IntoResponse,\n    routing::{get, post},\n    Router,\n};\nuse base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};\nuse pkarr::dns::Packet;\nuse std::{\n    collections::HashMap,\n    net::{IpAddr, SocketAddr},\n    sync::Arc,\n};\nuse tower_http::cors::{Any, CorsLayer};\n\n/// Error prefix for web browsers so users actually\n/// know what this url is about.\nconst ERROR_PREFIX: &str = \"\nHello to pkdns DNS-over-HTTPS!\nhttps://github.com/pubky/pkdns\n\nAdd this DNS url to your browsers to enable self-sovereign Public Key Domains (PKD).\n\n\n\n\n\ndev:\";\n\n/// Validates the accept header.\n/// Returns an error if the accept header is missing or not application/dns-message.\nfn validate_accept_header(headers: &HeaderMap) -> Result<(), (StatusCode, String)> {\n    let error: Result<(), (StatusCode, String)> = Err((\n        StatusCode::BAD_REQUEST,\n        format!(\"{ERROR_PREFIX} valid accept header missing\"),\n    ));\n    let accept_header = match headers.get(\"accept\") {\n        Some(value) => value,\n        None => return error,\n    };\n\n    let value_str = match accept_header.to_str() {\n        Ok(value) => value,\n        Err(_) => return error,\n    };\n\n    if value_str != \"application/dns-message\" {\n        return error;\n    }\n    Ok(())\n}\n\nfn decode_dns_base64_packet(param: &String) -> Result<Vec<u8>, (StatusCode, String)> {\n    let bytes = match URL_SAFE_NO_PAD.decode(param) {\n        Ok(bytes) => bytes,\n        Err(e) => {\n            return Err((\n                StatusCode::BAD_REQUEST,\n                format!(\"Error decoding the dns base64 query parameter. {e}\"),\n            ));\n        }\n    };\n\n    if let Err(e) = Packet::parse(&bytes) {\n        tracing::info!(\"Failed to parse the base64 as a valid dns packet. {e}\");\n        return Err((\n            StatusCode::BAD_REQUEST,\n            format!(\"Failed to parse the base64 as a valid dns packet. {e}\"),\n        ));\n    }\n    Ok(bytes)\n}\n\n/// Extract lowest ttl of answer to set caching parameter\nfn get_lowest_ttl(reply: &[u8]) -> u32 {\n    const DEFAULT_VALUE: u32 = 300;\n\n    let parsed_packet = match Packet::parse(reply) {\n        Ok(parsed) => parsed,\n        Err(_) => return DEFAULT_VALUE,\n    };\n\n    let val = parsed_packet\n        .answers\n        .iter()\n        .map(|answer| answer.ttl)\n        .reduce(std::cmp::min);\n\n    val.unwrap_or(DEFAULT_VALUE)\n}\n\n/// Extracts the client IP for rate limiting.\n/// Uses the \"x-forwarded-for\" header to support proxies.\n/// If not available, uses the client IP directly.\nfn extract_client_ip(request_addr: &SocketAddr, headers: &HeaderMap) -> IpAddr {\n    let origin_ip = match headers.get(\"x-forwarded-for\").and_then(|v| v.to_str().ok()) {\n        Some(value) => value,\n        None => return request_addr.ip(),\n    };\n\n    match origin_ip.parse() {\n        Ok(ip) => ip,\n        Err(e) => {\n            tracing::debug!(\"Failed to parse the 'x-forwarded-for' header ip address. {e}\");\n            request_addr.ip()\n        }\n    }\n}\n\nasync fn query_to_response(query: Vec<u8>, dns_socket: &mut DnsSocket, client_ip: IpAddr) -> Response<Body> {\n    let reply = dns_socket.query_me_recursively_raw(query, Some(client_ip)).await;\n    let lowest_ttl = get_lowest_ttl(&reply);\n\n    let response = Response::builder()\n        .status(StatusCode::OK)\n        .header(header::CONTENT_TYPE, \"application/dns-message\")\n        .header(header::CONTENT_LENGTH, reply.len())\n        .header(header::CACHE_CONTROL, format!(\"max-age={lowest_ttl}\"))\n        .body(Body::from(reply))\n        .expect(\"Failed to build response\");\n\n    response\n}\n\nasync fn dns_query_get(\n    headers: HeaderMap,\n    Query(params): Query<HashMap<String, String>>,\n    State(state): State<Arc<AppState>>,\n    ConnectInfo(client_addr): ConnectInfo<SocketAddr>,\n) -> Result<impl IntoResponse, impl IntoResponse> {\n    let client_ip = extract_client_ip(&client_addr, &headers);\n    validate_accept_header(&headers)?;\n\n    let dns_param = match params.get(\"dns\") {\n        Some(value) => value,\n        None => return Err((StatusCode::BAD_REQUEST, \"valid dns query param required\".to_string())),\n    };\n    let packet_bytes = decode_dns_base64_packet(dns_param)?;\n\n    let mut socket = state.socket.clone();\n    Ok(query_to_response(packet_bytes, &mut socket, client_ip).await)\n}\n\nasync fn dns_query_post(\n    headers: HeaderMap,\n    State(state): State<Arc<AppState>>,\n    ConnectInfo(client_addr): ConnectInfo<SocketAddr>,\n    request: axum::http::Request<axum::body::Body>,\n) -> Result<impl IntoResponse, impl IntoResponse> {\n    let client_ip = extract_client_ip(&client_addr, &headers);\n    validate_accept_header(&headers)?;\n\n    let packet_bytes = match axum::body::to_bytes(request.into_body(), 65535usize).await {\n        Ok(bytes) => bytes.to_vec(),\n        Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())),\n    };\n\n    let mut socket = state.socket.clone();\n    Ok(query_to_response(packet_bytes, &mut socket, client_ip).await)\n}\n\npub struct AppState {\n    pub socket: DnsSocket,\n}\n\nfn create_app(dns_socket: DnsSocket) -> Router {\n    let cors = CorsLayer::new()\n        .allow_origin(Any)\n        .allow_methods([Method::GET, Method::POST])\n        .allow_headers(Any);\n\n    Router::new()\n        .route(\"/dns-query\", get(dns_query_get))\n        .route(\"/dns-query\", post(dns_query_post))\n        .layer(cors)\n        .with_state(Arc::new(AppState { socket: dns_socket }))\n}\n\npub async fn run_doh_server(addr: SocketAddr, dns_socket: DnsSocket) -> Result<SocketAddr, anyhow::Error> {\n    let app = create_app(dns_socket);\n    let listener = tokio::net::TcpListener::bind(addr).await?;\n    let addr = listener.local_addr()?;\n    tokio::spawn(async move {\n        axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())\n            .await\n            .unwrap();\n    });\n    Ok(addr)\n}\n\n#[cfg(test)]\nmod tests {\n    use std::net::SocketAddr;\n\n    use crate::{app_context::AppContext, dns_over_https::server::create_app, resolution::DnsSocket};\n    use axum_test::TestServer;\n    use pkarr::dns::{Name, Packet, PacketFlag, Question};\n    use tracing_test::traced_test;\n\n    #[traced_test]\n    #[tokio::test]\n    async fn query_doh_wireformat_get() {\n        // RFC8484 example https://datatracker.ietf.org/doc/html/rfc8484#section-4.1\n        let context = AppContext::test();\n        let socket = DnsSocket::new(&context).await.unwrap();\n        let join_handle = socket.start_receive_loop();\n        let app = create_app(socket);\n        let server = TestServer::new(app.into_make_service_with_connect_info::<SocketAddr>()).unwrap();\n        let base64 = \"AAABAAABAAAAAAAAAWE-NjJjaGFyYWN0ZXJsYWJlbC1tYWtlcy1iYXNlNjR1cmwtZGlzdGluY3QtZnJvbS1zdGFuZGFyZC1iYXNlNjQHZXhhbXBsZQNjb20AAAEAAQ\";\n        let response = server\n            .get(\"/dns-query\")\n            .add_query_param(\"dns\", base64)\n            .add_header(\"accept\", \"application/dns-message\")\n            .await;\n\n        response.assert_status_ok();\n        assert_eq!(\n            response.maybe_header(\"content-type\").expect(\"content-type available\"),\n            \"application/dns-message\"\n        );\n        assert_eq!(\n            response\n                .maybe_header(\"content-length\")\n                .expect(\"content-length available\"),\n            \"94\"\n        );\n\n        let reply_bytes = response.into_bytes();\n        let packet = Packet::parse(&reply_bytes).expect(\"Should be valid packet\");\n        // dbg!(&packet);\n        assert_eq!(packet.answers.len(), 0);\n        assert_eq!(packet.name_servers.len(), 0);\n        assert_eq!(packet.additional_records.len(), 0);\n        assert!(packet.has_flags(PacketFlag::RESPONSE));\n        join_handle.send(()).unwrap();\n    }\n\n    #[traced_test]\n    #[tokio::test]\n    async fn query_doh_wireformat_post() {\n        let context = AppContext::test();\n        let socket = DnsSocket::new(&context).await.unwrap();\n        let join_handle = socket.start_receive_loop();\n        let app = create_app(socket);\n        let server = TestServer::new(app.into_make_service_with_connect_info::<SocketAddr>()).unwrap();\n\n        let mut query = Packet::new_query(50);\n        let question = Question::new(\n            Name::new_unchecked(\"example.com\"),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        );\n        query.questions.push(question);\n        let bytes = query.build_bytes_vec().unwrap();\n        let response = server\n            .post(\"/dns-query\")\n            .add_header(\"accept\", \"application/dns-message\")\n            .bytes(bytes.into())\n            .await;\n\n        response.assert_status_ok();\n        assert_eq!(\n            response.maybe_header(\"content-type\").expect(\"content-type available\"),\n            \"application/dns-message\"\n        );\n\n        let content_length_header = response\n            .maybe_header(\"content-length\")\n            .expect(\"content-length available\");\n        let reply_bytes = response.into_bytes();\n        assert_eq!(content_length_header, format!(\"{}\", reply_bytes.len()));\n        let packet = Packet::parse(&reply_bytes).expect(\"Should be valid packet\");\n        assert!(packet.answers.len() > 1);\n        join_handle.send(()).unwrap();\n    }\n\n    #[tokio::test]\n    async fn wrong_content_type() {\n        // RFC8484 example https://datatracker.ietf.org/doc/html/rfc8484#section-4.1\n        let context = AppContext::test();\n        let socket = DnsSocket::new(&context).await.unwrap();\n        socket.start_receive_loop();\n        let app = create_app(socket);\n        let server = TestServer::new(app.into_make_service_with_connect_info::<SocketAddr>()).unwrap();\n        let base64 = \"AAABAAABAAAAAAAAAWE-NjJjaGFyYWN0ZXJsYWJlbC1tYWtlcy1iYXNlNjR1cmwtZGlzdGluY3QtZnJvbS1zdGFuZGFyZC1iYXNlNjQHZXhhbXBsZQNjb20AAAEAAQ\";\n        let response = server\n            .get(\"/dns-query\")\n            .add_query_param(\"dns\", base64)\n            .add_header(\"accept\", \"application/wrong_type\")\n            .await;\n\n        response.assert_status_bad_request();\n    }\n}\n"
  },
  {
    "path": "server/src/helpers.rs",
    "content": "use std::env;\nuse tracing::Level;\nuse tracing_subscriber::{filter::Targets, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};\n\n/**\n * Sets `RUST_BACKTRACE=1` as default so we always get a full stacktrace\n * on an error.\n */\npub(crate) fn set_full_stacktrace_as_default() {\n    let key = \"RUST_BACKTRACE\";\n\n    let is_value_already_set = env::var(key).is_ok();\n    if is_value_already_set {\n        return;\n    }\n    env::set_var(key, \"1\");\n}\n\npub(crate) fn enable_logging(verbose: bool) {\n    let key = \"RUST_LOG\";\n    let value = match env::var(key) {\n        Ok(val) => val,\n        Err(_) => \"\".to_string(),\n    };\n\n    if !value.is_empty() {\n        tracing_subscriber::fmt()\n            .with_env_filter(EnvFilter::from_default_env())\n            .init();\n        tracing::info!(\"Use RUST_LOG={} env variable to set logging output.\", value);\n        if verbose {\n            tracing::warn!(\"RUST_LOG= environment variable is already set. Ignore --verbose flag.\")\n        }\n        return;\n    }\n\n    let regular_filter = tracing_subscriber::filter::Targets::new()\n        .with_target(\"pkdns\", Level::INFO)\n        .with_target(\"mainline\", Level::WARN);\n\n    let verbose_filter = tracing_subscriber::filter::Targets::new()\n        .with_target(\"pkdns\", Level::DEBUG)\n        .with_target(\"mainline\", Level::WARN);\n\n    let mut filter: Targets = regular_filter;\n    if verbose {\n        filter = verbose_filter;\n    }\n\n    tracing_subscriber::registry()\n        .with(tracing_subscriber::fmt::layer())\n        .with(filter)\n        .init();\n\n    if verbose {\n        tracing::info!(\"Verbose mode enabled.\");\n    }\n}\n\n/// Wait until the user hits CTRL+C\npub(crate) async fn wait_on_ctrl_c() {\n    match tokio::signal::ctrl_c().await {\n        Ok(()) => {}\n        Err(err) => {\n            eprintln!(\"Unable to listen for shutdown signal Ctrl+C: {}\", err);\n        }\n    }\n}\n"
  },
  {
    "path": "server/src/main.rs",
    "content": "use clap::Parser;\nuse dns_over_https::run_doh_server;\nuse helpers::{enable_logging, set_full_stacktrace_as_default, wait_on_ctrl_c};\n\nuse std::{error::Error, net::SocketAddr, path::PathBuf};\n\nuse crate::{app_context::AppContext, config::PersistentDataDir, resolution::DnsSocket};\n\nmod app_context;\nmod config;\nmod dns_over_https;\nmod helpers;\nmod resolution;\n\n#[derive(Parser, Debug)]\n#[command(\n    version,\n    about = \"pkdns - A DNS server for Public Key Domains (PDK) hosted on the Mainline DHT.\"\n)]\nstruct Cli {\n    /// ICANN fallback DNS server. Format: IP:Port. [default: 8.8.8.8:53]\n    #[arg(short, long)]\n    forward: Option<SocketAddr>,\n\n    /// Show verbose output. [default: false]\n    #[arg(short, long, action = clap::ArgAction::SetTrue)]\n    verbose: Option<bool>,\n\n    /// The base directory that contains pkdns's data, configuration file, etc.\n    #[arg(short, long, default_value = \"~/.pkdns\")]\n    pkdns_dir: PathBuf,\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn Error>> {\n    set_full_stacktrace_as_default();\n    let cli = Cli::parse();\n\n    let data_dir = PersistentDataDir::new(cli.pkdns_dir);\n    let mut app_context = AppContext::from_data_dir(data_dir)?;\n    if let Some(verbose) = cli.verbose {\n        app_context.config.general.verbose = verbose;\n    }\n    if let Some(forward) = cli.forward {\n        app_context.config.general.forward = forward;\n    }\n\n    enable_logging(app_context.config.general.verbose);\n    const VERSION: &str = env!(\"CARGO_PKG_VERSION\");\n\n    tracing::info!(\"Starting pkdns v{VERSION}\");\n    tracing::debug!(\"Configuration:\\n{:?}\", app_context.config);\n    tracing::info!(\"Forward ICANN queries to {}\", app_context.config.general.forward);\n\n    // Exit the main thread if anything panics\n    let orig_hook = std::panic::take_hook();\n    std::panic::set_hook(Box::new(move |panic_info| {\n        // invoke the default handler and exit the process\n        tracing::error!(\"Thread paniced. Stop main thread too.\");\n        orig_hook(panic_info);\n        std::process::exit(1);\n    }));\n\n    let dns_socket = DnsSocket::new(&app_context).await?;\n\n    let join_handle = dns_socket.start_receive_loop();\n\n    tracing::info!(\n        \"Listening on {}. Waiting for Ctrl-C...\",\n        app_context.config.general.socket\n    );\n\n    if let Some(http_socket) = &app_context.config.general.dns_over_http_socket {\n        let socket = run_doh_server(*http_socket, dns_socket).await?;\n        tracing::info!(\"[EXPERIMENTAL] DNS-over-HTTP listening on http://{socket}/dns-query.\");\n    };\n\n    wait_on_ctrl_c().await;\n    println!();\n    tracing::info!(\"Got it! Exiting...\");\n    join_handle\n        .send(())\n        .expect(\"Failed to send shutdown signal to DNS socket.\"); // If this fails, we panic as we are already trying to exit.\n\n    Ok(())\n}\n"
  },
  {
    "path": "server/src/resolution/dns_packets/mod.rs",
    "content": "mod parsed_packet;\nmod parsed_query;\n\npub use parsed_packet::ParsedPacket;\npub use parsed_query::{ParseQueryError, ParsedQuery};\n"
  },
  {
    "path": "server/src/resolution/dns_packets/parsed_packet.rs",
    "content": "use anyhow::anyhow;\nuse chrono::format::Parsed;\nuse pkarr::dns::{Packet, PacketFlag};\nuse self_cell::self_cell;\nuse std::{fmt::Display, pin::Pin};\n\n// Struct to hold the bytes and the packet in one place\n// to avoid lifetimes aka a self-referencing struct.\nself_cell!(\n    pub struct Inner {\n        owner: Vec<u8>,\n\n        #[covariant]\n        dependent: Packet,\n    }\n\n    impl {Debug}\n);\n\nimpl Inner {\n    /// Try to parse the packet from bytes\n    pub fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, pkarr::dns::SimpleDnsError> {\n        Self::try_new(bytes, |bytes| Packet::parse(bytes))\n    }\n\n    /// Parsed DNS packet\n    pub fn packet(&self) -> &Packet {\n        self.borrow_dependent()\n    }\n\n    /// Raw bytes the packet is build with\n    pub fn raw_bytes(&self) -> &Vec<u8> {\n        self.borrow_owner()\n    }\n}\n\nimpl Clone for Inner {\n    fn clone(&self) -> Self {\n        let bytes = self.raw_bytes().clone();\n        Self::try_from_bytes(bytes).unwrap()\n    }\n}\n\nimpl From<Inner> for Vec<u8> {\n    fn from(val: Inner) -> Self {\n        val.into_owner()\n    }\n}\n\n/// Parses a dns packet without having to deal with life times\n/// Both the raw bytes and the parsed struct is contained.\n#[derive(Debug, Clone)]\npub struct ParsedPacket {\n    pub inner: Inner,\n}\n\nimpl ParsedPacket {\n    pub fn new(raw_bytes: Vec<u8>) -> Result<Self, pkarr::dns::SimpleDnsError> {\n        let inner = Inner::try_from_bytes(raw_bytes)?;\n        Ok(Self { inner })\n    }\n\n    pub fn id(&self) -> u16 {\n        self.parsed().id()\n    }\n\n    /// Parsed DNS packet\n    pub fn parsed(&self) -> &Packet {\n        self.inner.packet()\n    }\n\n    /// Raw bytes the packet is build with\n    pub fn raw_bytes(&self) -> &Vec<u8> {\n        self.inner.raw_bytes()\n    }\n\n    /// If this packet is a reply\n    pub fn is_reply(&self) -> bool {\n        self.parsed().has_flags(PacketFlag::RESPONSE)\n    }\n\n    /// If this packet is a reply\n    pub fn is_query(&self) -> bool {\n        !self.parsed().has_flags(PacketFlag::RESPONSE)\n    }\n\n    /// Create a REFUSED reply\n    pub fn create_refused_reply(&self) -> Vec<u8> {\n        let mut reply = Packet::new_reply(self.id());\n        *reply.rcode_mut() = pkarr::dns::RCODE::Refused;\n        reply.build_bytes_vec_compressed().unwrap()\n    }\n\n    /// Create SRVFAIL reply\n    pub fn create_server_fail_reply(&self) -> Vec<u8> {\n        let mut reply = Packet::new_reply(self.id());\n        *reply.rcode_mut() = pkarr::dns::RCODE::ServerFailure;\n        reply.build_bytes_vec_compressed().unwrap()\n    }\n}\n\nimpl From<ParsedPacket> for Vec<u8> {\n    fn from(val: ParsedPacket) -> Self {\n        val.inner.into()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use pkarr::dns::{Name, Packet, PacketFlag, Question};\n\n    use super::*;\n    #[tokio::test]\n    async fn new() {\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"example.com\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let parsed = ParsedPacket::new(raw_query).unwrap();\n        assert_eq!(parsed.parsed().id(), 0);\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/dns_packets/parsed_query.rs",
    "content": "use std::fmt::Display;\n\nuse super::ParsedPacket;\nuse anyhow::anyhow;\nuse pkarr::dns::{Packet, PacketFlag, Question, QTYPE};\n\n#[derive(thiserror::Error, Debug)]\npub enum ParseQueryError {\n    #[error(\"Dns packet parse error: {0}\")]\n    Parse(#[from] pkarr::dns::SimpleDnsError),\n\n    #[error(\"Query validation error: {0}.\")]\n    Validation(#[from] anyhow::Error),\n}\n\n#[derive(Debug, Clone)]\npub struct ParsedQuery {\n    pub packet: ParsedPacket,\n}\n\nimpl ParsedQuery {\n    /// Create a new parsed query.\n    pub fn new(bytes: Vec<u8>) -> Result<Self, ParseQueryError> {\n        let packet = ParsedPacket::new(bytes)?;\n        let me = Self { packet };\n        me.validate()?;\n        Ok(me)\n    }\n\n    /// Checks if this packet is valid.\n    fn validate(&self) -> Result<(), anyhow::Error> {\n        if !self.packet.is_query() {\n            return Err(anyhow!(\"Packet is not a query.\"));\n        }\n        let question = self.packet.parsed().questions.first();\n        if question.is_none() {\n            return Err(anyhow!(\"Packet without a question.\"));\n        };\n        let question = question.unwrap();\n        let labels = question.qname.get_labels();\n        if labels.is_empty() {\n            return Err(anyhow!(\"Question with an empty qname.\"));\n        };\n\n        Ok(())\n    }\n\n    pub fn question(&self) -> &Question {\n        self.packet.parsed().questions.first().unwrap()\n    }\n\n    /// If this query is ANY type which is often used for DNS amplification attacks.\n    pub fn is_any_type(&self) -> bool {\n        self.question().qtype == QTYPE::ANY\n    }\n\n    pub fn is_recursion_desired(&self) -> bool {\n        self.packet.parsed().has_flags(PacketFlag::RECURSION_DESIRED)\n    }\n}\n\nimpl Display for ParsedQuery {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let question = self.question();\n        let query_id = self.packet.parsed().id();\n        let query_name = format!(\"{} {:?} query_id={query_id}\", question.qname, question.qtype);\n        write!(\n            f,\n            \"{} {:?} {:?} id={query_id} rd={}\",\n            question.qname,\n            question.qtype,\n            question.qclass,\n            self.packet.parsed().has_flags(PacketFlag::RECURSION_DESIRED)\n        )\n    }\n}\n\nimpl TryFrom<ParsedPacket> for ParsedQuery {\n    type Error = ParseQueryError;\n    fn try_from(value: ParsedPacket) -> Result<Self, Self::Error> {\n        let me = Self { packet: value };\n        me.validate()?;\n        Ok(me)\n    }\n}\n\nimpl From<ParsedQuery> for ParsedPacket {\n    fn from(val: ParsedQuery) -> Self {\n        val.packet\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use pkarr::dns::{Name, Packet, PacketFlag, Question};\n\n    use super::*;\n\n    #[tokio::test]\n    async fn new() {\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"example.com\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let parsed = ParsedQuery::new(raw_query).unwrap();\n    }\n\n    #[tokio::test]\n    async fn tryfrom_parsed_packet() {\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"example.com\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let parsed = ParsedPacket::new(raw_query).unwrap();\n        let parsed_query: ParsedQuery = parsed.try_into().unwrap();\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/dns_socket.rs",
    "content": "#![allow(unused)]\nuse crate::{\n    app_context::AppContext,\n    resolution::{helpers::replace_packet_id, pkd::CustomHandlerError},\n};\nuse rand::Rng;\nuse tracing_subscriber::fmt::format;\n\nuse super::{\n    dns_packets::{ParsedPacket, ParsedQuery},\n    pending_request::{PendingRequest, PendingRequestStore},\n    pkd::PkarrResolver,\n    query_id_manager::QueryIdManager,\n    rate_limiter::{RateLimiter, RateLimiterBuilder},\n    response_cache::IcannLruCache,\n};\nuse pkarr::dns::{\n    rdata::{RData, A, AAAA, NS},\n    Packet, PacketFlag, SimpleDnsError, QTYPE, RCODE,\n};\nuse std::{\n    hash::{Hash, Hasher},\n    num::NonZeroU64,\n    thread::current,\n};\nuse std::{\n    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},\n    num::NonZeroU32,\n    sync::Arc,\n    time::{Duration, Instant},\n};\nuse std::{\n    net::{SocketAddrV4, SocketAddrV6},\n    num,\n};\nuse tokio::{\n    net::UdpSocket,\n    sync::{oneshot, RwLock},\n    task::JoinHandle,\n};\nuse tracing::Level;\n\n/// Any error related to receiving and sending DNS packets on the UDP socket.\n#[derive(thiserror::Error, Debug)]\npub enum DnsSocketError {\n    #[error(\"Dns packet parse error: {0}\")]\n    Parse(#[from] SimpleDnsError),\n\n    #[error(transparent)]\n    IO(#[from] tokio::io::Error),\n\n    #[error(\"Timeout. No answer received from forward server.\")]\n    ForwardTimeout(#[from] tokio::time::error::Elapsed),\n\n    #[error(\"Rx receive error. {0}\")]\n    RxReceiedErr(#[from] oneshot::error::RecvError),\n}\n\n/**\n * DNS UDP socket\n */\n#[derive(Debug, Clone)]\npub struct DnsSocket {\n    socket: Arc<UdpSocket>,\n    pending: PendingRequestStore,\n    pkarr_resolver: PkarrResolver,\n    icann_fallback: SocketAddr,\n    id_manager: QueryIdManager,\n    rate_limiter: Arc<RateLimiter>,\n    disable_any_queries: bool,\n    icann_cache: IcannLruCache,\n    max_recursion_depth: u8,\n}\n\nimpl DnsSocket {\n    /// Default dns socket but with a random listening port. Made for testing.\n    #[cfg(test)]\n    pub async fn default_random_socket() -> tokio::io::Result<Self> {\n        let listening: SocketAddr = \"0.0.0.0:0\".parse().expect(\"Is always be a valid socket address\");\n        let icann_resolver: SocketAddr = \"8.8.8.8:53\".parse().expect(\"Should always be a valid socket address\");\n\n        let mut context = AppContext::test();\n\n        DnsSocket::new(&context).await\n    }\n\n    // Create a new DNS socket\n    // TODO: Fix this too many arguments\n    #[allow(clippy::too_many_arguments)]\n    pub async fn new(context: &AppContext) -> tokio::io::Result<Self> {\n        let socket = UdpSocket::bind(context.config.general.socket).await?;\n        let limiter = RateLimiterBuilder::new()\n            .max_per_second(context.config.dns.query_rate_limit)\n            .burst_size(context.config.dns.query_rate_limit_burst);\n\n        let pkarr_resolver = PkarrResolver::new(context).await;\n        Ok(Self {\n            socket: Arc::new(socket),\n            pending: PendingRequestStore::new(),\n            pkarr_resolver,\n            icann_fallback: context.config.general.forward,\n            id_manager: QueryIdManager::new(),\n            rate_limiter: Arc::new(limiter.build()),\n            disable_any_queries: context.config.dns.disable_any_queries,\n            icann_cache: IcannLruCache::new(\n                context.config.dns.icann_cache_mb,\n                context.config.dns.min_ttl,\n                context.config.dns.max_ttl,\n            ),\n            max_recursion_depth: context.config.dns.max_recursion_depth,\n        })\n    }\n\n    fn is_recursion_available(&self) -> bool {\n        self.max_recursion_depth > 1\n    }\n\n    // Send message to address\n    pub async fn send_to(&self, buffer: &[u8], target: &SocketAddr) -> tokio::io::Result<usize> {\n        self.socket.send_to(buffer, target).await\n    }\n\n    /// Starts the receive loop in the background.\n    /// Returns the JoinHandle to stop the loop again.\n    pub fn start_receive_loop(&self) -> oneshot::Sender<()> {\n        let mut cloned = self.clone();\n        let (tx, rx) = oneshot::channel::<()>();\n        tokio::spawn(async move {\n            let mut cancel = rx;\n            loop {\n                tokio::select! {\n                    _ = &mut cancel => {\n                        tracing::trace!(\"Stop UDP receive loop.\");\n                        break;\n                    }\n                    result = cloned.receive_datagram() => {\n                        if let Err(err) = result {\n                            tracing::error!(\"Error while trying to receive. {err}\");\n                        }\n                    }\n                }\n            }\n        });\n        tx\n    }\n\n    async fn receive_datagram(&mut self) -> Result<(), DnsSocketError> {\n        let mut buffer = [0; 1024];\n        let (size, from) = self.socket.recv_from(&mut buffer).await?;\n\n        let mut data = buffer.to_vec();\n        if data.len() > size {\n            data.drain((size + 1)..data.len());\n        }\n\n        let packet = ParsedPacket::new(data)?;\n\n        let packet_id = packet.id();\n        if let Some(query) = self.pending.remove_by_forward_id(&packet_id, &from) {\n            tracing::trace!(\"Received response from forward server. Send back to client.\");\n            let _ = query.tx.send(packet.into()); // TODO: Handle error properly. Sometimes the channel is broken.\n            return Ok(());\n        };\n\n        if packet.is_reply() {\n            tracing::debug!(\n                \"Received reply without an associated query {:?}. forward_id={packet_id} Ignore.\",\n                packet\n            );\n            return Ok(());\n        };\n\n        // New query\n        let query: ParsedQuery = match packet.try_into() {\n            Ok(query) => query,\n            Err(e) => {\n                tracing::debug!(\"Failed to parse query {from}. id={packet_id}. {e} Drop.\");\n                return Ok(());\n            }\n        };\n\n        if self.disable_any_queries && query.is_any_type() {\n            tracing::debug!(\"Received ANY type question from {from}. id={packet_id}. Drop.\");\n            return Ok(());\n        }\n\n        let mut socket = self.clone();\n        tokio::spawn(async move {\n            let start = Instant::now();\n            let reply = socket.query_me_recursively_with_log(&query, Some(from.ip())).await;\n            socket.send_to(&reply, &from).await;\n        });\n\n        Ok(())\n    }\n\n    /// Queries recursively with a byte query. If the query can't be parsed, return a server fail.\n    pub async fn query_me_recursively_raw(&mut self, query: Vec<u8>, from: Option<IpAddr>) -> Vec<u8> {\n        let packet: ParsedPacket = match ParsedPacket::new(query) {\n            Ok(packet) => packet,\n            Err(e) => {\n                tracing::trace!(\"Failed to parse query {e}. Drop\");\n                return vec![];\n            }\n        };\n\n        match ParsedQuery::try_from(packet.clone()) {\n            Ok(parsed) => self.query_me_recursively_with_log(&parsed, from).await,\n            Err(e) => packet.create_server_fail_reply(),\n        }\n    }\n\n    /// Queries recursively with a log.\n    pub async fn query_me_recursively_with_log(&mut self, query: &ParsedQuery, from: Option<IpAddr>) -> Vec<u8> {\n        let start = Instant::now();\n        let reply = self.query_me_recursively(query, from).await;\n        tracing::debug!(\"{query} processed within {}ms.\", start.elapsed().as_millis());\n        reply\n    }\n\n    /// Queries recursively. This is the main query function of this socket.\n    async fn query_me_recursively(&mut self, query: &ParsedQuery, from: Option<IpAddr>) -> Vec<u8> {\n        // Rate limit check\n        if let Some(ip) = &from {\n            if self.rate_limiter.check_is_limited_and_increase(ip) {\n                tracing::trace!(\"Rate limited {}. query_id={}\", query.packet.id(), ip);\n                return query.packet.create_refused_reply();\n            };\n        }\n\n        // Based on https://datatracker.ietf.org/doc/html/rfc1034#section-4.3.2\n\n        // Original query coming from the client\n        let original_client_query = query;\n\n        // Main reply to the client\n        let mut client_reply = original_client_query.packet.parsed().clone().into_reply();\n        if self.is_recursion_available() {\n            client_reply.set_flags(PacketFlag::RECURSION_AVAILABLE);\n        } else {\n            client_reply.remove_flags(PacketFlag::RECURSION_AVAILABLE);\n        }\n        let mut next_name_server: Option<SocketAddr> = None; // Name server to target. If none, falls back to default and DHT\n        let mut next_raw_query: ParsedQuery = original_client_query.clone();\n        for i in 0..self.max_recursion_depth {\n            let current_query = next_raw_query.clone();\n            tracing::trace!(\n                \"Recursive lookup {i}/{} NS:{next_name_server:?} - {current_query}\",\n                self.max_recursion_depth,\n            );\n            // println!(\"Recursive lookup {i}/{} NS:{next_name_server:?} - {:?}\", self.max_recursion_depth, current_query.question());\n            let reply = self.query_me_once(&current_query, from, next_name_server).await;\n            next_name_server = None; // Reset target DNS\n            let parsed_reply = Packet::parse(&reply).expect(\"Reply must be a valid dns packet.\");\n\n            if !self.is_recursion_available() {\n                tracing::trace!(\"Recursion not available return.\");\n                return reply;\n            }\n            if !original_client_query.is_recursion_desired() {\n                tracing::trace!(\"Recursion not desired. return.\");\n                return reply;\n            }\n\n            if parsed_reply.rcode() != RCODE::NoError {\n                // Downstream server returned error.\n                tracing::debug!(\n                    \"Downstream server returned error {:?} during recursion. Query: {current_query}\",\n                    parsed_reply.rcode()\n                );\n                *client_reply.rcode_mut() = parsed_reply.rcode();\n                return client_reply.build_bytes_vec().unwrap();\n            }\n\n            if parsed_reply.answers.is_empty() && parsed_reply.name_servers.is_empty() {\n                // No answers and NS received.\n                tracing::warn!(\"Empty reply {current_query}\");\n                return client_reply.build_bytes_vec().unwrap();\n            }\n\n            let matching_answers_names: Vec<&pkarr::dns::ResourceRecord<'_>> = parsed_reply\n                .answers\n                .iter()\n                .filter(|answer| answer.name == current_query.question().qname)\n                .collect();\n\n            // Check for direct matches\n            let matching_answers_names_and_qtype: Vec<&pkarr::dns::ResourceRecord<'_>> = matching_answers_names\n                .clone()\n                .into_iter()\n                .filter(|answer| answer.match_qtype(current_query.question().qtype))\n                .collect();\n\n            if !matching_answers_names_and_qtype.is_empty() {\n                // We found answers matching the name and the type.\n                // Copy everything over and return.\n                tracing::trace!(\"Recursion final answer found.\");\n\n                for answer in parsed_reply.answers {\n                    client_reply.answers.push(answer.into_owned());\n                }\n                for additional in parsed_reply.additional_records {\n                    client_reply.additional_records.push(additional.into_owned());\n                }\n                for ns in parsed_reply.name_servers {\n                    client_reply.name_servers.push(ns.into_owned());\n                }\n                return client_reply.build_bytes_vec().unwrap();\n            }\n\n            // No direct answer matches\n            // Look for a CNAME\n            let matching_cname = matching_answers_names\n                .clone()\n                .into_iter()\n                .find(|answer| answer.match_qtype(QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n\n            if let Some(rr) = matching_cname {\n                // Matching CNAME\n                tracing::trace!(\"Recursion: Matching CNAME {rr:?}\");\n                if let pkarr::dns::rdata::RData::CNAME(val) = &rr.rdata {\n                    // Clone CNAME answer to main reply.\n                    client_reply.answers.push(rr.clone().into_owned());\n                    // Replace question with the content of the cname\n                    let mut question = current_query.question().clone().into_owned();\n                    question.qname = val.0.clone();\n                    let mut next_query = current_query.packet.parsed().clone();\n                    next_query.questions = vec![question];\n                    next_query.set_flags(PacketFlag::RECURSION_DESIRED);\n                    next_raw_query = ParsedQuery::new(next_query.build_bytes_vec().unwrap()).unwrap();\n                    continue;\n                } else {\n                    panic!(\"CNAME match failure. Shouldnt happen.\")\n                };\n            };\n\n            // Look for NS referals\n            let ns_matches: Vec<&pkarr::dns::ResourceRecord<'_>> = parsed_reply\n                .name_servers\n                .iter()\n                .filter(|rr| {\n                    current_query.question().qname.is_subdomain_of(&rr.name)\n                        || current_query.question().qname == rr.name\n                })\n                .collect();\n            if ns_matches.is_empty() {\n                // No NS matches either; Copy additional and return main reply.\n                tracing::trace!(\"No direct and no ns matches\");\n                for additional in parsed_reply.additional_records {\n                    client_reply.additional_records.push(additional.into_owned());\n                }\n                return client_reply.build_bytes_vec().unwrap();\n            }\n\n            tracing::trace!(\"NS matches. {parsed_reply:?}\");\n            let found_name_server = parsed_reply.name_servers.iter().find_map(|ns| {\n                if let RData::NS(NS(ns_name)) = &ns.rdata {\n                    let ns_a_record = parsed_reply.additional_records.iter().find(|rr| {\n                        rr.name == *ns_name && rr.match_qtype(QTYPE::TYPE(pkarr::dns::TYPE::A))\n                            || rr.match_qtype(QTYPE::TYPE(pkarr::dns::TYPE::AAAA))\n                    });\n                    ns_a_record?;\n                    let ns_a_record = ns_a_record.unwrap();\n                    let glued_ns_socket: SocketAddr = match ns_a_record.rdata {\n                        RData::A(A { address }) => {\n                            let ip = Ipv4Addr::from_bits(address);\n                            let socket = SocketAddrV4::new(ip, 53);\n                            socket.into()\n                        }\n                        RData::AAAA(AAAA { address }) => {\n                            let ip = Ipv6Addr::from_bits(address);\n                            let socket = SocketAddrV6::new(ip, 53, 0, 0);\n                            socket.into()\n                        }\n                        _ => panic!(\"Prefiltered, shouldnt happen\"),\n                    };\n                    Some(glued_ns_socket)\n                } else {\n                    None\n                }\n            });\n            if let Some(socket) = &found_name_server {\n                tracing::trace!(\"Found glued nameserver {socket}\");\n                next_name_server = found_name_server;\n                continue;\n            };\n\n            // Unhandled NS response. Probably SOA. Return\n            for ns in parsed_reply.name_servers.iter() {\n                client_reply.name_servers.push(ns.clone().into_owned());\n            }\n            return client_reply.build_bytes_vec().unwrap();\n        }\n\n        // Max recursion exceeded\n        tracing::debug!(\"Max recursion exceeded. {query}\");\n        original_client_query.packet.create_server_fail_reply()\n    }\n\n    /// Query this DNS for data once without recursion.\n    /// from: Client ip used for rate limiting. None disables rate limiting\n    /// target_dns: dns server to query. None falls back to the default fallback DNS\n    async fn query_me_once(\n        &mut self,\n        query: &ParsedQuery,\n        from: Option<IpAddr>,\n        target_dns: Option<SocketAddr>,\n    ) -> Vec<u8> {\n        // Only try the DHT first if no target_dns is manually specified.\n        if target_dns.is_none() {\n            tracing::trace!(\"Trying to resolve the query with the custom handler.\");\n            let result = self.pkarr_resolver.resolve(query, from).await;\n            if result.is_ok() {\n                tracing::trace!(\"Custom handler resolved the query.\");\n                // All good. Handler handled the query\n                return result.unwrap();\n            }\n\n            match result.unwrap_err() {\n                CustomHandlerError::Unhandled => {\n                    tracing::trace!(\"Custom handler rejected the query. {query}\");\n                }\n                CustomHandlerError::Failed(err) => {\n                    tracing::error!(\"Internal error {query}: {}\", err);\n                    return query.packet.create_server_fail_reply();\n                }\n                CustomHandlerError::RateLimited(ip) => {\n                    tracing::error!(\"IP is rate limited {query}: {}\", ip);\n                    return query.packet.create_refused_reply();\n                }\n            };\n        }\n\n        // Forward to ICANN\n        let dns_socket = target_dns.unwrap_or(self.icann_fallback);\n        match self\n            .forward_to_icann(query.packet.clone().raw_bytes(), dns_socket, Duration::from_secs(5))\n            .await\n        {\n            Ok(reply) => reply,\n            Err(e) => {\n                tracing::warn!(\"Forwarding dns query failed. {e} {query}\");\n                query.packet.create_server_fail_reply()\n            }\n        }\n    }\n\n    /// Send dns request to configured forward server\n    pub async fn forward(\n        &mut self,\n        query: &[u8],\n        to: &SocketAddr,\n        timeout: Duration,\n    ) -> Result<Vec<u8>, DnsSocketError> {\n        let packet = Packet::parse(query)?;\n        let (tx, rx) = oneshot::channel::<Vec<u8>>();\n        let forward_id = self.id_manager.get_next(to);\n        let original_id = packet.id();\n        tracing::trace!(\"Fallback to forward server {to:?}. orignal_id={original_id} forward_id={forward_id}\");\n        let request = PendingRequest {\n            original_query_id: original_id,\n            forward_query_id: forward_id,\n            sent_at: Instant::now(),\n            to: *to,\n            tx,\n        };\n\n        let query = replace_packet_id(query, forward_id)?;\n\n        self.pending.insert(request);\n        self.send_to(&query, to).await?;\n\n        // Wait on response\n        let reply = tokio::time::timeout(timeout, rx).await??;\n        let reply = replace_packet_id(&reply, original_id)?;\n\n        Ok(reply)\n    }\n\n    /// Forward query to icann\n    pub async fn forward_to_icann(\n        &mut self,\n        query: &[u8],\n        dns_server: SocketAddr,\n        timeout: Duration,\n    ) -> Result<Vec<u8>, DnsSocketError> {\n        // Check cache first before forwarding\n        if let Ok(Some(item)) = self.icann_cache.get(query).await {\n            let query_packet = Packet::parse(query)?;\n            let new_response = replace_packet_id(&item.response, query_packet.id())?;\n            return Ok(new_response);\n        };\n\n        let reply = self.forward(query, &dns_server, timeout).await?;\n        // Store response in cache\n        if let Err(e) = self.icann_cache.add(query.to_vec(), reply.clone()).await {\n            tracing::warn!(\"Failed to add icann forward reply to cache. {e}\");\n        };\n\n        Ok(reply)\n    }\n\n    // Extracts the id of the query\n    fn extract_query_id(&self, query: &[u8]) -> Result<u16, SimpleDnsError> {\n        Packet::parse(query).map(|packet| packet.id())\n    }\n\n    /// Create a REFUSED reply\n    fn create_refused_reply(query_id: u16) -> Vec<u8> {\n        let mut reply = Packet::new_reply(query_id);\n        *reply.rcode_mut() = RCODE::Refused;\n        reply.build_bytes_vec_compressed().unwrap()\n    }\n\n    /// Create SRVFAIL reply\n    fn create_server_fail_reply(query_id: u16) -> Vec<u8> {\n        let mut reply = Packet::new_reply(query_id);\n        *reply.rcode_mut() = RCODE::ServerFailure;\n        reply.build_bytes_vec_compressed().unwrap()\n    }\n\n    // pub async fn default() -> Result<Self, anyhow::Error> {\n    //     let socket = UdpSocket::bind(\"0.0.0.0:53\").await?;\n    //     let config = get_global_config();\n    //     Ok(Self {\n    //         socket: Arc::new(socket),\n    //         pending: PendingRequestStore::new(),\n    //         pkarr_resolver: PkarrResolver::default().await,\n    //         icann_fallback: \"8.8.8.8:53\".parse().unwrap(),\n    //         id_manager: QueryIdManager::new(),\n    //         rate_limiter: Arc::new(RateLimiterBuilder::new().build()),\n    //         disable_any_queries: config.dns.disable_any_queries,\n    //         icann_cache: IcannLruCache::new(100, config.dns.min_ttl, config.dns.max_ttl),\n    //         max_recursion_depth: 5,\n    //     })\n    // }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::resolution::dns_packets::ParsedQuery;\n    use crate::resolution::pkd::PkarrResolver;\n    use pkarr::dns::rdata::{RData, NS};\n    use pkarr::dns::{\n        rdata::{A, CNAME},\n        Name, Packet, PacketFlag, Question, ResourceRecord, RCODE,\n    };\n    use pkarr::{Client, Keypair, SignedPacket, Timestamp};\n    use std::{\n        net::{Ipv4Addr, SocketAddr},\n        num::NonZeroU64,\n        time::Duration,\n    };\n    use tracing_test::traced_test;\n\n    use super::DnsSocket;\n\n    async fn publish_domain() {\n        // Public key csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\n        let seed = \"a3kco17a6mqawd9jewgwijrd64gb1rmrer1zptxgire7buufk3hy\";\n        let decoded = zbase32::decode_full_bytes_str(seed).unwrap();\n        let seed: [u8; 32] = decoded.try_into().unwrap();\n        let pair = Keypair::from_secret_key(&seed);\n        let pubkey = pair.public_key();\n        let seed = pair.to_z32();\n\n        let mut reply = Packet::new_reply(0);\n        // Regular ICANN CNAME\n        let cname_icann = ResourceRecord::new(\n            Name::new(\"cname-icann\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::CNAME(CNAME(Name::new(\"example.com\").unwrap().into_owned())),\n        );\n        reply.answers.push(cname_icann);\n        // PKD CNAME\n        let cname_pkd = ResourceRecord::new(\n            Name::new(\"cname-pkd\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::CNAME(CNAME(\n                Name::new(\"csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\")\n                    .unwrap()\n                    .into_owned(),\n            )),\n        );\n        reply.answers.push(cname_pkd);\n        // PKD CNAME that points on itself and therefore causes an infinite loop\n        let cname_infinte = ResourceRecord::new(\n            Name::new(\"cname-infinite\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::CNAME(CNAME(\n                Name::new(\"cname-infinite.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\")\n                    .unwrap()\n                    .into_owned(),\n            )),\n        );\n        reply.answers.push(cname_infinte);\n        // PKD CNAME that points on another PKD CNAME that points on a A\n        let cname_pkd2 = ResourceRecord::new(\n            Name::new(\"cname-pkd2\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::CNAME(CNAME(\n                Name::new(\"cname-pkd.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\")\n                    .unwrap()\n                    .into_owned(),\n            )),\n        );\n        reply.answers.push(cname_pkd2);\n        // Regular A entry that anchors the domain.\n        let a = ResourceRecord::new(\n            Name::new(\"\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::A(A {\n                address: Ipv4Addr::new(127, 0, 0, 1).to_bits(),\n            }),\n        );\n        reply.answers.push(a);\n        // Define BIND name server for the sub.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto zone.\n        let ns = ResourceRecord::new(\n            Name::new(\"ns.sub\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::A(A {\n                address: Ipv4Addr::new(95, 217, 214, 181).to_bits(),\n            }),\n        );\n        reply.answers.push(ns);\n        // Delegate sub.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto to the name server.\n        let sub = ResourceRecord::new(\n            Name::new(\"sub\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            300,\n            pkarr::dns::rdata::RData::NS(NS(Name::new(\n                \"ns.sub.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\",\n            )\n            .unwrap())),\n        );\n        reply.answers.push(sub);\n        let signed = SignedPacket::new(&pair, &reply.answers, Timestamp::now()).unwrap();\n        let client = Client::builder().no_relays().build().unwrap();\n        let _res = client.publish(&signed, None).await;\n    }\n\n    /// Create a new dns socket and query recursively.\n    async fn resolve_query_recursively(query: Vec<u8>) -> Vec<u8> {\n        let mut socket = DnsSocket::default_random_socket().await.unwrap();\n        let join_handle = socket.start_receive_loop();\n        let parsed_query = ParsedQuery::new(query).unwrap();\n        let result = socket.query_me_recursively(&parsed_query, None).await;\n        join_handle.send(());\n        result\n    }\n\n    #[tokio::test]\n    async fn recursion_cname_icann() {\n        publish_domain().await;\n\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"cname-icann.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert!(reply.answers.len() >= 2);\n        let cname = reply.answers.first().unwrap();\n        assert!(cname.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n        let a = reply.answers.get(1).unwrap().clone().into_owned();\n        assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n    }\n\n    #[tokio::test]\n    async fn recursion_cname_icann_with_tld() {\n        publish_domain().await;\n\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"cname-icann.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto.key\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert!(reply.answers.len() >= 2);\n        let cname = reply.answers.first().unwrap();\n        assert!(cname.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n        let a = reply.answers.get(1).unwrap().clone().into_owned();\n        assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n    }\n\n    #[tokio::test]\n    async fn recursion_cname_pkd() {\n        // Single recursion CNAME\n        publish_domain().await;\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"cname-pkd.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        dbg!(&reply);\n        assert_eq!(reply.answers.len(), 2);\n        let cname = reply.answers.first().unwrap();\n        assert!(cname.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n        let a = reply.answers.get(1).unwrap().clone().into_owned();\n        assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n    }\n\n    #[tokio::test]\n    async fn recursion_cname_pkd2() {\n        // Double recursion CNAME\n        publish_domain().await;\n\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"cname-pkd2.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert_eq!(reply.answers.len(), 3);\n        let cname1 = reply.answers.first().unwrap();\n        assert!(cname1.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n        let cname2 = reply.answers.get(1).unwrap();\n        assert!(cname2.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n        let a = reply.answers.get(2).unwrap().clone().into_owned();\n        assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n    }\n\n    #[tokio::test]\n    async fn recursion_cname_infinite() {\n        // Infinite recursion CNAME\n        // Check max recursion depth\n        publish_domain().await;\n\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"cname-infinite.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert_eq!(reply.rcode(), RCODE::ServerFailure);\n    }\n\n    #[tokio::test]\n    async fn recursion_not_found1() {\n        // Check if the error is copied to\n        publish_domain().await;\n\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"osjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        // dbg!(&reply);\n        assert_eq!(reply.rcode(), RCODE::NameError);\n    }\n\n    #[tokio::test]\n    async fn recursion_not_found2() {\n        publish_domain().await;\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"yolo.example.com\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert_eq!(reply.rcode(), RCODE::NameError);\n    }\n\n    #[tokio::test]\n    async fn recursion_ns_pkd() {\n        // Single recursion with a delegated zone with an external name server\n        // Domain is saved in the name server and not in the pkarr zone.\n        publish_domain().await;\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"sub.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let reply = Packet::parse(&raw_reply).unwrap();\n        assert_eq!(reply.answers.len(), 1);\n        let a = reply.answers.first().unwrap().clone().into_owned();\n        assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n        assert_eq!(\n            a.rdata,\n            RData::A(A {\n                address: Ipv4Addr::new(37, 27, 13, 182).to_bits()\n            })\n        );\n    }\n\n    #[tokio::test]\n    async fn recursion_ns_soa_icann() {\n        // NS SOA record with lots of cnames\n        let mut query = Packet::new_query(0);\n        let qname = Name::new(\"ap.lijit.com\").unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n        let question = Question::new(qname, qtype, qclass, false);\n        query.questions = vec![question];\n        query.set_flags(PacketFlag::RECURSION_DESIRED);\n        let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n        let raw_reply = resolve_query_recursively(raw_query).await;\n        let final_reply = Packet::parse(&raw_reply).unwrap();\n        dbg!(&final_reply);\n        assert!(!final_reply.answers.is_empty());\n    }\n\n    // TODO: tld support for NS referrals\n    // #[tokio::test]\n    // async fn recursion_ns_pkd_with_tld() {\n    //     // Single recursion with a delegated zone with an external name server\n    //     // Domain is saved in the name server and not in the pkarr zone.\n    //     publish_domain().await;\n    //     let mut query = Packet::new_query(0);\n    //     let qname = Name::new(\"sub.csjbhp9jpbomwh3m5eyrj1py41m8sjpkzzqmzpj5madsi7sc4mto.key\").unwrap();\n    //     let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n    //     let qclass = pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN);\n    //     let question = Question::new(qname, qtype, qclass, false);\n    //     query.questions = vec![question];\n    //     query.set_flags(PacketFlag::RECURSION_DESIRED);\n    //     let raw_query = query.build_bytes_vec_compressed().unwrap();\n\n    //     let raw_reply = resolve_query_recursively(raw_query).await;\n    //     let reply = Packet::parse(&raw_reply).unwrap();\n    //     assert_eq!(reply.answers.len(), 1);\n    //     let a = reply.answers.get(0).unwrap().clone().into_owned();\n    //     assert!(a.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A)));\n    //     assert_eq!(\n    //         a.rdata,\n    //         RData::A(A {\n    //             address: Ipv4Addr::new(37, 27, 13, 182).to_bits()\n    //         })\n    //     );\n    // }\n}\n"
  },
  {
    "path": "server/src/resolution/helpers.rs",
    "content": "use pkarr::dns::{Packet, SimpleDnsError};\n\n/// Replaces the id of a dns packet.\npub fn replace_packet_id(packet: &[u8], new_id: u16) -> Result<Vec<u8>, SimpleDnsError> {\n    let mut cloned = packet.to_vec();\n    let id_bytes = new_id.to_be_bytes();\n    std::mem::replace(&mut cloned[0], id_bytes[0]);\n    std::mem::replace(&mut cloned[1], id_bytes[1]);\n\n    let parsed_packet = Packet::parse(&cloned)?;\n    parsed_packet.build_bytes_vec()\n}\n"
  },
  {
    "path": "server/src/resolution/mod.rs",
    "content": "#![allow(unused)]\n\n/**\n * Basic module to process DNS queries with a UDP socket.\n * Allows to hook into the socket and process custom queries.\n */\nmod dns_socket;\n// mod dns_socket_builder;\nmod helpers;\nmod pending_request;\nmod pkd;\nmod query_id_manager;\nmod rate_limiter;\nmod response_cache;\n\nmod dns_packets;\n\npub use dns_socket::{DnsSocket, DnsSocketError};\n// pub use dns_socket_builder::DnsSocketBuilder;\npub use rate_limiter::{RateLimiter, RateLimiterBuilder};\n"
  },
  {
    "path": "server/src/resolution/pending_request.rs",
    "content": "#![allow(unused)]\n\nuse std::{\n    collections::HashMap,\n    net::SocketAddr,\n    sync::{Arc, Mutex},\n    time::Instant,\n};\n\nuse tokio::sync::oneshot;\n\n/// A pending request to a forward server.\n#[derive(Debug)]\npub struct PendingRequest {\n    /// Where the request was sent to\n    pub to: SocketAddr,\n    /// When the request was sent\n    pub sent_at: Instant,\n    /// The original query id coming from the client\n    pub original_query_id: u16,\n    /// The forward query id sent to the forward server\n    pub forward_query_id: u16,\n    /// The sender to send the response back to the client\n    pub tx: oneshot::Sender<Vec<u8>>,\n}\n\n#[derive(Debug, Clone, Hash, PartialEq)]\nstruct PendingRequestKey {\n    to: SocketAddr,\n    forward_query_id: u16,\n}\n\nimpl Eq for PendingRequestKey {}\n\n/**\n * Thread safe pending request store.\n * Use `.clone()` to give each thread one store struct.\n * The data will stay shared.\n */\n#[derive(Debug, Clone)]\npub struct PendingRequestStore {\n    pending: Arc<Mutex<HashMap<PendingRequestKey, PendingRequest>>>,\n}\n\nimpl PendingRequestStore {\n    /// Insert a new pending request\n    pub fn insert(&mut self, request: PendingRequest) {\n        let mut locked = self.pending.lock().expect(\"Lock is always successful except when poisoned. If poisened it will be poisened forever. We panic here because we can't recover from this.\");\n        let key = PendingRequestKey {\n            forward_query_id: request.forward_query_id,\n            to: request.to,\n        };\n        locked.insert(key, request);\n    }\n\n    /// Remove a pending request by forward query id and from address\n    pub fn remove_by_forward_id(&mut self, forward_query_id: &u16, from: &SocketAddr) -> Option<PendingRequest> {\n        let mut locked = self.pending.lock().expect(\"Lock is always successful except when poisoned. If poisened it will be poisened forever. We panic here because we can't recover from this.\");\n        let key = PendingRequestKey {\n            forward_query_id: *forward_query_id,\n            to: *from,\n        };\n        locked.remove(&key)\n    }\n\n    pub fn new() -> Self {\n        Self {\n            pending: Arc::new(Mutex::new(HashMap::new())),\n        }\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/pkd/bootstrap_nodes.rs",
    "content": "use std::{\n    net::{IpAddr, SocketAddr, UdpSocket},\n    time::Duration,\n};\n\nuse anyhow::anyhow;\nuse rustdns::{Class, Extension, Message, Resource, Type};\n\n#[derive(Debug)]\npub(crate) struct DomainPortAddr {\n    domain: &'static str,\n    port: u16,\n}\n\nimpl DomainPortAddr {\n    pub const fn new(domain: &'static str, port: u16) -> Self {\n        Self { domain, port }\n    }\n}\n\nimpl std::fmt::Display for DomainPortAddr {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}:{}\", self.domain, self.port)\n    }\n}\n\npub(crate) static DEFAULT_BOOTSTRAP_NODES: [DomainPortAddr; 4] = [\n    DomainPortAddr::new(\"router.bittorrent.com\", 6881),\n    DomainPortAddr::new(\"dht.transmissionbt.com\", 6881),\n    DomainPortAddr::new(\"dht.libtorrent.org\", 25401),\n    DomainPortAddr::new(\"router.utorrent.com\", 6881),\n];\n\n#[derive(Debug, thiserror::Error)]\npub enum MainlineBootstrapResolverError {\n    #[error(\"Failed to resolve any of the boostrap node domains\")]\n    DomainResolutionFailed,\n\n    #[error(\"Failed to create a network socket. {0}\")]\n    SocketError(#[from] std::io::Error),\n}\n\n/// Resolve the mainline dht boostrap nodes with a custom dns server.\n/// Used because if pkdns is set as the system dns on the machine, it can't rely\n/// on itself to resolve while starting.\npub(crate) struct MainlineBootstrapResolver {\n    socket: UdpSocket,\n}\n\nimpl MainlineBootstrapResolver {\n    pub fn new(dns_server: SocketAddr) -> Result<Self, MainlineBootstrapResolverError> {\n        let socket = UdpSocket::bind(\"0.0.0.0:0\")?;\n        socket.set_read_timeout(Some(Duration::new(3, 0)))?;\n        socket.connect(dns_server)?;\n        Ok(Self { socket })\n    }\n\n    /// Lookup a domain and return the first A record.\n    fn lookup_domain(&self, domain: &str) -> Result<Option<IpAddr>, MainlineBootstrapResolverError> {\n        let mut m = Message::default();\n        m.add_question(domain, Type::A, Class::Internet);\n        m.add_extension(Extension {\n            // Optionally add a EDNS extension\n            payload_size: 4096, // which supports a larger payload size.\n            ..Default::default()\n        });\n        let question = m.to_vec()?;\n        self.socket.send(&question)?;\n\n        // Wait for a response from the DNS server.\n        let mut resp = [0; 4096];\n        let len = self.socket.recv(&mut resp)?;\n\n        // Take the response bytes and turn it into another DNS Message.\n        let reply = Message::from_slice(&resp[0..len])?;\n        let first_answer = match reply.answers.first() {\n            Some(answer) => answer,\n            None => return Ok(None),\n        };\n\n        match first_answer.resource {\n            Resource::A(val) => Ok(Some(IpAddr::V4(val))),\n            _ => Ok(None),\n        }\n    }\n\n    /// Lookup the domain of the boostrap node and return a SocketAddr.\n    fn lookup(&self, boostrap_node: &DomainPortAddr) -> Result<Option<SocketAddr>, MainlineBootstrapResolverError> {\n        let res = self.lookup_domain(boostrap_node.domain)?;\n        Ok(res.map(|ip| SocketAddr::new(ip, boostrap_node.port)))\n    }\n\n    /// Lookup all the bootstrap nodes and return a list of SocketAddrs.\n    pub fn get_bootstrap_nodes(&self) -> Result<Vec<SocketAddr>, MainlineBootstrapResolverError> {\n        let mut addrs: Vec<SocketAddr> = vec![];\n        for node in DEFAULT_BOOTSTRAP_NODES.iter() {\n            match self.lookup(node) {\n                Ok(Some(val)) => {\n                    addrs.push(val);\n                }\n                Ok(None) => {\n                    tracing::debug!(\"Failed to resolve the DHT bootstrap node domain {node}. No ip found.\");\n                }\n                Err(err) => {\n                    tracing::trace!(\"Failed to resolve the DHT bootstrap node domain {node}. {err}\");\n                }\n            }\n        }\n        if !addrs.is_empty() {\n            Ok(addrs)\n        } else {\n            Err(MainlineBootstrapResolverError::DomainResolutionFailed)\n        }\n    }\n\n    /// Lookup all the bootstrap nodes and return a list of Strings.\n    pub fn get_addrs(dns_server: &SocketAddr) -> Result<Vec<SocketAddr>, MainlineBootstrapResolverError> {\n        let resolver = MainlineBootstrapResolver::new(*dns_server).unwrap();\n        let addrs = resolver.get_bootstrap_nodes()?;\n        Ok(addrs)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[tokio::test]\n    async fn query_domain() {\n        let google_dns: SocketAddr = \"8.8.8.8:53\".parse().expect(\"valid addr\");\n        let resolver = MainlineBootstrapResolver::new(google_dns).unwrap();\n        let res = resolver.lookup_domain(\"example.com\").unwrap().expect(\"Valid ip\");\n    }\n\n    #[tokio::test]\n    async fn query_bootstrap_node() {\n        let google_dns: SocketAddr = \"8.8.8.8:53\".parse().expect(\"valid addr\");\n        let node = DomainPortAddr::new(\"example.com\", 6881);\n        let resolver = MainlineBootstrapResolver::new(google_dns).unwrap();\n        let res = resolver.lookup(&node).expect(\"Valid ip address resolved\").unwrap();\n        assert_eq!(res.port(), 6881);\n    }\n\n    #[tokio::test]\n    async fn query_bootstrap_nodes() {\n        let google_dns: SocketAddr = \"8.8.8.8:53\".parse().expect(\"valid addr\");\n        let resolver = MainlineBootstrapResolver::new(google_dns).unwrap();\n        let addrs = resolver.get_bootstrap_nodes().unwrap();\n        assert_eq!(addrs.len(), 4);\n        assert_eq!(addrs.first().unwrap().to_string(), \"67.215.246.10:6881\");\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/pkd/mod.rs",
    "content": "mod bootstrap_nodes;\nmod pkarr_cache;\nmod pkarr_resolver;\nmod pubkey_parser;\nmod query_matcher;\n\npub use pkarr_resolver::{CustomHandlerError, PkarrResolver, PkarrResolverError};\nuse pubkey_parser::parse_pkarr_uri;\n"
  },
  {
    "path": "server/src/resolution/pkd/pkarr_cache.rs",
    "content": "//!\n//! Goal1: Cache things as long as possible to make any attack on the DHT unfeasible.\n//! Goal2: Prevent attackers from overflowing the cache and evict values this way.\n\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nuse moka::future::Cache;\nuse pkarr::{PublicKey, SignedPacket};\n\n/**\n * Timestamp in seconds since UNIX_EPOCH\n */\nfn get_timestamp_seconds() -> u64 {\n    let start = SystemTime::now();\n    let since_the_epoch = start.duration_since(UNIX_EPOCH).expect(\"Time went backwards\");\n    since_the_epoch.as_secs()\n}\n\n/**\n * Caches pkarr packets and not found pkarr packets.\n * Not found is important to avoid calling the DHT over and over again.\n */\n#[derive(Clone, Debug)]\npub enum CacheItem {\n    NotFound {\n        public_key: PublicKey,\n        /**\n         * When the packet got added to the cache or cache got updated. Seconds timestamp since UNIX_EPOCH.\n         */\n        last_updated_at: u64,\n    },\n    Packet {\n        packet: SignedPacket,\n        /**\n         * When the packet got added to the cache or cache got updated. Seconds timestamp since UNIX_EPOCH.\n         */\n        last_updated_at: u64,\n    },\n}\n\nimpl CacheItem {\n    pub fn new_packet(packet: SignedPacket) -> Self {\n        Self::Packet {\n            packet,\n            last_updated_at: get_timestamp_seconds(),\n        }\n    }\n\n    pub fn new_not_found(pubkey: PublicKey) -> Self {\n        Self::NotFound {\n            public_key: pubkey,\n            last_updated_at: get_timestamp_seconds(),\n        }\n    }\n\n    /// Checks if this cache includes a signed packet.\n    pub fn is_found(&self) -> bool {\n        matches!(\n            self,\n            CacheItem::Packet {\n                packet: _,\n                last_updated_at: _\n            }\n        )\n    }\n\n    pub fn not_found(&self) -> bool {\n        !self.is_found()\n    }\n\n    #[allow(dead_code)]\n    pub fn is_packet(&self) -> bool {\n        matches!(\n            self,\n            CacheItem::Packet {\n                packet: _,\n                last_updated_at: _\n            }\n        )\n    }\n\n    /**\n     * Returns signed packet. Panics if not found.\n     */\n    pub fn unwrap(self) -> SignedPacket {\n        if let CacheItem::Packet {\n            packet,\n            last_updated_at: _,\n        } = self\n        {\n            packet\n        } else {\n            panic!(\"Can not unwrap CacheItem without a packet.\")\n        }\n    }\n\n    pub fn public_key(&self) -> PublicKey {\n        match self {\n            CacheItem::NotFound {\n                public_key,\n                last_updated_at: _,\n            } => public_key.clone(),\n            CacheItem::Packet {\n                packet,\n                last_updated_at: _,\n            } => packet.public_key(),\n        }\n    }\n\n    /**\n     * Updates the cached_at timestamp to now.\n     */\n    pub fn refresh_updated_at(&mut self) {\n        match self {\n            CacheItem::NotFound {\n                public_key: _,\n                last_updated_at: cached_at,\n            } => {\n                *cached_at = get_timestamp_seconds();\n            }\n            CacheItem::Packet {\n                packet: _,\n                last_updated_at: cached_at,\n            } => {\n                *cached_at = get_timestamp_seconds();\n            }\n        }\n    }\n\n    /**\n     * Timestamp given by the controller of the keypair. Basically a version number of the packet.\n     * NotFound items always have a timestamp of 0.\n     */\n    pub fn controller_timestamp(&self) -> u64 {\n        match self {\n            CacheItem::NotFound {\n                public_key: _,\n                last_updated_at: _,\n            } => 0,\n            CacheItem::Packet {\n                packet,\n                last_updated_at: _,\n            } => packet.timestamp().as_u64(),\n        }\n    }\n\n    fn last_updated_at(&self) -> u64 {\n        match self {\n            CacheItem::NotFound {\n                public_key: _,\n                last_updated_at: cached_at,\n            } => *cached_at,\n            CacheItem::Packet {\n                packet: _,\n                last_updated_at: cached_at,\n            } => *cached_at,\n        }\n    }\n\n    /**\n     * Lowest ttl of any anwser in seconds. Used to determine when to update the cache.\n     * NotFound or packet with now answeres => None.\n     */\n    fn lowest_answer_ttl(&self) -> Option<u64> {\n        match self {\n            CacheItem::NotFound {\n                public_key: _,\n                last_updated_at: _,\n            } => None,\n            CacheItem::Packet {\n                packet,\n                last_updated_at: _,\n            } => packet.all_resource_records().map(|answer| answer.ttl as u64).min(),\n        }\n    }\n\n    /**\n     * Size of the cached value in the memory.\n     */\n    pub fn memory_size(&self) -> usize {\n        match self {\n            CacheItem::NotFound {\n                public_key: _,\n                last_updated_at: _,\n            } => {\n                32 + 8 // Public key 32 + cached_at 8\n            }\n            CacheItem::Packet {\n                packet,\n                last_updated_at: _,\n            } => packet.as_bytes().len() + 8,\n        }\n    }\n\n    /**\n     * When the next refresh of this cached element is needed.\n     */\n    pub fn next_refresh_needed_in_s(&self, min_ttl: u64, max_ttl: u64) -> u64 {\n        let ttl = self.lowest_answer_ttl().unwrap_or(min_ttl);\n\n        let ttl = if ttl < min_ttl { min_ttl } else { ttl };\n\n        let ttl = if ttl > max_ttl { max_ttl } else { ttl };\n\n        let now = SystemTime::now()\n            .duration_since(UNIX_EPOCH)\n            .expect(\"Time went backwards\")\n            .as_secs();\n\n        let age_seconds = now - self.last_updated_at();\n        ttl.saturating_sub(age_seconds)\n    }\n}\n\n/**\n * LRU cache for packets.\n */\n#[derive(Clone, Debug)]\npub struct PkarrPacketLruCache {\n    cache: Cache<PublicKey, CacheItem>, // Moka Cache is thread safe\n}\n\nimpl PkarrPacketLruCache {\n    pub fn new(cache_size_mb: Option<u64>) -> Self {\n        let cache_size_mb = cache_size_mb.unwrap_or(100); // 100MB by default\n        PkarrPacketLruCache {\n            cache: Cache::builder()\n                .weigher(|_key, value: &CacheItem| -> u32 { value.memory_size() as u32 })\n                .max_capacity(cache_size_mb * 1024 * 1024)\n                .build(),\n        }\n    }\n\n    /**\n     * Adds a new item to the cache. Makes sure that older items do not override newer items.\n     */\n    async fn add(&mut self, new_item: CacheItem) -> CacheItem {\n        if let Some(mut already_cached) = self.get(&new_item.public_key()).await {\n            // Already in cache\n            let same_age = new_item.controller_timestamp() == already_cached.controller_timestamp();\n            if same_age {\n                // Update cached_at timestamp\n                already_cached.refresh_updated_at();\n                self.cache\n                    .insert(already_cached.public_key(), already_cached.clone())\n                    .await;\n                return already_cached;\n            }\n\n            let new_packet_is_older = new_item.controller_timestamp() < already_cached.controller_timestamp();\n            if new_packet_is_older {\n                // Existing packet is newer than already cached one. Don't update cache. Return existing one.\n                return already_cached;\n            }\n        };\n\n        self.cache.insert(new_item.public_key(), new_item.clone()).await;\n        new_item\n    }\n\n    /**\n     * Adds packet. Makes sure to not override newer instances in the cache.\n     */\n    pub async fn add_packet(&mut self, packet: SignedPacket) -> CacheItem {\n        let new_item = CacheItem::new_packet(packet);\n        self.add(new_item).await\n    }\n\n    /**\n     * Adds not found. Makes sure to not override newer instances in the cache.\n     */\n    pub async fn add_not_found(&mut self, pubkey: PublicKey) -> CacheItem {\n        let new_item = CacheItem::new_not_found(pubkey);\n        self.add(new_item).await\n    }\n\n    /**\n     * Get packet\n     */\n    pub async fn get(&self, pubkey: &PublicKey) -> Option<CacheItem> {\n        let value = self.cache.get(pubkey).await;\n        value\n    }\n\n    /**\n     * Approximated size of the cache in bytes. May not be 100% accurate due to pending counts.\n     */\n    #[allow(dead_code)]\n    pub fn approx_size_bytes(&self) -> u64 {\n        self.cache.weighted_size()\n    }\n\n    #[allow(dead_code)]\n    pub fn entry_count(&self) -> u64 {\n        self.cache.entry_count()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use pkarr::{\n        dns::{Name, Packet, ResourceRecord},\n        Keypair, SignedPacket, Timestamp,\n    };\n\n    use super::*;\n    use std::net::Ipv4Addr;\n\n    fn example_signed_packet(keypair: Keypair) -> SignedPacket {\n        let mut packet = Packet::new_reply(0);\n        let ip: Ipv4Addr = \"93.184.216.34\".parse().unwrap();\n        let record = ResourceRecord::new(\n            Name::new(\"pknames.p2p\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            100,\n            pkarr::dns::rdata::RData::A(ip.into()),\n        );\n        packet.answers.push(record);\n        let record = ResourceRecord::new(\n            Name::new(\".\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            100,\n            pkarr::dns::rdata::RData::A(ip.into()),\n        );\n        packet.answers.push(record);\n        SignedPacket::new(&keypair, &packet.answers, Timestamp::now()).unwrap()\n    }\n\n    #[tokio::test]\n    async fn packet_memory_size() {\n        let packet = example_signed_packet(Keypair::random());\n        let cached = CacheItem::new_packet(packet.clone());\n        assert_eq!(cached.memory_size(), 220);\n    }\n\n    #[tokio::test]\n    async fn cache_size() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        assert_eq!(cache.approx_size_bytes(), 0);\n\n        for _ in 0..10 {\n            cache.add_packet(example_signed_packet(Keypair::random())).await;\n        }\n        cache.cache.run_pending_tasks().await;\n        assert_eq!(cache.approx_size_bytes(), 2200);\n    }\n\n    #[tokio::test]\n    async fn insert_get() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        let packet = example_signed_packet(Keypair::random());\n        cache.add_packet(packet.clone()).await;\n\n        for _ in 0..10 {\n            cache.add_packet(example_signed_packet(Keypair::random())).await;\n        }\n\n        let recalled = cache.get(&packet.public_key()).await.expect(\"Value must be in cache\");\n        assert_eq!(recalled.public_key(), packet.public_key());\n    }\n\n    #[tokio::test]\n    async fn override_old_cached_packet() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        let key = Keypair::random();\n        let packet1 = example_signed_packet(key.clone());\n        let packet2 = example_signed_packet(key.clone());\n        assert_ne!(packet1.timestamp(), packet2.timestamp());\n\n        cache.add_packet(packet1.clone()).await;\n        cache.add_packet(packet2.clone()).await;\n        let cached = cache.get(&key.public_key()).await.unwrap();\n        assert_eq!(packet2.timestamp().as_u64(), cached.controller_timestamp());\n    }\n\n    #[tokio::test]\n    async fn keep_newer_cached_packet() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        let key = Keypair::random();\n        let packet1 = example_signed_packet(key.clone());\n        let packet2 = example_signed_packet(key.clone());\n        assert_ne!(packet1.timestamp(), packet2.timestamp());\n\n        cache.add_packet(packet2.clone()).await;\n        cache.add_packet(packet1.clone()).await;\n        let cached = cache.get(&key.public_key()).await.unwrap();\n        assert_eq!(packet2.timestamp().as_u64(), cached.controller_timestamp());\n    }\n\n    #[tokio::test]\n    async fn override_old_not_found_cached_packet() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        let key = Keypair::random();\n        let packet1 = example_signed_packet(key.clone());\n        cache.add(CacheItem::new_not_found(key.public_key())).await;\n        let cached = cache.get(&key.public_key()).await.unwrap();\n        assert_eq!(cached.controller_timestamp(), 0);\n        cache.add_packet(packet1.clone()).await;\n        let cached = cache.get(&key.public_key()).await.unwrap();\n        assert_eq!(packet1.timestamp().as_u64(), cached.controller_timestamp());\n    }\n\n    #[tokio::test]\n    async fn not_found_not_overriding_cached_packet() {\n        let mut cache = PkarrPacketLruCache::new(Some(1));\n        let key = Keypair::random();\n        let packet1 = example_signed_packet(key.clone());\n        cache.add_packet(packet1.clone()).await;\n        cache.add(CacheItem::new_not_found(key.public_key())).await;\n        let cached = cache.get(&key.public_key()).await.unwrap();\n        assert_eq!(packet1.timestamp().as_u64(), cached.controller_timestamp());\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/pkd/pkarr_resolver.rs",
    "content": "use super::{pubkey_parser::parse_pkarr_uri, query_matcher::create_domain_not_found_reply};\nuse crate::{\n    app_context::AppContext,\n    config::TopLevelDomain,\n    resolution::{dns_packets::ParsedQuery, DnsSocket, DnsSocketError, RateLimiter, RateLimiterBuilder},\n};\nuse pkarr::{\n    dns::{Name, Question, ResourceRecord},\n    Client,\n};\nuse std::{\n    collections::HashMap,\n    net::{IpAddr, SocketAddr},\n    num::NonZeroU32,\n    sync::Arc,\n};\nuse tokio::sync::Mutex;\n\nuse super::{\n    bootstrap_nodes::MainlineBootstrapResolver,\n    pkarr_cache::{CacheItem, PkarrPacketLruCache},\n    query_matcher::resolve_query,\n};\nuse pkarr::{\n    dns::Packet,\n    // mainline::dht::DhtSettings, Error as PkarrError, PkarrClient, PkarrClientAsync,\n    PublicKey,\n};\n\n/// Errors that a CustomHandler can return.\n#[derive(thiserror::Error, Debug)]\npub enum CustomHandlerError {\n    /// Lookup failed. Error will be logged. SRVFAIL will be returned to the user.\n    #[error(transparent)]\n    Failed(#[from] Box<dyn std::error::Error + Send + Sync>),\n\n    /// Handler does not consider itself responsible for this query.\n    /// Will fallback to ICANN.\n    #[error(\"Query is not processed by handler. Fallback to ICANN.\")]\n    Unhandled,\n\n    /// Handler rate limited the IP. Will return RCODE::Refused.\n    #[error(\"Source ip address {0} is rate limited.\")]\n    RateLimited(IpAddr),\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum PkarrResolverError {\n    // #[error(\"Failed to query the DHT with pkarr: {0}\")]\n    // Dht(#[from] PkarrError),\n    #[error(\"Failed to query the DHT with pkarr: {0}\")]\n    DnsSocket(#[from] DnsSocketError),\n}\n\n/**\n * Pkarr resolver with cache.\n */\n#[derive(Clone, Debug)]\npub struct PkarrResolver {\n    client: Client,\n    cache: PkarrPacketLruCache,\n    /**\n     * Locks to use to update pkarr packets. This avoids concurrent updates.\n     */\n    lock_map: Arc<Mutex<HashMap<PublicKey, Arc<Mutex<()>>>>>,\n    context: AppContext,\n    rate_limiter: Arc<RateLimiter>,\n}\n\nimpl PkarrResolver {\n    /**\n     * Resolves the DHT boostrap nodes with the forward server.\n     */\n    fn resolve_bootstrap_nodes(forward_dns_server: &SocketAddr) -> Vec<SocketAddr> {\n        tracing::debug!(\n            \"Connecting to the DNS forward server {}...\",\n            forward_dns_server.to_string()\n        );\n\n        let addrs = match MainlineBootstrapResolver::get_addrs(forward_dns_server) {\n            Ok(addrs) => addrs,\n            Err(err) => {\n                tracing::error!(\"{}\", err);\n                tracing::error!(\"Connecting to the DNS forward server failed. Couldn't resolve the DHT bootstrap nodes. Is the DNS forward server active?\");\n                panic!(\"Resolving bootstrap nodes failed. {}\", err);\n            }\n        };\n\n        tracing::debug!(\"DHT bootstrap nodes resolved.\");\n        addrs\n    }\n\n    #[cfg(test)]\n    pub async fn default() -> Self {\n        let context = AppContext::test();\n        Self::new(&context).await\n    }\n\n    pub async fn new(context: &AppContext) -> Self {\n        let addrs = Self::resolve_bootstrap_nodes(&context.config.general.forward);\n        let client = Client::builder()\n            .minimum_ttl(0)\n            .maximum_ttl(0) // Disable Pkarr caching\n            .bootstrap(&addrs)\n            .build()\n            .unwrap();\n        let limiter = RateLimiterBuilder::new().max_per_second(context.config.dht.dht_query_rate_limit);\n        Self {\n            client,\n            cache: PkarrPacketLruCache::new(Some(context.config.dht.dht_cache_mb.into())),\n            lock_map: Arc::new(Mutex::new(HashMap::new())),\n            rate_limiter: Arc::new(limiter.build()),\n            context: context.clone(),\n        }\n    }\n\n    fn is_refresh_needed(&self, item: &CacheItem) -> bool {\n        let refresh_needed_in_s =\n            item.next_refresh_needed_in_s(self.context.config.dns.min_ttl, self.context.config.dns.max_ttl);\n        refresh_needed_in_s == 0\n    }\n\n    /**\n     * Resolves a public key. Checks the cache first.\n     */\n    async fn resolve_pubkey_respect_cache(\n        &mut self,\n        pubkey: &PublicKey,\n        from: Option<IpAddr>,\n    ) -> Result<CacheItem, CustomHandlerError> {\n        if let Some(cached) = self.cache.get(pubkey).await {\n            let refresh_needed_in_s =\n                cached.next_refresh_needed_in_s(self.context.config.dns.min_ttl, self.context.config.dns.max_ttl);\n\n            if refresh_needed_in_s > 0 {\n                tracing::trace!(\n                    \"Pkarr packet [{pubkey}] found in cache. Cache valid for {}s\",\n                    refresh_needed_in_s\n                );\n                return Ok(cached);\n            }\n        };\n\n        if let Some(ip) = from {\n            let is_rate_limited = self.rate_limiter.check_is_limited_and_increase(&ip);\n            if is_rate_limited {\n                tracing::debug!(\"{ip} is rate limited from querying the DHT.\");\n                return Err(CustomHandlerError::RateLimited(ip));\n            }\n        }\n\n        self.lookup_dht_and_cache(pubkey.clone())\n            .await\n            .map_err(|err| CustomHandlerError::Failed(err.into()))\n    }\n\n    /// Lookup DHT to pull pkarr packet. Will not check the cache first but store any new value in the cache. Returns cached value if lookup fails.\n    async fn lookup_dht_and_cache(&mut self, pubkey: PublicKey) -> Result<CacheItem, PkarrResolverError> {\n        let mut locked_map = self.lock_map.lock().await;\n        let mutex = locked_map\n            .entry(pubkey.clone())\n            .or_insert_with(|| Arc::new(Mutex::new(())));\n        let _guard = mutex.lock().await;\n\n        if let Some(cache) = self.cache.get(&pubkey).await {\n            if !self.is_refresh_needed(&cache) {\n                // Value got updated in the meantime while aquiring the lock.\n                tracing::trace!(\"Refresh for [{pubkey}] not needed. Value got updated in the meantime.\");\n                return Ok(cache);\n            }\n        }\n\n        tracing::trace!(\"Lookup [{pubkey}] on the DHT.\");\n        let signed_packet = self.client.resolve(&pubkey).await;\n        if signed_packet.is_none() {\n            tracing::debug!(\"DHT lookup for [{pubkey}] failed. Nothing found.\");\n            return Ok(self.cache.add_not_found(pubkey).await);\n        };\n\n        tracing::trace!(\"Refreshed cache for [{pubkey}].\");\n        let new_packet = signed_packet.unwrap();\n        Ok(self.cache.add_packet(new_packet).await)\n    }\n\n    fn remove_tld_if_necessary(&self, mut query: &mut Packet<'_>) -> bool {\n        if let Some(tld) = &self.context.config.dht.top_level_domain {\n            if tld.question_ends_with_pubkey_tld(query) {\n                tld.remove(query);\n                return true;\n            }\n        }\n        false\n    }\n\n    fn add_tld_if_necessary(&self, mut reply: &mut Packet<'_>) -> bool {\n        if let Some(tld) = &self.context.config.dht.top_level_domain {\n            tld.add(reply);\n            return true;\n        }\n        false\n    }\n\n    /**\n     * Resolves a domain with pkarr.\n     */\n    pub async fn resolve(\n        &mut self,\n        query: &ParsedQuery,\n        from: Option<IpAddr>,\n    ) -> std::prelude::v1::Result<Vec<u8>, CustomHandlerError> {\n        let mut request = query.packet.parsed().clone();\n        let mut removed_tld = self.remove_tld_if_necessary(&mut request);\n        if removed_tld {\n            tracing::trace!(\"Removed tld from question: {:?}\", request.questions.first().unwrap());\n        }\n\n        let question = request\n            .questions\n            .first()\n            .expect(\"No question in query in pkarr_resolver.\")\n            .clone();\n        let labels = question.qname.get_labels();\n        let mut public_key = labels\n            .last()\n            .expect(\"Question labels with no domain in pkarr_resolver\")\n            .to_string();\n\n        let parsed_option = parse_pkarr_uri(&public_key);\n        if let Err(e) = parsed_option {\n            return match e {\n                super::pubkey_parser::PubkeyParserError::InvalidKey(_) => {\n                    tracing::trace!(\"TLD .{public_key} is not a pkarr key. Fallback to ICANN.\");\n                    Err(CustomHandlerError::Unhandled)\n                }\n                super::pubkey_parser::PubkeyParserError::ValidButDifferent => {\n                    tracing::trace!(\"TLD .{public_key} is a pkarr key but its last bits are invalid.\");\n                    Ok(create_domain_not_found_reply(request.id()))\n                }\n            };\n        }\n\n        let pubkey = parsed_option.unwrap();\n\n        match self.resolve_pubkey_respect_cache(&pubkey, from).await {\n            Ok(item) => {\n                if item.not_found() {\n                    return Ok(create_domain_not_found_reply(request.id()));\n                };\n\n                let signed_packet = item.unwrap();\n                let mut packet = Packet::new_reply(0);\n                for rr in signed_packet.all_resource_records() {\n                    packet.answers.push(rr.clone());\n                }\n                let reply = resolve_query(&packet, &request).await;\n\n                let reply = if removed_tld {\n                    let mut packet = Packet::parse(&reply).unwrap();\n                    self.add_tld_if_necessary(&mut packet);\n                    packet.build_bytes_vec().unwrap()\n                } else {\n                    reply\n                };\n                Ok(reply)\n            }\n            Err(err) => Err(err),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use chrono::{DateTime, Utc};\n    use pkarr::{\n        dns::{Name, Packet, Question, ResourceRecord},\n        Keypair, SignedPacket, Timestamp,\n    };\n\n    // use pkarr::dns::{Name, Question, Packet};\n    use super::*;\n    use std::net::Ipv4Addr;\n\n    trait SignedPacketTimestamp {\n        fn chrono_timestamp(&self) -> DateTime<Utc>;\n    }\n\n    impl SignedPacketTimestamp for SignedPacket {\n        fn chrono_timestamp(&self) -> DateTime<Utc> {\n            let timestamp = self.timestamp().as_u64() / 1_000_000;\n\n            DateTime::from_timestamp((timestamp as u32).into(), 0).unwrap()\n        }\n    }\n\n    fn get_test_keypair() -> Keypair {\n        // pk:cb7xxx6wtqr5d6yqudkt47drqswxk57dzy3h7qj3udym5puy9cso\n        let secret = \"6kfe1u5jyqxg644eqfgk1cp4w9yjzwq51rn11ftysuo6xkpc64by\";\n        let seed = zbase32::decode_full_bytes_str(secret).unwrap();\n        let slice: &[u8; 32] = &seed[0..32].try_into().unwrap();\n\n        Keypair::from_secret_key(slice)\n    }\n\n    async fn publish_record() {\n        let keypair = get_test_keypair();\n        // let uri = keypair.to_uri_string();\n        // println!(\"Publish packet with pubkey {}\", uri);\n\n        let mut packet = Packet::new_reply(0);\n        let ip: Ipv4Addr = \"93.184.216.34\".parse().unwrap();\n        let record = ResourceRecord::new(\n            Name::new(\"pknames.p2p\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            100,\n            pkarr::dns::rdata::RData::A(ip.into()),\n        );\n        packet.answers.push(record);\n        let record = ResourceRecord::new(\n            Name::new(\".\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            100,\n            pkarr::dns::rdata::RData::A(ip.into()),\n        );\n        packet.answers.push(record);\n        let signed_packet = SignedPacket::new(&keypair, &packet.answers, Timestamp::now()).unwrap();\n\n        let client = Client::builder().no_relays().build().unwrap();\n        let result = client.publish(&signed_packet, None).await;\n        result.expect(\"Should have published.\");\n    }\n\n    #[tokio::test]\n    async fn query_domain() {\n        publish_record().await;\n\n        let keypair = get_test_keypair();\n        let domain = format!(\"pknames.p2p.{}\", keypair.to_z32());\n        let name = Name::new(&domain).unwrap();\n        let mut query = Packet::new_query(0);\n        let question = Question::new(\n            name.clone(),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            true,\n        );\n        query.questions.push(question);\n        let query = ParsedQuery::new(query.build_bytes_vec().unwrap()).unwrap();\n\n        let mut resolver = PkarrResolver::default().await;\n        let result = resolver.resolve(&query, None).await;\n        assert!(result.is_ok());\n        let reply_bytes = result.unwrap();\n        let reply = Packet::parse(&reply_bytes).unwrap();\n        assert_eq!(reply.id(), query.packet.id());\n        assert_eq!(reply.answers.len(), 1);\n        let answer = reply.answers.first().unwrap();\n        assert_eq!(answer.name.to_string(), name.to_string());\n        assert_eq!(answer.rdata.type_code(), pkarr::dns::TYPE::A);\n    }\n\n    #[tokio::test]\n    async fn query_pubkey() {\n        publish_record().await;\n\n        let keypair = get_test_keypair();\n        let domain = keypair.to_z32();\n        let name = Name::new(&domain).unwrap();\n        let mut query = Packet::new_query(0);\n        let question = Question::new(\n            name.clone(),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            true,\n        );\n        query.questions.push(question);\n        let query = ParsedQuery::new(query.build_bytes_vec().unwrap()).unwrap();\n        let mut resolver = PkarrResolver::default().await;\n        let result = resolver.resolve(&query, None).await;\n        assert!(result.is_ok());\n        let reply_bytes = result.unwrap();\n        let reply = Packet::parse(&reply_bytes).unwrap();\n        assert_eq!(reply.id(), query.packet.id());\n        assert_eq!(reply.answers.len(), 1);\n        let answer = reply.answers.first().unwrap();\n        assert_eq!(answer.name.to_string(), name.to_string());\n        assert_eq!(answer.rdata.type_code(), pkarr::dns::TYPE::A);\n    }\n\n    #[tokio::test]\n    async fn query_invalid_pubkey() {\n        let domain = \"invalid_pubkey\";\n        let name = Name::new(domain).unwrap();\n        let mut query = Packet::new_query(0);\n        let question = Question::new(\n            name.clone(),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            true,\n        );\n        query.questions.push(question);\n        let query = ParsedQuery::new(query.build_bytes_vec().unwrap()).unwrap();\n        let mut resolver = PkarrResolver::default().await;\n        let result = resolver.resolve(&query, None).await;\n        assert!(result.is_err());\n    }\n\n    #[tokio::test]\n    async fn pkarr_invalid_packet1() {\n        let pubkey = parse_pkarr_uri(\"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\").unwrap();\n\n        let mut resolver = PkarrResolver::default().await;\n        let _result = resolver.resolve_pubkey_respect_cache(&pubkey, None).await;\n        // assert!(result.is_some());\n    }\n\n    #[test]\n    fn pkarr_invalid_packet3() {\n        let keypair = Keypair::random();\n        let pubkey_z32 = keypair.to_z32();\n\n        // Construct reply with single CNAME record.\n        let mut packet = Packet::new_reply(0);\n\n        let name = Name::new(\"www.pknames.p2p\").unwrap();\n        let data = format!(\"pknames.p2p.{pubkey_z32}\");\n        let data = Name::new(&data).unwrap();\n        let answer3 = ResourceRecord::new(\n            name.clone(),\n            pkarr::dns::CLASS::IN,\n            100,\n            pkarr::dns::rdata::RData::CNAME(pkarr::dns::rdata::CNAME(data)),\n        );\n        packet.answers.push(answer3);\n\n        // Sign packet\n        let signed_packet = SignedPacket::new(&keypair, &packet.answers, Timestamp::now()).unwrap();\n\n        // Serialize and parse again\n        let reply_bytes = signed_packet.encoded_packet();\n        Packet::parse(&reply_bytes).unwrap(); // Fail\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/pkd/pubkey_parser.rs",
    "content": "use pkarr::PublicKey;\n\n#[derive(Debug, thiserror::Error)]\npub enum PubkeyParserError {\n    #[error(\"Invalid public key. {0}\")]\n    InvalidKey(String),\n    #[error(\"Key is valid zbase32 and length but the last bits are incorrect.\")]\n    ValidButDifferent,\n}\n\n/// Parses a public key domain from it's zbase32 format.\npub fn parse_pkarr_uri(uri: &str) -> Result<PublicKey, PubkeyParserError> {\n    let decoded = match zbase32::decode_full_bytes_str(uri) {\n        Ok(bytes) => bytes,\n        Err(e) => return Err(PubkeyParserError::InvalidKey(e.to_string())),\n    };\n    if decoded.len() != 32 {\n        return Err(PubkeyParserError::InvalidKey(\n            \"zbase32 pubkey should be 32 bytes but is not.\".to_string(),\n        ));\n    };\n    let encoded = zbase32::encode_full_bytes(&decoded);\n    if encoded.as_str() != uri {\n        tracing::trace!(\n            \"Uri {uri} is not a valid public key. Error corrected should be {encoded}. Failed to parse pkarr pubkey.\"\n        );\n        return Err(PubkeyParserError::ValidButDifferent);\n    }\n\n    let trying: Result<PublicKey, _> = uri.try_into();\n    trying.map_err(|err| PubkeyParserError::InvalidKey(err.to_string()))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_pkarr_uri() {\n        let uri = \"7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\";\n        let pubkey: PublicKey = uri.try_into().unwrap();\n        assert_eq!(pubkey.to_string(), uri);\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/pkd/query_matcher.rs",
    "content": "use std::{\n    net::{Ipv4Addr, Ipv6Addr, SocketAddr},\n    time::Duration,\n};\n\nuse crate::resolution::DnsSocket;\nuse pkarr::dns::{\n    rdata::{self, RData},\n    Name, Packet, PacketFlag, Question, ResourceRecord, QTYPE, RCODE, TYPE,\n};\n\n/**\n * Handles all possible ways on how to resolve a query into a reply.\n * Does not support forwards, only recursive queries.\n * Max CNAME depth == 1.\n * Uses a query to transforms a pkarr reply into an regular reply\n */\npub async fn resolve_query<'a>(pkarr_packet: &Packet<'a>, query: &Packet<'a>) -> Vec<u8> {\n    let question = query.questions.first().unwrap(); // Has at least 1 question based on previous checks.\n    let pkarr_reply = resolve_question(pkarr_packet, question).await;\n    let pkarr_reply = Packet::parse(&pkarr_reply).unwrap();\n\n    let mut reply = query.clone().into_reply();\n    reply.answers = pkarr_reply.answers;\n    reply.additional_records = pkarr_reply.additional_records;\n    reply.name_servers = pkarr_reply.name_servers;\n\n    reply.build_bytes_vec_compressed().unwrap()\n}\n\n/**\n * Resolves a question by filtering the pkarr packet and creating a corresponding reply.\n */\nasync fn resolve_question<'a>(pkarr_packet: &Packet<'a>, question: &Question<'a>) -> Vec<u8> {\n    let mut reply = Packet::new_reply(0);\n\n    let direct_matchs = direct_matches(pkarr_packet, &question.qname, &question.qtype);\n    reply.answers.extend(direct_matchs.clone());\n\n    if reply.answers.is_empty() {\n        // Not found. Maybe it is a cname?\n        let cname_matches = resolve_cname_for(pkarr_packet, question);\n        reply.answers.extend(cname_matches);\n    };\n\n    if reply.answers.is_empty() {\n        // Not found. Maybe we have a name server?\n        reply.name_servers = find_nameserver(pkarr_packet, &question.qname);\n\n        // Add all glued A/AAAA records to the additional section\n        for ns in reply.name_servers.iter() {\n            if let RData::NS(val) = &ns.rdata {\n                let name = &val.0;\n                let matches_a = direct_matches(pkarr_packet, name, &QTYPE::TYPE(TYPE::A));\n                let matches_aaaa = direct_matches(pkarr_packet, name, &QTYPE::TYPE(TYPE::AAAA));\n                let merged_matches: Vec<_> = matches_a.into_iter().chain(matches_aaaa.into_iter()).collect();\n                reply.additional_records.extend(merged_matches);\n            };\n        }\n    };\n\n    reply.build_bytes_vec_compressed().unwrap()\n}\n\n/**\n * Resolve a cnames for a given. Only goes to max 1 depth. CNAME always needs to point to a A/AAAA record.\n */\nfn resolve_cname_for<'a>(pkarr_packet: &Packet<'a>, question: &Question<'a>) -> Vec<ResourceRecord<'a>> {\n    let cname_matches = direct_matches(pkarr_packet, &question.qname, &QTYPE::TYPE(TYPE::CNAME));\n\n    let additional_data: Vec<ResourceRecord<'_>> = cname_matches\n        .iter()\n        .flat_map(|cname| {\n            let cname_content = if let RData::CNAME(rdata::CNAME(cname_pointer)) = &cname.rdata {\n                cname_pointer\n            } else {\n                panic!(\"Should be cname\");\n            };\n            let matches = direct_matches(pkarr_packet, cname_content, &question.qtype);\n            matches\n        })\n        .collect();\n\n    let mut result = vec![];\n    result.extend(cname_matches);\n    result.extend(additional_data);\n\n    result\n}\n\n/**\n * Resolve direct qname and qtype record matches.\n */\nfn direct_matches<'a>(pkarr_packet: &Packet<'a>, qname: &Name<'a>, qtype: &QTYPE) -> Vec<ResourceRecord<'a>> {\n    let matches: Vec<ResourceRecord<'_>> = pkarr_packet\n        .answers\n        .iter()\n        .filter(|record| record.name == *qname && record.match_qtype(*qtype))\n        .cloned()\n        .collect();\n    matches\n}\n\n/**\n * Find nameserver for given qname.\n */\nfn find_nameserver<'a>(pkarr_packet: &Packet<'a>, qname: &Name<'a>) -> Vec<ResourceRecord<'a>> {\n    let matches: Vec<ResourceRecord<'_>> = pkarr_packet\n        .answers\n        .iter()\n        .filter(|record| {\n            record.match_qtype(QTYPE::TYPE(TYPE::NS)) && (qname.is_subdomain_of(&record.name) || record.name == *qname)\n        })\n        .cloned()\n        .collect();\n    matches\n}\n\n// /**\n//  * Resolve name server ip\n//  */\n// async fn resolve_ns_ip<'a>(ns_name: &Name<'a>) -> Option<Vec<SocketAddr>> {\n//     let ns_question = Question::new(\n//         ns_name.clone(),\n//         QTYPE::TYPE(TYPE::A),\n//         pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n//         false,\n//     );\n//     let mut query = Packet::new_query(0);\n//     query.questions.push(ns_question);\n//     query.set_flags(PacketFlag::RECURSION_DESIRED);\n//     let query = query.build_bytes_vec_compressed().unwrap();\n//\n//     let reply = socket.query_me(&query, None).await;\n//     let reply = Packet::parse(&reply).ok()?;\n//     if reply.answers.len() == 0 {\n//         return None;\n//     };\n//\n//     let addresses: Vec<SocketAddr> = reply\n//         .answers\n//         .into_iter()\n//         .filter_map(|record| match record.rdata {\n//             RData::A(data) => {\n//                 let ip = Ipv4Addr::from(data.address);\n//                 Some(SocketAddr::new(ip.into(), 53))\n//             }\n//             RData::AAAA(data) => {\n//                 let ip = Ipv6Addr::from(data.address);\n//                 Some(SocketAddr::new(ip.into(), 53))\n//             }\n//             _ => None,\n//         })\n//         .collect();\n//\n//     Some(addresses)\n// }\n//\n// /**\n//  * Resolves the question with a single ns redirection.\n//  */\n// async fn resolve_with_ns<'a>(\n//     question: &Question<'a>,\n//     name_servers: &Vec<ResourceRecord<'a>>,\n// ) -> Option<Vec<u8>> {\n//     if name_servers.len() == 0 {\n//         return None;\n//     }\n//\n//     let ns_names: Vec<Name<'_>> = name_servers\n//         .iter()\n//         .filter_map(|record| {\n//             if let RData::NS(data) = record.clone().rdata {\n//                 Some(data.0)\n//             } else {\n//                 None\n//             }\n//         })\n//         .collect();\n//\n//     let ns_name = ns_names.first().unwrap();\n//     let addresses = resolve_ns_ip(ns_name).await?;\n//     let addr = addresses.first().unwrap();\n//\n//     let mut query = Packet::new_query(0);\n//     query.questions.push(question.clone());\n//     query.set_flags(PacketFlag::RECURSION_DESIRED);\n//     let query = query.build_bytes_vec_compressed().unwrap();\n//\n//     socket.forward(&query, addr, Duration::from_millis(1000)).await.ok()\n// }\n\n/**\n * Constructs a reply indicating that the query got rate limited.\n */\npub fn create_domain_not_found_reply(query_id: u16) -> Vec<u8> {\n    let mut reply = Packet::new_reply(query_id);\n    *reply.rcode_mut() = RCODE::NameError;\n    reply.build_bytes_vec_compressed().unwrap()\n}\n\n#[cfg(test)]\nmod tests {\n    use std::net::Ipv4Addr;\n\n    use crate::app_context::AppContext;\n    use crate::resolution::{pkd::PkarrResolver, DnsSocket};\n    use pkarr::dns::{rdata::RData, Question};\n    use pkarr::{\n        dns::{Name, Packet, ResourceRecord},\n        Keypair, PublicKey,\n    };\n\n    use super::{resolve_query, resolve_question};\n\n    fn example_pkarr_reply() -> (Vec<u8>, PublicKey) {\n        // pkarr.normalize_names makes sure all names end with the pubkey.\n        // @ is invalid and just syntactic sugar on top of pkarr.normalize_names\n        // No ending dot.\n\n        let keypair = Keypair::random();\n        let pubkey = keypair.public_key();\n        let pubkey_z32 = keypair.to_z32();\n        let mut packet = Packet::new_reply(0);\n\n        let name = Name::new(&pubkey_z32).unwrap();\n        let ip: Ipv4Addr = \"127.0.0.1\".parse().unwrap();\n        let answer1 = ResourceRecord::new(name.clone(), pkarr::dns::CLASS::IN, 100, RData::A(ip.into()));\n        packet.answers.push(answer1);\n\n        let name = format!(\"pknames.p2p.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let ip: Ipv4Addr = \"127.0.0.1\".parse().unwrap();\n        let answer1 = ResourceRecord::new(name.clone(), pkarr::dns::CLASS::IN, 100, RData::A(ip.into()));\n        packet.answers.push(answer1);\n\n        let name = format!(\"www.pknames.p2p.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let data = format!(\"pknames.p2p.{pubkey_z32}\");\n        let data = Name::new(&data).unwrap();\n        let answer3 = ResourceRecord::new(\n            name.clone(),\n            pkarr::dns::CLASS::IN,\n            100,\n            RData::CNAME(pkarr::dns::rdata::CNAME(data)),\n        );\n        packet.answers.push(answer3);\n\n        let name = format!(\"other.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let data = \"my.ns.example.com\".to_string();\n        let data = Name::new(&data).unwrap();\n        let answer4 = ResourceRecord::new(\n            name.clone(),\n            pkarr::dns::CLASS::IN,\n            100,\n            RData::NS(pkarr::dns::rdata::NS(data)),\n        );\n        packet.answers.push(answer4);\n\n        (packet.build_bytes_vec_compressed().unwrap(), pubkey)\n    }\n\n    #[tokio::test]\n    async fn simple_a_question() {\n        let (pkarr_packet, pubkey) = example_pkarr_reply();\n        let pkarr_packet = Packet::parse(&pkarr_packet).unwrap();\n        let pubkey_z32 = pubkey.to_z32();\n\n        let name = format!(\"pknames.p2p.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let question = Question::new(\n            name.clone(),\n            qtype,\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        );\n\n        let reply = resolve_question(&pkarr_packet, &question).await;\n        let reply = Packet::parse(&reply).unwrap();\n        assert_eq!(reply.answers.len(), 1);\n        assert_eq!(reply.additional_records.len(), 0);\n        assert_eq!(reply.name_servers.len(), 0);\n        let answer = reply.answers.first().unwrap();\n        assert_eq!(answer.name, name);\n        assert!(answer.match_qtype(qtype));\n    }\n\n    #[tokio::test]\n    async fn a_question_with_cname() {\n        let (pkarr_packet, pubkey) = example_pkarr_reply();\n        let pkarr_packet = Packet::parse(&pkarr_packet).unwrap();\n        let pubkey_z32 = pubkey.to_z32();\n\n        let name = format!(\"www.pknames.p2p.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let question = Question::new(\n            name.clone(),\n            qtype,\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        );\n\n        let context = AppContext::test();\n        let mut socket = DnsSocket::new(&context).await.unwrap();\n        let reply = resolve_question(&pkarr_packet, &question).await;\n        let reply = Packet::parse(&reply).unwrap();\n        assert_eq!(reply.answers.len(), 2);\n        assert_eq!(reply.additional_records.len(), 0);\n        assert_eq!(reply.name_servers.len(), 0);\n\n        let answer1 = reply.answers.first().unwrap();\n        assert_eq!(answer1.name, name);\n        assert!(answer1.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::CNAME)));\n\n        let answer2 = reply.answers.get(1).unwrap();\n        assert_eq!(answer2.name.to_string(), format!(\"pknames.p2p.{pubkey_z32}\"));\n        assert!(answer2.match_qtype(qtype));\n    }\n\n    #[tokio::test]\n    async fn a_question_with_ns() {\n        let (pkarr_packet, pubkey) = example_pkarr_reply();\n        let pkarr_packet = Packet::parse(&pkarr_packet).unwrap();\n        let pubkey_z32 = pubkey.to_z32();\n\n        let name = format!(\"other.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let question = Question::new(\n            name.clone(),\n            qtype,\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        );\n        let context = AppContext::test();\n        let socket = DnsSocket::new(&context).await.unwrap();\n        let reply = resolve_question(&pkarr_packet, &question).await;\n        let reply = Packet::parse(&reply).unwrap();\n        assert_eq!(reply.answers.len(), 0);\n        assert_eq!(reply.additional_records.len(), 0);\n        assert_eq!(reply.name_servers.len(), 1);\n\n        let ns1 = reply.name_servers.first().unwrap();\n        assert_eq!(ns1.name, name);\n        assert!(ns1.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::NS)));\n    }\n\n    #[tokio::test]\n    async fn a_question_with_ns_subdomain() {\n        let (pkarr_packet, pubkey) = example_pkarr_reply();\n        let pkarr_packet = Packet::parse(&pkarr_packet).unwrap();\n        let pubkey_z32 = pubkey.to_z32();\n\n        let name = format!(\"sub.other.{pubkey_z32}\");\n        let name = Name::new(&name).unwrap();\n        let qtype = pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A);\n        let question = Question::new(\n            name.clone(),\n            qtype,\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        );\n\n        let context = AppContext::test();\n        let mut socket = DnsSocket::new(&context).await.unwrap();\n        let reply = resolve_question(&pkarr_packet, &question).await;\n        let reply = Packet::parse(&reply).unwrap();\n        assert_eq!(reply.answers.len(), 0);\n        assert_eq!(reply.additional_records.len(), 0);\n        assert_eq!(reply.name_servers.len(), 1);\n\n        let ns1 = reply.name_servers.first().unwrap();\n        assert_eq!(ns1.name.to_string(), format!(\"other.{pubkey_z32}\"));\n        assert!(ns1.match_qtype(pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::NS)));\n    }\n\n    #[tokio::test]\n    async fn simple_a_query() {\n        let (pkarr_packet, _pubkey) = example_pkarr_reply();\n        let pkarr_packet = Packet::parse(&pkarr_packet).unwrap();\n\n        let mut query = Packet::new_query(0);\n        query.questions = vec![Question::new(\n            Name::new(\"pknames.p2p\").unwrap(),\n            pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),\n            pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),\n            false,\n        )];\n\n        let context = AppContext::test();\n        let socket = DnsSocket::new(&context).await.unwrap();\n        let _reply = resolve_query(&pkarr_packet, &query);\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/query_id_manager.rs",
    "content": "#![allow(unused)]\n\nuse std::{\n    collections::HashMap,\n    net::SocketAddr,\n    sync::{Arc, Mutex},\n};\n\n/**\n * Thread-safe QueryIdManager.\n * Use `.clone()` to give each thread one store struct.\n * The data will stay shared.\n */\n#[derive(Debug, Clone)]\npub struct QueryIdManager {\n    ids: Arc<Mutex<HashMap<SocketAddr, u16>>>,\n}\n\nimpl QueryIdManager {\n    /**\n     * Gets the next available query id\n     */\n    pub fn get_next(&mut self, server: &SocketAddr) -> u16 {\n        let mut locked = self.ids.lock().expect(\"Lock success\");\n        let current = match locked.get(server) {\n            Some(val) => *val,\n            None => 0,\n        };\n        let next = if current == u16::MAX { 0 } else { current + 1 };\n        locked.insert(*server, next);\n        next\n    }\n\n    pub fn new() -> Self {\n        Self {\n            ids: Arc::new(Mutex::new(HashMap::new())),\n        }\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/rate_limiter.rs",
    "content": "use std::{\n    hash::{Hash, Hasher},\n    net::{IpAddr, Ipv4Addr, Ipv6Addr},\n    num::NonZeroU32,\n    sync::Arc,\n};\n\nuse governor::{DefaultKeyedRateLimiter, Quota, RateLimiter as GovenerRateLimiter};\n\n/**\n * Custom rate limiting key. A device usually gets\n * either one IPv4 address OR a /64 bit IPv6 address.\n * To prevent IPv6 abuse, RateLimitingKey only uses the first 64 bits.\n */\n#[derive(Clone, Debug, Eq, PartialEq)]\nenum RateLimitingKey {\n    Ipv4(Ipv4Addr),\n    IpV6 { significant_bits: u64 },\n}\n\nimpl RateLimitingKey {\n    /**\n     * Generate a key from an IPv4 address.\n     */\n    pub fn from_ipv4(ip: Ipv4Addr) -> Self {\n        Self::Ipv4(ip)\n    }\n\n    /**\n     * Generate a key from an IPv6 address.\n     */\n    pub fn from_ipv6(ip: Ipv6Addr) -> Self {\n        let segments = ip.segments();\n        let key = ((segments[0] as u64) << 48)\n            | ((segments[1] as u64) << 32)\n            | ((segments[2] as u64) << 16)\n            | (segments[3] as u64);\n        Self::IpV6 { significant_bits: key }\n    }\n}\n\nimpl From<IpAddr> for RateLimitingKey {\n    fn from(value: IpAddr) -> Self {\n        match value {\n            IpAddr::V4(val) => Self::from_ipv4(val),\n            IpAddr::V6(val) => Self::from_ipv6(val),\n        }\n    }\n}\n\nimpl Hash for RateLimitingKey {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        match self {\n            RateLimitingKey::Ipv4(ipv4_addr) => {\n                0_u8.hash(state); // IPv4 indicator to prevent overlap with the Ipv6 space.\n                ipv4_addr.hash(state);\n            }\n            RateLimitingKey::IpV6 { significant_bits } => {\n                1_u8.hash(state); // IPv6 indicator to prevent overlap with the Ipv4 space.\n                significant_bits.hash(state);\n            }\n        }\n    }\n}\n\npub struct RateLimiterBuilder {\n    max_per_second: u32,\n    max_per_minute: u32,\n    burst_size: u32,\n}\n\nimpl RateLimiterBuilder {\n    pub fn new() -> Self {\n        Self {\n            max_per_second: 0,\n            max_per_minute: 0,\n            burst_size: 0,\n        }\n    }\n\n    /// Maximum number of request per second. Think of a bucket that gets filled with drops.\n    /// This is the rate at which the bucket is emptied.\n    /// Either seconds or minutes is allowed. Setting both is invalid.\n    /// 0 is disabled.\n    pub fn max_per_second(mut self, limit: u32) -> Self {\n        self.max_per_second = limit;\n        self\n    }\n\n    /// Maximum number of request per minute. Think of a bucket that gets filled with drops.\n    /// This is the rate at which the bucket is emptied.\n    /// Either seconds or minutes is allowed. Setting both is invalid.\n    /// 0 is disabled.\n    pub fn max_per_minute(mut self, limit: u32) -> Self {\n        self.max_per_minute = limit;\n        self\n    }\n\n    /// Burst size of requests a minute. Think of it as the bucket size.\n    /// 0 is disabled.\n    pub fn burst_size(mut self, size: u32) -> Self {\n        self.burst_size = size;\n        self\n    }\n\n    /// Builds the RateLimiter. Panics if max_per_minute AND max_per_second is set at the same time.\n    pub fn build(self) -> RateLimiter {\n        if self.max_per_minute > 0 && self.max_per_second > 0 {\n            panic!(\"Can't set max_per_minute and max_per_second at the same time.\")\n        };\n\n        let mut quota: Quota;\n        if self.max_per_minute > 0 {\n            quota = Quota::per_minute(NonZeroU32::new(self.max_per_minute).expect(\"max_per_minute is always non-zero\"));\n        } else if self.max_per_second > 0 {\n            quota = Quota::per_second(NonZeroU32::new(self.max_per_second).expect(\"max_per_second is always non-zero\"));\n        } else {\n            return RateLimiter { limiter: None };\n        }\n\n        if self.burst_size > 0 {\n            quota = quota.allow_burst(NonZeroU32::new(self.burst_size).expect(\"burst_size is always non-zero\"));\n        }\n\n        RateLimiter {\n            limiter: Some(GovenerRateLimiter::keyed(quota)),\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct RateLimiter {\n    limiter: Option<DefaultKeyedRateLimiter<RateLimitingKey>>,\n}\n\nimpl RateLimiter {\n    /**\n     * Checks if this IP address is limited. Increases the usage by one.\n     */\n    pub fn check_is_limited_and_increase(&self, ip: &IpAddr) -> bool {\n        if let Some(limiter) = &self.limiter {\n            let ip = *ip;\n            let is_rate_limited = limiter.check_key(&ip.into()).is_err();\n            return is_rate_limited;\n        };\n        false\n    }\n}\n"
  },
  {
    "path": "server/src/resolution/response_cache.rs",
    "content": "use std::time::{Duration, SystemTime, UNIX_EPOCH};\n\nuse anyhow::anyhow;\nuse moka::{future::Cache, policy::EvictionPolicy};\nuse pkarr::dns::Packet;\n\n/// Caches dns responses.\n#[derive(Clone, Debug)]\npub struct CacheItem {\n    pub query_key: String,\n    pub response: Vec<u8>,\n    created_at: SystemTime,\n}\n\nimpl CacheItem {\n    pub fn new(query: Vec<u8>, response: Vec<u8>) -> Result<Self, anyhow::Error> {\n        let packet = Packet::parse(&response)?;\n        let _ = Packet::parse(&response)?; // Validate that the response is parseable.\n        Ok(Self {\n            query_key: Self::derive_query_key(&query)?,\n            response,\n            created_at: SystemTime::now(),\n        })\n    }\n\n    /// Derives a query key from the first question. May fail if the packet cant be parsed\n    /// or the query doesn't have a question.\n    pub fn derive_query_key(query: &[u8]) -> Result<String, anyhow::Error> {\n        let packet = Packet::parse(query)?;\n        let question = packet\n            .questions\n            .first()\n            .ok_or(anyhow!(\"Query does not include a question.\"))?;\n        Ok(format!(\"{}:{:?}:{:?}\", question.qname, question.qclass, question.qtype))\n    }\n\n    fn response_packet(&self) -> Packet {\n        Packet::parse(&self.response).unwrap()\n    }\n\n    /// Lowest ttl of any anwser in seconds. Used to determine when to update the cache.\n    /// NotFound or packet with now answeres => None.\n    pub fn lowest_answer_ttl(&self) -> Option<u64> {\n        self.response_packet()\n            .answers\n            .iter()\n            .map(|answer| answer.ttl as u64)\n            .min()\n    }\n\n    /// Size of the cached value in the memory.\n    /// Approximation. Could be done better.\n    pub fn memory_size(&self) -> usize {\n        self.query_key.len() + self.response.len() + 11\n    }\n\n    /// When this cached item expires.\n    pub fn expires_in(&self, min_ttl: u64, max_ttl: u64) -> SystemTime {\n        let ttl = self.lowest_answer_ttl().unwrap_or(min_ttl);\n        let ttl = if ttl < min_ttl { min_ttl } else { ttl };\n        let ttl = if ttl > max_ttl { max_ttl } else { ttl };\n        self.created_at\n            .checked_add(Duration::from_secs(ttl))\n            .expect(\"Valid time because ttl is bound\")\n    }\n\n    /// If this cached item is outdated (expired ttl).\n    pub fn is_outdated(&self, min_ttl: u64, max_ttl: u64) -> bool {\n        self.expires_in(min_ttl, max_ttl) < SystemTime::now()\n    }\n}\n\n/**\n * LRU cache for ICANN responses.\n */\n#[derive(Clone, Debug)]\npub struct IcannLruCache {\n    cache: Cache<String, CacheItem>, // Moka Cache is thread safe\n    min_ttl: u64,\n    max_ttl: u64,\n}\n\nimpl IcannLruCache {\n    pub fn new(cache_size_mb: u64, min_ttl: u64, max_ttl: u64) -> Self {\n        IcannLruCache {\n            cache: Cache::builder()\n                .weigher(|_key, value: &CacheItem| -> u32 { value.memory_size() as u32 })\n                .max_capacity(cache_size_mb * 1024 * 1024)\n                .build(),\n            max_ttl,\n            min_ttl,\n        }\n    }\n\n    /// Adds a new item to the cache.\n    pub async fn add(&mut self, query: Vec<u8>, response: Vec<u8>) -> Result<(), anyhow::Error> {\n        let item = CacheItem::new(query, response)?;\n        self.cache.insert(item.query_key.clone(), item).await;\n        Ok(())\n    }\n\n    /// Get cached packet by query. Fails if the query can't per parsed.\n    pub async fn get(&self, query: &[u8]) -> Result<Option<CacheItem>, anyhow::Error> {\n        let key = CacheItem::derive_query_key(query)?;\n        let value = self.cache.get(&key).await;\n        if let Some(item) = &value {\n            if item.is_outdated(self.min_ttl, self.max_ttl) {\n                return Ok(None);\n            };\n        };\n\n        Ok(value)\n    }\n\n    /// Approximated size of the cache in bytes. May not be 100% accurate due to pending counts.\n    #[allow(dead_code)]\n    pub fn approx_size_bytes(&self) -> u64 {\n        self.cache.weighted_size()\n    }\n\n    #[allow(dead_code)]\n    pub fn entry_count(&self) -> u64 {\n        self.cache.entry_count()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use pkarr::dns::{rdata::A, Name, Question, ResourceRecord};\n\n    fn example_query_response(ttl: u32) -> (Vec<u8>, Vec<u8>) {\n        let mut query_packet = Packet::new_query(0);\n        let question = Question::new(\n            Name::new(\"example.com\").unwrap(),\n            pkarr::dns::QTYPE::ANY,\n            pkarr::dns::QCLASS::ANY,\n            false,\n        );\n\n        query_packet.questions.push(question);\n        let query = query_packet.build_bytes_vec().unwrap();\n\n        let mut response_packet = Packet::new_reply(0);\n        let answer = ResourceRecord::new(\n            Name::new(\"example.com\").unwrap(),\n            pkarr::dns::CLASS::IN,\n            ttl,\n            pkarr::dns::rdata::RData::A(A { address: 32 }),\n        );\n        response_packet.answers.push(answer);\n\n        let response = response_packet.build_bytes_vec().unwrap();\n        (query, response)\n    }\n\n    #[tokio::test]\n    async fn add_and_get() {\n        let mut cache = IcannLruCache::new(1, 0, 99999);\n        let (query, response) = example_query_response(60);\n        cache.add(query.clone(), response.clone()).await.unwrap();\n\n        let cache_option = cache.get(&query).await.expect(\"Previously cached item\");\n        let res = cache_option.unwrap();\n        assert_eq!(res.response, response);\n    }\n\n    #[tokio::test]\n    async fn outdated_get() {\n        let mut cache = IcannLruCache::new(1, 0, 99999);\n        let (query, response) = example_query_response(0);\n        cache.add(query.clone(), response.clone()).await.unwrap();\n\n        let cache_option = cache.get(&query).await.expect(\"Previously cached item\");\n        assert!(cache_option.is_none());\n    }\n\n    #[tokio::test]\n    async fn zero_cache_size() {\n        let mut cache = IcannLruCache::new(0, 0, 99999);\n        let (query, response) = example_query_response(60);\n        cache.add(query.clone(), response.clone()).await.unwrap();\n\n        let cache_option = cache.get(&query).await.expect(\"Previously cached item\");\n        assert!(cache_option.is_none());\n    }\n}\n"
  },
  {
    "path": "servers.txt",
    "content": "# A list of free to use pkdns servers.\n\n34.65.109.99\n66.78.40.76\n\n\n# DNS-over-HTTPS\nhttps://pkdns.pubky.org/dns-query"
  }
]