[
  {
    "path": ".dockerignore",
    "content": "*\n!cmd/smee/smee-*-*\n!cmd/smee/smee\n!test/\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "/.github/settings.yml @chrisdoherty4 @jacobweinstock\n/.github/CODEOWNERS @chrisdoherty4 @jacobweinstock\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "---\ncoverage:\n  precision: 0 # xx%\n  round: down # round down\n  range: 30..40 # red < yellow (this range) < green\n\n  status:\n    project:\n      default:\n        target: auto # automatically calculate coverage target - should increase\n        threshold: 2% # allow for 2% reduction without failing\n    patch:\n      default:\n        target: auto\n    changes: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"04:39\"\n      timezone: \"America/New_York\"\n    reviewers:\n      - chrisdoherty4\n      - jacobweinstock\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"03:52\"\n      timezone: \"America/New_York\"\n    reviewers:\n      - chrisdoherty4\n      - jacobweinstock\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"thursday\"\n      time: \"03:52\"\n      timezone: \"America/New_York\"\n    reviewers:\n      - chrisdoherty4\n      - jacobweinstock\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"04:22\"\n      timezone: \"America/New_York\"\n    reviewers:\n      - chrisdoherty4\n      - jacobweinstock\n    open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/mergify.yml",
    "content": "queue_rules:\n  - name: default\n    queue_conditions:\n      - base=main\n      - \"#approved-reviews-by>=1\"\n      - \"#changes-requested-reviews-by=0\"\n      - \"#review-requested=0\"\n      - check-success=DCO\n      - check-success=validation\n      - label!=do-not-merge\n      - label=ready-to-merge\n    merge_conditions:\n      # Conditions to get out of the queue (= merged)\n      - check-success=DCO\n      - check-success=validation\n    merge_method: merge\n    commit_message_template: |\n      {{ title }} (#{{ number }})\n\n      {{ body }}\n\npull_request_rules:\n  - name: refactored queue action rule\n    conditions: []\n    actions:\n      queue:\n"
  },
  {
    "path": ".github/settings.yml",
    "content": "# Collaborators: give specific users access to this repository.\n# See https://docs.github.com/en/rest/reference/repos#add-a-repository-collaborator for available options\ncollaborators:\n  # Maintainers, should also be added to the .github/CODEOWNERS file as owners of this settings.yml file.\n  - username: jacobweinstock\n    permission: maintain\n  - username: chrisdoherty4\n    permission: maintain\n  # Approvers\n  # Reviewers\n\n  # Note: `permission` is only valid on organization-owned repositories.\n  # The permission to grant the collaborator. Can be one of:\n  # * `pull` - can pull, but not push to or administer this repository.\n  # * `push` - can pull and push, but not administer this repository.\n  # * `admin` - can pull, push and administer this repository.\n  # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions.\n  # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access.\n"
  },
  {
    "path": ".github/workflows/ci-checks.sh",
    "content": "#!/usr/bin/env bash\n\nset -eux\n\nfailed=0\n\nif [[ -n $(go run golang.org/x/tools/cmd/goimports@latest -d -e -l .) ]]; then\n\tgo run golang.org/x/tools/cmd/goimports@latest -w .\n\tfailed=1\nfi\n\nif ! go mod tidy; then\n\tfailed=true\nfi\n\nif ! git diff | (! grep .); then\n\tfailed=1\nfi\n\nexit \"$failed\"\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: For each commit and PR\non:\n  push:\n    branches:\n      - \"*\"\n    tags-ignore:\n      - \"v*\"\n  pull_request:\n\nenv:\n  REGISTRY: quay.io\n  IMAGE: quay.io/${{ github.repository }}\n  CGO_ENABLED: 0\n  GO_VERSION: \"1.24\"\n\njobs:\n  validation:\n    runs-on: ubuntu-latest\n    env:\n      CGO_ENABLED: 0\n    steps:\n      - name: Setup Dynamic Env\n        run: |\n          echo \"MAKEFLAGS=-j$(nproc)\" | tee $GITHUB_ENV\n\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 5\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"${{ env.GO_VERSION }}\"\n          cache: true\n\n      - name: Fetch Deps\n        run: |\n          # fixes \"write /run/user/1001/355792648: no space left on device\" error\n          sudo mount -o remount,size=3G /run/user/1001 || true\n          go get -t ./... && go mod tidy\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Generate all files\n        run: make -j1 gen\n\n      - name: Run all the tests\n        run: make ci\n\n      - name: upload codecov\n        uses: codecov/codecov-action@v5\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n      - name: compile binaries\n        run: make crosscompile\n\n      - name: Figure out Docker Tags\n        id: docker-image-tag\n        run: |\n          echo ::set-output name=tags::${{ env.IMAGE }}:latest,${{ env.IMAGE }}:sha-${GITHUB_SHA::8}\n\n      - name: Login to quay.io\n        uses: docker/login-action@v3\n        if: ${{ startsWith(github.ref, 'refs/heads/main') }}\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ secrets.QUAY_USERNAME }}\n          password: ${{ secrets.QUAY_PASSWORD }}\n\n      - name: Build Docker Images\n        uses: docker/build-push-action@v6\n        with:\n          context: ./\n          file: ./Dockerfile\n          cache-from: type=registry,ref=${{ env.IMAGE }}:latest\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.docker-image-tag.outputs.tags }}\n\n      # looks just like Build Docker Images except with push:true and this will only run for builds for main\n      - name: Push Docker Images\n        uses: docker/build-push-action@v6\n        if: ${{ startsWith(github.ref, 'refs/heads/main') }}\n        with:\n          context: ./\n          file: ./Dockerfile\n          cache-from: type=registry,ref=${{ env.IMAGE }}:latest\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.docker-image-tag.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/tags.yaml",
    "content": "on:\n  push:\n    tags:\n      - \"v*\"\nname: Create release\nenv:\n  REGISTRY: quay.io\n  IMAGE_NAME: ${{ github.repository }}\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Generate Release Notes\n        run: |\n          release_notes=$(gh api repos/{owner}/{repo}/releases/generate-notes -F tag_name=${{ github.ref }} --jq .body)\n          echo 'RELEASE_NOTES<<EOF' >> $GITHUB_ENV\n          echo \"${release_notes}\" >> $GITHUB_ENV\n          echo 'EOF' >> $GITHUB_ENV\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          OWNER: ${{ github.repository_owner }}\n          REPO: ${{ github.event.repository.name }}\n\n      - name: Docker manager metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          flavor: latest=false\n          tags: type=ref,event=tag\n\n      - name: Set the from image tag\n        run: echo \"FROM_TAG=sha-${GITHUB_SHA::8}\" >> $GITHUB_ENV\n\n      - name: Copy the image using skopeo\n        run: skopeo copy --all --dest-creds=\"${DST_REG_USER}\":\"${DST_REG_PASS}\" docker://\"${SRC_IMAGE}\" docker://\"${DST_IMAGE}\"\n        env:\n          SRC_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.FROM_TAG }}\n          DST_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}\n          DST_REG_USER: ${{ secrets.QUAY_USERNAME }}\n          DST_REG_PASS: ${{ secrets.QUAY_PASSWORD }}\n\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          body: ${{ env.RELEASE_NOTES }}\n          draft: false\n          prerelease: true\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n*.orig\n*.test\n.idea*/**\n/bin/\n/cmd/smee/smee\n/cmd/smee/smee-*-*\ncoverage.txt\n.vscode\n\n# added by lint-install\nout/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  # The default runtime timeout is 1m, which doesn't work well on Github Actions.\n  timeout: 4m\nlinters:\n  default: none\n  enable:\n    - asciicheck\n    - bodyclose\n    - copyloopvar\n    - cyclop\n    - dogsled\n    - dupl\n    - durationcheck\n    - errcheck\n    - errname\n    - errorlint\n    - exhaustive\n    - forcetypeassert\n    - gocognit\n    - goconst\n    - gocritic\n    - godot\n    - goheader\n    - goprintffuncname\n    - gosec\n    - govet\n    - importas\n    - ineffassign\n    - makezero\n    - misspell\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - predeclared\n    - revive\n    - rowserrcheck\n    - sqlclosecheck\n    - staticcheck\n    - thelper\n    - tparallel\n    - unconvert\n    - unparam\n    - unused\n    - wastedassign\n    - whitespace\n  settings:\n    cyclop:\n      max-complexity: 37\n      package-average: 34\n    dupl:\n      threshold: 200\n    errorlint:\n      # Forcing %w in error wrapping forces authors to make errors part of their package APIs. The decision to make\n      # an error part of a package API should be a conscious decision by the author.\n      # Also see Hyrums Law.\n      errorf: false\n      asserts: false\n    exhaustive:\n      default-signifies-exhaustive: true\n    gocognit:\n      min-complexity: 98\n    goconst:\n      min-len: 4\n      min-occurrences: 5\n    gosec:\n      excludes:\n        - G107 # Potential HTTP request made with variable url\n        - G204 # Subprocess launched with function call as argument or cmd arguments\n        - G404 # Use of weak random number generator (math/rand instead of crypto/rand\n    nestif:\n      min-complexity: 8\n    nolintlint:\n      require-explanation: true\n      require-specific: true\n      allow-unused: false\n    revive:\n      severity: warning\n      rules:\n        - name: atomic\n        - name: blank-imports\n        - name: bool-literal-in-expr\n        - name: confusing-naming\n        - name: constant-logical-expr\n        - name: context-as-argument\n        - name: context-keys-type\n        - name: deep-exit\n        - name: defer\n        - name: range-val-in-closure\n        - name: range-val-address\n        - name: dot-imports\n        - name: error-naming\n        - name: error-return\n        - name: error-strings\n        - name: errorf\n        - name: exported\n        - name: identical-branches\n        - name: if-return\n        - name: import-shadowing\n        - name: increment-decrement\n        - name: indent-error-flow\n        - name: indent-error-flow\n        - name: package-comments\n        - name: range\n        - name: receiver-naming\n        - name: redefines-builtin-id\n        - name: superfluous-else\n        - name: struct-tag\n        - name: time-naming\n        - name: unexported-naming\n        - name: unexported-return\n        - name: unnecessary-stmt\n        - name: unreachable-code\n        - name: unused-parameter\n        - name: var-declaration\n        - name: var-naming\n        - name: unconditional-recursion\n        - name: waitgroup-by-value\n        # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag\n        - name: struct-tag\n          arguments:\n            - json,inline\n            - yaml,omitzero\n            - protobuf,casttype\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - dupl\n          - errcheck\n          - forcetypeassert\n          - gocyclo\n          - gosec\n          - noctx\n        path: _test\\.go\n      - linters:\n          # This check is of questionable value\n          - tparallel\n        text: call t.Parallel on the top level as well as its subtests\n      - linters:\n          - cyclop\n          - goconst\n        path: (.+)_test\\.go\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n      - internal/iso/internal/reverseproxy.go\n      - internal/iso/internal/reverseproxy_test.go\n      - internal/iso/internal/acsii.go\n      - internal/iso/internal/acsii_test.go\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofmt\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - internal/iso/internal/reverseproxy.go\n      - internal/iso/internal/reverseproxy_test.go\n      - internal/iso/internal/acsii.go\n      - internal/iso/internal/acsii_test.go\n      - third_party$\n      - builtin$\n      - examples$"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributor Guide\n\nWelcome to Smee!\nWe are really excited to have you.\nPlease use the following guide on your contributing journey.\nThanks for contributing!\n\n## Table of Contents\n\n- [Context](#Context)\n- [Architecture](#Architecture)\n  - [Design Docs](#Design-Docs)\n  - [Code Structure](#Code-Structure)\n- [Prerequisites](#Prerequisites)\n  - [DCO Sign Off](#DCO-Sign-Off)\n  - [Code of Conduct](#Code-of-Conduct)\n  - [Setting up your development environment](#Setting-up-your-development-environment)\n- [Development](#Development)\n  - [Building](#Building)\n  - [Unit testing](#Unit-testing)\n  - [Linting](#Linting)\n  - [Functional testing](#Functional-testing)\n  - [Running Smee locally](#Running-Smee-locally)\n- [Pull Requests](#Pull-Requests)\n  - [Branching strategy](#Branching-strategy)\n  - [Quality](#Quality)\n    - [CI](#CI)\n    - [Code coverage](#Code-coverage)\n  - [Pre PR Checklist](#Pre-PR-Checklist)\n\n---\n\n## Context\n\nSmee is a DHCP and PXE (TFTP & HTTP) service.\nIt is part of the [Tinkerbell stack](https://tinkerbell.org) and provides the first interaction for any machines being provisioned through Tinkerbell.\n\n## Architecture\n\n### Design Docs\n\nDetails and diagrams for Smee are found [here](docs/DESIGN.md).\n\n### Code Structure\n\nDetails on Smee's code structure is found [here](docs/CODE_STRUCTURE.md) (WIP)\n\n## Prerequisites\n\n### DCO Sign Off\n\nPlease read and understand the DCO found [here](docs/DCO.md).\n\n### Code of Conduct\n\nPlease read and understand the code of conduct found [here](https://github.com/tinkerbell/.github/blob/main/CODE_OF_CONDUCT.md).\n\n### Setting up your development environment\n\n---\n\n### Dependencies\n\n#### Build time dependencies\n\n#### Runtime dependencies\n\nAt runtime Smee needs to communicate with a Tink server.\nFollow this [guide](https://tinkerbell.org/docs/setup/getting_started/) for running Tink server.\n\n## Development\n\n### Building\n\n> At the moment, these instructions are only stable on Linux environments\n\nTo build Smee, run:\n\n```bash\n# build all ipxe files, embed them, and build the Go binary\n# Built binary can be found in the top level directory.\nmake build\n\n```\n\nTo build the amd64 Smee container image, run:\n\n```bash\n# make the amd64 container image\n# Built image will be named smee:latest\nmake image\n\n```\n\nTo build the IPXE binaries and embed them into Go, run:\n\n```bash\n# Note, this will not build the Smee binary\nmake bindata\n```\n\nTo build Smee binaries for all distro\n\n### Unit testing\n\nTo execute the unit tests, run:\n\n```bash\nmake test\n\n# to get code coverage numbers, run:\nmake coverage\n```\n\n### Linting\n\nTo execute linting, run:\n\n```bash\n# runs golangci-lint\nmake lint\n\n# runs goimports\nmake goimports\n\n# runs go vet\nmake vet\n```\n\n## Linting of Non Go files\n\n```bash\n# lints non Go files like shell scripts, markdown files, etc\n# this script is used in CI run, so be sure it passes before submitting a PR\n./.github/workflows/ci-non-go.sh\n```\n\n### Functional testing\n\n1. Create a hardware record in Tink server - follow the guide [here](https://tinkerbell.org/docs/concepts/hardware/)\n2. boot the machine\n\n### Running Smee\n\n1. Be sure all documented runtime dependencies are satisfied.\n2. Define all environment variables.\n\n   ```bash\n   # MIRROR_HOST is for downloading kernel, initrd\n   export MIRROR_HOST=192.168.2.3\n   # PUBLIC_FQDN is for phone home endpoint\n   export PUBLIC_FQDN=192.168.2.4\n   # DOCKER_REGISTRY, REGISTRY_USERNAME, REGISTRY_PASSWORD, TINKERBELL_GRPC_AUTHORITY, TINKERBELL_CERT_URL are needed for auto.ipxe file generation\n   # TINKERBELL_GRPC_AUTHORITY, TINKERBELL_CERT_URL are needed for getting hardware data\n   export DOCKER_REGISTRY=192.168.2.1:5000\n   export REGISTRY_USERNAME=admin\n   export REGISTRY_PASSWORD=secret\n   export TINKERBELL_GRPC_AUTHORITY=tinkerbell.tinkerbell:42113\n   export TINKERBELL_CERT_URL=http://tinkerbell.tinkerbell:42114/cert\n   # FACILITY_CODE is needed for ?\n   export FACILITY_CODE=onprem\n   export DATA_MODEL_VERSION=1\n   # API_AUTH_TOKEN, API_CONSUMER_TOKEN are needed to by pass panicking in main.go main func\n   export API_AUTH_TOKEN=none\n   export API_CONSUMER_TOKEN=none\n   ```\n\n3. Run Smee\n\n   ```bash\n   # Run the compiled smee\n   sudo ./smee -http-addr 192.168.2.225:80 -tftp-addr 192.168.2.225:69 -dhcp-addr 192.168.2.225:67\n   ```\n\n4. Faster iterating via `go run`\n\n   ```bash\n   # after the ipxe binaries have been compiled you can use `go run` to iterate a little more quickly than building the binary every time\n   sudo go run ./smee -http-addr 192.168.2.225:80 -tftp-addr 192.168.2.225:69 -dhcp-addr 192.168.2.225:67\n   ```\n\n## Pull Requests\n\n### Branching strategy\n\nSmee uses a fork and pull request model.\nSee this [doc](https://guides.github.com/activities/forking/) for more details.\n\n### Quality\n\n#### CI\n\nSmee uses GitHub Actions for CI.\nThe workflow is found in [.github/workflows/ci.yaml](.github/workflows/ci.yaml).\nIt is run for each commit and PR.\n\n#### Code coverage\n\nSmee does run code coverage with each PR.\nCoverage thresholds are not currently enforced.\nIt is always nice and very welcomed to add tests and keep or increase the code coverage percentage.\n\n### Pre PR Checklist\n\nThis checklist is a helper to make sure there's no gotchas that come up when you submit a PR.\n\n- [ ] You've reviewed the [code of conduct](#Code-of-Conduct)\n- [ ] All commits are DCO signed off\n- [ ] Code is [formatted and linted](#Linting)\n- [ ] Code [builds](#Building) successfully\n- [ ] All tests are [passing](#Unit-testing)\n- [ ] Code coverage [percentage](#Code-coverage). (main line is the base with which to compare)\n"
  },
  {
    "path": "Dockerfile",
    "content": "# run `make image` to build the binary + container\n# if you're using `make build` this Dockerfile will not find the binary\n# and you probably want `make smee-linux-amd64`\nFROM alpine:3.22\n\nARG TARGETARCH\nARG TARGETVARIANT\n\nENTRYPOINT [\"/usr/bin/smee\"]\n\nRUN apk add --update --upgrade --no-cache ca-certificates\nCOPY cmd/smee/smee-linux-${TARGETARCH:-amd64}${TARGETVARIANT} /usr/bin/smee\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2020 Packet Host, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "all: help\n\n-include lint.mk\n-include rules.mk\n\nbuild: cmd/smee/smee ## Compile smee for host OS and Architecture\n\ncrosscompile: $(crossbinaries) ## Compile smee for all architectures\n\ngen: $(generated_go_files) ## Generate go generate'd files\n\nIMAGE_TAG ?= smee:latest\nimage: cmd/smee/smee-linux-amd64  ## Build docker image\n\tdocker build -t $(IMAGE_TAG) .\n\ntest: gen ## Run go test\n\tCGO_ENABLED=1 go test -race -coverprofile=coverage.txt -covermode=atomic -v ${TEST_ARGS} ./...\n\ncoverage: test ## Show test coverage\n\tgo tool cover -func=coverage.txt\n\nvet: ## Run go vet\n\tgo vet ./...\n\ngoimports: gen ## Run goimports\n\t$(GOIMPORTS) -w .\n\nci-checks: .github/workflows/ci-checks.sh gen\n\t./.github/workflows/ci-checks.sh\n\nci: ci-checks coverage goimports lint vet ## Runs all the same validations and tests that run in CI\n\nhelp: ## Print this help\n\t@grep --no-filename -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sed 's/:.*##/·/' | sort | column -ts '·' -c 120\n"
  },
  {
    "path": "README.md",
    "content": "> [!IMPORTANT]  \n> The Smee repo has been deprecated. All functionality has been moved to https://github.com/tinkerbell/tinkerbell.\n> For more details, see the roadmap issue [#41](https://github.com/tinkerbell/roadmap/issues/41).\n> This repository is scheduled for archive by the end of 2025.\n\n# Smee\n\n[![Build Status](https://github.com/tinkerbell/smee/workflows/For%20each%20commit%20and%20PR/badge.svg)](https://github.com/tinkerbell/smee/actions?query=workflow%3A%22For+each+commit+and+PR%22+branch%3Amain)\n\nSmee is the network boot service in the [Tinkerbell stack](https://tinkerbell.org), formerly known as `Boots`. It is comprised of the following services.\n\n- DHCP server\n  - host reservations only\n  - mac address based lookups\n  - netboot options support\n  - backend support\n    - Kubernetes\n    - file based\n  - ProxyDHCP support\n- TFTP server\n  - serving iPXE binaries\n- HTTP server\n  - serving iPXE binaries and iPXE scripts\n  - iPXE script serving uses IP authentication\n  - backend support\n    - Kubernetes\n    - file based\n- Syslog server\n  - receives syslog messages and logs them\n\n## Definitions\n\n**DHCP Reservation:**\nA fixed IP address that is reserved for a specific client.\n\n**DHCP Lease:**\nAn IP address, that can potentially change, that is assigned to a client by the DHCP server.\nThe IP is typically pulled from a pool or subnet of available IP addresses.\n\n**ProxyDHCP:**\n\"[A] Proxy DHCP server behaves much like a DHCP server by listening for ordinary DHCP client traffic and responding to certain client requests. However, unlike the DHCP server, the PXE Proxy DHCP server does not administer network addresses, and it only responds to clients that identify themselves as PXE clients.\nThe responses given by the PXE Proxy DHCP server contain the mechanism by which the client locates the boot servers or the network addresses and descriptions of the supported, compatible boot servers.\"\n-- [IBM](https://www.ibm.com/docs/en/aix/7.1?topic=protocol-preboot-execution-environment-proxy-dhcp-daemon)\n\n## Running Smee\n\n### DHCP Modes\n\nSmee's DHCP functionality can operate in one of the following modes:\n\n1. **DHCP Reservation**  \n   To enable this mode set `-dhcp-mode=reservation`.\n   Smee will respond to DHCP requests from clients and provide them with IP and next boot info when netbooting. This is the default mode. IP info is all reservation based. There must be a corresponding Hardware record for the requesting client's MAC address.  \n\n1. **Proxy DHCP**  \n   To enable this mode set `-dhcp-mode=proxy`.\n   Smee will respond to PXE enabled DHCP requests from clients and provide them with next boot info when netbooting. In this mode an existing DHCP server that does not serve network boot information is required. Smee will respond to PXE enabled DHCP requests and provide the client with the next boot info. There must be a corresponding Hardware record for the requesting client's MAC address. The `auto.ipxe` script will be served with the MAC address in the URL and the MAC address will be used to lookup the corresponding Hardware record. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.\n\n1. **Auto Proxy DHCP**  \n   To enable this mode set `-dhcp-mode=auto-proxy`.\n   Smee will respond to PXE enabled DHCP requests from clients and provide them with next boot info when netbooting. In this mode an existing DHCP server that does not serve network boot information is required. In this mode, if no corresponding Hardware record is found for the requesting client's MAC address, Smee will provide the client with a statically defined iPXE script. If a Hardware record is found, then the normal `auto.ipxe` script will be served. Use `-backend-noop-enabled` to disable all backend look ups. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.\n\n   - When using Smee's auto.ipxe, you'll generally want to set the following flags:  \n     - `-dhcp-mode=auto-proxy`\n     - `-osie-url <URL to HookOS kernel and initrd>`\n     - `-tink-server <IP and port of Tink server>`\n     - `-extra-kernel-args=\"tink_worker_image=quay.io/tinkerbell/tink-worker:<use a version/commit tag>\"`\n   - When not using Smee's auto.ipxe, you'll generally want to set the following flags:  \n     - `-dhcp-mode=auto-proxy`\n     - `-dhcp-http-ipxe-script-url=https://boot.netboot.xyz`\n     - `-dhcp-http-ipxe-script-prepend-mac=false`\n\n1. **DHCP disabled**  \n   To enable this mode set `-dhcp-enabled=false`.\n   Smee will not respond to DHCP requests from clients. This is useful when the network has an existing DHCP server that will provide both IP and next boot info and Smee's TFTP and HTTP functionality will be used. The IP address in the Hardware record must be the same as the IP address of the client requesting the `auto.ipxe` script. See this [doc](docs/DHCP.md) for more details. In most situations`--dhcp-http-ipxe-script-prepend-mac=false` should also be set when in this mode.\n\n### Interoperability with other DHCP servers\n\nWhen a DHCP server exists on the network, Smee should be set to run `proxy` or `auto-proxy` mode. This will allow Smee to provide the next boot information to clients that request it. The existing DHCP server will provide the IP address and other network boot details. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.\n\nIt is not recommended, but it is possible for Smee to be run in `reservation` mode in networks with another DHCP server(s). To get the intended behavior from Smee one of the following must be true.\n\n1. All DHCP servers besides Smee are configured to ignore the MAC addresses that Smee is configured to serve.\n\n1. All DHCP servers are configured to serve the same IP address and network boot details as Smee. In this scenario the DHCP functionality of Smee is redundant. It would be recommended to run Smee with the DHCP server functionality disabled (`-dhcp=false`). See the [doc](./docs/DHCP.md) on using your existing DHCP service for more details.\n\n### Environment Variables and CLI Flags\n\nIt's important to note that CLI flags take precedence over environment variables. All CLI flags can be set as environment variables. Environment variable names are the same as the flag names with some modifications. For example, the flag `-dhcp-addr` has the environment variable of `SMEE_DHCP_ADDR`. The modifications of CLI flags to environment variables are as follows:\n\n- prefixed with `SMEE_`\n- all uppercase\n- hyphens (`-`) are replaced with underscores (`_`)\n\nThere is one environment variable that does not have a corresponding CLI flag. The environment variable is `SMEE_PUBLIC_IP_INTERFACE`. This environment variable takes a local network interface name and uses it to auto detect the IP address to use as the default in all other CLI flags that require an IP address. This is useful when the machine running Smee has multiple network interfaces and you want the default detected IP to be from this specified interface.\n\n### Local Setup\n\nRunning the Tests\n\n```bash\n# run the tests\nmake test\n```\n\nBuild/Run Smee\n\n```bash\n# make the binary\nmake build\n# run Smee\n./smee -h\n\nSmee is the DHCP and Network boot service for use in the Tinkerbell stack.\n\nUSAGE\n  smee [flags]\n\nFLAGS\n  -log-level                          log level (debug, info) (default \"info\")\n  -backend-file-enabled               [backend] enable the file backend for DHCP and the HTTP iPXE script (default \"false\")\n  -backend-file-path                  [backend] the hardware yaml file path for the file backend\n  -backend-kube-api                   [backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only\n  -backend-kube-config                [backend] the Kubernetes config file location, kube backend only\n  -backend-kube-enabled               [backend] enable the kubernetes backend for DHCP and the HTTP iPXE script (default \"true\")\n  -backend-kube-namespace             [backend] an optional Kubernetes namespace override to query hardware data from, kube backend only\n  -backend-noop-enabled               [backend] enable the noop backend for DHCP and the HTTP iPXE script (default \"false\")\n  -dhcp-addr                          [dhcp] local IP:Port to listen on for DHCP requests (default \"0.0.0.0:67\")\n  -dhcp-enabled                       [dhcp] enable DHCP server (default \"true\")\n  -dhcp-http-ipxe-binary-host         [dhcp] HTTP iPXE binaries host or IP to use in DHCP packets (default \"172.17.0.3\")\n  -dhcp-http-ipxe-binary-path         [dhcp] HTTP iPXE binaries path to use in DHCP packets (default \"/ipxe/\")\n  -dhcp-http-ipxe-binary-port         [dhcp] HTTP iPXE binaries port to use in DHCP packets (default \"8080\")\n  -dhcp-http-ipxe-binary-scheme       [dhcp] HTTP iPXE binaries scheme to use in DHCP packets (default \"http\")\n  -dhcp-http-ipxe-script-host         [dhcp] HTTP iPXE script host or IP to use in DHCP packets (default \"172.17.0.3\")\n  -dhcp-http-ipxe-script-path         [dhcp] HTTP iPXE script path to use in DHCP packets (default \"/auto.ipxe\")\n  -dhcp-http-ipxe-script-port         [dhcp] HTTP iPXE script port to use in DHCP packets (default \"8080\")\n  -dhcp-http-ipxe-script-prepend-mac  [dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe (default \"true\")\n  -dhcp-http-ipxe-script-scheme       [dhcp] HTTP iPXE script scheme to use in DHCP packets (default \"http\")\n  -dhcp-http-ipxe-script-url          [dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}\n  -dhcp-iface                         [dhcp] interface to bind to for DHCP requests\n  -dhcp-ip-for-packet                 [dhcp] IP address to use in DHCP packets (opt 54, etc) (default \"172.17.0.3\")\n  -dhcp-mode                          [dhcp] DHCP mode (reservation, proxy, auto-proxy) (default \"reservation\")\n  -dhcp-syslog-ip                     [dhcp] Syslog server IP address to use in DHCP packets (opt 7) (default \"172.17.0.3\")\n  -dhcp-tftp-ip                       [dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc) (default \"172.17.0.3\")\n  -dhcp-tftp-port                     [dhcp] TFTP server port to use in DHCP packets (opt 66, etc) (default \"69\")\n  -extra-kernel-args                  [http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script\n  -http-addr                          [http] local IP to listen on for iPXE HTTP script requests (default \"172.17.0.3\")\n  -http-ipxe-binary-enabled           [http] enable iPXE HTTP binary server (default \"true\")\n  -http-ipxe-script-enabled           [http] enable iPXE HTTP script server (default \"true\")\n  -http-port                          [http] local port to listen on for iPXE HTTP script requests (default \"8080\")\n  -ipxe-script-retries                [http] number of retries to attempt when fetching kernel and initrd files in the iPXE script (default \"0\")\n  -ipxe-script-retry-delay            [http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script (default \"2\")\n  -osie-url                           [http] URL where OSIE (HookOS) images are located\n  -tink-server                        [http] IP:Port for the Tink server\n  -tink-server-tls                    [http] use TLS for Tink server (default \"false\")\n  -trusted-proxies                    [http] comma separated list of trusted proxies in CIDR notation\n  -iso-enabled                        [iso] enable patching an OSIE ISO (default \"false\")\n  -iso-magic-string                   [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS\n  -iso-static-ipam-enabled            [iso] enable static IPAM for HookOS (default \"false\")\n  -iso-url                            [iso] an ISO source URL target for patching\n  -otel-endpoint                      [otel] OpenTelemetry collector endpoint\n  -otel-insecure                      [otel] OpenTelemetry collector insecure (default \"true\")\n  -syslog-addr                        [syslog] local IP to listen on for Syslog messages (default \"172.17.0.3\")\n  -syslog-enabled                     [syslog] enable Syslog server(receiver) (default \"true\")\n  -syslog-port                        [syslog] local port to listen on for Syslog messages (default \"514\")\n  -ipxe-script-patch                  [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP\n  -tftp-addr                          [tftp] local IP to listen on for iPXE TFTP binary requests (default \"172.17.0.3\")\n  -tftp-block-size                    [tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be) (default \"512\")\n  -tftp-enabled                       [tftp] enable iPXE TFTP binary server) (default \"true\")\n  -tftp-port                          [tftp] local port to listen on for iPXE TFTP binary requests (default \"69\")\n  -tftp-timeout                       [tftp] iPXE TFTP binary server requests timeout (default \"5s\")\n```\n\n### Developing using the file backend\n\nThe quickest way to get started is `docker-compose up`. This will start Smee using the file backend. This uses the example Yaml file (hardware.yaml) in the `test/` directory. It also starts a client container that runs some tests.\n\n```sh\ndocker compose up --build   # build images and start the network & services\n# it's fine to hit control-C twice for fast shutdown\ndocker compose down  # stop the network & containers\n```\n\nAlternatively Smee can be run by itself. It requires a few\nflags or environment variables for configuration.\n\n`test/hardware.yaml` should be safe enough for most developers to\nuse on the command line locally without getting a call from your network\nadministrator. That said, you might want to contact them before running a DHCP\nserver on their network. Best to isolate it in Docker or a VM if you're not\nsure.\n\n```sh\n# build the binary\nmake build\n\nexport SMEE_OSIE_URL=<http url to the OSIE (Operating System Installation Environment) artifacts>\n# For more info on the default OSIE (Hook) artifacts, please see https://github.com/tinkerbell/hook\nexport SMEE_BACKEND_KUBE_ENABLED=false\nexport SMEE_BACKEND_FILE_ENABLED=true\nexport SMEE_BACKEND_FILE_PATH=./test/hardware.yaml\nexport SMEE_EXTRA_KERNEL_ARGS=\"tink_worker_image=quay.io/tinkerbell/tink-worker:latest\"\n\n# By default, Smee needs to bind to low ports (67, 69, 514) so it needs root.\nsudo -E ./cmd/smee/smee\n\n# clean up the environment variables\nunset SMEE_OSIE_URL\nunset SMEE_BACKEND_KUBE_ENABLED\nunset SMEE_BACKEND_FILE_ENABLED\nunset SMEE_BACKEND_FILE_PATH\nunset SMEE_EXTRA_KERNEL_ARGS\n```\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Releasing\n\n## Process\n\nFor version v0.x.y:\n\n1. Create the annotated tag\n   > NOTE: To use your GPG signature when pushing the tag, use `SIGN_TAG=1 ./contrib/tag-release.sh v0.x.y` instead)\n   - `./contrib/tag-release.sh v0.x.y`\n1. Push the tag to the GitHub repository. This will automatically trigger a [Github Action](https://github.com/tinkerbell/smee/actions) to create a release.\n   > NOTE: `origin` should be the name of the remote pointing to `github.com/tinkerbell/smee`\n   - `git push origin v0.x.y`\n1. Review the release on GitHub.\n\n### Permissions\n\nReleasing requires a particular set of permissions.\n\n- Tag push access to the GitHub repository\n"
  },
  {
    "path": "Tiltfile",
    "content": "load('ext://restart_process', 'docker_build_with_restart')\nload('ext://local_output', 'local_output')\nload('ext://helm_resource', 'helm_resource')\n\nlocal_resource('compile smee',\n  cmd='make cmd/smee/smee-linux-amd64',\n  deps=[\"go.mod\", \"go.sum\", \"internal\", \"Dockerfile\", \"cmd/smee/main.go\", \"cmd/smee/flag.go\", \"cmd/smee/backend.go\"],\n)\n\ndocker_build_with_restart(\n  'quay.io/tinkerbell/smee',\n  '.',\n  dockerfile='Dockerfile',\n  entrypoint=['/usr/bin/smee'],\n  live_update=[\n    sync('cmd/smee/smee-linux-amd64', '/usr/bin/smee'),\n  ],\n)\ndefault_registry('ttl.sh/meohmy-dghentld')\n\ndefault_trusted_proxies = local_output(\"kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' | tr ' ' ','\")\ntrusted_proxies = os.getenv('TRUSTED_PROXIES', default_trusted_proxies)\nlb_ip = os.getenv('LB_IP', '')\nstack_version = os.getenv('STACK_CHART_VERSION', '0.5.0')\nstack_location = os.getenv('STACK_LOCATION', 'oci://ghcr.io/tinkerbell/charts/stack') # or a local path like '/home/tink/repos/tinkerbell/charts/tinkerbell/stack'\nnamespace = 'tink'\n\nif lb_ip == '':\n  fail('Please set the LB_IP environment variable. This is required to deploy the stack.')\n\n# to use a KinD cluster, add a macvlan interface into the KinD docker container. for example: `docker network connect macvlan kind-control-plane`\n# Then uncomment the 2 interface lines below.\nhelm_resource('stack',\n  chart=stack_location,\n  namespace=namespace,\n  image_deps=['quay.io/tinkerbell/smee'],\n  image_keys=[('smee.image')],\n  flags=[\n    '--create-namespace',\n    '--version=%s' % stack_version,\n    '--set=global.trustedProxies={%s}' % trusted_proxies,\n    '--set=global.publicIP=%s' % lb_ip,\n    #'--set=stack.kubevip.interface=eth1',\n    #'--set=stack.relay.sourceInterface=eth1',\n  ],\n  release_name='stack'\n)\n"
  },
  {
    "path": "cmd/smee/backend.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/tinkerbell/smee/internal/backend/file\"\n\t\"github.com/tinkerbell/smee/internal/backend/kube\"\n\t\"github.com/tinkerbell/smee/internal/backend/noop\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler\"\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/scale/scheme\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\tclientcmdapi \"k8s.io/client-go/tools/clientcmd/api\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cache\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cluster\"\n)\n\ntype Kube struct {\n\t// ConfigFilePath is the path to a kubernetes config file (kubeconfig).\n\tConfigFilePath string\n\t// APIURL is the Kubernetes API URL.\n\tAPIURL string\n\t// Namespace is an override for the Namespace the kubernetes client will watch.\n\t// The default is the Namespace the pod is running in.\n\tNamespace string\n\tEnabled   bool\n}\ntype File struct {\n\t// FilePath is the path to a JSON FilePath containing hardware data.\n\tFilePath string\n\tEnabled  bool\n}\n\ntype Noop struct {\n\tEnabled bool\n}\n\nfunc (n *Noop) backend() handler.BackendReader {\n\treturn &noop.Backend{}\n}\n\nfunc (k *Kube) getClient() (*rest.Config, error) {\n\tloadingRules := clientcmd.NewDefaultClientConfigLoadingRules()\n\tloadingRules.ExplicitPath = k.ConfigFilePath\n\n\toverrides := &clientcmd.ConfigOverrides{\n\t\tClusterInfo: clientcmdapi.Cluster{\n\t\t\tServer: k.APIURL,\n\t\t},\n\t\tContext: clientcmdapi.Context{\n\t\t\tNamespace: k.Namespace,\n\t\t},\n\t}\n\tloader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)\n\n\treturn loader.ClientConfig()\n}\n\nfunc (k *Kube) backend(ctx context.Context) (handler.BackendReader, error) {\n\tconfig, err := k.getClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trs := runtime.NewScheme()\n\n\tif err := scheme.AddToScheme(rs); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := v1alpha1.AddToScheme(rs); err != nil {\n\t\treturn nil, err\n\t}\n\n\tconf := func(opts *cluster.Options) {\n\t\topts.Scheme = rs\n\t\tif k.Namespace != \"\" {\n\t\t\topts.Cache.DefaultNamespaces = map[string]cache.Config{k.Namespace: {}}\n\t\t}\n\t}\n\n\tkb, err := kube.NewBackend(config, conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgo func() {\n\t\terr = kb.Start(ctx)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\treturn kb, nil\n}\n\nfunc (s *File) backend(ctx context.Context, logger logr.Logger) (handler.BackendReader, error) {\n\tf, err := file.NewWatcher(logger, s.FilePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgo f.Start(ctx)\n\n\treturn f, nil\n}\n"
  },
  {
    "path": "cmd/smee/flag.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"golang.org/x/sys/unix\"\n\n\t\"github.com/peterbourgon/ff/v3\"\n\t\"github.com/peterbourgon/ff/v3/ffcli\"\n\t\"github.com/vishvananda/netlink\"\n)\n\n// customUsageFunc is a custom UsageFunc used for all commands.\nfunc customUsageFunc(c *ffcli.Command) string {\n\tvar b strings.Builder\n\n\tif c.LongHelp != \"\" {\n\t\tfmt.Fprintf(&b, \"%s\\n\\n\", c.LongHelp)\n\t}\n\n\tfmt.Fprintf(&b, \"USAGE\\n\")\n\tif c.ShortUsage != \"\" {\n\t\tfmt.Fprintf(&b, \"  %s\\n\", c.ShortUsage)\n\t} else {\n\t\tfmt.Fprintf(&b, \"  %s\\n\", c.Name)\n\t}\n\tfmt.Fprintf(&b, \"\\n\")\n\n\tif len(c.Subcommands) > 0 {\n\t\tfmt.Fprintf(&b, \"SUBCOMMANDS\\n\")\n\t\ttw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)\n\t\tfor _, subcommand := range c.Subcommands {\n\t\t\tfmt.Fprintf(tw, \"  %s\\t%s\\n\", subcommand.Name, subcommand.ShortHelp)\n\t\t}\n\t\ttw.Flush()\n\t\tfmt.Fprintf(&b, \"\\n\")\n\t}\n\n\tif countFlags(c.FlagSet) > 0 {\n\t\tfmt.Fprintf(&b, \"FLAGS\\n\")\n\t\ttw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)\n\t\ttype flagUsage struct {\n\t\t\tname         string\n\t\t\tusage        string\n\t\t\tdefaultValue string\n\t\t}\n\t\tflags := []flagUsage{}\n\t\tc.FlagSet.VisitAll(func(f *flag.Flag) {\n\t\t\tf1 := flagUsage{name: f.Name, usage: f.Usage, defaultValue: f.DefValue}\n\t\t\tflags = append(flags, f1)\n\t\t})\n\n\t\tsort.SliceStable(flags, func(i, j int) bool {\n\t\t\t// sort by the service name between the brackets \"[]\" found in the usage string.\n\t\t\tr := regexp.MustCompile(`^\\[(.*?)\\]`)\n\t\t\treturn r.FindString(flags[i].usage) < r.FindString(flags[j].usage)\n\t\t})\n\t\tfor _, elem := range flags {\n\t\t\tif elem.defaultValue != \"\" {\n\t\t\t\tfmt.Fprintf(tw, \"  -%s\\t%s (default %q)\\n\", elem.name, elem.usage, elem.defaultValue)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(tw, \"  -%s\\t%s\\n\", elem.name, elem.usage)\n\t\t\t}\n\t\t}\n\t\ttw.Flush()\n\t\tfmt.Fprintf(&b, \"\\n\")\n\t}\n\n\treturn strings.TrimSpace(b.String()) + \"\\n\"\n}\n\nfunc countFlags(fs *flag.FlagSet) (n int) {\n\tfs.VisitAll(func(*flag.Flag) { n++ })\n\n\treturn n\n}\n\nfunc syslogFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.syslog.enabled, \"syslog-enabled\", true, \"[syslog] enable Syslog server(receiver)\")\n\tfs.StringVar(&c.syslog.bindAddr, \"syslog-addr\", detectPublicIPv4(), \"[syslog] local IP to listen on for Syslog messages\")\n\tfs.IntVar(&c.syslog.bindPort, \"syslog-port\", 514, \"[syslog] local port to listen on for Syslog messages\")\n}\n\nfunc tftpFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.tftp.enabled, \"tftp-enabled\", true, \"[tftp] enable iPXE TFTP binary server)\")\n\tfs.StringVar(&c.tftp.bindAddr, \"tftp-addr\", detectPublicIPv4(), \"[tftp] local IP to listen on for iPXE TFTP binary requests\")\n\tfs.IntVar(&c.tftp.bindPort, \"tftp-port\", 69, \"[tftp] local port to listen on for iPXE TFTP binary requests\")\n\tfs.DurationVar(&c.tftp.timeout, \"tftp-timeout\", time.Second*5, \"[tftp] iPXE TFTP binary server requests timeout\")\n\tfs.StringVar(&c.tftp.ipxeScriptPatch, \"ipxe-script-patch\", \"\", \"[tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP\")\n\tfs.IntVar(&c.tftp.blockSize, \"tftp-block-size\", 512, \"[tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be)\")\n}\n\nfunc ipxeHTTPBinaryFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.ipxeHTTPBinary.enabled, \"http-ipxe-binary-enabled\", true, \"[http] enable iPXE HTTP binary server\")\n}\n\nfunc ipxeHTTPScriptFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.ipxeHTTPScript.enabled, \"http-ipxe-script-enabled\", true, \"[http] enable iPXE HTTP script server\")\n\tfs.StringVar(&c.ipxeHTTPScript.bindAddr, \"http-addr\", detectPublicIPv4(), \"[http] local IP to listen on for iPXE HTTP script requests\")\n\tfs.IntVar(&c.ipxeHTTPScript.bindPort, \"http-port\", 8080, \"[http] local port to listen on for iPXE HTTP script requests\")\n\tfs.StringVar(&c.ipxeHTTPScript.extraKernelArgs, \"extra-kernel-args\", \"\", \"[http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script\")\n\tfs.StringVar(&c.ipxeHTTPScript.trustedProxies, \"trusted-proxies\", \"\", \"[http] comma separated list of trusted proxies in CIDR notation\")\n\tfs.StringVar(&c.ipxeHTTPScript.hookURL, \"osie-url\", \"\", \"[http] URL where OSIE (HookOS) images are located\")\n\tfs.StringVar(&c.ipxeHTTPScript.tinkServer, \"tink-server\", \"\", \"[http] IP:Port for the Tink server\")\n\tfs.BoolVar(&c.ipxeHTTPScript.tinkServerUseTLS, \"tink-server-tls\", false, \"[http] use TLS for Tink server\")\n\tfs.BoolVar(&c.ipxeHTTPScript.tinkServerInsecureTLS, \"tink-server-insecure-tls\", false, \"[http] use insecure TLS for Tink server\")\n\tfs.IntVar(&c.ipxeHTTPScript.retries, \"ipxe-script-retries\", 0, \"[http] number of retries to attempt when fetching kernel and initrd files in the iPXE script\")\n\tfs.IntVar(&c.ipxeHTTPScript.retryDelay, \"ipxe-script-retry-delay\", 2, \"[http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script\")\n}\n\nfunc dhcpFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.dhcp.enabled, \"dhcp-enabled\", true, \"[dhcp] enable DHCP server\")\n\tfs.StringVar(&c.dhcp.mode, \"dhcp-mode\", dhcpModeReservation.String(), fmt.Sprintf(\"[dhcp] DHCP mode (%s, %s, %s)\", dhcpModeReservation, dhcpModeProxy, dhcpModeAutoProxy))\n\tfs.StringVar(&c.dhcp.bindAddr, \"dhcp-addr\", \"0.0.0.0:67\", \"[dhcp] local IP:Port to listen on for DHCP requests\")\n\tfs.StringVar(&c.dhcp.bindInterface, \"dhcp-iface\", \"\", \"[dhcp] interface to bind to for DHCP requests\")\n\tfs.StringVar(&c.dhcp.ipForPacket, \"dhcp-ip-for-packet\", detectPublicIPv4(), \"[dhcp] IP address to use in DHCP packets (opt 54, etc)\")\n\tfs.StringVar(&c.dhcp.syslogIP, \"dhcp-syslog-ip\", detectPublicIPv4(), \"[dhcp] Syslog server IP address to use in DHCP packets (opt 7)\")\n\tfs.StringVar(&c.dhcp.tftpIP, \"dhcp-tftp-ip\", detectPublicIPv4(), \"[dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc)\")\n\tfs.IntVar(&c.dhcp.tftpPort, \"dhcp-tftp-port\", 69, \"[dhcp] TFTP server port to use in DHCP packets (opt 66, etc)\")\n\tfs.StringVar(&c.dhcp.httpIpxeBinaryURL.Scheme, \"dhcp-http-ipxe-binary-scheme\", \"http\", \"[dhcp] HTTP iPXE binaries scheme to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeBinaryURL.Host, \"dhcp-http-ipxe-binary-host\", detectPublicIPv4(), \"[dhcp] HTTP iPXE binaries host or IP to use in DHCP packets\")\n\tfs.IntVar(&c.dhcp.httpIpxeBinaryURL.Port, \"dhcp-http-ipxe-binary-port\", 8080, \"[dhcp] HTTP iPXE binaries port to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeBinaryURL.Path, \"dhcp-http-ipxe-binary-path\", \"/ipxe/\", \"[dhcp] HTTP iPXE binaries path to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeScript.Scheme, \"dhcp-http-ipxe-script-scheme\", \"http\", \"[dhcp] HTTP iPXE script scheme to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeScript.Host, \"dhcp-http-ipxe-script-host\", detectPublicIPv4(), \"[dhcp] HTTP iPXE script host or IP to use in DHCP packets\")\n\tfs.IntVar(&c.dhcp.httpIpxeScript.Port, \"dhcp-http-ipxe-script-port\", 8080, \"[dhcp] HTTP iPXE script port to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeScript.Path, \"dhcp-http-ipxe-script-path\", \"/auto.ipxe\", \"[dhcp] HTTP iPXE script path to use in DHCP packets\")\n\tfs.StringVar(&c.dhcp.httpIpxeScriptURL, \"dhcp-http-ipxe-script-url\", \"\", \"[dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}\")\n\tfs.BoolVar(&c.dhcp.httpIpxeScript.injectMacAddress, \"dhcp-http-ipxe-script-prepend-mac\", true, \"[dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe\")\n}\n\nfunc backendFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.backends.file.Enabled, \"backend-file-enabled\", false, \"[backend] enable the file backend for DHCP and the HTTP iPXE script\")\n\tfs.StringVar(&c.backends.file.FilePath, \"backend-file-path\", \"\", \"[backend] the hardware yaml file path for the file backend\")\n\tfs.BoolVar(&c.backends.kubernetes.Enabled, \"backend-kube-enabled\", true, \"[backend] enable the kubernetes backend for DHCP and the HTTP iPXE script\")\n\tfs.StringVar(&c.backends.kubernetes.ConfigFilePath, \"backend-kube-config\", \"\", \"[backend] the Kubernetes config file location, kube backend only\")\n\tfs.StringVar(&c.backends.kubernetes.APIURL, \"backend-kube-api\", \"\", \"[backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only\")\n\tfs.StringVar(&c.backends.kubernetes.Namespace, \"backend-kube-namespace\", \"\", \"[backend] an optional Kubernetes namespace override to query hardware data from, kube backend only\")\n\tfs.BoolVar(&c.backends.Noop.Enabled, \"backend-noop-enabled\", false, \"[backend] enable the noop backend for DHCP and the HTTP iPXE script\")\n}\n\nfunc otelFlags(c *config, fs *flag.FlagSet) {\n\tfs.StringVar(&c.otel.endpoint, \"otel-endpoint\", \"\", \"[otel] OpenTelemetry collector endpoint\")\n\tfs.BoolVar(&c.otel.insecure, \"otel-insecure\", true, \"[otel] OpenTelemetry collector insecure\")\n}\n\nfunc isoFlags(c *config, fs *flag.FlagSet) {\n\tfs.BoolVar(&c.iso.enabled, \"iso-enabled\", false, \"[iso] enable patching an OSIE ISO\")\n\tfs.StringVar(&c.iso.url, \"iso-url\", \"\", \"[iso] an ISO source URL target for patching\")\n\tfs.StringVar(&c.iso.magicString, \"iso-magic-string\", \"\", \"[iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS\")\n\tfs.BoolVar(&c.iso.staticIPAMEnabled, \"iso-static-ipam-enabled\", false, \"[iso] enable static IPAM for HookOS\")\n}\n\nfunc setFlags(c *config, fs *flag.FlagSet) {\n\tfs.StringVar(&c.logLevel, \"log-level\", \"info\", \"log level (debug, info)\")\n\tdhcpFlags(c, fs)\n\ttftpFlags(c, fs)\n\tipxeHTTPBinaryFlags(c, fs)\n\tipxeHTTPScriptFlags(c, fs)\n\tsyslogFlags(c, fs)\n\tbackendFlags(c, fs)\n\totelFlags(c, fs)\n\tisoFlags(c, fs)\n}\n\nfunc newCLI(cfg *config, fs *flag.FlagSet) *ffcli.Command {\n\tsetFlags(cfg, fs)\n\treturn &ffcli.Command{\n\t\tName:       name,\n\t\tShortUsage: \"smee [flags]\",\n\t\tLongHelp:   \"Smee is the DHCP and Network boot service for use in the Tinkerbell stack.\",\n\t\tFlagSet:    fs,\n\t\tOptions:    []ff.Option{ff.WithEnvVarPrefix(name)},\n\t\tUsageFunc:  customUsageFunc,\n\t}\n}\n\n// ipByInterface returns the first IPv4 address on the named network interface.\nfunc ipByInterface(name string) string {\n\tiface, err := net.InterfaceByName(name)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\taddrs, err := iface.Addrs()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tfor _, addr := range addrs {\n\t\tipNet, ok := addr.(*net.IPNet)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif ipNet.IP.To4() != nil {\n\t\t\treturn ipNet.IP.String()\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc detectPublicIPv4() string {\n\tif netint := os.Getenv(\"SMEE_PUBLIC_IP_INTERFACE\"); netint != \"\" {\n\t\tif ip := ipByInterface(netint); ip != \"\" {\n\t\t\treturn ip\n\t\t}\n\t}\n\tipDgw, err := autoDetectPublicIpv4WithDefaultGateway()\n\tif err == nil {\n\t\treturn ipDgw.String()\n\t}\n\n\tip, err := autoDetectPublicIPv4()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn ip.String()\n}\n\nfunc autoDetectPublicIPv4() (net.IP, error) {\n\taddrs, err := net.InterfaceAddrs()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to auto-detect public IPv4: %w\", err)\n\t}\n\tfor _, addr := range addrs {\n\t\tip, ok := addr.(*net.IPNet)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tv4 := ip.IP.To4()\n\t\tif v4 == nil || !v4.IsGlobalUnicast() {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn v4, nil\n\t}\n\n\treturn nil, errors.New(\"unable to auto-detect public IPv4\")\n}\n\n// autoDetectPublicIpv4WithDefaultGateway finds the network interface with a default gateway\n// and returns the first net.IP address of the first interface that has a default gateway.\nfunc autoDetectPublicIpv4WithDefaultGateway() (net.IP, error) {\n\t// Get the list of routes from netlink\n\troutes, err := netlink.RouteList(nil, unix.AF_INET)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list routes: %v\", err)\n\t}\n\n\t// Find the route with a default gateway (Dst == nil)\n\tfor _, route := range routes {\n\t\tif route.Dst == nil && route.Gw != nil {\n\t\t\t// Get the interface associated with this route\n\t\t\tiface, err := net.InterfaceByIndex(route.LinkIndex)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to get interface by index: %v\", err)\n\t\t\t}\n\n\t\t\t// Get the addresses assigned to this interface\n\t\t\taddrs, err := iface.Addrs()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to get addresses for interface %v: %v\", iface.Name, err)\n\t\t\t}\n\n\t\t\t// Return the first valid IP address found\n\t\t\tfor _, addr := range addrs {\n\t\t\t\tif ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {\n\t\t\t\t\tif ipNet.IP.To4() != nil {\n\t\t\t\t\t\treturn ipNet.IP, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"no default gateway found\")\n}\n"
  },
  {
    "path": "cmd/smee/flag_test.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestParser(t *testing.T) {\n\twant := config{\n\t\tsyslog: syslogConfig{\n\t\t\tenabled:  true,\n\t\t\tbindAddr: \"192.168.2.4\",\n\t\t\tbindPort: 514,\n\t\t},\n\t\ttftp: tftp{\n\t\t\tblockSize: 512,\n\t\t\tenabled:   true,\n\t\t\ttimeout:   5 * time.Second,\n\t\t\tbindAddr:  \"192.168.2.4\",\n\t\t\tbindPort:  69,\n\t\t},\n\t\tipxeHTTPBinary: ipxeHTTPBinary{\n\t\t\tenabled: true,\n\t\t},\n\t\tipxeHTTPScript: ipxeHTTPScript{\n\t\t\tenabled:    true,\n\t\t\tbindAddr:   \"192.168.2.4\",\n\t\t\tbindPort:   8080,\n\t\t\tretryDelay: 2,\n\t\t},\n\t\tdhcp: dhcpConfig{\n\t\t\tenabled:     true,\n\t\t\tmode:        \"reservation\",\n\t\t\tbindAddr:    \"0.0.0.0:67\",\n\t\t\tipForPacket: \"192.168.2.4\",\n\t\t\tsyslogIP:    \"192.168.2.4\",\n\t\t\ttftpIP:      \"192.168.2.4\",\n\t\t\ttftpPort:    69,\n\t\t\thttpIpxeBinaryURL: urlBuilder{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   \"192.168.2.4\",\n\t\t\t\tPort:   8080,\n\t\t\t\tPath:   \"/ipxe/\",\n\t\t\t},\n\t\t\thttpIpxeScript: httpIpxeScript{\n\t\t\t\turlBuilder: urlBuilder{\n\t\t\t\t\tScheme: \"http\",\n\t\t\t\t\tHost:   \"192.168.2.4\",\n\t\t\t\t\tPort:   8080,\n\t\t\t\t\tPath:   \"/auto.ipxe\",\n\t\t\t\t},\n\t\t\t\tinjectMacAddress: true,\n\t\t\t},\n\t\t},\n\t\tiso: isoConfig{\n\t\t\tenabled:     true,\n\t\t\turl:         \"http://10.10.10.10:8787/hook.iso\",\n\t\t\tmagicString: magicString,\n\t\t},\n\t\tlogLevel: \"info\",\n\t\tbackends: dhcpBackends{\n\t\t\tfile:       File{},\n\t\t\tkubernetes: Kube{Enabled: true},\n\t\t},\n\t\totel: otelConfig{\n\t\t\tinsecure: true,\n\t\t},\n\t}\n\tgot := config{}\n\tfs := flag.NewFlagSet(name, flag.ContinueOnError)\n\targs := []string{\n\t\t\"-log-level\", \"info\",\n\t\t\"-syslog-addr\", \"192.168.2.4\",\n\t\t\"-tftp-addr\", \"192.168.2.4\",\n\t\t\"-http-addr\", \"192.168.2.4\",\n\t\t\"-dhcp-ip-for-packet\", \"192.168.2.4\",\n\t\t\"-dhcp-syslog-ip\", \"192.168.2.4\",\n\t\t\"-dhcp-tftp-ip\", \"192.168.2.4\",\n\t\t\"-dhcp-http-ipxe-binary-host\", \"192.168.2.4\",\n\t\t\"-dhcp-http-ipxe-script-host\", \"192.168.2.4\",\n\t\t\"-iso-enabled=true\",\n\t\t\"-iso-magic-string\", magicString,\n\t\t\"-iso-url\", \"http://10.10.10.10:8787/hook.iso\",\n\t}\n\tcli := newCLI(&got, fs)\n\tcli.Parse(args)\n\topts := cmp.Options{\n\t\tcmp.AllowUnexported(config{}),\n\t\tcmp.AllowUnexported(syslogConfig{}),\n\t\tcmp.AllowUnexported(tftp{}),\n\t\tcmp.AllowUnexported(ipxeHTTPBinary{}),\n\t\tcmp.AllowUnexported(ipxeHTTPScript{}),\n\t\tcmp.AllowUnexported(dhcpConfig{}),\n\t\tcmp.AllowUnexported(dhcpBackends{}),\n\t\tcmp.AllowUnexported(httpIpxeScript{}),\n\t\tcmp.AllowUnexported(isoConfig{}),\n\t\tcmp.AllowUnexported(otelConfig{}),\n\t\tcmp.AllowUnexported(urlBuilder{}),\n\t}\n\n\tif diff := cmp.Diff(want, got, opts); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestCustomUsageFunc(t *testing.T) {\n\tdefaultIP := detectPublicIPv4()\n\twant := fmt.Sprintf(`Smee is the DHCP and Network boot service for use in the Tinkerbell stack.\n\nUSAGE\n  smee [flags]\n\nFLAGS\n  -log-level                          log level (debug, info) (default \"info\")\n  -backend-file-enabled               [backend] enable the file backend for DHCP and the HTTP iPXE script (default \"false\")\n  -backend-file-path                  [backend] the hardware yaml file path for the file backend\n  -backend-kube-api                   [backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only\n  -backend-kube-config                [backend] the Kubernetes config file location, kube backend only\n  -backend-kube-enabled               [backend] enable the kubernetes backend for DHCP and the HTTP iPXE script (default \"true\")\n  -backend-kube-namespace             [backend] an optional Kubernetes namespace override to query hardware data from, kube backend only\n  -backend-noop-enabled               [backend] enable the noop backend for DHCP and the HTTP iPXE script (default \"false\")\n  -dhcp-addr                          [dhcp] local IP:Port to listen on for DHCP requests (default \"0.0.0.0:67\")\n  -dhcp-enabled                       [dhcp] enable DHCP server (default \"true\")\n  -dhcp-http-ipxe-binary-host         [dhcp] HTTP iPXE binaries host or IP to use in DHCP packets (default \"%[1]v\")\n  -dhcp-http-ipxe-binary-path         [dhcp] HTTP iPXE binaries path to use in DHCP packets (default \"/ipxe/\")\n  -dhcp-http-ipxe-binary-port         [dhcp] HTTP iPXE binaries port to use in DHCP packets (default \"8080\")\n  -dhcp-http-ipxe-binary-scheme       [dhcp] HTTP iPXE binaries scheme to use in DHCP packets (default \"http\")\n  -dhcp-http-ipxe-script-host         [dhcp] HTTP iPXE script host or IP to use in DHCP packets (default \"%[1]v\")\n  -dhcp-http-ipxe-script-path         [dhcp] HTTP iPXE script path to use in DHCP packets (default \"/auto.ipxe\")\n  -dhcp-http-ipxe-script-port         [dhcp] HTTP iPXE script port to use in DHCP packets (default \"8080\")\n  -dhcp-http-ipxe-script-prepend-mac  [dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe (default \"true\")\n  -dhcp-http-ipxe-script-scheme       [dhcp] HTTP iPXE script scheme to use in DHCP packets (default \"http\")\n  -dhcp-http-ipxe-script-url          [dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}\n  -dhcp-iface                         [dhcp] interface to bind to for DHCP requests\n  -dhcp-ip-for-packet                 [dhcp] IP address to use in DHCP packets (opt 54, etc) (default \"%[1]v\")\n  -dhcp-mode                          [dhcp] DHCP mode (reservation, proxy, auto-proxy) (default \"reservation\")\n  -dhcp-syslog-ip                     [dhcp] Syslog server IP address to use in DHCP packets (opt 7) (default \"%[1]v\")\n  -dhcp-tftp-ip                       [dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc) (default \"%[1]v\")\n  -dhcp-tftp-port                     [dhcp] TFTP server port to use in DHCP packets (opt 66, etc) (default \"69\")\n  -extra-kernel-args                  [http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script\n  -http-addr                          [http] local IP to listen on for iPXE HTTP script requests (default \"%[1]v\")\n  -http-ipxe-binary-enabled           [http] enable iPXE HTTP binary server (default \"true\")\n  -http-ipxe-script-enabled           [http] enable iPXE HTTP script server (default \"true\")\n  -http-port                          [http] local port to listen on for iPXE HTTP script requests (default \"8080\")\n  -ipxe-script-retries                [http] number of retries to attempt when fetching kernel and initrd files in the iPXE script (default \"0\")\n  -ipxe-script-retry-delay            [http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script (default \"2\")\n  -osie-url                           [http] URL where OSIE (HookOS) images are located\n  -tink-server                        [http] IP:Port for the Tink server\n  -tink-server-insecure-tls           [http] use insecure TLS for Tink server (default \"false\")\n  -tink-server-tls                    [http] use TLS for Tink server (default \"false\")\n  -trusted-proxies                    [http] comma separated list of trusted proxies in CIDR notation\n  -iso-enabled                        [iso] enable patching an OSIE ISO (default \"false\")\n  -iso-magic-string                   [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS\n  -iso-static-ipam-enabled            [iso] enable static IPAM for HookOS (default \"false\")\n  -iso-url                            [iso] an ISO source URL target for patching\n  -otel-endpoint                      [otel] OpenTelemetry collector endpoint\n  -otel-insecure                      [otel] OpenTelemetry collector insecure (default \"true\")\n  -syslog-addr                        [syslog] local IP to listen on for Syslog messages (default \"%[1]v\")\n  -syslog-enabled                     [syslog] enable Syslog server(receiver) (default \"true\")\n  -syslog-port                        [syslog] local port to listen on for Syslog messages (default \"514\")\n  -ipxe-script-patch                  [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP\n  -tftp-addr                          [tftp] local IP to listen on for iPXE TFTP binary requests (default \"%[1]v\")\n  -tftp-block-size                    [tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be) (default \"512\")\n  -tftp-enabled                       [tftp] enable iPXE TFTP binary server) (default \"true\")\n  -tftp-port                          [tftp] local port to listen on for iPXE TFTP binary requests (default \"69\")\n  -tftp-timeout                       [tftp] iPXE TFTP binary server requests timeout (default \"5s\")\n`, defaultIP)\n\n\tc := &config{}\n\tfs := flag.NewFlagSet(name, flag.ContinueOnError)\n\tcli := newCLI(c, fs)\n\tgot := customUsageFunc(cli)\n\tif diff := cmp.Diff(want, got); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n"
  },
  {
    "path": "cmd/smee/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/server4\"\n\t\"github.com/tinkerbell/ipxedust\"\n\t\"github.com/tinkerbell/ipxedust/ihttp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler/proxy\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler/reservation\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/server\"\n\t\"github.com/tinkerbell/smee/internal/ipxe/http\"\n\t\"github.com/tinkerbell/smee/internal/ipxe/script\"\n\t\"github.com/tinkerbell/smee/internal/iso\"\n\t\"github.com/tinkerbell/smee/internal/metric\"\n\t\"github.com/tinkerbell/smee/internal/otel\"\n\t\"github.com/tinkerbell/smee/internal/syslog\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nvar (\n\t// GitRev is the git revision of the build. It is set by the Makefile.\n\tGitRev = \"unknown (use make)\"\n\n\tstartTime = time.Now()\n)\n\nconst (\n\tname                         = \"smee\"\n\tdhcpModeProxy       dhcpMode = \"proxy\"\n\tdhcpModeReservation dhcpMode = \"reservation\"\n\tdhcpModeAutoProxy   dhcpMode = \"auto-proxy\"\n\t// magicString comes from the HookOS repo\n\t// ref: https://github.com/tinkerbell/hook/blob/main/linuxkit-templates/hook.template.yaml\n\tmagicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit021bmpdb9ctrc87x2ymc8e7icu4ffi15x1hah9iyaiz38ckyap8hwx2vt5rm44ixv4hau8iw718q5yd019um5dt2xpqqa2rjtdypzr5v1gun8un110hhwp8cex7pqrh2ivh0ynpm4zkkwc8wcn367zyethzy7q8hzudyeyzx3cgmxqbkh825gcak7kxzjbgjajwizryv7ec1xm2h0hh7pz29qmvtgfjj1vphpgq1zcbiiehv52wrjy9yq473d9t1rvryy6929nk435hfx55du3ih05kn5tju3vijreru1p6knc988d4gfdz28eragvryq5x8aibe5trxd0t6t7jwxkde34v6pj1khmp50k6qqj3nzgcfzabtgqkmeqhdedbvwf3byfdma4nkv3rcxugaj2d0ru30pa2fqadjqrtjnv8bu52xzxv7irbhyvygygxu1nt5z4fh9w1vwbdcmagep26d298zknykf2e88kumt59ab7nq79d8amnhhvbexgh48e8qc61vq2e9qkihzt1twk1ijfgw70nwizai15iqyted2dt9gfmf2gg7amzufre79hwqkddc1cd935ywacnkrnak6r7xzcz7zbmq3kt04u2hg1iuupid8rt4nyrju51e6uejb2ruu36g9aibmz3hnmvazptu8x5tyxk820g2cdpxjdij766bt2n3djur7v623a2v44juyfgz80ekgfb9hkibpxh3zgknw8a34t4jifhf116x15cei9hwch0fye3xyq0acuym8uhitu5evc4rag3ui0fny3qg4kju7zkfyy8hwh537urd5uixkzwu5bdvafz4jmv7imypj543xg5em8jk8cgk7c4504xdd5e4e71ihaumt6u5u2t1w7um92fepzae8p0vq93wdrd1756npu1pziiur1payc7kmdwyxg3hj5n4phxbc29x0tcddamjrwt260b0w`\n)\n\ntype config struct {\n\tsyslog         syslogConfig\n\ttftp           tftp\n\tipxeHTTPBinary ipxeHTTPBinary\n\tipxeHTTPScript ipxeHTTPScript\n\tdhcp           dhcpConfig\n\tiso            isoConfig\n\n\t// loglevel is the log level for smee.\n\tlogLevel string\n\tbackends dhcpBackends\n\totel     otelConfig\n}\n\ntype syslogConfig struct {\n\tenabled  bool\n\tbindAddr string\n\tbindPort int\n}\n\ntype tftp struct {\n\tbindAddr        string\n\tbindPort        int\n\tblockSize       int\n\tenabled         bool\n\tipxeScriptPatch string\n\ttimeout         time.Duration\n}\n\ntype ipxeHTTPBinary struct {\n\tenabled bool\n}\n\ntype ipxeHTTPScript struct {\n\tenabled               bool\n\tbindAddr              string\n\tbindPort              int\n\textraKernelArgs       string\n\thookURL               string\n\ttinkServer            string\n\ttinkServerUseTLS      bool\n\ttinkServerInsecureTLS bool\n\ttrustedProxies        string\n\tretries               int\n\tretryDelay            int\n}\n\ntype dhcpMode string\n\ntype dhcpConfig struct {\n\tenabled           bool\n\tmode              string\n\tbindAddr          string\n\tbindInterface     string\n\tipForPacket       string\n\tsyslogIP          string\n\ttftpIP            string\n\ttftpPort          int\n\thttpIpxeBinaryURL urlBuilder\n\thttpIpxeScript    httpIpxeScript\n\thttpIpxeScriptURL string\n}\n\ntype urlBuilder struct {\n\tScheme string\n\tHost   string\n\tPort   int\n\tPath   string\n}\n\ntype httpIpxeScript struct {\n\turlBuilder\n\t// injectMacAddress will prepend the hardware mac address to the ipxe script URL file name.\n\t// For example: http://1.2.3.4/my/loc/auto.ipxe -> http://1.2.3.4/my/loc/40:15:ff:89:cc:0e/auto.ipxe\n\t// Setting this to false is useful when you are not using the auto.ipxe script in Smee.\n\tinjectMacAddress bool\n}\n\ntype dhcpBackends struct {\n\tfile       File\n\tkubernetes Kube\n\tNoop       Noop\n}\n\ntype otelConfig struct {\n\tendpoint string\n\tinsecure bool\n}\n\ntype isoConfig struct {\n\tenabled           bool\n\turl               string\n\tmagicString       string\n\tstaticIPAMEnabled bool\n}\n\nfunc main() {\n\tcfg := &config{}\n\tcli := newCLI(cfg, flag.NewFlagSet(name, flag.ExitOnError))\n\t_ = cli.Parse(os.Args[1:])\n\n\tlog := defaultLogger(cfg.logLevel)\n\tlog.Info(\"starting\", \"version\", GitRev)\n\n\tctx, done := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGHUP, syscall.SIGTERM)\n\tdefer done()\n\toCfg := otel.Config{\n\t\tServicename: \"smee\",\n\t\tEndpoint:    cfg.otel.endpoint,\n\t\tInsecure:    cfg.otel.insecure,\n\t\tLogger:      log,\n\t}\n\tctx, otelShutdown, err := otel.Init(ctx, oCfg)\n\tif err != nil {\n\t\tlog.Error(err, \"failed to initialize OpenTelemetry\")\n\t\tpanic(err)\n\t}\n\tdefer otelShutdown()\n\tmetric.Init()\n\n\tg, ctx := errgroup.WithContext(ctx)\n\t// syslog\n\tif cfg.syslog.enabled {\n\t\taddr := fmt.Sprintf(\"%s:%d\", cfg.syslog.bindAddr, cfg.syslog.bindPort)\n\t\tlog.Info(\"starting syslog server\", \"bind_addr\", addr)\n\t\tg.Go(func() error {\n\t\t\tif err := syslog.StartReceiver(ctx, log, addr, 1); err != nil {\n\t\t\t\tlog.Error(err, \"syslog server failure\")\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t<-ctx.Done()\n\t\t\tlog.Info(\"syslog server stopped\")\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// tftp\n\tif cfg.tftp.enabled {\n\t\ttftpServer := &ipxedust.Server{\n\t\t\tLog:                  log.WithValues(\"service\", \"github.com/tinkerbell/smee\").WithName(\"github.com/tinkerbell/ipxedust\"),\n\t\t\tHTTP:                 ipxedust.ServerSpec{Disabled: true}, // disabled because below we use the http handlerfunc instead.\n\t\t\tEnableTFTPSinglePort: true,\n\t\t}\n\t\ttftpServer.EnableTFTPSinglePort = true\n\t\taddr := fmt.Sprintf(\"%s:%d\", cfg.tftp.bindAddr, cfg.tftp.bindPort)\n\t\tif ip, err := netip.ParseAddrPort(addr); err == nil {\n\t\t\ttftpServer.TFTP = ipxedust.ServerSpec{\n\t\t\t\tDisabled:  false,\n\t\t\t\tAddr:      ip,\n\t\t\t\tTimeout:   cfg.tftp.timeout,\n\t\t\t\tPatch:     []byte(cfg.tftp.ipxeScriptPatch),\n\t\t\t\tBlockSize: cfg.tftp.blockSize,\n\t\t\t}\n\t\t\t// start the ipxe binary tftp server\n\t\t\tlog.Info(\"starting tftp server\", \"bind_addr\", addr)\n\t\t\tg.Go(func() error {\n\t\t\t\treturn tftpServer.ListenAndServe(ctx)\n\t\t\t})\n\t\t} else {\n\t\t\tlog.Error(err, \"invalid bind address\")\n\t\t\tpanic(fmt.Errorf(\"invalid bind address: %w\", err))\n\t\t}\n\t}\n\n\thandlers := http.HandlerMapping{}\n\t// http ipxe binaries\n\tif cfg.ipxeHTTPBinary.enabled {\n\t\t// serve ipxe binaries from the \"/ipxe/\" URI.\n\t\thandlers[\"/ipxe/\"] = ihttp.Handler{\n\t\t\tLog:   log.WithValues(\"service\", \"github.com/tinkerbell/smee\").WithName(\"github.com/tinkerbell/ipxedust\"),\n\t\t\tPatch: []byte(cfg.tftp.ipxeScriptPatch),\n\t\t}.Handle\n\t}\n\n\t// http ipxe script\n\tif cfg.ipxeHTTPScript.enabled {\n\t\tbr, err := cfg.backend(ctx, log)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to create backend: %w\", err))\n\t\t}\n\t\tjh := script.Handler{\n\t\t\tLogger:                log,\n\t\t\tBackend:               br,\n\t\t\tOSIEURL:               cfg.ipxeHTTPScript.hookURL,\n\t\t\tExtraKernelParams:     strings.Split(cfg.ipxeHTTPScript.extraKernelArgs, \" \"),\n\t\t\tPublicSyslogFQDN:      cfg.dhcp.syslogIP,\n\t\t\tTinkServerTLS:         cfg.ipxeHTTPScript.tinkServerUseTLS,\n\t\t\tTinkServerInsecureTLS: cfg.ipxeHTTPScript.tinkServerInsecureTLS,\n\t\t\tTinkServerGRPCAddr:    cfg.ipxeHTTPScript.tinkServer,\n\t\t\tIPXEScriptRetries:     cfg.ipxeHTTPScript.retries,\n\t\t\tIPXEScriptRetryDelay:  cfg.ipxeHTTPScript.retryDelay,\n\t\t\tStaticIPXEEnabled:     (dhcpMode(cfg.dhcp.mode) == dhcpModeAutoProxy),\n\t\t}\n\n\t\t// serve ipxe script from the \"/\" URI.\n\t\thandlers[\"/\"] = jh.HandlerFunc()\n\t}\n\n\tif cfg.iso.enabled {\n\t\tbr, err := cfg.backend(ctx, log)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to create backend: %w\", err))\n\t\t}\n\t\tih := iso.Handler{\n\t\t\tLogger:             log,\n\t\t\tBackend:            br,\n\t\t\tSourceISO:          cfg.iso.url,\n\t\t\tExtraKernelParams:  strings.Split(cfg.ipxeHTTPScript.extraKernelArgs, \" \"),\n\t\t\tSyslog:             cfg.dhcp.syslogIP,\n\t\t\tTinkServerTLS:      cfg.ipxeHTTPScript.tinkServerUseTLS,\n\t\t\tTinkServerGRPCAddr: cfg.ipxeHTTPScript.tinkServer,\n\t\t\tStaticIPAMEnabled:  cfg.iso.staticIPAMEnabled,\n\t\t\tMagicString: func() string {\n\t\t\t\tif cfg.iso.magicString == \"\" {\n\t\t\t\t\treturn magicString\n\t\t\t\t}\n\t\t\t\treturn cfg.iso.magicString\n\t\t\t}(),\n\t\t}\n\t\tisoHandler, err := ih.HandlerFunc()\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to create iso handler: %w\", err))\n\t\t}\n\t\thandlers[\"/iso/\"] = isoHandler\n\t}\n\n\tif len(handlers) > 0 {\n\t\t// start the http server for ipxe binaries and scripts\n\t\ttp := parseTrustedProxies(cfg.ipxeHTTPScript.trustedProxies)\n\t\thttpServer := &http.Config{\n\t\t\tGitRev:         GitRev,\n\t\t\tStartTime:      startTime,\n\t\t\tLogger:         log,\n\t\t\tTrustedProxies: tp,\n\t\t}\n\t\tbindAddr := fmt.Sprintf(\"%s:%d\", cfg.ipxeHTTPScript.bindAddr, cfg.ipxeHTTPScript.bindPort)\n\t\tlog.Info(\"serving http\", \"addr\", bindAddr, \"trusted_proxies\", tp)\n\t\tg.Go(func() error {\n\t\t\treturn httpServer.ServeHTTP(ctx, bindAddr, handlers)\n\t\t})\n\t}\n\n\t// dhcp serving\n\tif cfg.dhcp.enabled {\n\t\tdh, err := cfg.dhcpHandler(ctx, log)\n\t\tif err != nil {\n\t\t\tlog.Error(err, \"failed to create dhcp listener\")\n\t\t\tpanic(fmt.Errorf(\"failed to create dhcp listener: %w\", err))\n\t\t}\n\t\tlog.Info(\"starting dhcp server\", \"bind_addr\", cfg.dhcp.bindAddr)\n\t\tg.Go(func() error {\n\t\t\tbindAddr, err := netip.ParseAddrPort(cfg.dhcp.bindAddr)\n\t\t\tif err != nil {\n\t\t\t\tpanic(fmt.Errorf(\"invalid tftp address for DHCP server: %w\", err))\n\t\t\t}\n\t\t\tconn, err := server4.NewIPv4UDPConn(cfg.dhcp.bindInterface, net.UDPAddrFromAddrPort(bindAddr))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tdefer conn.Close()\n\t\t\tds := &server.DHCP{Logger: log, Conn: conn, Handlers: []server.Handler{dh}}\n\n\t\t\treturn ds.Serve(ctx)\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {\n\t\tlog.Error(err, \"failed running all Smee services\")\n\t\tpanic(err)\n\t}\n\tlog.Info(\"smee is shutting down\")\n}\n\nfunc numTrue(b ...bool) int {\n\tn := 0\n\tfor _, v := range b {\n\t\tif v {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\nfunc (c *config) backend(ctx context.Context, log logr.Logger) (handler.BackendReader, error) {\n\tif c.backends.file.Enabled || c.backends.Noop.Enabled {\n\t\t// the kubernetes backend is enabled by default so we disable it\n\t\t// if another backend is enabled so that users don't have to explicitly\n\t\t// set the CLI flag to disable it when using another backend.\n\t\tc.backends.kubernetes.Enabled = false\n\t}\n\tvar be handler.BackendReader\n\tswitch {\n\tcase numTrue(c.backends.file.Enabled, c.backends.kubernetes.Enabled, c.backends.Noop.Enabled) > 1:\n\t\treturn nil, errors.New(\"only one backend can be enabled at a time\")\n\tcase c.backends.Noop.Enabled:\n\t\tif c.dhcp.mode != string(dhcpModeAutoProxy) {\n\t\t\treturn nil, errors.New(\"noop backend can only be used with --dhcp-mode=auto-proxy\")\n\t\t}\n\t\tbe = c.backends.Noop.backend()\n\tcase c.backends.file.Enabled:\n\t\tb, err := c.backends.file.backend(ctx, log)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create file backend: %w\", err)\n\t\t}\n\t\tbe = b\n\tdefault: // default backend is kubernetes\n\t\tb, err := c.backends.kubernetes.backend(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create kubernetes backend: %w\", err)\n\t\t}\n\t\tbe = b\n\t}\n\n\treturn be, nil\n}\n\nfunc (c *config) dhcpHandler(ctx context.Context, log logr.Logger) (server.Handler, error) {\n\t// 1. create the handler\n\t// 2. create the backend\n\t// 3. add the backend to the handler\n\tpktIP, err := netip.ParseAddr(c.dhcp.ipForPacket)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid bind address: %w\", err)\n\t}\n\ttftpIP, err := netip.ParseAddrPort(fmt.Sprintf(\"%s:%d\", c.dhcp.tftpIP, c.dhcp.tftpPort))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tftp address for DHCP server: %w\", err)\n\t}\n\thttpBinaryURL := &url.URL{\n\t\tScheme: c.dhcp.httpIpxeBinaryURL.Scheme,\n\t\tHost:   fmt.Sprintf(\"%s:%d\", c.dhcp.httpIpxeBinaryURL.Host, c.dhcp.httpIpxeBinaryURL.Port),\n\t\tPath:   c.dhcp.httpIpxeBinaryURL.Path,\n\t}\n\tif _, err := url.Parse(httpBinaryURL.String()); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid http ipxe binary url: %w\", err)\n\t}\n\n\tvar httpScriptURL *url.URL\n\tif c.dhcp.httpIpxeScriptURL != \"\" {\n\t\thttpScriptURL, err = url.Parse(c.dhcp.httpIpxeScriptURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid http ipxe script url: %w\", err)\n\t\t}\n\t} else {\n\t\thttpScriptURL = &url.URL{\n\t\t\tScheme: c.dhcp.httpIpxeScript.Scheme,\n\t\t\tHost: func() string {\n\t\t\t\tswitch c.dhcp.httpIpxeScript.Scheme {\n\t\t\t\tcase \"http\":\n\t\t\t\t\tif c.dhcp.httpIpxeScript.Port == 80 {\n\t\t\t\t\t\treturn c.dhcp.httpIpxeScript.Host\n\t\t\t\t\t}\n\t\t\t\tcase \"https\":\n\t\t\t\t\tif c.dhcp.httpIpxeScript.Port == 443 {\n\t\t\t\t\t\treturn c.dhcp.httpIpxeScript.Host\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn fmt.Sprintf(\"%s:%d\", c.dhcp.httpIpxeScript.Host, c.dhcp.httpIpxeScript.Port)\n\t\t\t}(),\n\t\t\tPath: c.dhcp.httpIpxeScript.Path,\n\t\t}\n\t}\n\n\tif _, err := url.Parse(httpScriptURL.String()); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid http ipxe script url: %w\", err)\n\t}\n\tipxeScript := func(*dhcpv4.DHCPv4) *url.URL {\n\t\treturn httpScriptURL\n\t}\n\tif c.dhcp.httpIpxeScript.injectMacAddress {\n\t\tipxeScript = func(d *dhcpv4.DHCPv4) *url.URL {\n\t\t\tu := *httpScriptURL\n\t\t\tp := path.Base(u.Path)\n\t\t\tu.Path = path.Join(path.Dir(u.Path), d.ClientHWAddr.String(), p)\n\t\t\treturn &u\n\t\t}\n\t}\n\tbackend, err := c.backend(ctx, log)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create backend: %w\", err)\n\t}\n\n\tswitch dhcpMode(c.dhcp.mode) {\n\tcase dhcpModeReservation:\n\t\tsyslogIP, err := netip.ParseAddr(c.dhcp.syslogIP)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid syslog address: %w\", err)\n\t\t}\n\t\tdh := &reservation.Handler{\n\t\t\tBackend: backend,\n\t\t\tIPAddr:  pktIP,\n\t\t\tLog:     log,\n\t\t\tNetboot: reservation.Netboot{\n\t\t\t\tIPXEBinServerTFTP: tftpIP,\n\t\t\t\tIPXEBinServerHTTP: httpBinaryURL,\n\t\t\t\tIPXEScriptURL:     ipxeScript,\n\t\t\t\tEnabled:           true,\n\t\t\t},\n\t\t\tOTELEnabled: true,\n\t\t\tSyslogAddr:  syslogIP,\n\t\t}\n\t\treturn dh, nil\n\tcase dhcpModeProxy:\n\t\tdh := &proxy.Handler{\n\t\t\tBackend: backend,\n\t\t\tIPAddr:  pktIP,\n\t\t\tLog:     log,\n\t\t\tNetboot: proxy.Netboot{\n\t\t\t\tIPXEBinServerTFTP: tftpIP,\n\t\t\t\tIPXEBinServerHTTP: httpBinaryURL,\n\t\t\t\tIPXEScriptURL:     ipxeScript,\n\t\t\t\tEnabled:           true,\n\t\t\t},\n\t\t\tOTELEnabled:      true,\n\t\t\tAutoProxyEnabled: false,\n\t\t}\n\t\treturn dh, nil\n\tcase dhcpModeAutoProxy:\n\t\tdh := &proxy.Handler{\n\t\t\tBackend: backend,\n\t\t\tIPAddr:  pktIP,\n\t\t\tLog:     log,\n\t\t\tNetboot: proxy.Netboot{\n\t\t\t\tIPXEBinServerTFTP: tftpIP,\n\t\t\t\tIPXEBinServerHTTP: httpBinaryURL,\n\t\t\t\tIPXEScriptURL:     ipxeScript,\n\t\t\t\tEnabled:           true,\n\t\t\t},\n\t\t\tOTELEnabled:      true,\n\t\t\tAutoProxyEnabled: true,\n\t\t}\n\t\treturn dh, nil\n\t}\n\n\treturn nil, errors.New(\"invalid dhcp mode\")\n}\n\n// defaultLogger uses the slog logr implementation.\nfunc defaultLogger(level string) logr.Logger {\n\t// source file and function can be long. This makes the logs less readable.\n\t// truncate source file and function to last 3 parts for improved readability.\n\tcustomAttr := func(_ []string, a slog.Attr) slog.Attr {\n\t\tif a.Key == slog.SourceKey {\n\t\t\tss, ok := a.Value.Any().(*slog.Source)\n\t\t\tif !ok || ss == nil {\n\t\t\t\treturn a\n\t\t\t}\n\t\t\tf := strings.Split(ss.Function, \"/\")\n\t\t\tif len(f) > 3 {\n\t\t\t\tss.Function = filepath.Join(f[len(f)-3:]...)\n\t\t\t}\n\t\t\tp := strings.Split(ss.File, \"/\")\n\t\t\tif len(p) > 3 {\n\t\t\t\tss.File = filepath.Join(p[len(p)-3:]...)\n\t\t\t}\n\n\t\t\treturn a\n\t\t}\n\n\t\treturn a\n\t}\n\topts := &slog.HandlerOptions{AddSource: true, ReplaceAttr: customAttr}\n\tswitch level {\n\tcase \"debug\":\n\t\topts.Level = slog.LevelDebug\n\tdefault:\n\t\topts.Level = slog.LevelInfo\n\t}\n\tlog := slog.New(slog.NewJSONHandler(os.Stdout, opts))\n\n\treturn logr.FromSlogHandler(log.Handler())\n}\n\nfunc parseTrustedProxies(trustedProxies string) (result []string) {\n\tfor _, cidr := range strings.Split(trustedProxies, \",\") {\n\t\tcidr = strings.TrimSpace(cidr)\n\t\tif cidr == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t_, _, err := net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\t// Its not a cidr, but maybe its an IP\n\t\t\tif ip := net.ParseIP(cidr); ip != nil {\n\t\t\t\tif ip.To4() != nil {\n\t\t\t\t\tcidr += \"/32\"\n\t\t\t\t} else {\n\t\t\t\t\tcidr += \"/128\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// not an IP, panic\n\t\t\t\tpanic(\"invalid ip cidr in TRUSTED_PROXIES cidr=\" + cidr)\n\t\t\t}\n\t\t}\n\t\tresult = append(result, cidr)\n\t}\n\n\treturn result\n}\n\nfunc (d dhcpMode) String() string {\n\treturn string(d)\n}\n"
  },
  {
    "path": "contrib/tag-release.sh",
    "content": "#!/usr/bin/env bash\n\nset -o errexit -o nounset -o pipefail\n\nif [ -z \"${1-}\" ]; then\n\techo \"Must specify new tag\"\n\texit 1\nfi\n\nnew_tag=${1-}\n[[ $new_tag =~ ^v[0-9]*\\.[0-9]*\\.[0-9]*$ ]] || (\n\techo \"Tag must be in the form of vX.Y.Z\"\n\texit 1\n)\n\nif [[ $(git symbolic-ref HEAD) != refs/heads/main ]] && [[ -z ${ALLOW_NON_MAIN:-} ]]; then\n\techo \"Must be on main branch\" >&2\n\texit 1\nfi\nif [[ $(git describe --dirty) != $(git describe) ]]; then\n\techo \"Repo must be in a clean state\" >&2\n\texit 1\nfi\n\ngit fetch --all\n\nlast_tag=$(git describe --abbrev=0)\nlast_tag_commit=$(git rev-list -n1 \"$last_tag\")\nlast_specific_tag=$(git tag --contains=\"$last_tag_commit\" | grep -E \"^v[0-9]*\\.[0-9]*\\.[0-9]*$\" | tail -n 1)\nlast_specific_tag_commit=$(git rev-list -n1 \"$last_specific_tag\")\nif [[ $last_specific_tag_commit == $(git rev-list -n1 HEAD) ]]; then\n\techo \"No commits since last tag\" >&2\n\texit 1\nfi\n\nif [[ -n ${SIGN_TAG-} ]]; then\n\tgit tag -s -m \"${new_tag}\" \"${new_tag}\" &>/dev/null && echo \"created signed tag ${new_tag}\" >&2 && exit\nelse\n\tgit tag -a -m \"${new_tag}\" \"${new_tag}\" &>/dev/null && echo \"created annotated tag ${new_tag}\" >&2 && exit\nfi\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "---\n# Provides a docker-compose configuration for local fast iteration when\n# hacking on smee alone.\n# TODO: figure out if NET_ADMIN capability is really necessary\n\nversion: \"3.8\"\n\n# use a custom network configuration to enable macvlan mode and set explicit\n# IPs and MACs as well as support mainstream DHCP clients for easier testing\n# standalone-hardware.json references these IPs and MACs so we can write\n# (simpler) assertions against behavior on the client side.\nnetworks:\n  smee-test:\n    # enables a more realistic L2 network for the containers\n    driver: macvlan\n    ipam:\n      driver: default\n      config:\n        - subnet: 192.168.99.0/24\n          gateway: 192.168.99.1\n\nservices:\n  smee:\n    build: .\n    # entrypoint: [\"/usr/bin/smee\", \"--dhcp-addr\", \"0.0.0.0:67\"]\n    entrypoint: [\"/start-smee.sh\"]\n    networks:\n      smee-test:\n        ipv4_address: 192.168.99.42\n    mac_address: 02:00:00:00:00:01\n    environment:\n      SMEE_TINK_SERVER: tink-server:42113\n      SMEE_BACKEND_KUBE_ENABLED: false\n      SMEE_BACKEND_FILE_ENABLED: true\n      SMEE_BACKEND_FILE_PATH: /hardware.yaml\n      SMEE_OSIE_URL: \"http://192.168.8.5/osie/artifacts/\"\n      OTEL_EXPORTER_OTLP_ENDPOINT: otel-collector:4317\n      OTEL_EXPORTER_OTLP_INSECURE: \"true\"\n    volumes:\n      - ./test/hardware.yaml:/hardware.yaml\n      - ./test/start-smee.sh:/start-smee.sh\n    cap_add:\n      - NET_ADMIN\n  # eventually want to add more client containers, including one that smee will\n  # not recognize so we can validate it won't serve content to IPs it's not\n  # managing\n  client:\n    depends_on:\n      - smee\n    build: test\n    networks:\n      smee-test:\n        ipv4_address: 192.168.99.43\n    mac_address: 02:00:00:00:00:ff\n    cap_add:\n      - NET_ADMIN\n  otel-collector:\n    image: otel/opentelemetry-collector-contrib:0.38.0\n    networks:\n      smee-test:\n        ipv4_address: 192.168.99.44\n    volumes:\n      - ./test/otel-collector.yaml:/etc/otel-collector.yaml\n    command: --config /etc/otel-collector.yaml\n    ports:\n      - \"4317:4317\"\n"
  },
  {
    "path": "docs/Backend-File.md",
    "content": "# File Watcher Backend\n\nThis document gives an overview of the file watcher backend.\nThis backend will read in and watch a file on disk for changes.\nThe data from this file will then be used for serving DHCP requests.\n\n## Why\n\nThis backend exists mainly for testing and development.\nIt allows the DHCP server to be run without having to spin up any additional backend servers, like [Tink](https://github.com/tinkerbell/tink) or [Cacher](https://github.com/packethost/cacher).\n\n## Usage\n\n```bash\n# See the file example/main.go for details on how to select and use this backend in code.\ngo run example/main.go\n```\n\nBelow is an example of the format used for this file watcher backend.\nSee this [example.yaml](../backend/file/testdata/example.yaml) for a full working example of the data model.\n\n```yaml\n---\n08:00:27:29:4E:67:\n  ipAddress: \"192.168.2.153\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.2.1\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"pxe-virtualbox\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"https://boot.netboot.xyz\"\n52:54:00:aa:88:2a:\n  ipAddress: \"192.168.2.15\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.2.1\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"sandbox\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"https://boot.netboot.xyz\"\n```\n"
  },
  {
    "path": "docs/Code-Structure.md",
    "content": "# Code Structure\n\n## Backend\n\nResponsible for communicating with an external persistence source and returning data from said source.\nBackends live in the `backend/` directory.\n\n## Handler\n\nResponsible for reading a DHCP packet from a source, calling a backend, and responding to the source.\nAll business logic for responding or reacting to DHCP messages lives here.\nHandlers live in the `handler/` directory.\n\n## Listener\n\nResponsible for listening for UDP packets on the specified address and port.\nA default listener can be used.\n\n## Server\n\nResponsible for filtering for DHCP packets received by the listener and calling the specified handler.\n\n## Functional description\n\nServer(listener, handler(backend))\n"
  },
  {
    "path": "docs/DCO.md",
    "content": "# DCO Sign Off\n\nAll authors to the project retain copyright to their work. However, to ensure\nthat they are only submitting work that they have rights to, we are requiring\neveryone to acknowledge this by signing their work.\n\nSince this signature indicates your rights to the contribution and\ncertifies the statements below, it must contain your real name and\nemail address. Various forms of noreply email address must not be used.\n\nAny copyright notices in this repository should specify the authors as \"The\nproject authors\".\n\nTo sign your work, just add a line like this at the end of your commit message:\n\n```bash\nSigned-off-by: Jess Owens <jowens@tinkerbell.org>\n```\n\nThis can easily be done with the `--signoff` option to `git commit`.\n\nBy doing this you state that you can certify the following (from [https://developercertificate.org/][1]):\n\n```text\nDeveloper Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n1 Letterman Drive\nSuite D4700\nSan Francisco, CA, 94129\n\nEveryone is permitted to copy and distribute verbatim copies of this\nlicense document, but changing it is not allowed.\n\n\nDeveloper's Certificate of Origin 1.1\n\nBy making a contribution to this project, I certify that:\n\n(a) The contribution was created in whole or in part by me and I\n    have the right to submit it under the open source license\n    indicated in the file; or\n\n(b) The contribution is based upon previous work that, to the best\n    of my knowledge, is covered under an appropriate open source\n    license and I have the right under that license to submit that\n    work with modifications, whether created in whole or in part\n    by me, under the same open source license (unless I am\n    permitted to submit under a different license), as indicated\n    in the file; or\n\n(c) The contribution was provided directly to me by some other\n    person who certified (a), (b) or (c) and I have not modified\n    it.\n\n(d) I understand and agree that this project and the contribution\n    are public and that a record of the contribution (including all\n    personal information I submit with it, including my sign-off) is\n    maintained indefinitely and may be redistributed consistent with\n    this project or the open source license(s) involved.\n```\n"
  },
  {
    "path": "docs/DESIGN.md",
    "content": "# Smee Design Details\n\n## Table of Contents\n\n- [Smee Flow](#Smee-Flow)\n- [Smee Installers](#Smee-Installers)\n- [IPXE](#IPXE)\n\n---\n\n## Smee Flow\n\nHigh-level traffic flow for Smee.\n\n![smee-flow](smee-flow.png)\n\n<details>\n  <summary>Smee Flow Code</summary>\n\nCopy and paste the code below into [https://www.websequencediagrams.com](https://www.websequencediagrams.com) to modify\n\n```flow\ntitle Smee Flow\n# DHCP\nnote over Machine: DHCP start\nMachine->Smee: 1. DHCP Discover\nSmee->Tink: 2. Get Hardware data from MAC\nTink->Smee: 3. Send Hardware data\nSmee->Machine: 4. DHCP Offer\nMachine->Smee: 5. DHCP Request\nSmee->Tink: 6. Get Hardware data from MAC\nTink->Smee: 7. Send Hardware data\nSmee->Machine: 8. DHCP Ack\nnote over Machine: DHCP end\n\n# TFTP\nnote over Machine: TFTP start\nMachine->Smee: 9. TFTP Get ipxe binary\nSmee->Tink: 10. Get Hardware data from IP\nTink->Smee: 11. Send Hardware data\nSmee->Machine: 12. Send ipxe binary\nnote over Machine: TFTP end\n\n# DHCP\nnote over Machine: DHCP start\nMachine->Smee: 13. DHCP Discover\nSmee->Tink: 14. Get Hardware data from MAC\nTink->Smee: 15. Send Hardware data\nSmee->Machine: 16. DHCP Offer\nMachine->Smee: 17. DHCP Request\nSmee->Tink: 18. Get Hardware data from MAC\nTink->Smee: 19. Send Hardware data\nSmee->Machine: 20. DHCP Ack\nnote over Machine: DHCP end\n\n# HTTP\nnote over Machine: HTTP start\nMachine->Smee: 21. HTTP Get ipxe script\nSmee->Tink: 22. Get Hardware data from IP\nTink->Smee: 23. Send Hardware data\nSmee->Machine: 24. Send ipxe script\nnote over Machine: HTTP start\n\n```\n\n</details>\n\n## Smee Installers\n\nA Smee Installer is a custom iPXE script.\nThe code for each Installer lives in `installers/`\nThe idea of iPXE Installers that live in-tree here is an idea that doesn't follow the existing template/workflow paradigm.\nInstallers should eventually be deprecated.\nThe deprecation process is forthcoming.\n\n### How an Installers is requested\n\nDuring a PXE boot request, an iPXE script is provided to a PXE-ing machine through a dynamically generated endpoint (http://smee.addr/auto.ipxe).\nThe contents of the auto.ipxe script is determined through the following steps:\n\n1. A hardware record is retrieved based on the PXE-ing machines mac address.\n2. The following are tried, in order, to determine the content of the iPXE script ([code ref](https://github.com/tinkerbell/smee/blob/b2f4d15f9b55806f4636003948ed95975e1d475e/job/ipxe.go#L71))\n   1. If the `metadata.instance.operating_system.slug` matches a registered Installer, the iPXE script from that Installer is returned\n   2. If the `metadata.instance.operating_system.distro` matches a registered Installer, the iPXE script from that Installer\n   3. If neither of the first 2 is matched, then the default (OSIE) iPXE script is used\n\n### Registering an Installer\n\nTo register an Installer, at a minimum, the following is required\n\n1. A [blank import](https://github.com/golang/go/wiki/CodeReviewComments#import-blank) for your Installer should be added to `main.go`\n2. Your Installer pkg needs an `func init()` that calls `job.RegisterSlug(\"InstallerName\", funcThatReturnsAnIPXEScript)`\n\n### Testing Installers\n\nUnit tests should be created to validate that your registered func returns the iPXE script you're expecting.\nFunctional tests would be great but depending on what is in your iPXE script might be difficult because of external dependencies.\nAt a minimum try to create documentation that details these dependencies so that others can make them available for testing changes.\n\n## IPXE\n\nSmee serves the upstream IPXE binaries built from [https://github.com/ipxe/ipxe](https://github.com/ipxe/ipxe).\nThe IPXE binaries are built from source and then embedded into the Smee Go binary to be served via TFTP.\n\n### Building the IPXE binary\n\nThe IPXE binaries from [https://github.com/ipxe/ipxe](https://github.com/ipxe/ipxe) are built via a Make target.\n\n```make\nmake bindata\n```\n"
  },
  {
    "path": "docs/DESIGNPHILOSOPHY.md",
    "content": "# Design Philosophy\n\nThis living document describes some Go design philosophies we endeavor to incorporate when working, building, or writing in Go.\n\n## General\n\n1. Prefer easy to understand over easy to do\n2. First do it, then do it right, then do it better, then make it testable [14]\n3. When you spawn goroutines, make it clear when - or whether - they exit. [2]\n4. Packages that are imported only for their side effects should be avoided [4]\n5. Package level and global variables should be avoided\n6. magic is bad; global state is magic → no package level vars; no func init [13]\n\n## Dependencies\n\n1. External dependencies should be tried and fail fast or just keep trying\n   - For example, external connections, port binding, environment variables, secrets, etc\n   - Examples of \"failing fast\"\n     - Try external connections immediately\n     - Binding to ports immediately\n   - Examples of \"keep trying\"\n     - Block ingress traffic or calls until external connections are successful\n       - Should be accompanied by some way to check health status of external connections\n2. Make all dependencies explicit [11]\n\n## Naming\n\n1. Naming general rules [12]\n   - Structs are plain nouns: API, Replica, Object\n   - Interfaces are active nouns: Reader, Writer, JobProcessor\n   - Functions and methods are verbs: Read, Process, Sync\n2. Package names [15]\n   - Short: no more than one word\n   - No plural\n   - Lower case\n   - Informative about the service it provides\n   - Avoid packages named utility/utilities or model/models\n3. Avoid renaming imports except to avoid a name collision; good package names should not require renaming [3]\n\n## Interfaces\n\n1. Accept interfaces, return structs [5]\n2. Small interfaces are better [6]\n3. Define an interface when you actually need it, not when you foresee needing it [7]\n4. Interfaces [15]\n   - Use interfaces as function/method arguments & as field types\n   - Small interfaces are better\n\n## Functions/Methods\n\n1. All top-level, exported names should have doc comments, as should non-trivial unexported type or function declarations. [1]\n2. Methods/functions [15]\n   - One function has one goal\n   - Simple names\n   - Reduce the number of nesting levels\n3. Only func main has the right to decide which flags, env variables, config files are available to the user [10a],[10b]\n4. `context.Context` should, in most cases, be the first argument of all functions or methods\n5. Prefer synchronous functions - functions which return their results directly or finish any callbacks or channel ops before returning - over asynchronous ones. [8]\n\n## Errors\n\n1. Error Handling [15]\n   - Func `main` should normally be the only one calling fatal errors or `os.Exit`\n\n## Source files\n\n1. One file should be named like the package [9]\n2. One file = One responsibility [9]\n3. If you only have one command prefer a top level `main.go`, if you have more than one command put them in a `cmd/` package\n\n---\n\n[1]: https://github.com/golang/go/wiki/CodeReviewComments#doc-comments\n[2]: https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes\n[3]: https://github.com/golang/go/wiki/CodeReviewComments#imports\n[4]: https://github.com/golang/go/wiki/CodeReviewComments#import-blank\n[5]: https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8\n[6]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#use-interfaces\n[7]: http://c2.com/xp/YouArentGonnaNeedIt.html\n[8]: https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions\n[9]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#source-files\n[10a]: https://thoughtbot.com/blog/where-to-define-command-line-flags-in-go\n[10b]: https://peter.bourgon.org/go-best-practices-2016/#configuration\n[11]: https://peter.bourgon.org/go-best-practices-2016/#top-tip-9\n[12]: https://twitter.com/peterbourgon/status/1121023995107782656\n[13]: https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html\n[14]: https://code.tutsplus.com/articles/master-developers-addy-osmani--net-31661\n[15]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#key-takeaways\n"
  },
  {
    "path": "docs/DHCP.md",
    "content": "# Use an existing DHCP service\n\nThere can be numerous reasons why you may want to use an existing DHCP service instead of Smee: Security, compliance, access issues, existing layer 2 constraints, existing automation, and so on.\n\nIn environments where there is an existing DHCP service, this DHCP service can be configured to interoperate with Smee. This document will cover how to make your existing DHCP service interoperate with Smee. In this scenario Smee will have no layer 2 DHCP responsibilities.\n\n> Note: Currently, Smee is responsible for more than just DHCP. So generally speaking, Smee can't be entirely avoided in the provisioning process.\n\n## Additional Services in Smee\n\n- HTTP and TFTP servers for iPXE binaries\n- HTTP server for iPXE script\n- Syslog server (receiver)\n\n## Process\n\nAs a prerequisite, your existing DHCP must serve [host/address/static reservations](https://kb.isc.org/docs/what-are-host-reservations-how-to-use-them) for all machines. The IP address you select will need to be used in a corresponding Hardware object.\n\nConfigure your existing DHCP service to provide the location of the iPXE binary and script. This is a two-step interaction between machines and the DHCP service and enables the network boot process to start.\n\n- **Step 1**: The machine broadcasts a request to network boot. Your existing DHCP service then provides the machine with all IPAM info as well as the location of the Tinkerbell iPXE binary (`ipxe.efi`). The machine configures its network interface with the IPAM info then downloads the Tinkerbell iPXE binary from the location provided by the DHCP service and runs it.\n\n- **Step 2**: Now with the Tinkerbell iPXE binary loaded and running, iPXE again broadcasts a request to network boot. The DHCP service again provides all IPAM info as well as the location of the Tinkerbell iPXE script (`auto.ipxe`). iPXE configures its network interface using the IPAM info and then downloads the Tinkerbell iPXE script from the location provided by the DHCP service and runs it.\n\n> Note The `auto.ipxe` is an [iPXE script](https://ipxe.org/scripting) that tells iPXE from where to download the [HookOS](https://github.com/tinkerbell/hook) kernel and initrd so that they can be loaded into memory.\n\nThe following diagram illustrates the process described above. Note that the diagram only describes the network booting parts of the DHCP interaction, not the exchange of IPAM info.\n\n![process](images/BYO_DHCP.png)\n\n## Configuration\n\nBelow you will find code snippets showing how to add the two-step process from above to an existing DHCP service. Each config checks if DHCP option 77 ([user class option](https://www.rfc-editor.org/rfc/rfc3004.html)) equals \"`Tinkerbell`\". If it does match, then the Tinkerbell iPXE script (`auto.ipxe`) will be served. If option 77 does not match, then the iPXE binary (`ipxe.efi`) will be served.\n\n### DHCP option: `next server`\n\nMost DHCP services all customization of a `next server` option. This option generally corresponds to either DHCP option 66 or the DHCP header `sname`, [reference.](https://www.rfc-editor.org/rfc/rfc2132.html#section-9.4) This option is used to tell a machine where to download the initial bootloader, [reference.](https://networkboot.org/fundamentals/)\n\n### Code snippets\n\nThe following code snippets are generic examples of the config needed to enable the two-step process to an existing DHCP service. It does not cover the IPAM info that is also required.\n\n[dnsmasq](https://linux.die.net/man/8/dnsmasq)\n\n`dnsmasq.conf`\n\n```text\ndhcp-match=tinkerbell, option:user-class, Tinkerbell\ndhcp-boot=tag:!tinkerbell,ipxe.efi,none,192.168.2.112\ndhcp-boot=tag:tinkerbell,http://192.168.2.112/auto.ipxe\n```\n\n[Kea DHCP](https://www.isc.org/kea/)\n\n`kea.json`\n\n```json\n{\n  \"Dhcp4\": {\n    \"client-classes\": [\n      {\n        \"name\": \"tinkerbell\",\n        \"test\": \"substring(option[77].hex,0,10) == 'Tinkerbell'\",\n        \"boot-file-name\": \"http://192.168.2.112/auto.ipxe\"\n      },\n      {\n        \"name\": \"default\",\n        \"test\": \"not(substring(option[77].hex,0,10) == 'Tinkerbell')\",\n        \"boot-file-name\": \"ipxe.efi\"\n      }\n    ],\n    \"subnet4\": [\n      {\n        \"next-server\": \"192.168.2.112\"\n      }\n    ]\n  }\n}\n```\n\n[ISC DHCP](https://ipxe.org/howto/dhcpd)\n\n`dhcpd.conf`\n\n```text\n if exists user-class and option user-class = \"Tinkerbell\" {\n     filename \"http://192.168.2.112/auto.ipxe\";\n } else {\n     filename \"ipxe.efi\";\n }\n next-server \"192.168.1.112\";\n```\n\n[Microsoft DHCP server](https://learn.microsoft.com/en-us/windows-server/networking/technologies/dhcp/dhcp-top)\n\nPlease follow the ipxe.org [guide](https://ipxe.org/howto/msdhcp) on how to configure Microsoft DHCP server.\n"
  },
  {
    "path": "docs/Design-Philosophy.md",
    "content": "# Design Philosophy\n\nThis living document describes some Go design philosophies we endeavor to incorporate when working, building, or writing in Go.\n\n## General\n\n1. Prefer easy to understand over easy to do\n2. First do it, then do it right, then do it better, then make it testable [14]\n3. When you spawn goroutines, make it clear when - or whether - they exit. [2]\n4. Packages that are imported only for their side effects should be avoided [4]\n5. Package level and global variables should be avoided\n6. magic is bad; global state is magic → no package level vars; no func init [13]\n\n## Dependencies\n\n1. External dependencies should be tried and fail fast or just keep trying\n   - For example, external connections, port binding, environment variables, secrets, etc\n   - Examples of \"failing fast\"\n     - Try external connections immediately\n     - Binding to ports immediately\n   - Examples of \"keep trying\"\n     - Block ingress traffic or calls until external connections are successful\n       - Should be accompanied by some way to check health status of external connections\n2. Make all dependencies explicit [11]\n\n## Naming\n\n1. Naming general rules [12]\n   - Structs are plain nouns: API, Replica, Object\n   - Interfaces are active nouns: Reader, Writer, JobProcessor\n   - Functions and methods are verbs: Read, Process, Sync\n2. Package names [15]\n   - Short: no more than one word\n   - No plural\n   - Lower case\n   - Informative about the service it provides\n   - Avoid packages named utility/utilities or model/models\n3. Avoid renaming imports except to avoid a name collision; good package names should not require renaming [3]\n\n## Interfaces\n\n1. Accept interfaces, return structs [5]\n2. Small interfaces are better [6]\n3. Define an interface when you actually need it, not when you foresee needing it [7]\n4. Interfaces [15]\n   - Use interfaces as function/method arguments & as field types\n   - Small interfaces are better\n\n## Functions/Methods\n\n1. All top-level, exported names should have doc comments, as should non-trivial unexported type or function declarations. [1]\n2. Methods/functions [15]\n   - One function has one goal\n   - Simple names\n   - Reduce the number of nesting levels\n3. Only func main has the right to decide which flags, env variables, config files are available to the user [10a],[10b]\n4. `context.Context` should, in most cases, be the first argument of all functions or methods\n5. Prefer synchronous functions - functions which return their results directly or finish any callbacks or channel ops before returning - over asynchronous ones. [8]\n\n## Errors\n\n1. Error Handling [15]\n   - Func `main` should normally be the only one calling fatal errors or `os.Exit`\n\n## Source files\n\n1. One file should be named like the package [9]\n2. One file = One responsibility [9]\n3. If you only have one command prefer a top level `main.go`, if you have more than one command put them in a `cmd/` package\n\n---\n\n[1]: https://github.com/golang/go/wiki/CodeReviewComments#doc-comments\n[2]: https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes\n[3]: https://github.com/golang/go/wiki/CodeReviewComments#imports\n[4]: https://github.com/golang/go/wiki/CodeReviewComments#import-blank\n[5]: https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8\n[6]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#use-interfaces\n[7]: http://c2.com/xp/YouArentGonnaNeedIt.html\n[8]: https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions\n[9]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#source-files\n[10a]: https://thoughtbot.com/blog/where-to-define-command-line-flags-in-go\n[10b]: https://peter.bourgon.org/go-best-practices-2016/#configuration\n[11]: https://peter.bourgon.org/go-best-practices-2016/#top-tip-9\n[12]: https://twitter.com/peterbourgon/status/1121023995107782656\n[13]: https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html\n[14]: https://code.tutsplus.com/articles/master-developers-addy-osmani--net-31661\n[15]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#key-takeaways\n"
  },
  {
    "path": "docs/ISO-Static-IPAM.md",
    "content": "# Static IP Address Management in the OSIE ISO\n\nOSIE stands for operating system installation environment. In Tinkerbell we currently have just one, [HookOS](https://github.com/tinkerbell/hook).\nSmee has the capability to Patch the HookOS ISO at runtime to include information about the target machine's network configuration. This is enabled by setting the CLI flag `-iso-static-ipam-enabled=true` along with both `-iso-enabled` and `-iso-url`.\nThis document defines the specification/data format for passing this info to the HookOS ISO.\n\n## Specification/Data format\n\nThis is the spec/ data format for passing the static IP address management information to the HookOS ISO.\n\n```ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>```\n\nExample:\n\n```ipam=de-ad-be-ef-fe-ed:30:192.168.2.193:255.255.255.0:192.168.2.1:server.example.com:1.1.1.1,8.8.8.8:example.com,team.example.com:132.163.97.1,132.163.96.1```\n\n### Fields\n\nSome fields are required so that basic network communication can function properly.\n\n| Field | Description | Required | Example |\n|-------|-------------|----------|---------|\n| mac-address | MAC address. Must be in dash notation. | Yes |`00-00-00-00-00-00` |\n| vlan-id | VLAN ID. Must be a string integer between 0 and 4096 or an empty string for no VLAN tagging. | No | `30` |\n| ip-address | IPv4 address. | Yes | `10.148.56.3` |\n| netmask | Netmask. | Yes | `255.255.240.0` |\n| gateway | IPv4 Gateway. | No | `10.148.56.1` |\n| hostname | Hostname for the system. Can be fully qualified or not. | No | `hookos` or `hookos.example.com` |\n| dns | Comma separated list of IPv4 DNS nameservers. Must be IPv4 addresses, not hostnames. | Yes | `1.1.1.1,8.8.8.8` |\n| search-domains | Comma separated list of search domains. | No | `example.com,example.org` |\n| ntp | Comma separated list of IPv4 NTP servers. Must be IPv4 addresses, not hostnames. | No | `132.163.97.1,132.163.96.1` |\n\n## Implementation details\n\nSmee will set the kernel commandline parameter `ipam=` with the above format. In HookOS, there is a service that reads this cmdline parameter and writes the file(s) and runs the command(s) necessary to configure HookOS the use of all the values. See HookOS for more details on the service and how it works.\n"
  },
  {
    "path": "docs/images/BYO_DHCP.uml",
    "content": "title Bring your own DHCP service\n\nparticipant Machine\nparticipant DHCP\nparticipant Smee\n\nrbox over Machine,DHCP: 192.168.5.5 represents the IP from which the Smee service is available\n\ngroup #2f2e7b In firmware iPXE #white\nautonumber 1\nMachine->DHCP: DHCP discover\n\nDHCP->Machine: DHCP OFFER\\nnext server: 192.168.2.5.5\\nboot file: ipxe.efi\n\nMachine->DHCP: DHCP REQUEST\n\nDHCP->Machine: DHCP ACK\\nnext server: 192.168.5.5\\nboot file: ipxe.efi\n\nMachine->Smee: Download and boot **ipxe.efi** (TFTP or HTTP)\nend\n\ngroup #2f2e7b In Tinkerbell iPXE #white\nMachine->DHCP: DHCP DISCOVER\n \nDHCP->Machine: DHCP OFFER\\nnext server: 192.168.5.5\\nboot file: http://192.168.5.5/auto.ipxe\n\nMachine->DHCP: DHCP REQUEST\n\nDHCP->Machine: DHCP ACK\\nnext server: 192.168.5.5\\nboot file: http://192.168.5.5/auto.ipxe\n\nMachine->Smee: Download and execute **auto.ipxe** iPXE script (HTTP)\n\ndestroysilent Machine\ndestroysilent DHCP\ndestroysilent Smee\nend\n"
  },
  {
    "path": "docs/manifests/README.md",
    "content": "# Deploying Smee\n\nThis directory contains the manifests for deploying Smee to various environments. This document will describe how to use the different Smee deployment options.\n\n## Variables\n\nRegardless of the option you choose it is recommended you get started by updating the following environment variables in the [`manifests/kustomize/base/deployment.yaml`](./kustomize/base/deployment.yaml) file to match your setup.\n\n| Variable                    | Description                                                                                         |\n| --------------------------- | --------------------------------------------------------------------------------------------------- |\n| `TINKERBELL_GRPC_AUTHORITY` | This is the IP:Port that a Tink worker will use for communicated with the Tink server               |\n| `MIRROR_BASE_URL`           | The URL from where the \"OSIE\" or Hook kernel(s) and initrd(s) will be downloaded by netboot clients |\n| `PUBLIC_IP`                 | This is the IP that netboot clients and/or DHCP relay's will use to reach Smee                      |\n| `PUBLIC_SYSLOG_FQDN`        | This is the IP that syslog clients will use to send messages                                        |\n\n## Deployment Options\n\n- [Kind](kind.md)\n- [Kubernetes](kubernetes.md)\n- [K3D](k3d.md)\n- [Tilt](tilt.md)\n"
  },
  {
    "path": "docs/manifests/k3d.md",
    "content": "# K3D (K3S in Docker)\n\nThis describes deploying Smee into a K3S in Docker (K3D) cluster.\n\n## Prerequisites\n\n- [K3D >= v5.4.1](https://k3d.io/v5.4.1/#installation)\n- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)\n- Supported platforms: Linux\n\n### Steps\n\n1. Create K3D cluster\n\n   ```bash\n   # Create the K3D cluster\n   k3d cluster create --network host --no-lb --k3s-arg \"--disable=traefik\"\n   ```\n\n2. Deploy Smee\n\n   Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.\n\n   ```bash\n   # Deploy Smee to K3D\n   kubectl kustomize manifests/kustomize/overlays/k3d | kubectl apply -f -\n   ```\n\n3. Watch the logs\n\n   ```bash\n   kubectl -n tinkerbell logs -f -l app=tinkerbell-smee\n   ```\n"
  },
  {
    "path": "docs/manifests/kind.md",
    "content": "# KinD (Kubernetes in Docker)\n\nThis describes deploying Smee into a Kubernetes in Docker (KinD) cluster.\n\n## Prerequisites\n\n- [KinD >= v0.12.0](https://kind.sigs.k8s.io/docs/user/quick-start#installation)\n- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)\n\n## Steps\n\n1. Create KinD cluster\n\n   ```bash\n   # Create the KinD cluster\n   kind create cluster --config ./manifests/kind/config.yaml\n   ```\n\n2. Deploy Smee\n\n   Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.\n\n   ```bash\n   # Deploy Smee to KinD\n   kubectl kustomize manifests/kustomize/overlays/kind | kubectl apply -f -\n   ```\n\n3. Watch the logs\n\n   ```bash\n   kubectl -n tinkerbell logs -f -l app=tinkerbell-smee\n   ```\n\n> **Note:** KinD will not be able to listen for DHCP broadcast traffic. Using a DHCP relay is recommended.\n>\n> ```bash\n> # Linux direct\n> ipaddr=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane)\n> sudo -E dhcrelay -id <interface to listen on for DHCP broadcast>  -iu $(ip -o route get ${ipaddr} | cut -d\" \" -f3) -d ${ipaddr}\n>\n> # Linux Container\n> ipaddr=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane)\n> docker run -d --network host --name dhcrelay modem7/dhcprelay:latest -id <interface to listen on for DHCP broadcast>  -iu $(ip -o route get ${ipaddr} | cut -d\" \" -f3) -d ${ipaddr}\n>\n> # MacOS TBD\n> ```\n"
  },
  {
    "path": "docs/manifests/kubernetes.md",
    "content": "# Kubernetes\n\nThis deployment requires a running Kubernetes cluster. It can be a single node cluster. It is required to be running directly on a Linux machine, not in a container. This deployment is under development and is not guaranteed to work at this time.\n\n## Prerequisites\n\nTBD\n\n## Steps\n\n1. Deploy Smee\n\n   Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.\n\n   ```bash\n   # Deploy Smee to Kubernetes\n   kubectl kustomize manifests/kustomize/overlays/dev | kubectl apply -f -\n   ```\n\n2. Watch the logs\n\n   ```bash\n   kubectl -n tinkerbell logs -f -l app=tinkerbell-smee\n   ```\n"
  },
  {
    "path": "docs/manifests/tilt.md",
    "content": "# Tilt\n\nThis deployment method is for quick local development. Tilt will build and deploy Smee to the Kubernetes cluster pointed to in the current context of your Kubernetes config file. It will use the KinD manifest, documented [here](KIND.md), for deployment.\n\n## Prerequisites\n\n- [Tilt >= v0.28.1](https://docs.tilt.dev/install.html)\n- Go >= 1.18\n- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)\n- KinD cluster\n\n## Steps\n\n1. Deploy Smee\n\n   Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the `manifests/kustomize/base/deployment.yaml` file.\n   This deployment method uses the kustomize kind overlay (`manifests/kustomize/overlays/kind`). See the `Tiltfile` modify this.\n\n   ```bash\n   # Deploy Smee with Tilt\n   tilt up --stream\n   ```\n\n2. Watch the logs\n\n   ```bash\n   kubectl -n tinkerbell logs -f -l app=tinkerbell-smee\n   ```\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/tinkerbell/smee\n\ngo 1.24.0\n\ntoolchain go1.24.1\n\nrequire (\n\tgithub.com/ccoveille/go-safecast v1.6.1\n\tgithub.com/diskfs/go-diskfs v1.6.0\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/ghodss/yaml v1.0.0\n\tgithub.com/go-logr/logr v1.4.3\n\tgithub.com/go-logr/stdr v1.2.2\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f\n\tgithub.com/peterbourgon/ff/v3 v3.4.0\n\tgithub.com/prometheus/client_golang v1.22.0\n\tgithub.com/stretchr/testify v1.10.0\n\tgithub.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be\n\tgithub.com/tinkerbell/tink v0.12.2\n\tgithub.com/vishvananda/netlink v1.3.1\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0\n\tgo.opentelemetry.io/otel v1.37.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0\n\tgo.opentelemetry.io/otel/sdk v1.37.0\n\tgo.opentelemetry.io/otel/trace v1.37.0\n\tgolang.org/x/net v0.41.0\n\tgolang.org/x/sync v0.15.0\n\tgolang.org/x/sys v0.33.0\n\tgoogle.golang.org/grpc v1.73.0\n\tk8s.io/apimachinery v0.33.2\n\tk8s.io/client-go v0.33.2\n\tsigs.k8s.io/controller-runtime v0.21.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.1 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.2 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/djherbis/times v1.6.0 // indirect\n\tgithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.12.1 // indirect\n\tgithub.com/evanphx/json-patch/v5 v5.9.11 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.6 // indirect\n\tgithub.com/go-logr/zerologr v1.2.3 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.0 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.22.1 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/gnostic-models v0.6.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/josharian/native v1.1.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mdlayher/packet v1.1.2 // indirect\n\tgithub.com/mdlayher/socket v0.4.1 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.19 // indirect\n\tgithub.com/pin/tftp/v3 v3.1.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/xattr v0.4.9 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.1 // indirect\n\tgithub.com/prometheus/common v0.62.0 // indirect\n\tgithub.com/prometheus/procfs v0.15.1 // indirect\n\tgithub.com/rs/zerolog v1.33.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect\n\tgithub.com/ulikunitz/xz v0.5.11 // indirect\n\tgithub.com/vishvananda/netns v0.0.5 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.37.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.7.0 // indirect\n\tgolang.org/x/crypto v0.39.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/term v0.32.0 // indirect\n\tgolang.org/x/text v0.26.0 // indirect\n\tgolang.org/x/time v0.9.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect\n\tgoogle.golang.org/protobuf v1.36.6 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/api v0.33.2 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect\n\tk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect\n\tsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect\n\tsigs.k8s.io/yaml v1.4.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=\ngithub.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=\ngithub.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=\ngithub.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=\ngithub.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=\ngithub.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/diskfs/go-diskfs v1.6.0 h1:YmK5+vLSfkwC6kKKRTRPGaDGNF+Xh8FXeiNHwryDfu4=\ngithub.com/diskfs/go-diskfs v1.6.0/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=\ngithub.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=\ngithub.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=\ngithub.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=\ngithub.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=\ngithub.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=\ngithub.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=\ngithub.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=\ngithub.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=\ngithub.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=\ngithub.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=\ngithub.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=\ngithub.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=\ngithub.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=\ngithub.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=\ngithub.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=\ngithub.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=\ngithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=\ngithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=\ngithub.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=\ngithub.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=\ngithub.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=\ngithub.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=\ngithub.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=\ngithub.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=\ngithub.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=\ngithub.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=\ngithub.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=\ngithub.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=\ngithub.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=\ngithub.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=\ngithub.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=\ngithub.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=\ngithub.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=\ngithub.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=\ngithub.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=\ngithub.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=\ngithub.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=\ngithub.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=\ngithub.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be h1:PRUY/EEvGGjwohNGn1ncj5y8BlU5p42C/GUwYzmJI/4=\ngithub.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be/go.mod h1:gO18k34se3edSoBttsayVjT9lPA7xTZ+yiXMU1oQAC8=\ngithub.com/tinkerbell/tink v0.12.2 h1:ROe5SAx5X8hHEROm9OJzc6XLhEzOhUcdGpY2bLVAOnk=\ngithub.com/tinkerbell/tink v0.12.2/go.mod h1:Cpv7pSazMhq6HYVAByHJu2tkLIsR9K/mBY1S87RQbC4=\ngithub.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=\ngithub.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=\ngithub.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=\ngithub.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=\ngithub.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=\ngo.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=\ngo.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=\ngo.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=\ngo.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=\ngo.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=\ngo.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=\ngo.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=\ngo.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=\ngo.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=\ngo.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=\ngo.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=\ngo.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=\ngolang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=\ngolang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=\ngolang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=\ngolang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=\ngolang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=\ngolang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=\ngolang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=\ngolang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=\ngomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=\ngoogle.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=\ngoogle.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=\ngoogle.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=\ngoogle.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=\ngopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nk8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=\nk8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=\nk8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=\nk8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=\nk8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=\nk8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=\nk8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=\nk8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=\nk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=\nk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=\nk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nsigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=\nsigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=\nsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=\nsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=\nsigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=\nsigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\n"
  },
  {
    "path": "internal/backend/file/file.go",
    "content": "// Package file watches a file for changes and updates the in memory DHCP data.\npackage file\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/ccoveille/go-safecast\"\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/ghodss/yaml\"\n\t\"github.com/go-logr/logr\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/codes\"\n)\n\nconst tracerName = \"github.com/tinkerbell/smee/dhcp\"\n\n// Errors used by the file watcher.\nvar (\n\t// errFileFormat is returned when the file is not in the correct format, e.g. not valid YAML.\n\terrFileFormat     = fmt.Errorf(\"invalid file format\")\n\terrRecordNotFound = fmt.Errorf(\"record not found\")\n\terrParseIP        = fmt.Errorf(\"failed to parse IP from File\")\n\terrParseSubnet    = fmt.Errorf(\"failed to parse subnet mask from File\")\n\terrParseURL       = fmt.Errorf(\"failed to parse URL\")\n)\n\n// netboot is the structure for the data expected in a file.\ntype netboot struct {\n\tAllowPXE      bool   `yaml:\"allowPxe\"`      // If true, the client will be provided netboot options in the DHCP offer/ack.\n\tIPXEScriptURL string `yaml:\"ipxeScriptUrl\"` // Overrides default value of that is passed into DHCP on startup.\n\tIPXEScript    string `yaml:\"ipxeScript\"`    // Overrides a default value that is passed into DHCP on startup.\n\tConsole       string `yaml:\"console\"`\n\tFacility      string `yaml:\"facility\"`\n}\n\n// dhcp is the structure for the data expected in a file.\ntype dhcp struct {\n\tMACAddress       net.HardwareAddr // The MAC address of the client.\n\tIPAddress        string           `yaml:\"ipAddress\"`        // yiaddr DHCP header.\n\tSubnetMask       string           `yaml:\"subnetMask\"`       // DHCP option 1.\n\tDefaultGateway   string           `yaml:\"defaultGateway\"`   // DHCP option 3.\n\tNameServers      []string         `yaml:\"nameServers\"`      // DHCP option 6.\n\tHostname         string           `yaml:\"hostname\"`         // DHCP option 12.\n\tDomainName       string           `yaml:\"domainName\"`       // DHCP option 15.\n\tBroadcastAddress string           `yaml:\"broadcastAddress\"` // DHCP option 28.\n\tNTPServers       []string         `yaml:\"ntpServers\"`       // DHCP option 42.\n\tVLANID           string           `yaml:\"vlanID\"`           // DHCP option 43.116.\n\tLeaseTime        int              `yaml:\"leaseTime\"`        // DHCP option 51.\n\tArch             string           `yaml:\"arch\"`             // DHCP option 93.\n\tDomainSearch     []string         `yaml:\"domainSearch\"`     // DHCP option 119.\n\tDisabled         bool             // If true, no DHCP response should be sent.\n\tNetboot          netboot          `yaml:\"netboot\"`\n}\n\n// Watcher represents the backend for watching a file for changes and updating the in memory DHCP data.\ntype Watcher struct {\n\tfileMu sync.RWMutex // protects FilePath for reads\n\n\t// FilePath is the path to the file to watch.\n\tFilePath string\n\n\t// Log is the logger to be used in the File backend.\n\tLog     logr.Logger\n\tdataMu  sync.RWMutex // protects data\n\tdata    []byte       // data from file\n\twatcher *fsnotify.Watcher\n}\n\n// NewWatcher creates a new file watcher.\nfunc NewWatcher(l logr.Logger, f string) (*Watcher, error) {\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := watcher.Add(f); err != nil {\n\t\treturn nil, err\n\t}\n\n\tw := &Watcher{\n\t\tFilePath: f,\n\t\twatcher:  watcher,\n\t\tLog:      l,\n\t}\n\n\tw.fileMu.RLock()\n\tw.data, err = os.ReadFile(filepath.Clean(f))\n\tw.fileMu.RUnlock()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn w, nil\n}\n\n// GetByMac is the implementation of the Backend interface.\n// It reads a given file from the in memory data (w.data).\nfunc (w *Watcher) GetByMac(ctx context.Context, mac net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\ttracer := otel.Tracer(tracerName)\n\t_, span := tracer.Start(ctx, \"backend.file.GetByMac\")\n\tdefer span.End()\n\n\t// get data from file, translate it, then pass it into setDHCPOpts and setNetworkBootOpts\n\tw.dataMu.RLock()\n\td := w.data\n\tw.dataMu.RUnlock()\n\tr := make(map[string]dhcp)\n\tif err := yaml.Unmarshal(d, &r); err != nil {\n\t\terr := fmt.Errorf(\"%w: %w\", err, errFileFormat)\n\t\tw.Log.Error(err, \"failed to unmarshal file data\")\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\tfor k, v := range r {\n\t\tif strings.EqualFold(k, mac.String()) {\n\t\t\t// found a record for this mac address\n\t\t\tv.MACAddress = mac\n\t\t\td, n, err := w.translate(v)\n\t\t\tif err != nil {\n\t\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tspan.SetAttributes(d.EncodeToAttributes()...)\n\t\t\tspan.SetAttributes(n.EncodeToAttributes()...)\n\t\t\tspan.SetStatus(codes.Ok, \"\")\n\n\t\t\treturn d, n, nil\n\t\t}\n\t}\n\n\terr := fmt.Errorf(\"%w: %s\", errRecordNotFound, mac.String())\n\tspan.SetStatus(codes.Error, err.Error())\n\n\treturn nil, nil, err\n}\n\n// GetByIP is the implementation of the Backend interface.\n// It reads a given file from the in memory data (w.data).\nfunc (w *Watcher) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP, *data.Netboot, error) {\n\ttracer := otel.Tracer(tracerName)\n\t_, span := tracer.Start(ctx, \"backend.file.GetByIP\")\n\tdefer span.End()\n\n\t// get data from file, translate it, then pass it into setDHCPOpts and setNetworkBootOpts\n\tw.dataMu.RLock()\n\td := w.data\n\tw.dataMu.RUnlock()\n\tr := make(map[string]dhcp)\n\tif err := yaml.Unmarshal(d, &r); err != nil {\n\t\terr := fmt.Errorf(\"%w: %w\", err, errFileFormat)\n\t\tw.Log.Error(err, \"failed to unmarshal file data\")\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\tfor k, v := range r {\n\t\tif v.IPAddress == ip.String() {\n\t\t\t// found a record for this ip address\n\t\t\tv.IPAddress = ip.String()\n\t\t\tmac, err := net.ParseMAC(k)\n\t\t\tif err != nil {\n\t\t\t\terr := fmt.Errorf(\"%w: %w\", err, errFileFormat)\n\t\t\t\tw.Log.Error(err, \"failed to parse mac address\")\n\t\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tv.MACAddress = mac\n\t\t\td, n, err := w.translate(v)\n\t\t\tif err != nil {\n\t\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tspan.SetAttributes(d.EncodeToAttributes()...)\n\t\t\tspan.SetAttributes(n.EncodeToAttributes()...)\n\t\t\tspan.SetStatus(codes.Ok, \"\")\n\n\t\t\treturn d, n, nil\n\t\t}\n\t}\n\n\terr := fmt.Errorf(\"%w: %s\", errRecordNotFound, ip.String())\n\tspan.SetStatus(codes.Error, err.Error())\n\n\treturn nil, nil, err\n}\n\n// Start starts watching a file for changes and updates the in memory data (w.data) on changes.\n// Start is a blocking method. Use a context cancellation to exit.\nfunc (w *Watcher) Start(ctx context.Context) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tw.Log.Info(\"stopping watcher\")\n\t\t\treturn\n\t\tcase event, ok := <-w.watcher.Events:\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif event.Op&fsnotify.Write == fsnotify.Write {\n\t\t\t\tw.Log.Info(\"file changed, updating cache\")\n\t\t\t\tw.fileMu.RLock()\n\t\t\t\td, err := os.ReadFile(w.FilePath)\n\t\t\t\tw.fileMu.RUnlock()\n\t\t\t\tif err != nil {\n\t\t\t\t\tw.Log.Error(err, \"failed to read file\", \"file\", w.FilePath)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tw.dataMu.Lock()\n\t\t\t\tw.data = d\n\t\t\t\tw.dataMu.Unlock()\n\t\t\t}\n\t\tcase err, ok := <-w.watcher.Errors:\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tw.Log.Info(\"error watching file\", \"err\", err)\n\t\t}\n\t}\n}\n\n// translate converts the data from the file into a data.DHCP and data.Netboot structs.\nfunc (w *Watcher) translate(r dhcp) (*data.DHCP, *data.Netboot, error) {\n\td := new(data.DHCP)\n\tn := new(data.Netboot)\n\n\td.MACAddress = r.MACAddress\n\t// ip address, required\n\tip, err := netip.ParseAddr(r.IPAddress)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"%w: %w\", err, errParseIP)\n\t}\n\td.IPAddress = ip\n\n\t// subnet mask, required\n\tsm := net.ParseIP(r.SubnetMask)\n\tif sm == nil {\n\t\treturn nil, nil, errParseSubnet\n\t}\n\td.SubnetMask = net.IPMask(sm.To4())\n\n\t// default gateway, optional\n\tif dg, err := netip.ParseAddr(r.DefaultGateway); err != nil {\n\t\tw.Log.Info(\"failed to parse default gateway\", \"defaultGateway\", r.DefaultGateway, \"err\", err)\n\t} else {\n\t\td.DefaultGateway = dg\n\t}\n\n\t// name servers, optional\n\tfor _, s := range r.NameServers {\n\t\tip := net.ParseIP(s)\n\t\tif ip == nil {\n\t\t\tw.Log.Info(\"failed to parse name server\", \"nameServer\", s)\n\t\t\tbreak\n\t\t}\n\t\td.NameServers = append(d.NameServers, ip)\n\t}\n\n\t// hostname, optional\n\td.Hostname = r.Hostname\n\n\t// domain name, optional\n\td.DomainName = r.DomainName\n\n\t// broadcast address, optional\n\tif ba, err := netip.ParseAddr(r.BroadcastAddress); err != nil {\n\t\tw.Log.Info(\"failed to parse broadcast address\", \"broadcastAddress\", r.BroadcastAddress, \"err\", err)\n\t} else {\n\t\td.BroadcastAddress = ba\n\t}\n\n\t// ntp servers, optional\n\tfor _, s := range r.NTPServers {\n\t\tip := net.ParseIP(s)\n\t\tif ip == nil {\n\t\t\tw.Log.Info(\"failed to parse ntp server\", \"ntpServer\", s)\n\t\t\tbreak\n\t\t}\n\t\td.NTPServers = append(d.NTPServers, ip)\n\t}\n\n\t// vlanid\n\td.VLANID = r.VLANID\n\n\t// lease time\n\t// Default to one week\n\td.LeaseTime = 604800\n\tif v, err := safecast.ToUint32(r.LeaseTime); err == nil {\n\t\td.LeaseTime = v\n\t}\n\n\t// arch\n\td.Arch = r.Arch\n\n\t// domain search\n\td.DomainSearch = r.DomainSearch\n\n\t// disabled\n\td.Disabled = r.Disabled\n\n\t// allow machine to netboot\n\tn.AllowNetboot = r.Netboot.AllowPXE\n\n\t// ipxe script url is optional but if provided, it must be a valid url\n\tif r.Netboot.IPXEScriptURL != \"\" {\n\t\tu, err := url.Parse(r.Netboot.IPXEScriptURL)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"%w: %w\", err, errParseURL)\n\t\t}\n\t\tn.IPXEScriptURL = u\n\t}\n\n\t// ipxe script\n\tif r.Netboot.IPXEScript != \"\" {\n\t\tn.IPXEScript = r.Netboot.IPXEScript\n\t}\n\n\t// console\n\tif r.Netboot.Console != \"\" {\n\t\tn.Console = r.Netboot.Console\n\t}\n\n\t// facility\n\tif r.Netboot.Facility != \"\" {\n\t\tn.Facility = r.Netboot.Facility\n\t}\n\n\treturn d, n, nil\n}\n"
  },
  {
    "path": "internal/backend/file/file_test.go",
    "content": "package file\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/go-logr/logr\"\n\t\"github.com/go-logr/stdr\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nfunc TestNewWatcher(t *testing.T) {\n\ttests := map[string]struct {\n\t\tcreateFile bool\n\t\twant       string\n\t\twantErr    error\n\t}{\n\t\t\"contents equal\": {createFile: true, want: \"test content here\"},\n\t\t\"file not found\": {createFile: false, wantErr: &fs.PathError{}},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tvar name string\n\t\t\tif tt.createFile {\n\t\t\t\tvar err error\n\t\t\t\tname, err = createFile([]byte(tt.want))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tdefer os.Remove(name)\n\t\t\t}\n\t\t\tw, err := NewWatcher(logr.Discard(), name)\n\t\t\tif (err != nil) != (tt.wantErr != nil) {\n\t\t\t\tt.Fatalf(\"NewWatcher() error = %v; type = %[1]T, wantErr %v; type = %[2]T\", err, tt.wantErr)\n\t\t\t}\n\t\t\tvar got string\n\t\t\tif tt.wantErr != nil {\n\t\t\t\tgot = \"\"\n\t\t\t} else {\n\t\t\t\tgot = string(w.data)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc createFile(content []byte) (string, error) {\n\tfile, err := os.CreateTemp(\"\", \"prefix\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer file.Close()\n\tif _, err := file.Write(content); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn file.Name(), nil\n}\n\ntype testData struct {\n\tinitial     string\n\tafter       string\n\taction      string\n\texpectedOut string\n}\n\nfunc TestStartAndStop(t *testing.T) {\n\ttt := &testData{action: \"cancel\", expectedOut: `\"level\"=0 \"msg\"=\"stopping watcher\"` + \"\\n\"}\n\tout := &bytes.Buffer{}\n\tl := stdr.New(log.New(out, \"\", 0))\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw := &Watcher{Log: l, watcher: watcher}\n\tw.Start(ctx)\n\tif diff := cmp.Diff(out.String(), tt.expectedOut); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestStartFileUpdateError(t *testing.T) {\n\ttt := &testData{expectedOut: `\"level\"=0 \"msg\"=\"file changed, updating cache\"` + \"\\n\" + `\"msg\"=\"failed to read file\" \"error\"=\"open not-found.txt: no such file or directory\" \"file\"=\"not-found.txt\"` + \"\\n\" + `\"level\"=0 \"msg\"=\"stopping watcher\"` + \"\\n\"}\n\tout := &bytes.Buffer{}\n\tl := stdr.New(log.New(out, \"\", 0))\n\tgot, name := tt.helper(t, l)\n\tdefer os.Remove(name)\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-time.After(time.Millisecond)\n\t\tgot.FilePath = \"not-found.txt\"\n\t\tgot.watcher.Events <- fsnotify.Event{Op: fsnotify.Write}\n\t\tcancel()\n\t}()\n\tgot.Start(ctx)\n\ttime.Sleep(time.Second)\n\tif diff := cmp.Diff(out.String(), tt.expectedOut); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestStartFileUpdate(t *testing.T) {\n\ttt := &testData{initial: \"once upon a time\", after: \"\\nhello world\", expectedOut: \"once upon a time\\nhello world\"}\n\tgot, name := tt.helper(t, logr.Discard())\n\tdefer os.Remove(name)\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-time.After(time.Millisecond)\n\t\tgot.fileMu.Lock()\n\t\tf, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0o644)\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t}\n\t\tf.Write([]byte(tt.after))\n\t\tf.Close()\n\t\tgot.fileMu.Unlock()\n\t\ttime.Sleep(time.Millisecond)\n\t\tcancel()\n\t}()\n\tgot.Start(ctx)\n\tgot.dataMu.RLock()\n\td := got.data\n\tgot.dataMu.RUnlock()\n\tif diff := cmp.Diff(string(d), tt.expectedOut); diff != \"\" {\n\t\tt.Log(string(d))\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestStartFileUpdateClosedChan(t *testing.T) {\n\tout := &bytes.Buffer{}\n\tl := stdr.New(log.New(out, \"\", 0))\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw := &Watcher{Log: l, watcher: watcher}\n\tgo w.Start(ctx)\n\tclose(w.watcher.Events)\n\ttime.Sleep(time.Millisecond)\n\tif diff := cmp.Diff(out.String(), \"\"); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestStartError(t *testing.T) {\n\ttt := &testData{expectedOut: `\"level\"=0 \"msg\"=\"error watching file\" \"err\"=\"test error\"` + \"\\n\" + `\"level\"=0 \"msg\"=\"stopping watcher\"` + \"\\n\"}\n\tout := &bytes.Buffer{}\n\tl := stdr.New(log.New(out, \"\", 0))\n\tctx, cancel := context.WithCancel(context.Background())\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw := &Watcher{Log: l, watcher: watcher}\n\tgo func() {\n\t\ttime.Sleep(time.Millisecond)\n\t\tw.watcher.Errors <- fmt.Errorf(\"test error\")\n\t\tcancel()\n\t}()\n\tw.Start(ctx)\n\tif diff := cmp.Diff(out.String(), tt.expectedOut); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestStartErrorContinue(t *testing.T) {\n\tout := &bytes.Buffer{}\n\tl := stdr.New(log.New(out, \"\", 0))\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw := &Watcher{Log: l, watcher: watcher}\n\tgo w.Start(ctx)\n\tclose(w.watcher.Errors)\n\ttime.Sleep(time.Millisecond)\n\tif diff := cmp.Diff(out.String(), \"\"); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc (tt *testData) helper(t *testing.T, l logr.Logger) (*Watcher, string) {\n\tt.Helper()\n\tname, err := createFile([]byte(tt.initial))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw, err := NewWatcher(l, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw.dataMu.RLock()\n\tbefore := string(w.data)\n\tw.dataMu.RUnlock()\n\tif diff := cmp.Diff(before, tt.initial); diff != \"\" {\n\t\tt.Fatal(\"before\", diff)\n\t}\n\n\treturn w, name\n}\n\nfunc TestTranslate(t *testing.T) {\n\tinput := dhcp{\n\t\tMACAddress:       []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},\n\t\tIPAddress:        \"192.168.2.150\",\n\t\tSubnetMask:       \"255.255.255.0\",\n\t\tDefaultGateway:   \"192.168.2.1\",\n\t\tNameServers:      []string{\"1.1.1.1\", \"8.8.8.8\"},\n\t\tHostname:         \"test-server\",\n\t\tDomainName:       \"example.com\",\n\t\tBroadcastAddress: \"192.168.2.255\",\n\t\tNTPServers:       []string{\"132.163.96.2\"},\n\t\tVLANID:           \"100\",\n\t\tLeaseTime:        86400,\n\t\tArch:             \"x86_64\",\n\t\tDomainSearch:     []string{\"example.com\"},\n\t\tNetboot: netboot{\n\t\t\tAllowPXE:      true,\n\t\t\tIPXEScriptURL: \"http://boot.netboot.xyz\",\n\t\t\tIPXEScript:    \"#!ipxe\\nchain http://boot.netboot.xyz\",\n\t\t\tConsole:       \"ttyS0\",\n\t\t\tFacility:      \"onprem\",\n\t\t},\n\t}\n\twantDHCP := &data.DHCP{\n\t\tMACAddress:       []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},\n\t\tIPAddress:        netip.MustParseAddr(\"192.168.2.150\"),\n\t\tSubnetMask:       net.IPv4Mask(255, 255, 255, 0),\n\t\tDefaultGateway:   netip.MustParseAddr(\"192.168.2.1\"),\n\t\tNameServers:      []net.IP{{1, 1, 1, 1}, {8, 8, 8, 8}},\n\t\tHostname:         \"test-server\",\n\t\tDomainName:       \"example.com\",\n\t\tBroadcastAddress: netip.MustParseAddr(\"192.168.2.255\"),\n\t\tNTPServers:       []net.IP{{132, 163, 96, 2}},\n\t\tVLANID:           \"100\",\n\t\tLeaseTime:        86400,\n\t\tArch:             \"x86_64\",\n\t\tDomainSearch:     []string{\"example.com\"},\n\t}\n\twantNetboot := &data.Netboot{\n\t\tAllowNetboot:  true,\n\t\tIPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"boot.netboot.xyz\"},\n\t\tIPXEScript:    \"#!ipxe\\nchain http://boot.netboot.xyz\",\n\t\tConsole:       \"ttyS0\",\n\t\tFacility:      \"onprem\",\n\t}\n\tw := &Watcher{Log: logr.Discard()}\n\tgotDHCP, gotNetboot, err := w.translate(input)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif diff := cmp.Diff(gotDHCP, wantDHCP, cmpopts.IgnoreUnexported(netip.Addr{})); diff != \"\" {\n\t\tt.Error(diff)\n\t}\n\tif diff := cmp.Diff(gotNetboot, wantNetboot); diff != \"\" {\n\t\tt.Error(diff)\n\t}\n}\n\nfunc TestTranslateErrors(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   dhcp\n\t\twantErr error\n\t}{\n\t\t\"invalid IP\":                {input: dhcp{IPAddress: \"not an IP\"}, wantErr: errParseIP},\n\t\t\"invalid subnet mask\":       {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"not a mask\"}, wantErr: errParseSubnet},\n\t\t\"invalid gateway\":           {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"192.168.1.255\", DefaultGateway: \"not a gateway\"}, wantErr: nil},\n\t\t\"invalid broadcast address\": {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"192.168.1.255\"}, wantErr: nil},\n\t\t\"invalid NameServers\":       {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"192.168.1.255\", NameServers: []string{\"no good\"}}, wantErr: nil},\n\t\t\"invalid ntpservers\":        {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"192.168.1.255\", NTPServers: []string{\"no good\"}}, wantErr: nil},\n\t\t\"invalid ipxe script url\":   {input: dhcp{IPAddress: \"1.1.1.1\", SubnetMask: \"255.255.255.0\", Netboot: netboot{IPXEScriptURL: \":not a url\"}}, wantErr: errParseURL},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tw := &Watcher{Log: stdr.New(log.New(os.Stdout, \"\", log.Lshortfile))}\n\t\t\tif _, _, err := w.translate(tt.input); !errors.Is(err, tt.wantErr) {\n\t\t\t\tt.Errorf(\"translate() = %T, want %T\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByMac(t *testing.T) {\n\ttests := map[string]struct {\n\t\tmac     net.HardwareAddr\n\t\tbadData bool\n\t\twantErr error\n\t}{\n\t\t\"no record found\":        {mac: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}, wantErr: errRecordNotFound},\n\t\t\"record found\":           {mac: net.HardwareAddr{0x08, 0x00, 0x27, 0x29, 0x4e, 0x67}, wantErr: nil},\n\t\t\"fail error translating\": {mac: net.HardwareAddr{0x08, 0x00, 0x27, 0x29, 0x4e, 0x68}, wantErr: errParseIP},\n\t\t\"fail parsing file\":      {badData: true, wantErr: errFileFormat},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tdata := \"testdata/example.yaml\"\n\t\t\tif tt.badData {\n\t\t\t\tvar err error\n\t\t\t\tdata, err = createFile([]byte(\"not a yaml file\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tdefer os.Remove(data)\n\t\t\t}\n\t\t\tw, err := NewWatcher(logr.Discard(), data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t_, _, err = w.GetByMac(context.Background(), tt.mac)\n\t\t\tif !errors.Is(err, tt.wantErr) {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIP(t *testing.T) {\n\ttests := map[string]struct {\n\t\tip      net.IP\n\t\tbadData bool\n\t\twantErr error\n\t}{\n\t\t\"no record found\":   {ip: net.IPv4(172, 168, 2, 1), wantErr: errRecordNotFound},\n\t\t\"record found\":      {ip: net.IPv4(192, 168, 2, 153), wantErr: nil},\n\t\t\"fail parsing file\": {badData: true, wantErr: errFileFormat},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tdata := \"testdata/example.yaml\"\n\t\t\tif tt.badData {\n\t\t\t\tvar err error\n\t\t\t\tdata, err = createFile([]byte(\"not a yaml file\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tdefer os.Remove(data)\n\t\t\t}\n\t\t\tw, err := NewWatcher(logr.Discard(), data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t_, _, err = w.GetByIP(context.Background(), tt.ip)\n\t\t\tif !errors.Is(err, tt.wantErr) {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/file/testdata/example.yaml",
    "content": "---\n08:00:27:29:4E:67:\n  ipAddress: \"192.168.2.153\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.2.1\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"pxe-virtualbox\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"https://boot.netboot.xyz\"\n52:54:00:aa:88:2a:\n  ipAddress: \"192.168.2.15\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.2.1\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"sandbox\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"https://boot.netboot.xyz\"\n86:96:b0:6e:ca:36:\n  ipAddress: \"192.168.2.158\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.2.1\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"pxe-proxmox\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"http://boot.netboot.xyz\"\nb4:96:91:6f:33:d0:\n  ipAddress: \"192.168.56.15\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.56.4\"\n  nameServers:\n    - \"8.8.8.8\"\n    - \"1.1.1.1\"\n  hostname: \"dhcp-testing\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.56.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n    - \"132.163.96.3\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n    ipxeScriptUrl: \"https://boot.netboot.xyz\"\n08:00:27:29:4E:68: # bad data\n  ipAddress: \"3\"\n  subnetMask: \"255.255.255.0\"\n"
  },
  {
    "path": "internal/backend/kube/error.go",
    "content": "package kube\n\nimport (\n\t\"net/http\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\ntype hardwareNotFoundError struct{}\n\nfunc (hardwareNotFoundError) NotFound() bool { return true }\n\nfunc (hardwareNotFoundError) Error() string { return \"hardware not found\" }\n\n// Status() implements the APIStatus interface from apimachinery/pkg/api/errors\n// so that IsNotFound function could be used against this error type.\nfunc (hardwareNotFoundError) Status() metav1.Status {\n\treturn metav1.Status{\n\t\tReason: metav1.StatusReasonNotFound,\n\t\tCode:   http.StatusNotFound,\n\t}\n}\n"
  },
  {
    "path": "internal/backend/kube/index.go",
    "content": "package kube\n\nimport (\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n)\n\n// MACAddrIndex is an index used with a controller-runtime client to lookup hardware by MAC.\nconst MACAddrIndex = \".Spec.Interfaces.MAC\"\n\n// MACAddrs returns a list of MAC addresses for a Hardware object.\nfunc MACAddrs(obj client.Object) []string {\n\thw, ok := obj.(*v1alpha1.Hardware)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn GetMACs(hw)\n}\n\n// GetMACs retrieves all MACs associated with h.\nfunc GetMACs(h *v1alpha1.Hardware) []string {\n\tvar macs []string\n\tfor _, i := range h.Spec.Interfaces {\n\t\tif i.DHCP != nil && i.DHCP.MAC != \"\" {\n\t\t\tmacs = append(macs, i.DHCP.MAC)\n\t\t}\n\t}\n\n\treturn macs\n}\n\n// IPAddrIndex is an index used with a controller-runtime client to lookup hardware by IP.\nconst IPAddrIndex = \".Spec.Interfaces.DHCP.IP\"\n\n// IPAddrs returns a list of IP addresses for a Hardware object.\nfunc IPAddrs(obj client.Object) []string {\n\thw, ok := obj.(*v1alpha1.Hardware)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn GetIPs(hw)\n}\n\n// GetIPs retrieves all IP addresses.\nfunc GetIPs(h *v1alpha1.Hardware) []string {\n\tvar ips []string\n\tfor _, i := range h.Spec.Interfaces {\n\t\tif i.DHCP != nil && i.DHCP.IP != nil && i.DHCP.IP.Address != \"\" {\n\t\t\tips = append(ips, i.DHCP.IP.Address)\n\t\t}\n\t}\n\treturn ips\n}\n"
  },
  {
    "path": "internal/backend/kube/index_test.go",
    "content": "package kube\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n)\n\nfunc TestMACAddrs(t *testing.T) {\n\ttests := map[string]struct {\n\t\thw   client.Object\n\t\twant []string\n\t}{\n\t\t\"not a v1alpha1.Hardware object\": {hw: &v1alpha1.Workflow{}, want: nil},\n\t\t\"2 MACs\": {hw: &v1alpha1.Hardware{\n\t\t\tSpec: v1alpha1.HardwareSpec{\n\t\t\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\t\t\tMAC: \"00:00:00:00:00:00\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\t\t\tMAC: \"00:00:00:00:00:01\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, want: []string{\"00:00:00:00:00:00\", \"00:00:00:00:00:01\"}},\n\t\t\"no interfaces\": {hw: &v1alpha1.Hardware{}, want: nil},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmacs := MACAddrs(tc.hw)\n\t\t\tif diff := cmp.Diff(macs, tc.want); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected MACs (+want -got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIPAddrs(t *testing.T) {\n\ttests := map[string]struct {\n\t\thw   client.Object\n\t\twant []string\n\t}{\n\t\t\"not a v1alpha1.Hardware object\": {hw: &v1alpha1.Workflow{}, want: nil},\n\t\t\"2 IPs\": {hw: &v1alpha1.Hardware{\n\t\t\tSpec: v1alpha1.HardwareSpec{\n\t\t\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\t\t\tAddress: \"192.168.2.1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\t\t\tAddress: \"192.168.2.2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\t\t\tIP: &v1alpha1.IP{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, want: []string{\"192.168.2.1\", \"192.168.2.2\"}},\n\t\t\"no interfaces\": {hw: &v1alpha1.Hardware{}, want: nil},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := IPAddrs(tc.hw)\n\t\t\tif diff := cmp.Diff(tc.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected IPs (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/kube/kube.go",
    "content": "// Package kube is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.\npackage kube\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\n\t\"github.com/ccoveille/go-safecast\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"k8s.io/client-go/rest\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cluster\"\n)\n\nconst tracerName = \"github.com/tinkerbell/smee/dhcp\"\n\n// Backend is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.\ntype Backend struct {\n\tcluster cluster.Cluster\n}\n\n// NewBackend returns a controller-runtime cluster.Cluster with the Tinkerbell runtime\n// scheme registered, and indexers for:\n// * Hardware by MAC address\n// * Hardware by IP address\n//\n// Callers must instantiate the client-side cache by calling Start() before use.\nfunc NewBackend(conf *rest.Config, opts ...cluster.Option) (*Backend, error) {\n\tc, err := cluster.New(conf, opts...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create new cluster config: %w\", err)\n\t}\n\n\tif err := c.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.Hardware{}, MACAddrIndex, MACAddrs); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup indexer: %w\", err)\n\t}\n\n\tif err := c.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.Hardware{}, IPAddrIndex, IPAddrs); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup indexer(.spec.interfaces.dhcp.ip.address): %w\", err)\n\t}\n\n\treturn &Backend{cluster: c}, nil\n}\n\n// Start starts the client-side cache.\nfunc (b *Backend) Start(ctx context.Context) error {\n\treturn b.cluster.Start(ctx)\n}\n\n// GetByMac implements the handler.BackendReader interface and returns DHCP and netboot data based on a mac address.\nfunc (b *Backend) GetByMac(ctx context.Context, mac net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\ttracer := otel.Tracer(tracerName)\n\tctx, span := tracer.Start(ctx, \"backend.kube.GetByMac\")\n\tdefer span.End()\n\thardwareList := &v1alpha1.HardwareList{}\n\n\tif err := b.cluster.GetClient().List(ctx, hardwareList, &client.MatchingFields{MACAddrIndex: mac.String()}); err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, fmt.Errorf(\"failed listing hardware for (%v): %w\", mac, err)\n\t}\n\n\tif len(hardwareList.Items) == 0 {\n\t\terr := hardwareNotFoundError{}\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\tif len(hardwareList.Items) > 1 {\n\t\terr := fmt.Errorf(\"got %d hardware objects for mac %s, expected only 1\", len(hardwareList.Items), mac)\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\ti := v1alpha1.Interface{}\n\tfor _, iface := range hardwareList.Items[0].Spec.Interfaces {\n\t\tif iface.DHCP.MAC == mac.String() {\n\t\t\ti = iface\n\t\t\tbreak\n\t\t}\n\t}\n\n\td, n, err := transform(i, hardwareList.Items[0].Spec.Metadata)\n\tif err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\tspan.SetAttributes(d.EncodeToAttributes()...)\n\tspan.SetAttributes(n.EncodeToAttributes()...)\n\tspan.SetStatus(codes.Ok, \"\")\n\n\treturn d, n, nil\n}\n\n// GetByIP implements the handler.BackendReader interface and returns DHCP and netboot data based on an IP address.\nfunc (b *Backend) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP, *data.Netboot, error) {\n\ttracer := otel.Tracer(tracerName)\n\tctx, span := tracer.Start(ctx, \"backend.kube.GetByIP\")\n\tdefer span.End()\n\thardwareList := &v1alpha1.HardwareList{}\n\n\tif err := b.cluster.GetClient().List(ctx, hardwareList, &client.MatchingFields{IPAddrIndex: ip.String()}); err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, fmt.Errorf(\"failed listing hardware for (%v): %w\", ip, err)\n\t}\n\n\tif len(hardwareList.Items) == 0 {\n\t\terr := hardwareNotFoundError{}\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\tif len(hardwareList.Items) > 1 {\n\t\terr := fmt.Errorf(\"got %d hardware objects for ip: %s, expected only 1\", len(hardwareList.Items), ip)\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\ti := v1alpha1.Interface{}\n\tfor _, iface := range hardwareList.Items[0].Spec.Interfaces {\n\t\tif iface.DHCP.IP.Address == ip.String() {\n\t\t\ti = iface\n\t\t\tbreak\n\t\t}\n\t}\n\n\td, n, err := transform(i, hardwareList.Items[0].Spec.Metadata)\n\tif err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\tspan.SetAttributes(d.EncodeToAttributes()...)\n\tspan.SetAttributes(n.EncodeToAttributes()...)\n\tspan.SetStatus(codes.Ok, \"\")\n\n\treturn d, n, nil\n}\n\n// toDHCPData converts a v1alpha1.DHCP to a data.DHCP data structure.\n// if required fields are missing, an error is returned.\n// Required fields: v1alpha1.Interface.DHCP.MAC, v1alpha1.Interface.DHCP.IP.Address, v1alpha1.Interface.DHCP.IP.Netmask.\nfunc toDHCPData(h *v1alpha1.DHCP) (*data.DHCP, error) {\n\tif h == nil {\n\t\treturn nil, errors.New(\"no DHCP data\")\n\t}\n\td := new(data.DHCP)\n\n\tvar err error\n\t// MACAddress is required\n\tif d.MACAddress, err = net.ParseMAC(h.MAC); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif h.IP != nil {\n\t\t// IPAddress is required\n\t\tif d.IPAddress, err = netip.ParseAddr(h.IP.Address); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Netmask is required\n\t\tsm := net.ParseIP(h.IP.Netmask)\n\t\tif sm == nil {\n\t\t\treturn nil, errors.New(\"no netmask\")\n\t\t}\n\t\td.SubnetMask = net.IPMask(sm.To4())\n\t} else {\n\t\treturn nil, errors.New(\"no IP data\")\n\t}\n\n\t// Gateway is optional, but should be a valid IP address if present\n\tif h.IP.Gateway != \"\" {\n\t\tif d.DefaultGateway, err = netip.ParseAddr(h.IP.Gateway); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// name servers, optional\n\tfor _, s := range h.NameServers {\n\t\tip := net.ParseIP(s)\n\t\tif ip == nil {\n\t\t\tbreak\n\t\t}\n\t\td.NameServers = append(d.NameServers, ip)\n\t}\n\n\t// timeservers, optional\n\tfor _, s := range h.TimeServers {\n\t\tip := net.ParseIP(s)\n\t\tif ip == nil {\n\t\t\tbreak\n\t\t}\n\t\td.NTPServers = append(d.NTPServers, ip)\n\t}\n\n\t// hostname, optional\n\td.Hostname = h.Hostname\n\n\t// lease time required\n\t// Default to one week\n\td.LeaseTime = 604800\n\tif v, err := safecast.ToUint32(h.LeaseTime); err == nil {\n\t\td.LeaseTime = v\n\t}\n\n\t// arch\n\td.Arch = h.Arch\n\n\t// vlanid\n\td.VLANID = h.VLANID\n\n\treturn d, nil\n}\n\n// toNetbootData converts a hardware interface to a data.Netboot data structure.\nfunc toNetbootData(i *v1alpha1.Netboot, facility string) (*data.Netboot, error) {\n\tif i == nil {\n\t\treturn nil, errors.New(\"no netboot data\")\n\t}\n\tn := new(data.Netboot)\n\n\t// allow machine to netboot\n\tif i.AllowPXE != nil {\n\t\tn.AllowNetboot = *i.AllowPXE\n\t}\n\n\t// ipxe script url is optional but if provided, it must be a valid url\n\tif i.IPXE != nil {\n\t\tif i.IPXE.URL != \"\" {\n\t\t\tu, err := url.ParseRequestURI(i.IPXE.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tn.IPXEScriptURL = u\n\t\t}\n\t}\n\n\t// ipxescript\n\tif i.IPXE != nil {\n\t\tn.IPXEScript = i.IPXE.Contents\n\t}\n\n\t// console\n\tn.Console = \"\"\n\n\t// facility\n\tn.Facility = facility\n\n\t// OSIE data\n\tn.OSIE = data.OSIE{}\n\tif i.OSIE != nil {\n\t\tif b, err := url.Parse(i.OSIE.BaseURL); err == nil {\n\t\t\tn.OSIE.BaseURL = b\n\t\t}\n\t\tn.OSIE.Kernel = i.OSIE.Kernel\n\t\tn.OSIE.Initrd = i.OSIE.Initrd\n\t}\n\n\treturn n, nil\n}\n\n// transform returns data.DHCP and data.Netboot from part a v1alpha1.Interface and *v1alpha1.HardwareMetadata.\nfunc transform(i v1alpha1.Interface, m *v1alpha1.HardwareMetadata) (*data.DHCP, *data.Netboot, error) {\n\td, err := toDHCPData(i.DHCP)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to convert hardware to DHCP data: %w\", err)\n\t}\n\td.Disabled = i.DisableDHCP\n\n\t// Facility is used in the default HookOS iPXE script so we get it from the hardware metadata, if set.\n\tfacility := \"\"\n\tif m != nil {\n\t\tif m.Facility != nil {\n\t\t\tfacility = m.Facility.FacilityCode\n\t\t}\n\t}\n\n\tn, err := toNetbootData(i.Netboot, facility)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to convert hardware to netboot data: %w\", err)\n\t}\n\n\treturn d, n, nil\n}\n"
  },
  {
    "path": "internal/backend/kube/kube_test.go",
    "content": "package kube\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\tv1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/kubernetes/scheme\"\n\t\"k8s.io/client-go/rest\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cache\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cache/informertest\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/fake\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cluster\"\n)\n\nfunc TestNewBackend(t *testing.T) {\n\ttests := map[string]struct {\n\t\tconf      *rest.Config\n\t\topt       cluster.Option\n\t\tshouldErr bool\n\t}{\n\t\t\"no config\": {shouldErr: true},\n\t\t\"failed index field\": {shouldErr: true, conf: new(rest.Config), opt: func(o *cluster.Options) {\n\t\t\tcl := fake.NewClientBuilder().Build()\n\t\t\to.NewClient = func(*rest.Config, client.Options) (client.Client, error) {\n\t\t\t\treturn cl, nil\n\t\t\t}\n\t\t\to.MapperProvider = func(*rest.Config, *http.Client) (meta.RESTMapper, error) {\n\t\t\t\treturn cl.RESTMapper(), nil\n\t\t\t}\n\t\t}},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tb, err := NewBackend(tt.conf, tt.opt)\n\t\t\tif tt.shouldErr && err == nil {\n\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t}\n\t\t\tif !tt.shouldErr && err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif !tt.shouldErr && b == nil {\n\t\t\t\tt.Fatal(\"expected backend\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToDHCPData(t *testing.T) {\n\ttests := map[string]struct {\n\t\tin        *v1alpha1.DHCP\n\t\twant      *data.DHCP\n\t\tshouldErr bool\n\t}{\n\t\t\"nil input\": {\n\t\t\tin:        nil,\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"no mac\": {\n\t\t\tin:        &v1alpha1.DHCP{},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"bad mac\": {\n\t\t\tin:        &v1alpha1.DHCP{MAC: \"bad\"},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"no ip\": {\n\t\t\tin:        &v1alpha1.DHCP{MAC: \"aa:bb:cc:dd:ee:ff\", IP: &v1alpha1.IP{}},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"no subnet\": {\n\t\t\tin:        &v1alpha1.DHCP{MAC: \"aa:bb:cc:dd:ee:ff\", IP: &v1alpha1.IP{Address: \"192.168.2.4\"}},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"v1alpha1.IP == nil\": {\n\t\t\tin:        &v1alpha1.DHCP{MAC: \"aa:bb:cc:dd:ee:ff\", IP: nil},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"bad gateway\": {\n\t\t\tin:        &v1alpha1.DHCP{MAC: \"aa:bb:cc:dd:ee:ff\", IP: &v1alpha1.IP{Address: \"192.168.2.4\", Netmask: \"255.255.254.0\", Gateway: \"bad\"}},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t\"one bad nameserver\": {\n\t\t\tin: &v1alpha1.DHCP{\n\t\t\t\tMAC:         \"00:00:00:00:00:04\",\n\t\t\t\tNameServers: []string{\"1.1.1.1\", \"bad\"},\n\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\tAddress: \"192.168.2.4\",\n\t\t\t\t\tNetmask: \"255.255.0.0\",\n\t\t\t\t\tGateway: \"192.168.2.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &data.DHCP{\n\t\t\t\tSubnetMask:     net.IPv4Mask(255, 255, 0, 0),\n\t\t\t\tDefaultGateway: netip.MustParseAddr(\"192.168.2.1\"),\n\t\t\t\tNameServers:    []net.IP{net.IPv4(1, 1, 1, 1)},\n\t\t\t\tIPAddress:      netip.MustParseAddr(\"192.168.2.4\"),\n\t\t\t\tMACAddress:     net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x04},\n\t\t\t},\n\t\t},\n\t\t\"full\": {\n\t\t\tin: &v1alpha1.DHCP{\n\t\t\t\tMAC:         \"00:00:00:00:00:04\",\n\t\t\t\tHostname:    \"test\",\n\t\t\t\tLeaseTime:   3600,\n\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\tAddress: \"192.168.1.4\",\n\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\tGateway: \"192.168.1.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &data.DHCP{\n\t\t\t\tSubnetMask:     net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\tDefaultGateway: netip.MustParseAddr(\"192.168.1.1\"),\n\t\t\t\tNameServers:    []net.IP{net.IPv4(1, 1, 1, 1)},\n\t\t\t\tHostname:       \"test\",\n\t\t\t\tLeaseTime:      3600,\n\t\t\t\tIPAddress:      netip.MustParseAddr(\"192.168.1.4\"),\n\t\t\t\tMACAddress:     net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x04},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := toDHCPData(tt.in)\n\t\t\tif tt.shouldErr && err == nil {\n\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(netip.Addr{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToNetbootData(t *testing.T) {\n\ttests := map[string]struct {\n\t\tin        *v1alpha1.Netboot\n\t\twant      *data.Netboot\n\t\tshouldErr bool\n\t}{\n\t\t\"nil input\":    {in: nil, shouldErr: true},\n\t\t\"bad ipxe url\": {in: &v1alpha1.Netboot{IPXE: &v1alpha1.IPXE{URL: \"bad\"}}, shouldErr: true},\n\t\t\"successful\":   {in: &v1alpha1.Netboot{IPXE: &v1alpha1.IPXE{URL: \"http://example.com/ipxe.ipxe\"}}, want: &data.Netboot{IPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"example.com\", Path: \"/ipxe.ipxe\"}}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := toNetbootData(tt.in, \"\")\n\t\t\tif tt.shouldErr && err == nil {\n\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(netip.Addr{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIP(t *testing.T) {\n\ttests := map[string]struct {\n\t\thwObject    []v1alpha1.Hardware\n\t\twantDHCP    *data.DHCP\n\t\twantNetboot *data.Netboot\n\t\tshouldErr   bool\n\t\tfailToList  bool\n\t}{\n\t\t\"empty hardware list\":    {shouldErr: true, hwObject: []v1alpha1.Hardware{}},\n\t\t\"more than one hardware\": {shouldErr: true, hwObject: []v1alpha1.Hardware{hwObject1, hwObject2}},\n\t\t\"bad dhcp data\":          {shouldErr: true, hwObject: []v1alpha1.Hardware{badDHCPObject2}},\n\t\t\"bad netboot data\":       {shouldErr: true, hwObject: []v1alpha1.Hardware{badNetbootObject2}},\n\t\t\"fail to list hardware\":  {shouldErr: true, failToList: true},\n\t\t\"good data\": {hwObject: []v1alpha1.Hardware{hwObject1}, wantDHCP: &data.DHCP{\n\t\t\tMACAddress:     net.HardwareAddr{0x3c, 0xec, 0xef, 0x4c, 0x4f, 0x54},\n\t\t\tIPAddress:      netip.MustParseAddr(\"172.16.10.100\"),\n\t\t\tSubnetMask:     []byte{0xff, 0xff, 0xff, 0x00},\n\t\t\tDefaultGateway: netip.MustParseAddr(\"255.255.255.0\"),\n\t\t\tNameServers: []net.IP{\n\t\t\t\t{0x1, 0x1, 0x1, 0x1},\n\t\t\t},\n\t\t\tHostname:  \"sm01\",\n\t\t\tLeaseTime: 86400,\n\t\t\tArch:      \"x86_64\",\n\t\t}, wantNetboot: &data.Netboot{\n\t\t\tAllowNetboot: true,\n\t\t\tIPXEScriptURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   \"netboot.xyz\",\n\t\t\t},\n\t\t\tFacility: \"onprem\",\n\t\t}},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\trs := runtime.NewScheme()\n\t\t\tif err := scheme.AddToScheme(rs); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := v1alpha1.AddToScheme(rs); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tct := fake.NewClientBuilder()\n\t\t\tif !tc.failToList {\n\t\t\t\tct = ct.WithScheme(rs)\n\t\t\t\tct = ct.WithRuntimeObjects(&v1alpha1.HardwareList{})\n\t\t\t\tct = ct.WithIndex(&v1alpha1.Hardware{}, IPAddrIndex, func(client.Object) []string {\n\t\t\t\t\tvar list []string\n\t\t\t\t\tfor _, elem := range tc.hwObject {\n\t\t\t\t\t\tlist = append(list, elem.Spec.Interfaces[0].DHCP.IP.Address)\n\t\t\t\t\t}\n\t\t\t\t\treturn list\n\t\t\t\t})\n\t\t\t}\n\t\t\tif len(tc.hwObject) > 0 {\n\t\t\t\tt.Logf(\"%+v\", tc.hwObject[0].Spec.Interfaces[0].DHCP)\n\t\t\t\tt.Logf(\"%+v\", tc.hwObject[0].Spec.Interfaces[0].DHCP.IP)\n\t\t\t\tct = ct.WithLists(&v1alpha1.HardwareList{Items: tc.hwObject})\n\t\t\t}\n\t\t\tcl := ct.Build()\n\n\t\t\tfn := func(o *cluster.Options) {\n\t\t\t\to.NewClient = func(*rest.Config, client.Options) (client.Client, error) {\n\t\t\t\t\treturn cl, nil\n\t\t\t\t}\n\t\t\t\to.MapperProvider = func(*rest.Config, *http.Client) (meta.RESTMapper, error) {\n\t\t\t\t\treturn cl.RESTMapper(), nil\n\t\t\t\t}\n\t\t\t\to.NewCache = func(*rest.Config, cache.Options) (cache.Cache, error) {\n\t\t\t\t\treturn &informertest.FakeInformers{Scheme: cl.Scheme()}, nil\n\t\t\t\t}\n\t\t\t}\n\t\t\trc := new(rest.Config)\n\t\t\tb, err := NewBackend(rc, fn)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tgo b.Start(context.Background())\n\t\t\tgotDHCP, gotNetboot, err := b.GetByIP(context.Background(), net.IPv4(172, 16, 10, 100))\n\t\t\tif tc.shouldErr && err == nil {\n\t\t\t\tt.Log(err)\n\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(gotDHCP, tc.wantDHCP, cmpopts.IgnoreUnexported(netip.Addr{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(gotNetboot, tc.wantNetboot); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByMac(t *testing.T) {\n\ttests := map[string]struct {\n\t\thwObject    []v1alpha1.Hardware\n\t\twantDHCP    *data.DHCP\n\t\twantNetboot *data.Netboot\n\t\tshouldErr   bool\n\t\tfailToList  bool\n\t}{\n\t\t\"empty hardware list\":    {shouldErr: true},\n\t\t\"more than one hardware\": {shouldErr: true, hwObject: []v1alpha1.Hardware{hwObject1, hwObject2}},\n\t\t\"bad dhcp data\":          {shouldErr: true, hwObject: []v1alpha1.Hardware{badDHCPObject}},\n\t\t\"bad netboot data\":       {shouldErr: true, hwObject: []v1alpha1.Hardware{badNetbootObject}},\n\t\t\"fail to list hardware\":  {shouldErr: true, failToList: true},\n\t\t\"good data\": {hwObject: []v1alpha1.Hardware{hwObject1}, wantDHCP: &data.DHCP{\n\t\t\tMACAddress:     net.HardwareAddr{0x3c, 0xec, 0xef, 0x4c, 0x4f, 0x54},\n\t\t\tIPAddress:      netip.MustParseAddr(\"172.16.10.100\"),\n\t\t\tSubnetMask:     []byte{0xff, 0xff, 0xff, 0x00},\n\t\t\tDefaultGateway: netip.MustParseAddr(\"255.255.255.0\"),\n\t\t\tNameServers: []net.IP{\n\t\t\t\t{0x1, 0x1, 0x1, 0x1},\n\t\t\t},\n\t\t\tHostname:  \"sm01\",\n\t\t\tLeaseTime: 86400,\n\t\t\tArch:      \"x86_64\",\n\t\t}, wantNetboot: &data.Netboot{\n\t\t\tAllowNetboot: true,\n\t\t\tIPXEScriptURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   \"netboot.xyz\",\n\t\t\t},\n\t\t\tFacility: \"onprem\",\n\t\t}},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\trs := runtime.NewScheme()\n\t\t\tif err := scheme.AddToScheme(rs); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := v1alpha1.AddToScheme(rs); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tct := fake.NewClientBuilder()\n\t\t\tif !tc.failToList {\n\t\t\t\tct = ct.WithScheme(rs)\n\t\t\t\tct = ct.WithRuntimeObjects(&v1alpha1.HardwareList{})\n\t\t\t\tct = ct.WithIndex(&v1alpha1.Hardware{}, MACAddrIndex, func(client.Object) []string {\n\t\t\t\t\tvar list []string\n\t\t\t\t\tfor _, elem := range tc.hwObject {\n\t\t\t\t\t\tlist = append(list, elem.Spec.Interfaces[0].DHCP.MAC)\n\t\t\t\t\t}\n\t\t\t\t\treturn list\n\t\t\t\t})\n\t\t\t}\n\t\t\tif len(tc.hwObject) > 0 {\n\t\t\t\tt.Logf(\"%+v\", tc.hwObject[0].Spec.Interfaces[0].DHCP)\n\t\t\t\tt.Logf(\"%+v\", tc.hwObject[0].Spec.Interfaces[0].DHCP.MAC)\n\t\t\t\tct = ct.WithLists(&v1alpha1.HardwareList{Items: tc.hwObject})\n\t\t\t}\n\t\t\tcl := ct.Build()\n\n\t\t\tfn := func(o *cluster.Options) {\n\t\t\t\to.NewClient = func(*rest.Config, client.Options) (client.Client, error) {\n\t\t\t\t\treturn cl, nil\n\t\t\t\t}\n\t\t\t\to.MapperProvider = func(*rest.Config, *http.Client) (meta.RESTMapper, error) {\n\t\t\t\t\treturn cl.RESTMapper(), nil\n\t\t\t\t}\n\t\t\t\to.NewCache = func(*rest.Config, cache.Options) (cache.Cache, error) {\n\t\t\t\t\treturn &informertest.FakeInformers{Scheme: cl.Scheme()}, nil\n\t\t\t\t}\n\t\t\t}\n\t\t\trc := new(rest.Config)\n\t\t\tb, err := NewBackend(rc, fn)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tgo b.Start(context.Background())\n\t\t\tgotDHCP, gotNetboot, err := b.GetByMac(context.Background(), net.HardwareAddr{0x3c, 0xec, 0xef, 0x4c, 0x4f, 0x54})\n\t\t\tif tc.shouldErr && err == nil {\n\t\t\t\tt.Log(err)\n\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(gotDHCP, tc.wantDHCP, cmpopts.IgnoreUnexported(netip.Addr{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(gotNetboot, tc.wantNetboot); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar hwObject1 = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine1\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tMetadata: &v1alpha1.HardwareMetadata{\n\t\t\tFacility: &v1alpha1.MetadataFacility{\n\t\t\t\tFacilityCode: \"onprem\",\n\t\t\t},\n\t\t},\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tAllowPXE:      &[]bool{true}[0],\n\t\t\t\t\tAllowWorkflow: &[]bool{true}[0],\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"http://netboot.xyz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tArch:     \"x86_64\",\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.100\",\n\t\t\t\t\t\tGateway: \"172.16.10.1\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:54\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t\tUEFI:        true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar hwObject2 = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine2\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tAllowPXE:      &[]bool{true}[0],\n\t\t\t\t\tAllowWorkflow: &[]bool{true}[0],\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"http://netboot.xyz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tArch:     \"x86_64\",\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.101\",\n\t\t\t\t\t\tGateway: \"172.16.10.1\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:55\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t\tUEFI:        true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMetadata: &v1alpha1.HardwareMetadata{\n\t\t\tFacility: &v1alpha1.MetadataFacility{\n\t\t\t\tFacilityCode: \"ewr2\",\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar badDHCPObject = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine2\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tAllowPXE:      &[]bool{true}[0],\n\t\t\t\t\tAllowWorkflow: &[]bool{true}[0],\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"http://netboot.xyz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tArch:     \"x86_64\",\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.100\",\n\t\t\t\t\t\tGateway: \"bad-address\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:54\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t\tUEFI:        true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar badDHCPObject2 = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine2\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tAllowPXE:      &[]bool{true}[0],\n\t\t\t\t\tAllowWorkflow: &[]bool{true}[0],\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"http://netboot.xyz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tArch:     \"x86_64\",\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.100\",\n\t\t\t\t\t\tGateway: \"bad-address\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:55\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t\tUEFI:        true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar badNetbootObject = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine2\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"bad-url\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.101\",\n\t\t\t\t\t\tGateway: \"172.16.10.1\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:54\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar badNetbootObject2 = v1alpha1.Hardware{\n\tTypeMeta: v1.TypeMeta{\n\t\tKind:       \"Hardware\",\n\t\tAPIVersion: \"tinkerbell.org/v1alpha1\",\n\t},\n\tObjectMeta: v1.ObjectMeta{\n\t\tName:      \"machine2\",\n\t\tNamespace: \"default\",\n\t},\n\tSpec: v1alpha1.HardwareSpec{\n\t\tInterfaces: []v1alpha1.Interface{\n\t\t\t{\n\t\t\t\tNetboot: &v1alpha1.Netboot{\n\t\t\t\t\tIPXE: &v1alpha1.IPXE{\n\t\t\t\t\t\tURL: \"bad-url\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDHCP: &v1alpha1.DHCP{\n\t\t\t\t\tHostname: \"sm01\",\n\t\t\t\t\tIP: &v1alpha1.IP{\n\t\t\t\t\t\tAddress: \"172.16.10.100\",\n\t\t\t\t\t\tGateway: \"172.16.10.1\",\n\t\t\t\t\t\tNetmask: \"255.255.255.0\",\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime:   86400,\n\t\t\t\t\tMAC:         \"3c:ec:ef:4c:4f:54\",\n\t\t\t\t\tNameServers: []string{\"1.1.1.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "internal/backend/noop/noop.go",
    "content": "package noop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nvar errAlways = errors.New(\"noop backend always returns an error\")\n\ntype Backend struct{}\n\nfunc (n Backend) GetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\treturn nil, nil, errAlways\n}\n\nfunc (n Backend) GetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboot, error) {\n\treturn nil, nil, errAlways\n}\n"
  },
  {
    "path": "internal/backend/noop/noop_test.go",
    "content": "package noop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestBackend(t *testing.T) {\n\tb := Backend{}\n\tctx := context.Background()\n\t_, _, err := b.GetByMac(ctx, nil)\n\tif err == nil {\n\t\tt.Error(\"expected error\")\n\t}\n\tif !errors.Is(err, errAlways) {\n\t\tt.Error(\"expected errAlways\")\n\t}\n\t_, _, err = b.GetByIP(ctx, nil)\n\tif err == nil {\n\t\tt.Error(\"expected error\")\n\t}\n\tif !errors.Is(err, errAlways) {\n\t\tt.Error(\"expected errAlways\")\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/data/data.go",
    "content": "// Package data is an interface between DHCP backend implementations and the DHCP server.\npackage data\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"go.opentelemetry.io/otel/attribute\"\n)\n\n// Packet holds the data that is passed to a DHCP handler.\ntype Packet struct {\n\t// Peer is the address of the client that sent the DHCP message.\n\tPeer net.Addr\n\t// Pkt is the DHCP message.\n\tPkt *dhcpv4.DHCPv4\n\t// Md is the metadata that was passed to the DHCP server.\n\tMd *Metadata\n}\n\n// Metadata holds metadata about the DHCP packet that was received.\ntype Metadata struct {\n\t// IfName is the name of the interface that the DHCP message was received on.\n\tIfName string\n\t// IfIndex is the index of the interface that the DHCP message was received on.\n\tIfIndex int\n}\n\n// DHCP holds the DHCP headers and options to be set in a DHCP handler response.\n// This is the API between a DHCP handler and a backend.\ntype DHCP struct {\n\tMACAddress       net.HardwareAddr // chaddr DHCP header.\n\tIPAddress        netip.Addr       // yiaddr DHCP header.\n\tSubnetMask       net.IPMask       // DHCP option 1.\n\tDefaultGateway   netip.Addr       // DHCP option 3.\n\tNameServers      []net.IP         // DHCP option 6.\n\tHostname         string           // DHCP option 12.\n\tDomainName       string           // DHCP option 15.\n\tBroadcastAddress netip.Addr       // DHCP option 28.\n\tNTPServers       []net.IP         // DHCP option 42.\n\tVLANID           string           // DHCP option 43.116.\n\tLeaseTime        uint32           // DHCP option 51.\n\tArch             string           // DHCP option 93.\n\tDomainSearch     []string         // DHCP option 119.\n\tDisabled         bool             // If true, no DHCP response should be sent.\n}\n\n// Netboot holds info used in netbooting a client.\ntype Netboot struct {\n\tAllowNetboot  bool     // If true, the client will be provided netboot options in the DHCP offer/ack.\n\tIPXEScriptURL *url.URL // Overrides a default value that is passed into DHCP on startup.\n\tIPXEScript    string   // Overrides a default value that is passed into DHCP on startup.\n\tConsole       string\n\tFacility      string\n\tOSIE          OSIE\n}\n\n// OSIE or OS Installation Environment is the data about where the OSIE parts are located.\ntype OSIE struct {\n\t// BaseURL is the URL where the OSIE parts are located.\n\tBaseURL *url.URL\n\t// Kernel is the name of the kernel file.\n\tKernel string\n\t// Initrd is the name of the initrd file.\n\tInitrd string\n}\n\n// EncodeToAttributes returns a slice of opentelemetry attributes that can be used to set span.SetAttributes.\nfunc (d *DHCP) EncodeToAttributes() []attribute.KeyValue {\n\tvar ns []string\n\tfor _, e := range d.NameServers {\n\t\tns = append(ns, e.String())\n\t}\n\n\tvar ntp []string\n\tfor _, e := range d.NTPServers {\n\t\tntp = append(ntp, e.String())\n\t}\n\n\tvar ip string\n\tif d.IPAddress.Compare(netip.Addr{}) != 0 {\n\t\tip = d.IPAddress.String()\n\t}\n\n\tvar sm string\n\tif d.SubnetMask != nil {\n\t\tsm = net.IP(d.SubnetMask).String()\n\t}\n\n\tvar dfg string\n\tif d.DefaultGateway.Compare(netip.Addr{}) != 0 {\n\t\tdfg = d.DefaultGateway.String()\n\t}\n\n\tvar ba string\n\tif d.BroadcastAddress.Compare(netip.Addr{}) != 0 {\n\t\tba = d.BroadcastAddress.String()\n\t}\n\n\treturn []attribute.KeyValue{\n\t\tattribute.String(\"DHCP.MACAddress\", d.MACAddress.String()),\n\t\tattribute.String(\"DHCP.IPAddress\", ip),\n\t\tattribute.String(\"DHCP.SubnetMask\", sm),\n\t\tattribute.String(\"DHCP.DefaultGateway\", dfg),\n\t\tattribute.String(\"DHCP.NameServers\", strings.Join(ns, \",\")),\n\t\tattribute.String(\"DHCP.Hostname\", d.Hostname),\n\t\tattribute.String(\"DHCP.DomainName\", d.DomainName),\n\t\tattribute.String(\"DHCP.BroadcastAddress\", ba),\n\t\tattribute.String(\"DHCP.NTPServers\", strings.Join(ntp, \",\")),\n\t\tattribute.Int64(\"DHCP.LeaseTime\", int64(d.LeaseTime)),\n\t\tattribute.String(\"DHCP.DomainSearch\", strings.Join(d.DomainSearch, \",\")),\n\t}\n}\n\n// EncodeToAttributes returns a slice of opentelemetry attributes that can be used to set span.SetAttributes.\nfunc (n *Netboot) EncodeToAttributes() []attribute.KeyValue {\n\tvar s string\n\tif n.IPXEScriptURL != nil {\n\t\ts = n.IPXEScriptURL.String()\n\t}\n\treturn []attribute.KeyValue{\n\t\tattribute.Bool(\"Netboot.AllowNetboot\", n.AllowNetboot),\n\t\tattribute.String(\"Netboot.IPXEScriptURL\", s),\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/data/data_test.go",
    "content": "package data\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"go.opentelemetry.io/otel/attribute\"\n)\n\nfunc TestDHCPEncodeToAttributes(t *testing.T) {\n\ttests := map[string]struct {\n\t\tdhcp *DHCP\n\t\twant []attribute.KeyValue\n\t}{\n\t\t\"successful encode of zero value DHCP struct\": {\n\t\t\tdhcp: &DHCP{},\n\t\t\twant: []attribute.KeyValue{\n\t\t\t\tattribute.String(\"DHCP.MACAddress\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.IPAddress\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.Hostname\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.SubnetMask\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.DefaultGateway\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.NameServers\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.DomainName\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.BroadcastAddress\", \"\"),\n\t\t\t\tattribute.String(\"DHCP.NTPServers\", \"\"),\n\t\t\t\tattribute.Int64(\"DHCP.LeaseTime\", 0),\n\t\t\t\tattribute.String(\"DHCP.DomainSearch\", \"\"),\n\t\t\t},\n\t\t},\n\t\t\"successful encode of populated DHCP struct\": {\n\t\t\tdhcp: &DHCP{\n\t\t\t\tMACAddress:       []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},\n\t\t\t\tIPAddress:        netip.MustParseAddr(\"192.168.2.150\"),\n\t\t\t\tSubnetMask:       []byte{255, 255, 255, 0},\n\t\t\t\tDefaultGateway:   netip.MustParseAddr(\"192.168.2.1\"),\n\t\t\t\tNameServers:      []net.IP{{1, 1, 1, 1}, {8, 8, 8, 8}},\n\t\t\t\tHostname:         \"test\",\n\t\t\t\tDomainName:       \"example.com\",\n\t\t\t\tBroadcastAddress: netip.MustParseAddr(\"192.168.2.255\"),\n\t\t\t\tNTPServers:       []net.IP{{132, 163, 96, 2}},\n\t\t\t\tLeaseTime:        86400,\n\t\t\t\tDomainSearch:     []string{\"example.com\", \"example.org\"},\n\t\t\t},\n\t\t\twant: []attribute.KeyValue{\n\t\t\t\tattribute.String(\"DHCP.MACAddress\", \"00:01:02:03:04:05\"),\n\t\t\t\tattribute.String(\"DHCP.IPAddress\", \"192.168.2.150\"),\n\t\t\t\tattribute.String(\"DHCP.Hostname\", \"test\"),\n\t\t\t\tattribute.String(\"DHCP.SubnetMask\", \"255.255.255.0\"),\n\t\t\t\tattribute.String(\"DHCP.DefaultGateway\", \"192.168.2.1\"),\n\t\t\t\tattribute.String(\"DHCP.NameServers\", \"1.1.1.1,8.8.8.8\"),\n\t\t\t\tattribute.String(\"DHCP.DomainName\", \"example.com\"),\n\t\t\t\tattribute.String(\"DHCP.BroadcastAddress\", \"192.168.2.255\"),\n\t\t\t\tattribute.String(\"DHCP.NTPServers\", \"132.163.96.2\"),\n\t\t\t\tattribute.Int64(\"DHCP.LeaseTime\", 86400),\n\t\t\t\tattribute.String(\"DHCP.DomainSearch\", \"example.com,example.org\"),\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\twant := attribute.NewSet(tt.want...)\n\t\t\tgot := attribute.NewSet(tt.dhcp.EncodeToAttributes()...)\n\t\t\tenc := attribute.DefaultEncoder()\n\t\t\tif diff := cmp.Diff(got.Encoded(enc), want.Encoded(enc)); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNetbootEncodeToAttributes(t *testing.T) {\n\ttests := map[string]struct {\n\t\tnetboot *Netboot\n\t\twant    []attribute.KeyValue\n\t}{\n\t\t\"successful encode of zero value Netboot struct\": {\n\t\t\tnetboot: &Netboot{},\n\t\t\twant: []attribute.KeyValue{\n\t\t\t\tattribute.Bool(\"Netboot.AllowNetboot\", false),\n\t\t\t\tattribute.String(\"Netboot.IPXEScriptURL\", \"\"),\n\t\t\t},\n\t\t},\n\t\t\"successful encode of populated Netboot struct\": {\n\t\t\tnetboot: &Netboot{\n\t\t\t\tAllowNetboot:  true,\n\t\t\t\tIPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"example.com\"},\n\t\t\t},\n\t\t\twant: []attribute.KeyValue{\n\t\t\t\tattribute.Bool(\"Netboot.AllowNetboot\", true),\n\t\t\t\tattribute.String(\"Netboot.IPXEScriptURL\", \"http://example.com\"),\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\twant := attribute.NewSet(tt.want...)\n\t\t\tgot := attribute.NewSet(tt.netboot.EncodeToAttributes()...)\n\t\t\tenc := attribute.DefaultEncoder()\n\t\t\tif diff := cmp.Diff(got.Encoded(enc), want.Encoded(enc)); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/dhcp.go",
    "content": "package dhcp\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n)\n\nconst (\n\tPXEClient  ClientType = \"PXEClient\"\n\tHTTPClient ClientType = \"HTTPClient\"\n)\n\n// known user-class types. must correspond to DHCP option 77 - User-Class\n// https://www.rfc-editor.org/rfc/rfc3004.html\nconst (\n\t// If the client has had iPXE burned into its ROM (or is a VM\n\t// that uses iPXE as the PXE \"ROM\"), special handling is\n\t// needed because in this mode the client is using iPXE native\n\t// drivers and chainloading to a UNDI stack won't work.\n\tIPXE UserClass = \"iPXE\"\n\t// If the client identifies as \"Tinkerbell\", we've already\n\t// chainloaded this client to the full-featured copy of iPXE\n\t// we supply. We have to distinguish this case so we don't\n\t// loop on the chainload step.\n\tTinkerbell UserClass = \"Tinkerbell\"\n)\n\n// UserClass is DHCP option 77 (https://www.rfc-editor.org/rfc/rfc3004.html).\ntype UserClass string\n\n// ClientType is from DHCP option 60. Normally only PXEClient or HTTPClient.\ntype ClientType string\n\n// ArchToBootFile maps supported hardware PXE architectures types to iPXE binary files.\nvar ArchToBootFile = map[iana.Arch]string{\n\tiana.INTEL_X86PC:       \"undionly.kpxe\",\n\tiana.NEC_PC98:          \"undionly.kpxe\",\n\tiana.EFI_ITANIUM:       \"undionly.kpxe\",\n\tiana.DEC_ALPHA:         \"undionly.kpxe\",\n\tiana.ARC_X86:           \"undionly.kpxe\",\n\tiana.INTEL_LEAN_CLIENT: \"undionly.kpxe\",\n\tiana.EFI_IA32:          \"ipxe.efi\",\n\tiana.EFI_X86_64:        \"ipxe.efi\",\n\tiana.EFI_XSCALE:        \"ipxe.efi\",\n\tiana.EFI_BC:            \"ipxe.efi\",\n\tiana.EFI_ARM32:         \"snp.efi\",\n\tiana.EFI_ARM64:         \"snp.efi\",\n\tiana.EFI_X86_HTTP:      \"ipxe.efi\",\n\tiana.EFI_X86_64_HTTP:   \"ipxe.efi\",\n\tiana.EFI_ARM32_HTTP:    \"snp.efi\",\n\tiana.EFI_ARM64_HTTP:    \"snp.efi\",\n\tiana.Arch(41):          \"snp.efi\", // arm rpiboot (0x29): https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#processor-architecture\n}\n\n// ErrUnknownArch is used when the PXE client request is from an unknown architecture.\nvar ErrUnknownArch = fmt.Errorf(\"could not determine client architecture from option 93\")\n\n// Info holds details about the dhcp request. Use NewInfo to populate the struct fields from a dhcp packet.\ntype Info struct {\n\t// Pkt is the dhcp packet that was received from the client.\n\tPkt *dhcpv4.DHCPv4\n\t// Arch is the architecture of the client. Use NewInfo to automatically populate this field.\n\tArch iana.Arch\n\t// Mac is the mac address of the client. Use NewInfo to automatically populate this field.\n\tMac net.HardwareAddr\n\t// UserClass is the user class of the client. Use NewInfo to automatically populate this field.\n\tUserClass UserClass\n\t// ClientType is the client type of the client. Use NewInfo to automatically populate this field.\n\tClientType ClientType\n\t// IsNetbootClient returns nil if the client is a valid netboot client.\tOtherwise it returns an error.\n\t// Use NewInfo to automatically populate this field.\n\tIsNetbootClient error\n\t// IPXEBinary is the iPXE binary file to boot. Use NewInfo to automatically populate this field.\n\tIPXEBinary string\n}\n\nfunc NewInfo(pkt *dhcpv4.DHCPv4) Info {\n\ti := Info{Pkt: pkt}\n\tif pkt != nil {\n\t\ti.Arch = Arch(pkt)\n\t\ti.Mac = pkt.ClientHWAddr\n\t\ti.UserClass = i.UserClassFrom()\n\t\ti.ClientType = i.ClientTypeFrom()\n\t\ti.IsNetbootClient = IsNetbootClient(pkt)\n\t\ti.IPXEBinary = i.IPXEBinaryFrom()\n\t}\n\n\treturn i\n}\n\n// isRaspberryPI checks if the mac address is from a Raspberry PI by matching prefixes against OUI registrations of the Raspberry Pi Trading Ltd.\n// https://www.netify.ai/resources/macs/brands/raspberry-pi\n// https://udger.com/resources/mac-address-vendor-detail?name=raspberry_pi_foundation\n// https://macaddress.io/statistics/company/27594\nfunc isRaspberryPI(mac net.HardwareAddr) bool {\n\tprefixes := [][]byte{\n\t\t{0xb8, 0x27, 0xeb}, // B8:27:EB\n\t\t{0xdc, 0xa6, 0x32}, // DC:A6:32\n\t\t{0xe4, 0x5f, 0x01}, // E4:5F:01\n\t\t{0x28, 0xcd, 0xc1}, // 28:CD:C1\n\t\t{0xd8, 0x3a, 0xdd}, // D8:3A:DD\n\t}\n\tfor _, prefix := range prefixes {\n\t\tif bytes.HasPrefix(mac, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Arch returns the Arch of the client pulled from DHCP option 93.\nfunc Arch(d *dhcpv4.DHCPv4) iana.Arch {\n\t// if the mac address is from a Raspberry PI, use the Raspberry PI architecture.\n\t// Some Raspberry PI's (Raspberry PI 5) report an option 93 of 0.\n\t// This translates to iana.INTEL_X86PC and causes us to map to undionly.kpxe.\n\tif isRaspberryPI(d.ClientHWAddr) {\n\t\treturn iana.Arch(41)\n\t}\n\n\t// get option 93 ; arch\n\tfwt := d.ClientArch()\n\tif len(fwt) == 0 {\n\t\treturn iana.Arch(255) // unknown arch\n\t}\n\tvar archKnown bool\n\tvar a iana.Arch\n\tfor _, elem := range fwt {\n\t\tif !strings.Contains(elem.String(), \"unknown\") {\n\t\t\tarchKnown = true\n\t\t\t// Basic architecture identification, based purely on\n\t\t\t// the PXE architecture option.\n\t\t\t// https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#processor-architecture\n\t\t\ta = elem\n\t\t\tbreak\n\t\t}\n\t}\n\tif !archKnown {\n\t\treturn iana.Arch(255) // unknown arch\n\t}\n\n\treturn a\n}\n\nfunc (i Info) IPXEBinaryFrom() string {\n\tbin, found := ArchToBootFile[i.Arch]\n\tif !found {\n\t\treturn \"\"\n\t}\n\n\treturn bin\n}\n\n// String function for clientType.\nfunc (c ClientType) String() string {\n\treturn string(c)\n}\n\n// String function for UserClass.\nfunc (u UserClass) String() string {\n\treturn string(u)\n}\n\nfunc (i Info) UserClassFrom() UserClass {\n\tvar u UserClass\n\tif i.Pkt != nil {\n\t\tif val := i.Pkt.Options.Get(dhcpv4.OptionUserClassInformation); val != nil {\n\t\t\tu = UserClass(string(val))\n\t\t}\n\t}\n\n\treturn u\n}\n\nfunc (i Info) ClientTypeFrom() ClientType {\n\tvar c ClientType\n\tif i.Pkt != nil {\n\t\tif val := i.Pkt.Options.Get(dhcpv4.OptionClassIdentifier); val != nil {\n\t\t\tif strings.HasPrefix(string(val), HTTPClient.String()) {\n\t\t\t\tc = HTTPClient\n\t\t\t} else {\n\t\t\t\tc = PXEClient\n\t\t\t}\n\t\t}\n\t}\n\n\treturn c\n}\n\n// IsNetbootClient returns nil if the client is a valid netboot client.\tOtherwise it returns an error.\n//\n// A valid netboot client will have the following in its DHCP request:\n// 1. is a DHCP discovery/request message type.\n// 2. option 93 is set.\n// 3. option 94 is set.\n// 4. option 97 is correct length.\n// 5. option 60 is set with this format: \"PXEClient:Arch:xxxxx:UNDI:yyyzzz\" or \"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\".\n//\n// See: http://www.pix.net/software/pxeboot/archive/pxespec.pdf\n//\n// See: https://www.rfc-editor.org/rfc/rfc4578.html\nfunc IsNetbootClient(pkt *dhcpv4.DHCPv4) error {\n\tvar err error\n\t// only response to DISCOVER and REQUEST packets\n\tif pkt.MessageType() != dhcpv4.MessageTypeDiscover && pkt.MessageType() != dhcpv4.MessageTypeRequest {\n\t\terr = wrapNonNil(err, \"message type must be either Discover or Request\")\n\t}\n\t// option 60 must be set\n\tif !pkt.Options.Has(dhcpv4.OptionClassIdentifier) {\n\t\terr = wrapNonNil(err, \"option 60 not set\")\n\t}\n\t// option 60 must start with PXEClient or HTTPClient\n\topt60 := pkt.GetOneOption(dhcpv4.OptionClassIdentifier)\n\tif !strings.HasPrefix(string(opt60), string(PXEClient)) && !strings.HasPrefix(string(opt60), string(HTTPClient)) {\n\t\terr = wrapNonNil(err, \"option 60 not PXEClient or HTTPClient\")\n\t}\n\n\t// option 93 must be set\n\tif !pkt.Options.Has(dhcpv4.OptionClientSystemArchitectureType) {\n\t\terr = wrapNonNil(err, \"option 93 not set\")\n\t}\n\n\t// option 94 must be set\n\tif !pkt.Options.Has(dhcpv4.OptionClientNetworkInterfaceIdentifier) {\n\t\terr = wrapNonNil(err, \"option 94 not set\")\n\t}\n\n\t// option 97 must be have correct length or not be set\n\tguid := pkt.GetOneOption(dhcpv4.OptionClientMachineIdentifier)\n\tswitch len(guid) {\n\tcase 0:\n\t\t// A missing GUID is invalid according to the spec, however\n\t\t// there are PXE ROMs in the wild that omit the GUID and still\n\t\t// expect to boot. The only thing we do with the GUID is\n\t\t// mirror it back to the client if it's there, so we might as\n\t\t// well accept these buggy ROMs.\n\tcase 17:\n\t\tif guid[0] != 0 {\n\t\t\terr = wrapNonNil(err, \"option 97 does not start with 0\")\n\t\t}\n\tdefault:\n\t\terr = wrapNonNil(err, \"option 97 has invalid length (must be 0 or 17)\")\n\t}\n\n\treturn err\n}\n\nfunc wrapNonNil(err error, format string) error {\n\tif err == nil {\n\t\treturn errors.New(format)\n\t}\n\n\treturn fmt.Errorf(\"%w: %v\", err, format)\n}\n\n// Bootfile returns the calculated dhcp header: \"file\" value. see https://datatracker.ietf.org/doc/html/rfc2131#section-2 .\nfunc (i Info) Bootfile(customUC UserClass, ipxeScript, ipxeHTTPBinServer *url.URL, ipxeTFTPBinServer netip.AddrPort) string {\n\tbootfile := \"/no-ipxe-script-defined\"\n\n\t// If a machine is in an ipxe boot loop, it is likely to be that we aren't matching on IPXE or Tinkerbell userclass (option 77).\n\tswitch { // order matters here.\n\tcase i.UserClass == Tinkerbell, (customUC != \"\" && i.UserClass == customUC): // this case gets us out of an ipxe boot loop.\n\t\tif ipxeScript != nil {\n\t\t\tbootfile = ipxeScript.String()\n\t\t}\n\tcase i.ClientType == HTTPClient: // Check the client type from option 60.\n\t\tif ipxeHTTPBinServer != nil {\n\t\t\tpaths := []string{i.IPXEBinary}\n\t\t\tif i.Mac != nil {\n\t\t\t\tpaths = append([]string{i.Mac.String()}, paths...)\n\t\t\t}\n\t\t\tbootfile = ipxeHTTPBinServer.JoinPath(paths...).String()\n\t\t}\n\tcase i.UserClass == IPXE: // if the \"iPXE\" user class is found it means we aren't in our custom version of ipxe, but because of the option 43 we're setting we need to give a full tftp url from which to boot.\n\t\tt := url.URL{\n\t\t\tScheme: \"tftp\",\n\t\t\tHost:   ipxeTFTPBinServer.String(),\n\t\t}\n\t\tpaths := []string{i.IPXEBinary}\n\t\tif i.Mac != nil {\n\t\t\tpaths = append([]string{i.Mac.String()}, paths...)\n\t\t}\n\t\tbootfile = t.JoinPath(paths...).String()\n\tdefault:\n\t\tif i.IPXEBinary != \"\" {\n\t\t\tbootfile = i.IPXEBinary\n\t\t}\n\t}\n\n\treturn bootfile\n}\n\n// NextServer returns the calculated dhcp header (ServerIPAddr): \"siaddr\" value. see https://datatracker.ietf.org/doc/html/rfc2131#section-2 .\nfunc (i Info) NextServer(ipxeHTTPBinServer *url.URL, ipxeTFTPBinServer netip.AddrPort) net.IP {\n\tvar nextServer net.IP\n\n\t// If a machine is in an ipxe boot loop, it is likely to be that we aren't matching on IPXE or Tinkerbell userclass (option 77).\n\tswitch { // order matters here.\n\tcase i.ClientType == HTTPClient: // Check the client type from option 60.\n\t\tif ipxeHTTPBinServer != nil {\n\t\t\tnextServer = net.ParseIP(ipxeHTTPBinServer.Hostname())\n\t\t}\n\tcase i.UserClass == IPXE: // if the \"iPXE\" user class is found it means we aren't in our custom version of ipxe, but because of the option 43 we're setting we need to give a full tftp url from which to boot.\n\t\tnextServer = net.IP(ipxeTFTPBinServer.Addr().AsSlice())\n\tdefault:\n\t\tnextServer = net.IP(ipxeTFTPBinServer.Addr().AsSlice())\n\t}\n\n\treturn nextServer\n}\n\n// AddRPIOpt43 adds the Raspberry PI required option43 sub options to an existing opt 43.\nfunc (i Info) AddRPIOpt43(opts dhcpv4.Options) []byte {\n\t// these are suboptions of option43. ref: https://datatracker.ietf.org/doc/html/rfc2132#section-8.4\n\tif isRaspberryPI(i.Mac) {\n\t\t// TODO document what these hex strings are and why they are needed.\n\t\t// https://www.raspberrypi.org/documentation/computers/raspberry-pi.html#PXE_OPTION43\n\t\t// tested with Raspberry Pi 4 using UEFI from here: https://github.com/pftf/RPi4/releases/tag/v1.31\n\t\t// all files were served via a tftp server and lived at the top level dir of the tftp server (i.e tftp://server/)\n\t\t// \"\\x00\\x00\\x11\" is equal to NUL(Null), NUL(Null), DC1(Device Control 1)\n\t\topt9, _ := hex.DecodeString(\"00001152617370626572727920506920426f6f74\") // \"\\x00\\x00\\x11Raspberry Pi Boot\"\n\t\topts[9] = opt9\n\t\t// \"\\x0a\\x04\\x00\" is equal to LF(Line Feed), EOT(End of Transmission), NUL(Null)\n\t\topt10, _ := hex.DecodeString(\"00505845\") // \"\\x0a\\x04\\x00PXE\"\n\t\topts[10] = opt10\n\t}\n\n\treturn opts.ToBytes()\n}\n"
  },
  {
    "path": "internal/dhcp/dhcp_test.go",
    "content": "package dhcp\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n)\n\nconst (\n\texamplePXEClient  = \"PXEClient:Arch:00007:UNDI:003001\"\n\texampleHTTPClient = \"HTTPClient:Arch:00016:UNDI:003001\"\n)\n\nfunc TestNewInfo(t *testing.T) {\n\ttests := map[string]struct {\n\t\tpkt  *dhcpv4.DHCPv4\n\t\twant Info\n\t}{\n\t\t\"valid http client\": {\n\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\tClientIPAddr: []byte{0x00, 0x00, 0x00, 0x00},\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_X86_64_HTTP),\n\t\t\t\t\tdhcpv4.OptUserClass(Tinkerbell.String()),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(exampleHTTPClient),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x05, 0x06, 0x07}),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant: Info{\n\t\t\t\tArch:            iana.EFI_X86_64_HTTP,\n\t\t\t\tMac:             net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tUserClass:       Tinkerbell,\n\t\t\t\tClientType:      HTTPClient,\n\t\t\t\tIsNetbootClient: nil,\n\t\t\t\tIPXEBinary:      \"ipxe.efi\",\n\t\t\t},\n\t\t},\n\t\t\"arch not found\": {\n\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\tClientIPAddr: []byte{0x00, 0x00, 0x00, 0x00},\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t\tdhcpv4.OptClientArch(iana.Arch(255)),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(examplePXEClient),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x05, 0x06, 0x07}),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant: Info{\n\t\t\t\tArch:       iana.Arch(255),\n\t\t\t\tMac:        net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tClientType: PXEClient,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := NewInfo(tt.pkt)\n\t\t\tif diff := cmp.Diff(tt.want, got, cmpopts.IgnoreFields(Info{}, \"Pkt\")); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestArch(t *testing.T) {\n\ttests := map[string]struct {\n\t\tpkt  *dhcpv4.DHCPv4\n\t\twant iana.Arch\n\t}{\n\t\t\"found\": {\n\t\t\tpkt:  &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(dhcpv4.OptClientArch(iana.INTEL_X86PC))},\n\t\t\twant: iana.INTEL_X86PC,\n\t\t},\n\t\t\"raspberry pi\": {\n\t\t\tpkt:  &dhcpv4.DHCPv4{ClientHWAddr: net.HardwareAddr{0xb8, 0x27, 0xeb, 0x00, 0x00, 0x00}},\n\t\t\twant: iana.Arch(41),\n\t\t},\n\t\t\"unknown\": {\n\t\t\tpkt:  &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(dhcpv4.OptClientArch(iana.Arch(255)))},\n\t\t\twant: iana.Arch(255),\n\t\t},\n\t\t\"unknown: opt 93 len 0\": {\n\t\t\tpkt:  &dhcpv4.DHCPv4{},\n\t\t\twant: iana.Arch(255),\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Arch(tt.pkt)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsNetbootClient(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput *dhcpv4.DHCPv4\n\t\twant  error\n\t}{\n\t\t\"fail invalid message type\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(dhcpv4.OptMessageType(dhcpv4.MessageTypeInform))}, want: errors.New(\"\")},\n\t\t\"fail no opt60\":             {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(dhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover))}, want: errors.New(\"\")},\n\t\t\"fail bad opt60\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"BadClient\"),\n\t\t)}, want: errors.New(\"\")},\n\t\t\"fail no opt93\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t)}, want: errors.New(\"\")},\n\t\t\"fail no opt94\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t)}, want: errors.New(\"\")},\n\t\t\"fail invalid opt97[0] != 0\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05}),\n\t\t)}, want: errors.New(\"\")},\n\t\t\"fail invalid len(opt97)\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x01, 0x02}),\n\t\t)}, want: errors.New(\"\")},\n\t\t\"success len(opt97) == 0\": {input: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{}),\n\t\t)}, want: nil},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := IsNetbootClient(tt.input); (err == nil) != (tt.want == nil) {\n\t\t\t\tt.Errorf(\"isNetbootClient() = %v, want %v\", err, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBootfile(t *testing.T) {\n\ttype args struct {\n\t\tcustomUC          UserClass\n\t\tipxeTFTPBinServer netip.AddrPort\n\t\tipxeScript        *url.URL\n\t\tipxeHTTPBinServer *url.URL\n\t}\n\ttests := map[string]struct {\n\t\tinfo Info\n\t\targs args\n\t\twant string\n\t}{\n\t\t\"ipxe script\": {\n\t\t\tinfo: Info{\n\t\t\t\tUserClass: Tinkerbell,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tipxeScript: &url.URL{Path: \"/ipxe-script\"},\n\t\t\t},\n\t\t\twant: \"/ipxe-script\",\n\t\t},\n\t\t\"http client\": {\n\t\t\tinfo: Info{\n\t\t\t\tClientType: HTTPClient,\n\t\t\t\tMac:        net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tIPXEBinary: \"ipxe.efi\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tipxeHTTPBinServer: &url.URL{Scheme: \"http\", Host: \"1.2.3.4:8080\"},\n\t\t\t},\n\t\t\twant: \"http://1.2.3.4:8080/01:02:03:04:05:06/ipxe.efi\",\n\t\t},\n\t\t\"firmware ipxe\": {\n\t\t\tinfo: Info{\n\t\t\t\tUserClass:  IPXE,\n\t\t\t\tMac:        net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tIPXEBinary: \"undionly.kpxe\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tipxeTFTPBinServer: netip.MustParseAddrPort(\"1.2.3.4:69\"),\n\t\t\t},\n\t\t\twant: \"tftp://1.2.3.4:69/01:02:03:04:05:06/undionly.kpxe\",\n\t\t},\n\t\t\"no user class\": {\n\t\t\tinfo: Info{\n\t\t\t\tMac:        net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tIPXEBinary: \"undionly.kpxe\",\n\t\t\t},\n\t\t\twant: \"undionly.kpxe\",\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := tt.info.Bootfile(tt.args.customUC, tt.args.ipxeScript, tt.args.ipxeHTTPBinServer, tt.args.ipxeTFTPBinServer)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNextServer(t *testing.T) {\n\ttype args struct {\n\t\tipxeTFTPBinServer netip.AddrPort\n\t\tipxeHTTPBinServer *url.URL\n\t}\n\ttests := map[string]struct {\n\t\tinfo Info\n\t\targs args\n\t\twant net.IP\n\t}{\n\t\t\"http client\": {\n\t\t\tinfo: Info{\n\t\t\t\tClientType: HTTPClient,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tipxeHTTPBinServer: &url.URL{Scheme: \"http\", Host: \"1.2.3.4:8989\"},\n\t\t\t},\n\t\t\twant: net.ParseIP(\"1.2.3.4\"),\n\t\t},\n\t\t\"firmware ipxe\": {\n\t\t\tinfo: Info{\n\t\t\t\tUserClass: IPXE,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tipxeTFTPBinServer: netip.MustParseAddrPort(\"1.2.3.4:69\"),\n\t\t\t},\n\t\t\twant: net.ParseIP(\"1.2.3.4\"),\n\t\t},\n\t\t\"no user class\": {\n\t\t\tinfo: Info{},\n\t\t\targs: args{\n\t\t\t\tipxeTFTPBinServer: netip.MustParseAddrPort(\"1.2.3.4:69\"),\n\t\t\t},\n\t\t\twant: net.ParseIP(\"1.2.3.4\"),\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := tt.info.NextServer(tt.args.ipxeHTTPBinServer, tt.args.ipxeTFTPBinServer)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOpt43(t *testing.T) {\n\trpi9, _ := hex.DecodeString(\"00001152617370626572727920506920426f6f74\")\n\trpi10, _ := hex.DecodeString(\"00505845\")\n\n\ttests := map[string]struct {\n\t\tinfo Info\n\t\topts dhcpv4.Options\n\t\twant []byte\n\t}{\n\t\t\"not a raspberry pi\": {\n\t\t\tinfo: Info{\n\t\t\t\tMac: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t},\n\t\t\topts: dhcpv4.Options{},\n\t\t\twant: dhcpv4.Options{}.ToBytes(),\n\t\t},\n\t\t\"raspberry pi\": {\n\t\t\tinfo: Info{\n\t\t\t\tMac: net.HardwareAddr{0xb8, 0x27, 0xeb, 0x00, 0x00, 0x00},\n\t\t\t},\n\t\t\topts: dhcpv4.Options{},\n\t\t\twant: dhcpv4.Options{\n\t\t\t\t9:  rpi9,\n\t\t\t\t10: rpi10,\n\t\t\t}.ToBytes(),\n\t\t},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := tt.info.AddRPIOpt43(tt.opts)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUserClassString(t *testing.T) {\n\tu := UserClass(\"test\")\n\tif diff := cmp.Diff(\"test\", u.String()); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n\nfunc TestIsRaspberryPI(t *testing.T) {\n\ttests := map[string]struct {\n\t\tmac  net.HardwareAddr\n\t\twant bool\n\t}{\n\t\t\"not a raspberry pi\": {\n\t\t\tmac:  net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\twant: false,\n\t\t},\n\t\t\"raspberry pi\": {\n\t\t\tmac:  net.HardwareAddr{0xb8, 0x27, 0xeb, 0x00, 0x00, 0x00},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := isRaspberryPI(tt.mac)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/handler/handler.go",
    "content": "// Package handler holds the interface that backends implement, handlers take in, and the top level dhcp package passes to handlers.\npackage handler\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\n// BackendReader is the interface for getting data from a backend.\n//\n// Backends implement this interface to provide DHCP and Netboot data to the handlers.\ntype BackendReader interface {\n\t// Read data (from a backend) based on a mac address\n\t// and return DHCP headers and options, including netboot info.\n\tGetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error)\n\tGetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboot, error)\n}\n"
  },
  {
    "path": "internal/dhcp/handler/proxy/proxy.go",
    "content": "/*\nPackage proxy implements a DHCP handler that provides proxyDHCP functionality.\n\n\"[A] Proxy DHCP server behaves much like a DHCP server by listening for ordinary\nDHCP client traffic and responding to certain client requests. However, unlike the\nDHCP server, the PXE Proxy DHCP server does not administer network addresses, and\nit only responds to clients that identify themselves as PXE clients. The responses\ngiven by the PXE Proxy DHCP server contain the mechanism by which the client locates\nthe boot servers or the network addresses and descriptions of the supported,\ncompatible boot servers.\"\n\nReference: https://www.ibm.com/docs/en/aix/7.1?topic=protocol-preboot-execution-environment-proxy-dhcp-daemon\n*/\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler\"\n\toteldhcp \"github.com/tinkerbell/smee/internal/dhcp/otel\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"golang.org/x/net/ipv4\"\n)\n\nconst tracerName = \"github.com/tinkerbell/smee/internal/dhcp/handler/proxy\"\n\n// Handler holds the configuration details for the running the DHCP server.\ntype Handler struct {\n\t// Backend is the backend to use for getting DHCP data.\n\tBackend handler.BackendReader\n\n\t// IPAddr is the IP address to use in DHCP responses.\n\t// Option 54 and the sname DHCP header.\n\t// This could be a load balancer IP address or an ingress IP address or a local IP address.\n\tIPAddr netip.Addr\n\n\t// Log is used to log messages.\n\t// `logr.Discard()` can be used if no logging is desired.\n\tLog logr.Logger\n\n\t// Netboot configuration\n\tNetboot Netboot\n\n\t// OTELEnabled is used to determine if netboot options include otel naming.\n\t// When true, the netboot filename will be appended with otel information.\n\t// For example, the filename will be \"snp.efi-00-23b1e307bb35484f535a1f772c06910e-d887dc3912240434-01\".\n\t// <original filename>-00-<trace id>-<span id>-<trace flags>\n\tOTELEnabled bool\n\n\t// AutoProxyEnabled is used to determine if the proxyDHCP handler should do any Backend calls or not.\n\t// When enabled no Backend calls are made and responses are sent to all valid network boot clients.\n\tAutoProxyEnabled bool\n}\n\n// Netboot holds the netboot configuration details used in running a DHCP server.\ntype Netboot struct {\n\t// iPXE binary server IP:Port serving via TFTP.\n\tIPXEBinServerTFTP netip.AddrPort\n\n\t// IPXEBinServerHTTP is the URL to the IPXE binary server serving via HTTP(s).\n\tIPXEBinServerHTTP *url.URL\n\n\t// IPXEScriptURL is the URL to the IPXE script to use.\n\tIPXEScriptURL func(*dhcpv4.DHCPv4) *url.URL\n\n\t// Enabled is whether to enable sending netboot DHCP options.\n\tEnabled bool\n\n\t// UserClass (for network booting) allows a custom DHCP option 77 to be used to break out of an iPXE loop.\n\tUserClass dhcp.UserClass\n}\n\n// Redirection name comes from section 2.5 of http://www.pix.net/software/pxeboot/archive/pxespec.pdf\nfunc (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, dp data.Packet) {\n\t// validations\n\tif dp.Pkt == nil {\n\t\th.Log.Error(errors.New(\"incoming packet is nil\"), \"not able to respond when the incoming packet is nil\")\n\t\treturn\n\t}\n\tupeer, ok := dp.Peer.(*net.UDPAddr)\n\tif !ok {\n\t\th.Log.Error(errors.New(\"peer is not a UDP connection\"), \"not able to respond when the peer is not a UDP connection\")\n\t\treturn\n\t}\n\tif upeer == nil {\n\t\th.Log.Error(errors.New(\"peer is nil\"), \"not able to respond when the peer is nil\")\n\t\treturn\n\t}\n\tif conn == nil {\n\t\th.Log.Error(errors.New(\"connection is nil\"), \"not able to respond when the connection is nil\")\n\t\treturn\n\t}\n\n\tvar ifName string\n\tif dp.Md != nil {\n\t\tifName = dp.Md.IfName\n\t}\n\tlog := h.Log.WithValues(\"mac\", dp.Pkt.ClientHWAddr.String(), \"xid\", dp.Pkt.TransactionID.String(), \"interface\", ifName)\n\ttracer := otel.Tracer(tracerName)\n\tvar span trace.Span\n\tctx, span = tracer.Start(\n\t\tctx,\n\t\tfmt.Sprintf(\"DHCP Packet Received: %v\", dp.Pkt.MessageType().String()),\n\t\ttrace.WithAttributes(h.encodeToAttributes(dp.Pkt, \"request\")...),\n\t\ttrace.WithAttributes(attribute.String(\"DHCP.peer\", dp.Peer.String())),\n\t\ttrace.WithAttributes(attribute.String(\"DHCP.server.ifname\", ifName)),\n\t)\n\n\tdefer span.End()\n\n\t// We ignore the error here because:\n\t// 1. it's only non-nil if the generation of a transaction id (XID) fails.\n\t// 2. We always use the clients transaction id (XID) in responses. See dhcpv4.WithReply().\n\treply, _ := dhcpv4.NewReplyFromRequest(dp.Pkt)\n\n\tif dp.Pkt.OpCode != dhcpv4.OpcodeBootRequest { // TODO(jacobweinstock): dont understand this, found it in an example here: https://github.com/insomniacslk/dhcp/blob/c51060810aaab9c8a0bd1b0fcbf72bc0b91e6427/dhcpv4/server4/server_test.go#L31\n\t\tlog.V(1).Info(\"Ignoring packet\", \"OpCode\", dp.Pkt.OpCode)\n\t\tspan.SetStatus(codes.Ok, \"Ignoring packet: OpCode not BootRequest\")\n\n\t\treturn\n\t}\n\n\tif err := setMessageType(reply, dp.Pkt.MessageType()); err != nil {\n\t\tlog.V(1).Info(\"Ignoring packet\", \"error\", err.Error())\n\t\tspan.SetStatus(codes.Ok, err.Error())\n\n\t\treturn\n\t}\n\n\t// Set option 97\n\treply.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, dp.Pkt.GetOneOption(dhcpv4.OptionClientMachineIdentifier)))\n\n\ti := dhcp.NewInfo(dp.Pkt)\n\n\tif !h.Netboot.Enabled {\n\t\tlog.V(1).Info(\"Ignoring packet: netboot is not enabled\")\n\t\tspan.SetStatus(codes.Ok, \"Ignoring packet: netboot is not enabled\")\n\n\t\treturn\n\t}\n\tif err := i.IsNetbootClient; err != nil {\n\t\tlog.V(1).Info(\"Ignoring packet: not from a PXE enabled client\", \"error\", err.Error())\n\t\tspan.SetStatus(codes.Ok, fmt.Sprintf(\"Ignoring packet: not from a PXE enabled client: %s\", err.Error()))\n\n\t\treturn\n\t}\n\tif i.IPXEBinary == \"\" {\n\t\tlog.V(1).Info(\"Ignoring packet: no iPXE binary was able to be determined\")\n\t\tspan.SetStatus(codes.Ok, \"Ignoring packet: no iPXE binary was able to be determined\")\n\n\t\treturn\n\t}\n\n\t// Set option 43\n\topts := dhcpv4.Options{6: []byte{8}} // PXE Boot Server Discovery Control - bypass, just boot from dhcp header: bootfile. No need to set opt for tftp server address.\n\treply.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, i.AddRPIOpt43(opts)))\n\n\t// Set option 60\n\t// The PXE spec says the server should identify itself as a PXEClient or HTTPClient\n\treply.UpdateOption(dhcpv4.OptClassIdentifier(i.ClientTypeFrom().String()))\n\n\t// Set option 54, without this the pxe client will try to broadcast a request message to port 4011 for the ipxe binary. only found to be needed for PXEClient but not prohibitive for HTTPClient.\n\t// probably will want this to be the public IP of the proxyDHCP server\n\tns := i.NextServer(h.Netboot.IPXEBinServerHTTP, h.Netboot.IPXEBinServerTFTP)\n\treply.UpdateOption(dhcpv4.OptServerIdentifier(ns))\n\t// add the siaddr (IP address of next server) dhcp packet header to a given packet pkt.\n\t// see https://datatracker.ietf.org/doc/html/rfc2131#section-2\n\t// without this the pxe client will try to broadcast a request message to port 4011 for the ipxe script. The value doesnt seem to matter.\n\treply.ServerIPAddr = ns\n\n\t// set sname header\n\t// see https://datatracker.ietf.org/doc/html/rfc2131#section-2\n\treply.ServerHostName = ns.String()\n\t// setSNAME(reply, dp.Pkt.GetOneOption(dhcpv4.OptionClassIdentifier), h.Netboot.IPXEBinServerTFTP.Addr().AsSlice(), net.ParseIP(h.Netboot.IPXEBinServerHTTP.Hostname()))\n\n\t// set bootfile header\n\treply.BootFileName = i.Bootfile(\"\", h.Netboot.IPXEScriptURL(dp.Pkt), h.Netboot.IPXEBinServerHTTP, h.Netboot.IPXEBinServerTFTP)\n\n\tif !h.AutoProxyEnabled {\n\t\t// check the backend, if PXE is NOT allowed, set the boot file name to \"/<mac address>/not-allowed\"\n\t\t_, n, err := h.Backend.GetByMac(ctx, dp.Pkt.ClientHWAddr)\n\t\tif err != nil || (n != nil && !n.AllowNetboot) {\n\t\t\tl := log.V(1)\n\t\t\tif err != nil {\n\t\t\t\tl = l.WithValues(\"error\", err.Error())\n\t\t\t}\n\t\t\tif n != nil {\n\t\t\t\tl = l.WithValues(\"netbootAllowed\", n.AllowNetboot)\n\t\t\t}\n\t\t\tl.Info(\"Ignoring packet\")\n\t\t\tspan.SetStatus(codes.Ok, \"netboot not allowed\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tlog.Info(\n\t\t\"received DHCP packet\",\n\t\t\"type\", dp.Pkt.MessageType().String(),\n\t\t\"clientType\", i.ClientTypeFrom().String(),\n\t\t\"userClass\", i.UserClassFrom().String(),\n\t)\n\n\tdst := replyDestination(dp.Peer, dp.Pkt.GatewayIPAddr)\n\tcm := &ipv4.ControlMessage{}\n\tif dp.Md != nil {\n\t\tcm.IfIndex = dp.Md.IfIndex\n\t}\n\tlog = log.WithValues(\n\t\t\"destination\", dst.String(),\n\t\t\"bootFileName\", reply.BootFileName,\n\t\t\"nextServer\", reply.ServerIPAddr.String(),\n\t\t\"messageType\", reply.MessageType().String(),\n\t\t\"serverHostname\", reply.ServerHostName,\n\t)\n\t// send the DHCP packet\n\tif _, err := conn.WriteTo(reply.ToBytes(), cm, dst); err != nil {\n\t\tlog.Error(err, \"failed to send ProxyDHCP response\")\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn\n\t}\n\tlog.Info(\"Sent ProxyDHCP response\")\n\tspan.SetAttributes(h.encodeToAttributes(reply, \"reply\")...)\n\tspan.SetStatus(codes.Ok, \"sent DHCP response\")\n}\n\n// encodeToAttributes takes a DHCP packet and returns opentelemetry key/value attributes.\nfunc (h *Handler) encodeToAttributes(d *dhcpv4.DHCPv4, namespace string) []attribute.KeyValue {\n\ta := &oteldhcp.Encoder{Log: h.Log}\n\n\treturn a.Encode(d, namespace, oteldhcp.AllEncoders()...)\n}\n\nfunc setMessageType(reply *dhcpv4.DHCPv4, reqMsg dhcpv4.MessageType) error {\n\tswitch mt := reqMsg; mt {\n\tcase dhcpv4.MessageTypeDiscover:\n\t\treply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))\n\tcase dhcpv4.MessageTypeRequest:\n\t\treply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))\n\tdefault:\n\t\treturn IgnorePacketError{PacketType: mt, Details: \"proxyDHCP only responds to Discover or Request message types\"}\n\t}\n\treturn nil\n}\n\n// IgnorePacketError is for when a DHCP packet should be ignored.\ntype IgnorePacketError struct {\n\tPacketType dhcpv4.MessageType\n\tDetails    string\n}\n\n// Error returns the string representation of ErrIgnorePacket.\nfunc (e IgnorePacketError) Error() string {\n\treturn fmt.Sprintf(\"Ignoring packet: message type %s: details %s\", e.PacketType, e.Details)\n}\n\n// replyDestination determines the destination address for the DHCP reply.\n// If the giaddr is set, then the reply should be sent to the giaddr.\n// Otherwise, the reply should be sent to the direct peer.\n//\n// From page 22 of https://www.ietf.org/rfc/rfc2131.txt:\n// \"If the 'giaddr' field in a DHCP message from a client is non-zero,\n// the server sends any return messages to the 'DHCP server' port on\n// the BOOTP relay agent whose address appears in 'giaddr'.\".\nfunc replyDestination(directPeer net.Addr, giaddr net.IP) net.Addr {\n\tif !giaddr.IsUnspecified() && giaddr != nil {\n\t\treturn &net.UDPAddr{IP: giaddr, Port: dhcpv4.ServerPort}\n\t}\n\n\treturn directPeer\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/handler.go",
    "content": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\toteldhcp \"github.com/tinkerbell/smee/internal/dhcp/otel\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"golang.org/x/net/ipv4\"\n)\n\nconst tracerName = \"github.com/tinkerbell/smee\"\n\n// setDefaults will update the Handler struct to have default values so as\n// to avoid panic for nil pointers and such.\nfunc (h *Handler) setDefaults() {\n\tif h.Backend == nil {\n\t\th.Backend = noop{}\n\t}\n\tif h.Log.GetSink() == nil {\n\t\th.Log = logr.Discard()\n\t}\n}\n\n// Handle responds to DHCP messages with DHCP server options.\nfunc (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, p data.Packet) {\n\th.setDefaults()\n\tif p.Pkt == nil {\n\t\th.Log.Error(errors.New(\"incoming packet is nil\"), \"not able to respond when the incoming packet is nil\")\n\t\treturn\n\t}\n\tupeer, ok := p.Peer.(*net.UDPAddr)\n\tif !ok {\n\t\th.Log.Error(errors.New(\"peer is not a UDP connection\"), \"not able to respond when the peer is not a UDP connection\")\n\t\treturn\n\t}\n\tif upeer == nil {\n\t\th.Log.Error(errors.New(\"peer is nil\"), \"not able to respond when the peer is nil\")\n\t\treturn\n\t}\n\tif conn == nil {\n\t\th.Log.Error(errors.New(\"connection is nil\"), \"not able to respond when the connection is nil\")\n\t\treturn\n\t}\n\n\tvar ifName string\n\tif p.Md != nil {\n\t\tifName = p.Md.IfName\n\t}\n\tlog := h.Log.WithValues(\"mac\", p.Pkt.ClientHWAddr.String(), \"xid\", p.Pkt.TransactionID.String(), \"interface\", ifName)\n\ttracer := otel.Tracer(tracerName)\n\tvar span trace.Span\n\tctx, span = tracer.Start(\n\t\tctx,\n\t\tfmt.Sprintf(\"DHCP Packet Received: %v\", p.Pkt.MessageType().String()),\n\t\ttrace.WithAttributes(h.encodeToAttributes(p.Pkt, \"request\")...),\n\t\ttrace.WithAttributes(attribute.String(\"DHCP.peer\", p.Peer.String())),\n\t\ttrace.WithAttributes(attribute.String(\"DHCP.server.ifname\", ifName)),\n\t)\n\n\tdefer span.End()\n\n\tvar reply *dhcpv4.DHCPv4\n\tswitch mt := p.Pkt.MessageType(); mt {\n\tcase dhcpv4.MessageTypeDiscover:\n\t\td, n, err := h.readBackend(ctx, p.Pkt.ClientHWAddr)\n\t\tif err != nil {\n\t\t\tif hardwareNotFound(err) {\n\t\t\t\tspan.SetStatus(codes.Ok, \"no reservation found\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Info(\"error reading from backend\", \"error\", err)\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\treturn\n\t\t}\n\t\tif d.Disabled {\n\t\t\tlog.Info(\"DHCP is disabled for this MAC address, no response sent\", \"type\", p.Pkt.MessageType().String())\n\t\t\tspan.SetStatus(codes.Ok, \"disabled DHCP response\")\n\n\t\t\treturn\n\t\t}\n\t\tlog.Info(\"received DHCP packet\", \"type\", p.Pkt.MessageType().String())\n\t\treply = h.updateMsg(ctx, p.Pkt, d, n, dhcpv4.MessageTypeOffer)\n\t\tlog = log.WithValues(\"type\", dhcpv4.MessageTypeOffer.String())\n\tcase dhcpv4.MessageTypeRequest:\n\t\td, n, err := h.readBackend(ctx, p.Pkt.ClientHWAddr)\n\t\tif err != nil {\n\t\t\tif hardwareNotFound(err) {\n\t\t\t\tspan.SetStatus(codes.Ok, \"no reservation found\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Info(\"error reading from backend\", \"error\", err)\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\treturn\n\t\t}\n\t\tif d.Disabled {\n\t\t\tlog.Info(\"DHCP is disabled for this MAC address, no response sent\", \"type\", p.Pkt.MessageType().String())\n\t\t\tspan.SetStatus(codes.Ok, \"disabled DHCP response\")\n\n\t\t\treturn\n\t\t}\n\t\tlog.Info(\"received DHCP packet\", \"type\", p.Pkt.MessageType().String())\n\t\treply = h.updateMsg(ctx, p.Pkt, d, n, dhcpv4.MessageTypeAck)\n\t\tlog = log.WithValues(\"type\", dhcpv4.MessageTypeAck.String())\n\tcase dhcpv4.MessageTypeRelease:\n\t\t// Since the design of this DHCP server is that all IP addresses are\n\t\t// Host reservations, when a client releases an address, the server\n\t\t// doesn't have anything to do. This case is included for clarity of this\n\t\t// design decision.\n\t\tlog.Info(\"received DHCP release packet, no response required, all IPs are host reservations\", \"type\", p.Pkt.MessageType().String())\n\t\tspan.SetStatus(codes.Ok, \"received release, no response required\")\n\n\t\treturn\n\tdefault:\n\t\tlog.Info(\"received unknown message type\", \"type\", p.Pkt.MessageType().String())\n\t\tspan.SetStatus(codes.Error, \"received unknown message type\")\n\n\t\treturn\n\t}\n\n\tif bf := reply.BootFileName; bf != \"\" {\n\t\tlog = log.WithValues(\"bootFileName\", bf)\n\t}\n\tif ns := reply.ServerIPAddr; ns != nil {\n\t\tlog = log.WithValues(\"nextServer\", ns.String())\n\t}\n\n\tdst := replyDestination(p.Peer, p.Pkt.GatewayIPAddr)\n\tlog = log.WithValues(\"ipAddress\", reply.YourIPAddr.String(), \"destination\", dst.String())\n\tcm := &ipv4.ControlMessage{}\n\tif p.Md != nil {\n\t\tcm.IfIndex = p.Md.IfIndex\n\t}\n\n\tif _, err := conn.WriteTo(reply.ToBytes(), cm, dst); err != nil {\n\t\tlog.Error(err, \"failed to send DHCP\")\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn\n\t}\n\n\tlog.Info(\"sent DHCP response\")\n\tspan.SetAttributes(h.encodeToAttributes(reply, \"reply\")...)\n\tspan.SetStatus(codes.Ok, \"sent DHCP response\")\n}\n\n// replyDestination determines the destination address for the DHCP reply.\n// If the giaddr is set, then the reply should be sent to the giaddr.\n// Otherwise, the reply should be sent to the direct peer.\n//\n// From page 22 of https://www.ietf.org/rfc/rfc2131.txt:\n// \"If the 'giaddr' field in a DHCP message from a client is non-zero,\n// the server sends any return messages to the 'DHCP server' port on\n// the BOOTP relay agent whose address appears in 'giaddr'.\".\nfunc replyDestination(directPeer net.Addr, giaddr net.IP) net.Addr {\n\tif !giaddr.IsUnspecified() && giaddr != nil {\n\t\treturn &net.UDPAddr{IP: giaddr, Port: dhcpv4.ServerPort}\n\t}\n\n\treturn directPeer\n}\n\n// readBackend encapsulates the backend read and opentelemetry handling.\nfunc (h *Handler) readBackend(ctx context.Context, mac net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\th.setDefaults()\n\n\ttracer := otel.Tracer(tracerName)\n\tctx, span := tracer.Start(ctx, \"Hardware data get\")\n\tdefer span.End()\n\n\td, n, err := h.Backend.GetByMac(ctx, mac)\n\tif err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn nil, nil, err\n\t}\n\n\tspan.SetAttributes(d.EncodeToAttributes()...)\n\tspan.SetAttributes(n.EncodeToAttributes()...)\n\tspan.SetStatus(codes.Ok, \"done reading from backend\")\n\n\treturn d, n, nil\n}\n\n// updateMsg handles updating DHCP packets with the data from the backend.\nfunc (h *Handler) updateMsg(ctx context.Context, pkt *dhcpv4.DHCPv4, d *data.DHCP, n *data.Netboot, msgType dhcpv4.MessageType) *dhcpv4.DHCPv4 {\n\th.setDefaults()\n\tmods := []dhcpv4.Modifier{\n\t\tdhcpv4.WithMessageType(msgType),\n\t\tdhcpv4.WithGeneric(dhcpv4.OptionServerIdentifier, h.IPAddr.AsSlice()),\n\t\tdhcpv4.WithServerIP(h.IPAddr.AsSlice()),\n\t}\n\tmods = append(mods, h.setDHCPOpts(ctx, pkt, d)...)\n\n\tif h.Netboot.Enabled && dhcp.IsNetbootClient(pkt) == nil {\n\t\tmods = append(mods, h.setNetworkBootOpts(ctx, pkt, n))\n\t}\n\t// We ignore the error here because:\n\t// 1. it's only non-nil if the generation of a transaction id (XID) fails.\n\t// 2. We always use the clients transaction id (XID) in responses. See dhcpv4.WithReply().\n\treply, _ := dhcpv4.NewReplyFromRequest(pkt, mods...)\n\n\treturn reply\n}\n\n// encodeToAttributes takes a DHCP packet and returns opentelemetry key/value attributes.\nfunc (h *Handler) encodeToAttributes(d *dhcpv4.DHCPv4, namespace string) []attribute.KeyValue {\n\th.setDefaults()\n\ta := &oteldhcp.Encoder{Log: h.Log}\n\n\treturn a.Encode(d, namespace, oteldhcp.AllEncoders()...)\n}\n\n// hardwareNotFound returns true if the error is from a hardware record not being found.\nfunc hardwareNotFound(err error) bool {\n\ttype hardwareNotFound interface {\n\t\tNotFound() bool\n\t}\n\tte, ok := err.(hardwareNotFound)\n\treturn ok && te.NotFound()\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/handler_test.go",
    "content": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-logr/stdr\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n\t\"github.com/insomniacslk/dhcp/rfc1035label\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"golang.org/x/net/ipv4\"\n\t\"golang.org/x/net/nettest\"\n)\n\nvar errBadBackend = fmt.Errorf(\"bad backend\")\n\ntype mockBackend struct {\n\terr              error\n\tallowNetboot     bool\n\tipxeScript       *url.URL\n\thardwareNotFound bool\n}\n\ntype hwNotFoundError struct{}\n\nfunc (hwNotFoundError) NotFound() bool { return true }\nfunc (hwNotFoundError) Error() string  { return \"not found\" }\n\nfunc (m *mockBackend) GetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\tif m.err != nil {\n\t\treturn nil, nil, m.err\n\t}\n\tif m.hardwareNotFound {\n\t\treturn nil, nil, hwNotFoundError{}\n\t}\n\td := &data.DHCP{\n\t\tMACAddress:     []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\tIPAddress:      netip.MustParseAddr(\"192.168.1.100\"),\n\t\tSubnetMask:     []byte{255, 255, 255, 0},\n\t\tDefaultGateway: netip.MustParseAddr(\"192.168.1.1\"),\n\t\tNameServers: []net.IP{\n\t\t\t{1, 1, 1, 1},\n\t\t},\n\t\tHostname:         \"test-host\",\n\t\tDomainName:       \"mydomain.com\",\n\t\tBroadcastAddress: netip.MustParseAddr(\"192.168.1.255\"),\n\t\tNTPServers: []net.IP{\n\t\t\t{132, 163, 96, 2},\n\t\t},\n\t\tLeaseTime: 60,\n\t\tDomainSearch: []string{\n\t\t\t\"mydomain.com\",\n\t\t},\n\t}\n\tn := &data.Netboot{\n\t\tAllowNetboot:  m.allowNetboot,\n\t\tIPXEScriptURL: m.ipxeScript,\n\t}\n\n\treturn d, n, m.err\n}\n\nfunc (m *mockBackend) GetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboot, error) {\n\tif m.hardwareNotFound {\n\t\treturn nil, nil, hwNotFoundError{}\n\t}\n\treturn nil, nil, errors.New(\"not implemented\")\n}\n\nfunc TestHandle(t *testing.T) {\n\ttests := map[string]struct {\n\t\tserver  Handler\n\t\treq     *dhcpv4.DHCPv4\n\t\twant    *dhcpv4.DHCPv4\n\t\twantErr error\n\t\tnilPeer bool\n\t}{\n\t\t\"success discover message type with netboot options\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{\n\t\t\t\t\tallowNetboot: true,\n\t\t\t\t\tipxeScript:   &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"},\n\t\t\t\t},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t\tNetboot: Netboot{Enabled: true},\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t\tdhcpv4.OptUserClass(\"Tinkerbell\"),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_X86_64_HTTP),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05}),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:        dhcpv4.OpcodeBootReply,\n\t\t\t\tClientHWAddr:  []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tClientIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tYourIPAddr:    []byte{192, 168, 1, 100},\n\t\t\t\tServerIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tGatewayIPAddr: []byte{0, 0, 0, 0},\n\t\t\t\tBootFileName:  \"http://localhost:8181/auto.ipxe\",\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),\n\t\t\t\t\tdhcpv4.OptServerIdentifier(net.IP{127, 0, 0, 1}),\n\t\t\t\t\tdhcpv4.OptIPAddressLeaseTime(time.Minute),\n\t\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask(net.IP{255, 255, 255, 0}.To4())),\n\t\t\t\t\tdhcpv4.OptRouter([]net.IP{{192, 168, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDNS([]net.IP{{1, 1, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDomainName(\"mydomain.com\"),\n\t\t\t\t\tdhcpv4.OptHostName(\"test-host\"),\n\t\t\t\t\tdhcpv4.OptBroadcastAddress(net.IP{192, 168, 1, 255}),\n\t\t\t\t\tdhcpv4.OptNTPServers([]net.IP{{132, 163, 96, 2}}...),\n\t\t\t\t\tdhcpv4.OptDomainSearch(&rfc1035label.Labels{Labels: []string{\"mydomain.com\"}}),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, dhcpv4.Options{\n\t\t\t\t\t\t6:  []byte{8},\n\t\t\t\t\t\t69: otel.TraceparentFromContext(context.Background()),\n\t\t\t\t\t}.ToBytes()),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\"failure discover message type\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{err: errBadBackend},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t\"success request message type with netboot options\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{\n\t\t\t\t\tallowNetboot: true,\n\t\t\t\t\tipxeScript:   &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"},\n\t\t\t\t},\n\t\t\t\tNetboot: Netboot{Enabled: true},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:        dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr:  []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tClientIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tYourIPAddr:    []byte{192, 168, 1, 100},\n\t\t\t\tServerIPAddr:  []byte{127, 0, 0, 1},\n\t\t\t\tGatewayIPAddr: []byte{0, 0, 0, 0},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeRequest),\n\t\t\t\t\tdhcpv4.OptServerIdentifier(net.IP{127, 0, 0, 1}),\n\t\t\t\t\tdhcpv4.OptIPAddressLeaseTime(time.Minute),\n\t\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask(net.IP{255, 255, 255, 0}.To4())),\n\t\t\t\t\tdhcpv4.OptRouter([]net.IP{{192, 168, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDNS([]net.IP{{1, 1, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDomainName(\"mydomain.com\"),\n\t\t\t\t\tdhcpv4.OptHostName(\"test-host\"),\n\t\t\t\t\tdhcpv4.OptBroadcastAddress(net.IP{192, 168, 1, 255}),\n\t\t\t\t\tdhcpv4.OptNTPServers([]net.IP{{132, 163, 96, 2}}...),\n\t\t\t\t\tdhcpv4.OptDomainSearch(&rfc1035label.Labels{Labels: []string{\"mydomain.com\"}}),\n\t\t\t\t\tdhcpv4.OptUserClass(\"Tinkerbell\"),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:Arch:xxxxx:UNDI:yyyzzz\"),\n\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_X86_64_HTTP),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05}),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:        dhcpv4.OpcodeBootReply,\n\t\t\t\tClientHWAddr:  []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tClientIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tYourIPAddr:    []byte{192, 168, 1, 100},\n\t\t\t\tServerIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tGatewayIPAddr: []byte{0, 0, 0, 0},\n\t\t\t\tBootFileName:  \"http://localhost:8181/auto.ipxe\",\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeAck),\n\t\t\t\t\tdhcpv4.OptServerIdentifier(net.IP{127, 0, 0, 1}),\n\t\t\t\t\tdhcpv4.OptIPAddressLeaseTime(time.Minute),\n\t\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask(net.IP{255, 255, 255, 0}.To4())),\n\t\t\t\t\tdhcpv4.OptRouter([]net.IP{{192, 168, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDNS([]net.IP{{1, 1, 1, 1}}...),\n\t\t\t\t\tdhcpv4.OptDomainName(\"mydomain.com\"),\n\t\t\t\t\tdhcpv4.OptHostName(\"test-host\"),\n\t\t\t\t\tdhcpv4.OptBroadcastAddress(net.IP{192, 168, 1, 255}),\n\t\t\t\t\tdhcpv4.OptNTPServers([]net.IP{{132, 163, 96, 2}}...),\n\t\t\t\t\tdhcpv4.OptDomainSearch(&rfc1035label.Labels{Labels: []string{\"mydomain.com\"}}),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, dhcpv4.Options{\n\t\t\t\t\t\t6:  []byte{8},\n\t\t\t\t\t\t69: otel.TraceparentFromContext(context.Background()),\n\t\t\t\t\t}.ToBytes()),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\"failure request message type\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{err: errBadBackend},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeRequest),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t\"request release type\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{err: errBadBackend},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeRelease),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t\"unknown message type\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{err: errBadBackend},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeInform),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t\"fail WriteTo\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{},\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantErr: errBadBackend,\n\t\t\tnilPeer: true,\n\t\t},\n\t\t\"nil incoming packet\": {\n\t\t\twant:    nil,\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t/*\"nil incoming packet\": {\n\t\t\twant:    nil,\n\t\t\twantErr: errBadBackend,\n\t\t},*/\n\t\t\"failure no hardware found discover\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{hardwareNotFound: true},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t\t\"failure no hardware found request\": {\n\t\t\tserver: Handler{\n\t\t\t\tBackend: &mockBackend{hardwareNotFound: true},\n\t\t\t\tIPAddr:  netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t},\n\t\t\treq: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeRequest),\n\t\t\t\t),\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts := tt.server\n\t\t\tconn, err := nettest.NewLocalPacketListener(\"udp\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"1\", err)\n\t\t\t}\n\t\t\tdefer conn.Close()\n\n\t\t\tpc, err := net.ListenPacket(\"udp4\", \":0\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"2\", err)\n\t\t\t}\n\t\t\tdefer pc.Close()\n\t\t\tpeer := &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: pc.LocalAddr().(*net.UDPAddr).Port}\n\t\t\tif tt.nilPeer {\n\t\t\t\tpeer = nil\n\t\t\t}\n\n\t\t\tcon := ipv4.NewPacketConn(conn)\n\t\t\tcon.SetControlMessage(ipv4.FlagInterface, true)\n\n\t\t\tn, err := net.InterfaceByName(\"lo\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\ts.Handle(context.Background(), con, data.Packet{Peer: peer, Pkt: tt.req, Md: &data.Metadata{IfName: n.Name, IfIndex: n.Index}})\n\n\t\t\tmsg, err := client(pc)\n\t\t\tif !errors.Is(err, tt.wantErr) {\n\t\t\t\tt.Fatalf(\"client() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(msg, tt.want, cmpopts.IgnoreUnexported(dhcpv4.DHCPv4{})); diff != \"\" {\n\t\t\t\tt.Fatal(\"diff\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc client(pc net.PacketConn) (*dhcpv4.DHCPv4, error) {\n\tbuf := make([]byte, 1024)\n\tpc.SetReadDeadline(time.Now().Add(time.Millisecond * 100))\n\tif _, _, err := pc.ReadFrom(buf); err != nil {\n\t\treturn nil, errBadBackend\n\t}\n\tmsg, err := dhcpv4.FromBytes(buf)\n\tif err != nil {\n\t\treturn nil, errBadBackend\n\t}\n\n\treturn msg, nil\n}\n\nfunc TestUpdateMsg(t *testing.T) {\n\ttype args struct {\n\t\tm       *dhcpv4.DHCPv4\n\t\tdata    *data.DHCP\n\t\tnetboot *data.Netboot\n\t\tmsg     dhcpv4.MessageType\n\t}\n\ttests := map[string]struct {\n\t\targs    args\n\t\twant    *dhcpv4.DHCPv4\n\t\twantErr bool\n\t}{\n\t\t\"success\": {\n\t\t\targs: args{\n\t\t\t\tm: &dhcpv4.DHCPv4{\n\t\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptUserClass(\"Tinkerbell\"),\n\t\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05}),\n\t\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tdata:    &data.DHCP{IPAddress: netip.MustParseAddr(\"192.168.1.100\"), SubnetMask: net.IPMask(net.IP{255, 255, 255, 0}.To4())},\n\t\t\t\tnetboot: &data.Netboot{AllowNetboot: true, IPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"}},\n\t\t\t\tmsg:     dhcpv4.MessageTypeDiscover,\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootReply,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tYourIPAddr:   []byte{192, 168, 1, 100},\n\t\t\t\tClientIPAddr: []byte{0, 0, 0, 0},\n\t\t\t\tBootFileName: \"http://localhost:8181/auto.ipxe\",\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t\tdhcpv4.OptServerIdentifier(net.IP{127, 0, 0, 1}),\n\t\t\t\t\tdhcpv4.OptIPAddressLeaseTime(3600),\n\t\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask(net.IP{255, 255, 255, 0}.To4())),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, dhcpv4.Options{\n\t\t\t\t\t\t6:  []byte{8},\n\t\t\t\t\t\t69: otel.TraceparentFromContext(context.Background()),\n\t\t\t\t\t}.ToBytes()),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts := &Handler{\n\t\t\t\tLog:    stdr.New(log.New(os.Stdout, \"\", log.Lshortfile)),\n\t\t\t\tIPAddr: netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t\tNetboot: Netboot{\n\t\t\t\t\tEnabled: true,\n\t\t\t\t},\n\t\t\t\tBackend: &mockBackend{\n\t\t\t\t\tallowNetboot: true,\n\t\t\t\t\tipxeScript:   &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"},\n\t\t\t\t},\n\t\t\t\t// Listener: netip.AddrPortFrom(netip.MustParseAddr(\"127.0.0.1\"), 67),\n\t\t\t}\n\t\t\tgot := s.updateMsg(context.Background(), tt.args.m, tt.args.data, tt.args.netboot, tt.args.msg)\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(dhcpv4.DHCPv4{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOne(t *testing.T) {\n\tt.Skip()\n\th := &Handler{}\n\t_, _, err := h.readBackend(context.Background(), nil)\n\tt.Fatal(err)\n}\n\nfunc TestReadBackend(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput       *dhcpv4.DHCPv4\n\t\twantDHCP    *data.DHCP\n\t\twantNetboot *data.Netboot\n\t\twantErr     error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:       dhcpv4.OpcodeBootRequest,\n\t\t\t\tClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptUserClass(\"Tinkerbell\"),\n\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x02, 0x03, 0x04, 0x05}),\n\t\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeDiscover),\n\t\t\t\t),\n\t\t\t},\n\t\t\twantDHCP: &data.DHCP{\n\t\t\t\tMACAddress:       []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\tIPAddress:        netip.MustParseAddr(\"192.168.1.100\"),\n\t\t\t\tSubnetMask:       []byte{255, 255, 255, 0},\n\t\t\t\tDefaultGateway:   netip.MustParseAddr(\"192.168.1.1\"),\n\t\t\t\tNameServers:      []net.IP{{1, 1, 1, 1}},\n\t\t\t\tHostname:         \"test-host\",\n\t\t\t\tDomainName:       \"mydomain.com\",\n\t\t\t\tBroadcastAddress: netip.MustParseAddr(\"192.168.1.255\"),\n\t\t\t\tNTPServers:       []net.IP{{132, 163, 96, 2}},\n\t\t\t\tLeaseTime:        60,\n\t\t\t\tDomainSearch:     []string{\"mydomain.com\"},\n\t\t\t},\n\t\t\twantNetboot: &data.Netboot{AllowNetboot: true, IPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"}},\n\t\t\twantErr:     nil,\n\t\t},\n\t\t\"failure\": {\n\t\t\tinput:   &dhcpv4.DHCPv4{},\n\t\t\twantErr: errBadBackend,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts := &Handler{\n\t\t\t\tLog:    stdr.New(log.New(os.Stdout, \"\", log.Lshortfile)),\n\t\t\t\tIPAddr: netip.MustParseAddr(\"127.0.0.1\"),\n\t\t\t\tNetboot: Netboot{\n\t\t\t\t\tEnabled: true,\n\t\t\t\t},\n\t\t\t\tBackend: &mockBackend{\n\t\t\t\t\terr:          tt.wantErr,\n\t\t\t\t\tallowNetboot: true,\n\t\t\t\t\tipxeScript:   &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"auto.ipxe\"},\n\t\t\t\t},\n\t\t\t\t// Listener: netip.AddrPortFrom(netip.MustParseAddr(\"127.0.0.1\"), 67),\n\t\t\t}\n\t\t\tnetaddrComparer := cmp.Comparer(func(x, y netip.Addr) bool {\n\t\t\t\ti := x.Compare(y)\n\t\t\t\treturn i == 0\n\t\t\t})\n\t\t\tgotDHCP, gotNetboot, err := s.readBackend(context.Background(), tt.input.ClientHWAddr)\n\t\t\tif !errors.Is(err, tt.wantErr) {\n\t\t\t\tt.Fatalf(\"gotErr: %v, wantErr: %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(gotDHCP, tt.wantDHCP, netaddrComparer); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(gotNetboot, tt.wantNetboot); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncodeToAttributes(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    []attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{BootFileName: \"snp.efi\"},\n\t\t\twant: []attribute.KeyValue{\n\t\t\t\tattribute.String(\"DHCP.testing.Header.file\", \"snp.efi\"),\n\t\t\t\tattribute.String(\"DHCP.testing.Header.flags\", \"Unicast\"),\n\t\t\t\tattribute.String(\"DHCP.testing.Header.transactionID\", \"0x00000000\"),\n\t\t\t},\n\t\t},\n\t\t\"error\": {},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tstdr.SetVerbosity(1)\n\t\t\ts := &Handler{Log: stdr.New(log.New(os.Stdout, \"\", log.Lshortfile))}\n\t\t\tkvs := s.encodeToAttributes(tt.input, \"testing\")\n\t\t\tgot := attribute.NewSet(kvs...)\n\t\t\twant := attribute.NewSet(tt.want...)\n\t\t\tenc := attribute.DefaultEncoder()\n\t\t\tif diff := cmp.Diff(got.Encoded(enc), want.Encoded(enc)); diff != \"\" {\n\t\t\t\tt.Log(got.Encoded(enc))\n\t\t\t\tt.Log(want.Encoded(enc))\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/noop.go",
    "content": "// Package noop is a backend handler that does nothing.\npackage reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\n// Handler is a noop backend.\ntype noop struct{}\n\n// GetByMac returns an error.\nfunc (h noop) GetByMac(_ context.Context, _ net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\treturn nil, nil, errors.New(\"no backend specified, please specify a backend\")\n}\n\n// GetByIP returns an error.\nfunc (h noop) GetByIP(_ context.Context, _ net.IP) (*data.DHCP, *data.Netboot, error) {\n\treturn nil, nil, errors.New(\"no backend specified, please specify a backend\")\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/noop_test.go",
    "content": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestNoop(t *testing.T) {\n\twant := errors.New(\"no backend specified, please specify a backend\")\n\t_, _, got := noop{}.GetByMac(context.TODO(), nil)\n\tif diff := cmp.Diff(want.Error(), got.Error()); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n\t_, _, got = noop{}.GetByIP(context.TODO(), nil)\n\tif diff := cmp.Diff(want.Error(), got.Error()); diff != \"\" {\n\t\tt.Fatal(diff)\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/option.go",
    "content": "package reservation\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\tdhcpotel \"github.com/tinkerbell/smee/internal/dhcp/otel\"\n\t\"github.com/tinkerbell/smee/internal/otel\"\n)\n\n// setDHCPOpts takes a client dhcp packet and data (typically from a backend) and creates a slice of DHCP packet modifiers.\n// m is the DHCP request from a client. d is the data to use to create the DHCP packet modifiers.\n// This is most likely the place where we would have any business logic for determining DHCP option setting.\nfunc (h *Handler) setDHCPOpts(_ context.Context, _ *dhcpv4.DHCPv4, d *data.DHCP) []dhcpv4.Modifier {\n\tmods := []dhcpv4.Modifier{\n\t\tdhcpv4.WithLeaseTime(d.LeaseTime),\n\t\tdhcpv4.WithYourIP(d.IPAddress.AsSlice()),\n\t}\n\tif len(d.NameServers) > 0 {\n\t\tmods = append(mods, dhcpv4.WithDNS(d.NameServers...))\n\t}\n\tif len(d.DomainSearch) > 0 {\n\t\tmods = append(mods, dhcpv4.WithDomainSearchList(d.DomainSearch...))\n\t}\n\tif len(d.NTPServers) > 0 {\n\t\tmods = append(mods, dhcpv4.WithOption(dhcpv4.OptNTPServers(d.NTPServers...)))\n\t}\n\tif d.BroadcastAddress.Compare(netip.Addr{}) != 0 {\n\t\tmods = append(mods, dhcpv4.WithGeneric(dhcpv4.OptionBroadcastAddress, d.BroadcastAddress.AsSlice()))\n\t}\n\tif d.DomainName != \"\" {\n\t\tmods = append(mods, dhcpv4.WithGeneric(dhcpv4.OptionDomainName, []byte(d.DomainName)))\n\t}\n\tif d.Hostname != \"\" {\n\t\tmods = append(mods, dhcpv4.WithGeneric(dhcpv4.OptionHostName, []byte(d.Hostname)))\n\t}\n\tif len(d.SubnetMask) > 0 {\n\t\tmods = append(mods, dhcpv4.WithNetmask(d.SubnetMask))\n\t}\n\tif d.DefaultGateway.Compare(netip.Addr{}) != 0 {\n\t\tmods = append(mods, dhcpv4.WithRouter(d.DefaultGateway.AsSlice()))\n\t}\n\tif h.SyslogAddr.Compare(netip.Addr{}) != 0 {\n\t\tmods = append(mods, dhcpv4.WithOption(dhcpv4.OptGeneric(dhcpv4.OptionLogServer, h.SyslogAddr.AsSlice())))\n\t}\n\n\treturn mods\n}\n\n// setNetworkBootOpts purpose is to sets 3 or 4 values. 2 DHCP headers, option 43 and optionally option (60).\n// These headers and options are returned as a dhcvp4.Modifier that can be used to modify a dhcp response.\n// github.com/insomniacslk/dhcp uses this method to simplify packet manipulation.\n//\n// DHCP Headers (https://datatracker.ietf.org/doc/html/rfc2131#section-2)\n// 'siaddr': IP address of next bootstrap server. represented below as `.ServerIPAddr`.\n// 'file': Client boot file name. represented below as `.BootFileName`.\n//\n// DHCP option\n// option 60: Class Identifier. https://www.rfc-editor.org/rfc/rfc2132.html#section-9.13\n// option 60 is set if the client's option 60 (Class Identifier) starts with HTTPClient.\nfunc (h *Handler) setNetworkBootOpts(ctx context.Context, m *dhcpv4.DHCPv4, n *data.Netboot) dhcpv4.Modifier {\n\t// m is a received DHCPv4 packet.\n\t// d is the reply packet we are building.\n\twithNetboot := func(d *dhcpv4.DHCPv4) {\n\t\t// if the client sends opt 60 with HTTPClient then we need to respond with opt 60\n\t\t// This is outside of the n.AllowNetboot check because we will be sending \"/netboot-not-allowed\" regardless.\n\t\tif val := m.Options.Get(dhcpv4.OptionClassIdentifier); val != nil {\n\t\t\tif strings.HasPrefix(string(val), dhcp.HTTPClient.String()) {\n\t\t\t\td.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionClassIdentifier, []byte(dhcp.HTTPClient)))\n\t\t\t}\n\t\t}\n\t\td.BootFileName = \"/netboot-not-allowed\"\n\t\td.ServerIPAddr = net.IPv4(0, 0, 0, 0)\n\t\tif n.AllowNetboot {\n\t\t\ti := dhcp.NewInfo(m)\n\t\t\tif i.IPXEBinary == \"\" {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar ipxeScript *url.URL\n\t\t\t// If the global IPXEScriptURL is set, use that.\n\t\t\tif h.Netboot.IPXEScriptURL != nil {\n\t\t\t\tipxeScript = h.Netboot.IPXEScriptURL(m)\n\t\t\t}\n\t\t\t// If the IPXE script URL is set on the hardware record, use that.\n\t\t\tif n.IPXEScriptURL != nil {\n\t\t\t\tipxeScript = n.IPXEScriptURL\n\t\t\t}\n\t\t\td.BootFileName, d.ServerIPAddr = h.bootfileAndNextServer(ctx, m, h.Netboot.UserClass, h.Netboot.IPXEBinServerTFTP, h.Netboot.IPXEBinServerHTTP, ipxeScript)\n\t\t\tpxe := dhcpv4.Options{ // FYI, these are suboptions of option43. ref: https://datatracker.ietf.org/doc/html/rfc2132#section-8.4\n\t\t\t\t// PXE Boot Server Discovery Control - bypass, just boot from filename.\n\t\t\t\t6:  []byte{8},\n\t\t\t\t69: dhcpotel.TraceparentFromContext(ctx),\n\t\t\t}\n\t\t\td.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, i.AddRPIOpt43(pxe)))\n\t\t}\n\t}\n\n\treturn withNetboot\n}\n\n// bootfileAndNextServer returns the bootfile (string) and next server (net.IP).\n// input arguments `tftp`, `ipxe` and `iscript` use non string types so as to attempt to be more clear about the expectation around what is wanted for these values.\n// It also helps us avoid having to validate a string in multiple ways.\nfunc (h *Handler) bootfileAndNextServer(ctx context.Context, pkt *dhcpv4.DHCPv4, customUC dhcp.UserClass, tftp netip.AddrPort, ipxe, iscript *url.URL) (string, net.IP) {\n\tvar nextServer net.IP\n\tvar bootfile string\n\ti := dhcp.NewInfo(pkt)\n\tif tp := otel.TraceparentStringFromContext(ctx); h.OTELEnabled && tp != \"\" {\n\t\ti.IPXEBinary = fmt.Sprintf(\"%s-%v\", i.IPXEBinary, tp)\n\t}\n\tnextServer = i.NextServer(ipxe, tftp)\n\tbootfile = i.Bootfile(customUC, iscript, ipxe, tftp)\n\n\treturn bootfile, nextServer\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/option_test.go",
    "content": "package reservation\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n\t\"github.com/insomniacslk/dhcp/rfc1035label\"\n\t\"github.com/tinkerbell/smee/internal/dhcp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\toteldhcp \"github.com/tinkerbell/smee/internal/dhcp/otel\"\n\tdhcpotel \"github.com/tinkerbell/smee/internal/otel\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/propagation\"\n)\n\nconst (\n\texamplePXEClient  = \"PXEClient:Arch:00007:UNDI:003001\"\n\texampleHTTPClient = \"HTTPClient:Arch:00016:UNDI:003001\"\n)\n\nfunc TestSetDHCPOpts(t *testing.T) {\n\ttype args struct {\n\t\tin0 context.Context\n\t\tm   *dhcpv4.DHCPv4\n\t\td   *data.DHCP\n\t}\n\ttests := map[string]struct {\n\t\tserver Handler\n\t\targs   args\n\t\twant   *dhcpv4.DHCPv4\n\t}{\n\t\t\"success\": {\n\t\t\tserver: Handler{Log: logr.Discard(), SyslogAddr: netip.MustParseAddr(\"192.168.7.7\")},\n\t\t\targs: args{\n\t\t\t\tin0: context.Background(),\n\t\t\t\tm:   &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(dhcpv4.OptParameterRequestList(dhcpv4.OptionSubnetMask))},\n\t\t\t\td: &data.DHCP{\n\t\t\t\t\tMACAddress:     net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\t\t\tIPAddress:      netip.MustParseAddr(\"192.168.4.4\"),\n\t\t\t\t\tSubnetMask:     []byte{255, 255, 255, 0},\n\t\t\t\t\tDefaultGateway: netip.MustParseAddr(\"192.168.4.1\"),\n\t\t\t\t\tNameServers: []net.IP{\n\t\t\t\t\t\t{8, 8, 8, 8},\n\t\t\t\t\t\t{8, 8, 4, 4},\n\t\t\t\t\t},\n\t\t\t\t\tHostname:         \"test-server\",\n\t\t\t\t\tDomainName:       \"mynet.local\",\n\t\t\t\t\tBroadcastAddress: netip.MustParseAddr(\"192.168.4.255\"),\n\t\t\t\t\tNTPServers: []net.IP{\n\t\t\t\t\t\t{132, 163, 96, 2},\n\t\t\t\t\t\t{132, 163, 96, 3},\n\t\t\t\t\t},\n\t\t\t\t\tLeaseTime: 84600,\n\t\t\t\t\tDomainSearch: []string{\n\t\t\t\t\t\t\"mynet.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{\n\t\t\t\tOpCode:        dhcpv4.OpcodeBootRequest,\n\t\t\t\tHWType:        iana.HWTypeEthernet,\n\t\t\t\tClientHWAddr:  net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\t\tClientIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tYourIPAddr:    []byte{192, 168, 4, 4},\n\t\t\t\tServerIPAddr:  []byte{0, 0, 0, 0},\n\t\t\t\tGatewayIPAddr: []byte{0, 0, 0, 0},\n\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionLogServer, []byte{192, 168, 7, 7}),\n\t\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask{255, 255, 255, 0}),\n\t\t\t\t\tdhcpv4.OptBroadcastAddress(net.IP{192, 168, 4, 255}),\n\t\t\t\t\tdhcpv4.OptIPAddressLeaseTime(time.Duration(84600)*time.Second),\n\t\t\t\t\tdhcpv4.OptDomainName(\"mynet.local\"),\n\t\t\t\t\tdhcpv4.OptHostName(\"test-server\"),\n\t\t\t\t\tdhcpv4.OptRouter(net.IP{192, 168, 4, 1}),\n\t\t\t\t\tdhcpv4.OptDNS([]net.IP{\n\t\t\t\t\t\t{8, 8, 8, 8},\n\t\t\t\t\t\t{8, 8, 4, 4},\n\t\t\t\t\t}...),\n\t\t\t\t\tdhcpv4.OptNTPServers([]net.IP{\n\t\t\t\t\t\t{132, 163, 96, 2},\n\t\t\t\t\t\t{132, 163, 96, 3},\n\t\t\t\t\t}...),\n\t\t\t\t\tdhcpv4.OptDomainSearch(&rfc1035label.Labels{\n\t\t\t\t\t\tLabels: []string{\"mynet.local\"},\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts := &Handler{\n\t\t\t\tLog: tt.server.Log,\n\t\t\t\tNetboot: Netboot{\n\t\t\t\t\tIPXEBinServerTFTP: tt.server.Netboot.IPXEBinServerTFTP,\n\t\t\t\t\tIPXEBinServerHTTP: tt.server.Netboot.IPXEBinServerHTTP,\n\t\t\t\t\tIPXEScriptURL:     tt.server.Netboot.IPXEScriptURL,\n\t\t\t\t\tEnabled:           tt.server.Netboot.Enabled,\n\t\t\t\t\tUserClass:         tt.server.Netboot.UserClass,\n\t\t\t\t},\n\t\t\t\tIPAddr:     tt.server.IPAddr,\n\t\t\t\tBackend:    tt.server.Backend,\n\t\t\t\tSyslogAddr: tt.server.SyslogAddr,\n\t\t\t}\n\t\t\tmods := s.setDHCPOpts(tt.args.in0, tt.args.m, tt.args.d)\n\t\t\tfinalPkt, err := dhcpv4.New(mods...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"setDHCPOpts() error = %v, wantErr nil\", err)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, finalPkt, cmpopts.IgnoreFields(dhcpv4.DHCPv4{}, \"TransactionID\")); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBootfileAndNextServer(t *testing.T) {\n\ttype args struct {\n\t\tpkt     *dhcpv4.DHCPv4\n\t\tuClass  dhcp.UserClass\n\t\ttftp    netip.AddrPort\n\t\tipxe    *url.URL\n\t\tiscript *url.URL\n\t}\n\ttests := map[string]struct {\n\t\tserver       *Handler\n\t\targs         args\n\t\totelEnabled  bool\n\t\twantBootFile string\n\t\twantNextSrv  net.IP\n\t}{\n\t\t\"success bootfile only\": {\n\t\t\tserver: &Handler{Log: logr.Discard()},\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptUserClass(dhcp.Tinkerbell.String()),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tiscript: &url.URL{Scheme: \"http\", Host: \"localhost:8080\", Path: \"/auto.ipxe\"},\n\t\t\t},\n\t\t\twantBootFile: \"http://localhost:8080/auto.ipxe\",\n\t\t\twantNextSrv:  nil,\n\t\t},\n\t\t\"success httpClient\": {\n\t\t\tserver: &Handler{Log: logr.Discard()},\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_ARM64_HTTP),\n\t\t\t\t\t\tdhcpv4.OptClassIdentifier(exampleHTTPClient),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tipxe: &url.URL{Scheme: \"http\", Host: \"127.0.0.1:8181\"},\n\t\t\t},\n\t\t\twantBootFile: \"http://127.0.0.1:8181/01:02:03:04:05:06/snp.efi\",\n\t\t\twantNextSrv:  net.IPv4(127, 0, 0, 1),\n\t\t},\n\t\t\"success userclass iPXE\": {\n\t\t\tserver: &Handler{Log: logr.Discard()},\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x07},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.INTEL_X86PC),\n\t\t\t\t\t\tdhcpv4.OptUserClass(dhcp.IPXE.String()),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\ttftp: netip.MustParseAddrPort(\"192.168.6.5:69\"),\n\t\t\t},\n\t\t\twantBootFile: \"tftp://192.168.6.5:69/01:02:03:04:05:07/undionly.kpxe\",\n\t\t\twantNextSrv:  net.ParseIP(\"192.168.6.5\"),\n\t\t},\n\t\t\"success userclass iPXE with otel\": {\n\t\t\tserver:      &Handler{Log: logr.Discard(), OTELEnabled: true},\n\t\t\totelEnabled: true,\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x07},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.INTEL_X86PC),\n\t\t\t\t\t\tdhcpv4.OptUserClass(dhcp.IPXE.String()),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\ttftp: netip.MustParseAddrPort(\"192.168.6.5:69\"),\n\t\t\t\tipxe: &url.URL{Scheme: \"tftp\", Host: \"192.168.6.5:69\"},\n\t\t\t},\n\t\t\twantBootFile: \"tftp://192.168.6.5:69/01:02:03:04:05:07/undionly.kpxe-00-23b1e307bb35484f535a1f772c06910e-d887dc3912240434-01\",\n\t\t\twantNextSrv:  net.ParseIP(\"192.168.6.5\"),\n\t\t},\n\t\t\"success default\": {\n\t\t\tserver: &Handler{Log: logr.Discard()},\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x07},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.INTEL_X86PC),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\ttftp: netip.MustParseAddrPort(\"192.168.6.5:69\"),\n\t\t\t\tipxe: &url.URL{Scheme: \"tftp\", Host: \"192.168.6.5:69\"},\n\t\t\t},\n\t\t\twantBootFile: \"undionly.kpxe\",\n\t\t\twantNextSrv:  net.ParseIP(\"192.168.6.5\"),\n\t\t},\n\t\t\"success otel enabled, no traceparent\": {\n\t\t\tserver: &Handler{Log: logr.Discard(), OTELEnabled: true},\n\t\t\targs: args{\n\t\t\t\tpkt: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x07},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.INTEL_X86PC),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\ttftp: netip.MustParseAddrPort(\"192.168.6.5:69\"),\n\t\t\t\tipxe: &url.URL{Scheme: \"tftp\", Host: \"192.168.6.5:69\"},\n\t\t\t},\n\t\t\twantBootFile: \"undionly.kpxe\",\n\t\t\twantNextSrv:  net.ParseIP(\"192.168.6.5\"),\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := context.Background()\n\t\t\tif tt.otelEnabled {\n\t\t\t\t// set global propagator to tracecontext (the default is no-op).\n\t\t\t\tprop := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})\n\t\t\t\totel.SetTextMapPropagator(prop)\n\t\t\t\tctx = dhcpotel.ContextWithTraceparentString(ctx, \"00-23b1e307bb35484f535a1f772c06910e-d887dc3912240434-01\")\n\t\t\t}\n\t\t\tbootfile, nextServer := tt.server.bootfileAndNextServer(ctx, tt.args.pkt, tt.args.uClass, tt.args.tftp, tt.args.ipxe, tt.args.iscript)\n\t\t\tif diff := cmp.Diff(bootfile, tt.wantBootFile); diff != \"\" {\n\t\t\t\tt.Fatal(\"bootfile\", diff)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(nextServer, tt.wantNextSrv); diff != \"\" {\n\t\t\t\tt.Fatal(\"nextServer\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetNetworkBootOpts(t *testing.T) {\n\ttype args struct {\n\t\tin0 context.Context\n\t\tm   *dhcpv4.DHCPv4\n\t\tn   *data.Netboot\n\t}\n\ttests := map[string]struct {\n\t\tserver *Handler\n\t\targs   args\n\t\twant   *dhcpv4.DHCPv4\n\t}{\n\t\t\"netboot not allowed\": {\n\t\t\tserver: &Handler{Log: logr.Discard()},\n\t\t\targs: args{\n\t\t\t\tin0: context.Background(),\n\t\t\t\tm:   &dhcpv4.DHCPv4{},\n\t\t\t\tn:   &data.Netboot{AllowNetboot: false},\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{ServerIPAddr: net.IPv4(0, 0, 0, 0), BootFileName: \"/netboot-not-allowed\"},\n\t\t},\n\t\t\"netboot allowed\": {\n\t\t\tserver: &Handler{Log: logr.Discard(), Netboot: Netboot{IPXEScriptURL: func(*dhcpv4.DHCPv4) *url.URL {\n\t\t\t\treturn &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"/01:02:03:04:05:06/auto.ipxe\"}\n\t\t\t}}},\n\t\t\targs: args{\n\t\t\t\tin0: context.Background(),\n\t\t\t\tm: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptUserClass(dhcp.Tinkerbell.String()),\n\t\t\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient:xxxxx\"),\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.EFI_X86_64_HTTP),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tn: &data.Netboot{AllowNetboot: true, IPXEScriptURL: &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"/01:02:03:04:05:06/auto.ipxe\"}},\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{BootFileName: \"http://localhost:8181/01:02:03:04:05:06/auto.ipxe\", Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionVendorSpecificInformation, dhcpv4.Options{\n\t\t\t\t\t6:  []byte{8},\n\t\t\t\t\t69: oteldhcp.TraceparentFromContext(context.Background()),\n\t\t\t\t}.ToBytes()),\n\t\t\t\tdhcpv4.OptClassIdentifier(\"HTTPClient\"),\n\t\t\t)},\n\t\t},\n\t\t\"netboot not allowed, arch unknown\": {\n\t\t\tserver: &Handler{Log: logr.Discard(), Netboot: Netboot{IPXEScriptURL: func(*dhcpv4.DHCPv4) *url.URL {\n\t\t\t\treturn &url.URL{Scheme: \"http\", Host: \"localhost:8181\", Path: \"/01:02:03:04:05:06/auto.ipxe\"}\n\t\t\t}}},\n\t\t\targs: args{\n\t\t\t\tin0: context.Background(),\n\t\t\t\tm: &dhcpv4.DHCPv4{\n\t\t\t\t\tClientHWAddr: net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},\n\t\t\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\t\t\tdhcpv4.OptUserClass(dhcp.Tinkerbell.String()),\n\t\t\t\t\t\tdhcpv4.OptClientArch(iana.UBOOT_ARM64),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tn: &data.Netboot{AllowNetboot: true},\n\t\t\t},\n\t\t\twant: &dhcpv4.DHCPv4{ServerIPAddr: net.IPv4(0, 0, 0, 0), BootFileName: \"/netboot-not-allowed\"},\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts := &Handler{\n\t\t\t\tLog: tt.server.Log,\n\t\t\t\tNetboot: Netboot{\n\t\t\t\t\tIPXEBinServerTFTP: tt.server.Netboot.IPXEBinServerTFTP,\n\t\t\t\t\tIPXEBinServerHTTP: tt.server.Netboot.IPXEBinServerHTTP,\n\t\t\t\t\tIPXEScriptURL:     tt.server.Netboot.IPXEScriptURL,\n\t\t\t\t\tEnabled:           tt.server.Netboot.Enabled,\n\t\t\t\t\tUserClass:         tt.server.Netboot.UserClass,\n\t\t\t\t},\n\t\t\t\tIPAddr:  tt.server.IPAddr,\n\t\t\t\tBackend: tt.server.Backend,\n\t\t\t}\n\t\t\tgotFunc := s.setNetworkBootOpts(tt.args.in0, tt.args.m, tt.args.n)\n\t\t\tgot := new(dhcpv4.DHCPv4)\n\t\t\tgotFunc(got)\n\t\t\tif diff := cmp.Diff(got, tt.want); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/handler/reservation/reservation.go",
    "content": "// Package reservation is the handler for responding to DHCPv4 messages with only host reservations.\npackage reservation\n\nimport (\n\t\"net/netip\"\n\t\"net/url\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler\"\n)\n\n// Handler holds the configuration details for the running the DHCP server.\ntype Handler struct {\n\t// Backend is the backend to use for getting DHCP data.\n\tBackend handler.BackendReader\n\n\t// IPAddr is the IP address to use in DHCP responses.\n\t// Option 54 and the sname DHCP header.\n\t// This could be a load balancer IP address or an ingress IP address or a local IP address.\n\tIPAddr netip.Addr\n\n\t// Log is used to log messages.\n\t// `logr.Discard()` can be used if no logging is desired.\n\tLog logr.Logger\n\n\t// Netboot configuration\n\tNetboot Netboot\n\n\t// OTELEnabled is used to determine if netboot options include otel naming.\n\t// When true, the netboot filename will be appended with otel information.\n\t// For example, the filename will be \"snp.efi-00-23b1e307bb35484f535a1f772c06910e-d887dc3912240434-01\".\n\t// <original filename>-00-<trace id>-<span id>-<trace flags>\n\tOTELEnabled bool\n\n\t// SyslogAddr is the address to send syslog messages to. DHCP Option 7.\n\tSyslogAddr netip.Addr\n}\n\n// Netboot holds the netboot configuration details used in running a DHCP server.\ntype Netboot struct {\n\t// iPXE binary server IP:Port serving via TFTP.\n\tIPXEBinServerTFTP netip.AddrPort\n\n\t// IPXEBinServerHTTP is the URL to the IPXE binary server serving via HTTP(s).\n\tIPXEBinServerHTTP *url.URL\n\n\t// IPXEScriptURL is the URL to the IPXE script to use.\n\tIPXEScriptURL func(*dhcpv4.DHCPv4) *url.URL\n\n\t// Enabled is whether to enable sending netboot DHCP options.\n\tEnabled bool\n\n\t// UserClass (for network booting) allows a custom DHCP option 77 to be used to break out of an iPXE loop.\n\tUserClass dhcp.UserClass\n}\n"
  },
  {
    "path": "internal/dhcp/otel/otel.go",
    "content": "// Package otel handles translating DHCP headers and options to otel key/value attributes.\npackage otel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\nconst keyNamespace = \"DHCP\"\n\n// Encoder holds the otel key/value attributes.\ntype Encoder struct {\n\tLog logr.Logger\n}\n\ntype notFoundError struct {\n\toptName string\n}\n\nfunc (e *notFoundError) Error() string {\n\treturn fmt.Sprintf(\"%q not found in DHCP packet\", e.optName)\n}\n\nfunc (e *notFoundError) found() bool {\n\treturn true\n}\n\ntype found interface {\n\tfound() bool\n}\n\n// OptNotFound returns true if err is an option not found error.\nfunc OptNotFound(err error) bool {\n\tte, ok := err.(found)\n\treturn ok && te.found()\n}\n\n// Encode runs a slice of encoders against a DHCPv4 packet turning the values into opentelemetry attribute key/value pairs.\nfunc (e *Encoder) Encode(pkt *dhcpv4.DHCPv4, namespace string, encoders ...func(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error)) []attribute.KeyValue {\n\tif e.Log.GetSink() == nil {\n\t\te.Log = logr.Discard()\n\t}\n\tvar attrs []attribute.KeyValue\n\tfor _, elem := range encoders {\n\t\tkv, err := elem(pkt, namespace)\n\t\tif err != nil {\n\t\t\te.Log.V(2).Info(\"opentelemetry attribute not added\", \"error\", fmt.Sprintf(\"%v\", err))\n\t\t\tcontinue\n\t\t}\n\t\tattrs = append(attrs, kv)\n\t}\n\n\treturn attrs\n}\n\n// AllEncoders returns a slice of all available DHCP otel encoders.\nfunc AllEncoders() []func(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\treturn []func(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error){\n\t\tEncodeFlags, EncodeTransactionID,\n\t\tEncodeYIADDR, EncodeSIADDR,\n\t\tEncodeCHADDR, EncodeFILE,\n\t\tEncodeOpt1, EncodeOpt3, EncodeOpt6,\n\t\tEncodeOpt12, EncodeOpt15, EncodeOpt28,\n\t\tEncodeOpt42, EncodeOpt51, EncodeOpt53,\n\t\tEncodeOpt54, EncodeOpt60, EncodeOpt93,\n\t\tEncodeOpt94, EncodeOpt97, EncodeOpt119,\n\t}\n}\n\n// EncodeFlags takes DHCP flags from a DHCP packet and returns an OTEL key/value pair.\n// key/value pair. See https://datatracker.ietf.org/doc/html/rfc2131#page-9\nfunc EncodeFlags(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.flags\", keyNamespace, namespace)\n\tif d != nil {\n\t\treturn attribute.String(key, d.FlagsToString()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeTransactionID takes the Transaction ID header from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeTransactionID(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.transactionID\", keyNamespace, namespace)\n\tif d != nil {\n\t\treturn attribute.String(key, d.TransactionID.String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt1 takes DHCP Opt 1 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt1(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\topt := \"Opt1.SubnetMask\"\n\tkey := fmt.Sprintf(\"%v.%v.%v\", keyNamespace, namespace, opt)\n\tif d != nil && d.SubnetMask() != nil {\n\t\tsm := net.IP(d.SubnetMask()).String()\n\t\treturn attribute.String(key, sm), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: opt}\n}\n\n// EncodeOpt3 takes DHCP Opt 3 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt3(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt3.DefaultGateway\", keyNamespace, namespace)\n\tif d != nil {\n\t\tvar routers []string\n\t\tfor _, e := range d.Router() {\n\t\t\trouters = append(routers, e.String())\n\t\t}\n\t\tif len(routers) > 0 {\n\t\t\treturn attribute.String(key, strings.Join(routers, \",\")), nil\n\t\t}\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt6 takes DHCP Opt 6 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt6(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt6.NameServers\", keyNamespace, namespace)\n\tif d != nil {\n\t\tvar ns []string\n\t\tfor _, e := range d.DNS() {\n\t\t\tns = append(ns, e.String())\n\t\t}\n\t\tif len(ns) > 0 {\n\t\t\treturn attribute.String(key, strings.Join(ns, \",\")), nil\n\t\t}\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt12 takes DHCP Opt 12 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt12(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt12.Hostname\", keyNamespace, namespace)\n\tif d != nil && d.HostName() != \"\" {\n\t\treturn attribute.String(key, d.HostName()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt15 takes DHCP Opt 15 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt15(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt15.DomainName\", keyNamespace, namespace)\n\tif d != nil && d.DomainName() != \"\" {\n\t\treturn attribute.String(key, d.DomainName()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt28 takes DHCP Opt 28 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt28(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt28.BroadcastAddress\", keyNamespace, namespace)\n\tif d != nil && d.BroadcastAddress() != nil {\n\t\treturn attribute.String(key, d.BroadcastAddress().String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt42 takes DHCP Opt 42 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt42(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt42.NTPServers\", keyNamespace, namespace)\n\tif d != nil {\n\t\tvar ntp []string\n\t\tfor _, e := range d.NTPServers() {\n\t\t\tntp = append(ntp, e.String())\n\t\t}\n\t\tif len(ntp) > 0 {\n\t\t\treturn attribute.String(key, strings.Join(ntp, \",\")), nil\n\t\t}\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt51 takes DHCP Opt 51 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt51(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt51.LeaseTime\", keyNamespace, namespace)\n\tif d != nil && d.IPAddressLeaseTime(0) != 0 {\n\t\treturn attribute.Float64(key, d.IPAddressLeaseTime(0).Seconds()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt53 takes DHCP Opt 53 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt53(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt53.MessageType\", keyNamespace, namespace)\n\tif d != nil && d.MessageType() != dhcpv4.MessageTypeNone {\n\t\treturn attribute.String(key, d.MessageType().String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt54 takes DHCP Opt 54 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt54(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt54.ServerIdentifier\", keyNamespace, namespace)\n\tif d != nil && d.ServerIdentifier() != nil {\n\t\treturn attribute.String(key, d.ServerIdentifier().String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt60 takes DHCP Opt 60 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt60(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt60.ClassIdentifier\", keyNamespace, namespace)\n\tif d != nil && d.ClassIdentifier() != \"\" {\n\t\treturn attribute.String(key, d.ClassIdentifier()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt93 takes DHCP Opt 93 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt93(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt93.ClientIdentifier\", keyNamespace, namespace)\n\tif d != nil && len(d.ClientArch()) > 0 {\n\t\tvar r []string\n\t\tfor _, i := range d.ClientArch() {\n\t\t\tr = append(r, i.String())\n\t\t}\n\n\t\treturn attribute.StringSlice(key, r), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt94 takes DHCP Opt 94 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt94(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt94.ClientNetworkInterfaceIdentifier\", keyNamespace, namespace)\n\tif d != nil && len(d.GetOneOption(dhcpv4.OptionClientNetworkInterfaceIdentifier)) > 0 {\n\t\tvar r []string\n\t\tfor _, i := range d.GetOneOption(dhcpv4.OptionClientNetworkInterfaceIdentifier) {\n\t\t\tr = append(r, fmt.Sprintf(\"%v\", i))\n\t\t}\n\n\t\t// \".\" delimited follows the same format from tcpdump\n\t\treturn attribute.String(key, strings.Join(r, \".\")), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt97 takes DHCP Opt 97 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt97(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt97.ClientMachineIdentifier\", keyNamespace, namespace)\n\tif d != nil && len(d.GetOneOption(dhcpv4.OptionClientMachineIdentifier)) > 0 {\n\t\tvar r []string\n\t\tfor _, i := range d.GetOneOption(dhcpv4.OptionClientMachineIdentifier) {\n\t\t\tr = append(r, fmt.Sprintf(\"%v\", i))\n\t\t}\n\n\t\t// \".\" delimited follows the same format from tcpdump\n\t\treturn attribute.String(key, strings.Join(r, \".\")), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeOpt119 takes DHCP Opt 119 from a DHCP packet and returns an OTEL key/value pair.\n// See https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml\nfunc EncodeOpt119(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Opt119.DomainSearch\", keyNamespace, namespace)\n\tif d != nil {\n\t\tif l := d.DomainSearch(); l != nil {\n\t\t\treturn attribute.String(key, strings.Join(l.Labels, \",\")), nil\n\t\t}\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeYIADDR takes the yiaddr header from a DHCP packet and returns an OTEL\n// key/value pair. See https://datatracker.ietf.org/doc/html/rfc2131#page-9\nfunc EncodeYIADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.yiaddr\", keyNamespace, namespace)\n\tif d != nil && d.YourIPAddr != nil {\n\t\treturn attribute.String(key, d.YourIPAddr.String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeSIADDR takes the siaddr header from a DHCP packet and returns an OTEL\n// key/value pair. See https://datatracker.ietf.org/doc/html/rfc2131#page-9\nfunc EncodeSIADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.siaddr\", keyNamespace, namespace)\n\tif d != nil && d.ServerIPAddr != nil {\n\t\treturn attribute.String(key, d.ServerIPAddr.String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeCHADDR takes the CHADDR header from a DHCP packet and returns an OTEL\n// key/value pair. See https://datatracker.ietf.org/doc/html/rfc2131#page-9\nfunc EncodeCHADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.chaddr\", keyNamespace, namespace)\n\tif d != nil && d.ClientHWAddr != nil {\n\t\treturn attribute.String(key, d.ClientHWAddr.String()), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// EncodeFILE takes the file header from a DHCP packet and returns an OTEL\n// key/value pair. See https://datatracker.ietf.org/doc/html/rfc2131#page-9\nfunc EncodeFILE(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue, error) {\n\tkey := fmt.Sprintf(\"%v.%v.Header.file\", keyNamespace, namespace)\n\tif d != nil && d.BootFileName != \"\" {\n\t\treturn attribute.String(key, d.BootFileName), nil\n\t}\n\n\treturn attribute.KeyValue{}, &notFoundError{optName: key}\n}\n\n// TraceparentFromContext extracts the binary trace id, span id, and trace flags\n// from the running span in ctx and returns a 26 byte []byte with the traceparent\n// encoded and ready to pass into a suboption (most likely 69) of opt43.\nfunc TraceparentFromContext(ctx context.Context) []byte {\n\tsc := trace.SpanContextFromContext(ctx)\n\ttpBytes := make([]byte, 0, 26)\n\n\t// the otel spec says 16 bytes for trace id and 8 for spans are good enough\n\t// for everyone copy them into a []byte that we can deliver over option43\n\ttid := [16]byte(sc.TraceID()) // type TraceID [16]byte\n\tsid := [8]byte(sc.SpanID())   // type SpanID [8]byte\n\n\ttpBytes = append(tpBytes, 0x00)      // traceparent version\n\ttpBytes = append(tpBytes, tid[:]...) // trace id\n\ttpBytes = append(tpBytes, sid[:]...) // span id\n\tif sc.IsSampled() {\n\t\ttpBytes = append(tpBytes, 0x01) // trace flags\n\t} else {\n\t\ttpBytes = append(tpBytes, 0x00)\n\t}\n\n\treturn tpBytes\n}\n"
  },
  {
    "path": "internal/dhcp/otel/otel_test.go",
    "content": "package otel\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n\t\"github.com/insomniacslk/dhcp/rfc1035label\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\nfunc TestEncode(t *testing.T) {\n\ttests := map[string]struct {\n\t\tallEncoders bool\n\t\tpkt         *dhcpv4.DHCPv4\n\t\twant        []attribute.KeyValue\n\t}{\n\t\t\"no encoders\": {pkt: &dhcpv4.DHCPv4{}, want: nil},\n\t\t\"all encoders\": {allEncoders: true, pkt: &dhcpv4.DHCPv4{BootFileName: \"ipxe.efi\", Flags: 0}, want: []attribute.KeyValue{\n\t\t\t{Key: attribute.Key(\"DHCP.test.Header.flags\"), Value: attribute.StringValue(\"Unicast\")},\n\t\t\t{Key: attribute.Key(\"DHCP.test.Header.transactionID\"), Value: attribute.StringValue(\"0x00000000\")},\n\t\t\t{Key: attribute.Key(\"DHCP.test.Header.file\"), Value: attribute.StringValue(\"ipxe.efi\")},\n\t\t}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\te := &Encoder{}\n\t\t\tgot := e.Encode(tt.pkt, \"test\")\n\t\t\tif tt.allEncoders {\n\t\t\t\tgot = e.Encode(tt.pkt, \"test\", AllEncoders()...)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Logf(\"%+v\", got)\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncodeError(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput *notFoundError\n\t\twant  string\n\t}{\n\t\t\"success\":           {input: &notFoundError{optName: \"opt1\"}, want: \"\\\"opt1\\\" not found in DHCP packet\"},\n\t\t\"success nil error\": {input: &notFoundError{}, want: \"\\\"\\\" not found in DHCP packet\"},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := tt.input.Error()\n\t\t\tif diff := cmp.Diff(got, tt.want); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt1(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptSubnetMask(net.IPMask(net.IP{255, 255, 255, 0}.To4())),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt1.SubnetMask\", \"255.255.255.0\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt1(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt1() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt3(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptRouter([]net.IP{{192, 168, 1, 1}}...),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt3.DefaultGateway\", \"192.168.1.1\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt3(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt13() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt6(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptDNS([]net.IP{{1, 1, 1, 1}}...),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt6.NameServers\", \"1.1.1.1\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt6(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt6() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt12(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptHostName(\"test-host\"),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt12.Hostname\", \"test-host\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt12(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt12() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt15(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptDomainName(\"example.com\"),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt15.DomainName\", \"example.com\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt15(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt15() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt28(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptBroadcastAddress(net.IP{192, 168, 1, 255}),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt28.BroadcastAddress\", \"192.168.1.255\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt28(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt28() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt42(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptNTPServers([]net.IP{{132, 163, 96, 2}}...),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt42.NTPServers\", \"132.163.96.2\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt42(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt42() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt51(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptIPAddressLeaseTime(time.Minute),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt51.LeaseTime\", \"60\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt51(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt51() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt53(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt53.MessageType\", \"OFFER\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt53(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt53() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt54(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptServerIdentifier(net.IP{127, 0, 0, 1}),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt54.ServerIdentifier\", \"127.0.0.1\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt54(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt54() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt60(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptClassIdentifier(\"foobar\"),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt60.ClassIdentifier\", \"foobar\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt60(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt60() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt93(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptClientArch(iana.INTEL_X86PC),\n\t\t\t)},\n\t\t\twant: attribute.StringSlice(\"DHCP.testing.Opt93.ClientIdentifier\", []string{\"Intel x86PC\"}),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt93(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt93() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Log(tt.input.ClientArch())\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt94(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientNetworkInterfaceIdentifier, []byte{0x01, 0x02, 0x01}),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt94.ClientNetworkInterfaceIdentifier\", \"1.2.1\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt94(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt94() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Log(tt.input.ClientArch())\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt97(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt97.ClientMachineIdentifier\", \"0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt97(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt97() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Log(tt.input.GetOneOption(dhcpv4.OptionClientMachineIdentifier))\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetOpt119(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{Options: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptDomainSearch(&rfc1035label.Labels{Labels: []string{\"mydomain.com\"}}),\n\t\t\t)},\n\t\t\twant: attribute.String(\"DHCP.testing.Opt119.DomainSearch\", \"mydomain.com\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeOpt119(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setOpt119() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderFlags(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.flags\", \"Unicast\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeFlags(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setHeaderFlags() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderTransactionID(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{TransactionID: dhcpv4.TransactionID{0x00, 0x00, 0x00, 0x00}},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.transactionID\", \"0x00000000\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeTransactionID(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"EncodeTransactionID() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderYIADDR(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{YourIPAddr: []byte{192, 168, 2, 100}},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.yiaddr\", \"192.168.2.100\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeYIADDR(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setHeaderYIADDR() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderSIADDR(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{ServerIPAddr: []byte{127, 0, 0, 1}},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.siaddr\", \"127.0.0.1\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeSIADDR(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setHeaderSIADDR() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderCHADDR(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{ClientHWAddr: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.chaddr\", \"01:02:03:04:05:06\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeCHADDR(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setHeaderCHADDR() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHeaderFILE(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput   *dhcpv4.DHCPv4\n\t\twant    attribute.KeyValue\n\t\twantErr error\n\t}{\n\t\t\"success\": {\n\t\t\tinput: &dhcpv4.DHCPv4{BootFileName: \"snp.efi\"},\n\t\t\twant:  attribute.String(\"DHCP.testing.Header.file\", \"snp.efi\"),\n\t\t},\n\t\t\"error\": {wantErr: &notFoundError{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := EncodeFILE(tt.input, \"testing\")\n\t\t\tif tt.wantErr != nil && !OptNotFound(err) {\n\t\t\t\tt.Fatalf(\"setHeaderFILE() error (type: %T) = %[1]v, wantErr (type: %T) %[2]v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(attribute.Value{})); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraceparentFromContext(t *testing.T) {\n\twant := []byte{0, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 8, 0, 0, 0, 0, 1}\n\tsc := trace.NewSpanContext(trace.SpanContextConfig{\n\t\tTraceID:    trace.TraceID{0x01, 0x02, 0x03, 0x04},\n\t\tSpanID:     trace.SpanID{0x05, 0x06, 0x07, 0x08},\n\t\tTraceFlags: trace.TraceFlags(1),\n\t})\n\trmSpan := trace.ContextWithRemoteSpanContext(context.Background(), sc)\n\n\tgot := TraceparentFromContext(rmSpan)\n\tif !bytes.Equal(got, want) {\n\t\tt.Errorf(\"binaryTpFromContext() = %v, want %v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "internal/dhcp/server/dhcp.go",
    "content": "// Package dhcp providers UDP listening and serving functionality.\npackage server\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/server4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"golang.org/x/net/ipv4\"\n)\n\n// Handler is a type that defines the handler function to be called every time a\n// valid DHCPv4 message is received\n// type Handler func(ctx context.Context, conn net.PacketConn, d data.Packet).\ntype Handler interface {\n\tHandle(ctx context.Context, conn *ipv4.PacketConn, d data.Packet)\n}\n\n// DHCP represents a DHCPv4 server object.\ntype DHCP struct {\n\tConn     net.PacketConn\n\tHandlers []Handler\n\tLogger   logr.Logger\n}\n\n// Serve serves requests.\nfunc (s *DHCP) Serve(ctx context.Context) error {\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = s.Close()\n\t}()\n\ts.Logger.Info(\"Server listening on\", \"addr\", s.Conn.LocalAddr())\n\n\tnConn := ipv4.NewPacketConn(s.Conn)\n\tif err := nConn.SetControlMessage(ipv4.FlagInterface, true); err != nil {\n\t\ts.Logger.Info(\"error setting control message\", \"err\", err)\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\t_ = nConn.Close()\n\t}()\n\tfor {\n\t\t// Max UDP packet size is 65535. Max DHCPv4 packet size is 576. An ethernet frame is 1500 bytes.\n\t\t// We use 4096 as a reasonable buffer size. dhcpv4.FromBytes will handle the rest.\n\t\trbuf := make([]byte, 4096)\n\t\tn, cm, peer, err := nConn.ReadFrom(rbuf)\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t}\n\t\t\ts.Logger.Info(\"error reading from packet conn\", \"err\", err)\n\t\t\treturn err\n\t\t}\n\n\t\tm, err := dhcpv4.FromBytes(rbuf[:n])\n\t\tif err != nil {\n\t\t\ts.Logger.Info(\"error parsing DHCPv4 request\", \"err\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tupeer, ok := peer.(*net.UDPAddr)\n\t\tif !ok {\n\t\t\ts.Logger.Info(\"not a UDP connection? Peer is\", \"peer\", peer)\n\t\t\tcontinue\n\t\t}\n\t\t// Set peer to broadcast if the client did not have an IP.\n\t\tif upeer.IP == nil || upeer.IP.To4().Equal(net.IPv4zero) {\n\t\t\tupeer = &net.UDPAddr{\n\t\t\t\tIP:   net.IPv4bcast,\n\t\t\t\tPort: upeer.Port,\n\t\t\t}\n\t\t}\n\n\t\tvar ifName string\n\t\tif n, err := net.InterfaceByIndex(cm.IfIndex); err == nil {\n\t\t\tifName = n.Name\n\t\t}\n\n\t\tfor _, handler := range s.Handlers {\n\t\t\tgo handler.Handle(ctx, nConn, data.Packet{Peer: upeer, Pkt: m, Md: &data.Metadata{IfName: ifName, IfIndex: cm.IfIndex}})\n\t\t}\n\t}\n}\n\n// Close sends a termination request to the server, and closes the UDP listener.\nfunc (s *DHCP) Close() error {\n\treturn s.Conn.Close()\n}\n\n// NewServer initializes and returns a new Server object.\nfunc NewServer(ifname string, addr *net.UDPAddr, handler ...Handler) (*DHCP, error) {\n\ts := &DHCP{\n\t\tHandlers: handler,\n\t\tLogger:   logr.Discard(),\n\t}\n\n\tif s.Conn == nil {\n\t\tvar err error\n\t\tconn, err := server4.NewIPv4UDPConn(ifname, addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ts.Conn = conn\n\t}\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/dhcp/server/dhcp_test.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/nclient4\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"golang.org/x/net/ipv4\"\n\t\"golang.org/x/net/nettest\"\n)\n\ntype mock struct {\n\tLog         logr.Logger\n\tServerIP    net.IP\n\tLeaseTime   uint32\n\tYourIP      net.IP\n\tNameServers []net.IP\n\tSubnetMask  net.IPMask\n\tRouter      net.IP\n}\n\nfunc (m *mock) Handle(_ context.Context, conn *ipv4.PacketConn, d data.Packet) {\n\tif m.Log.GetSink() == nil {\n\t\tm.Log = logr.Discard()\n\t}\n\n\tmods := m.setOpts()\n\tswitch mt := d.Pkt.MessageType(); mt {\n\tcase dhcpv4.MessageTypeDiscover:\n\t\tmods = append(mods, dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer))\n\tcase dhcpv4.MessageTypeRequest:\n\t\tmods = append(mods, dhcpv4.WithMessageType(dhcpv4.MessageTypeAck))\n\tcase dhcpv4.MessageTypeRelease:\n\t\tmods = append(mods, dhcpv4.WithMessageType(dhcpv4.MessageTypeAck))\n\tdefault:\n\t\tm.Log.Info(\"unsupported message type\", \"type\", mt.String())\n\t\treturn\n\t}\n\treply, err := dhcpv4.NewReplyFromRequest(d.Pkt, mods...)\n\tif err != nil {\n\t\tm.Log.Error(err, \"error creating reply\")\n\t\treturn\n\t}\n\tcm := &ipv4.ControlMessage{IfIndex: d.Md.IfIndex}\n\tif _, err := conn.WriteTo(reply.ToBytes(), cm, d.Peer); err != nil {\n\t\tm.Log.Error(err, \"failed to send reply\")\n\t\treturn\n\t}\n\tm.Log.Info(\"sent reply\")\n}\n\nfunc (m *mock) setOpts() []dhcpv4.Modifier {\n\tmods := []dhcpv4.Modifier{\n\t\tdhcpv4.WithGeneric(dhcpv4.OptionServerIdentifier, m.ServerIP),\n\t\tdhcpv4.WithServerIP(m.ServerIP),\n\t\tdhcpv4.WithLeaseTime(m.LeaseTime),\n\t\tdhcpv4.WithYourIP(m.YourIP),\n\t\tdhcpv4.WithDNS(m.NameServers...),\n\t\tdhcpv4.WithNetmask(m.SubnetMask),\n\t\tdhcpv4.WithRouter(m.Router),\n\t}\n\n\treturn mods\n}\n\nfunc dhcp(ctx context.Context) (*dhcpv4.DHCPv4, error) {\n\trifs, err := nettest.RoutedInterface(\"ip\", net.FlagUp|net.FlagBroadcast)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := nclient4.New(rifs.Name,\n\t\tnclient4.WithServerAddr(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 7676}),\n\t\tnclient4.WithUnicast(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 7677}),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer c.Close()\n\n\treturn c.DiscoverOffer(ctx)\n}\n\nfunc TestServe(t *testing.T) {\n\ttests := map[string]struct {\n\t\th    Handler\n\t\taddr netip.AddrPort\n\t}{\n\t\t\"success\": {addr: netip.MustParseAddrPort(\"127.0.0.1:7676\"), h: &mock{}},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ts, err := NewServer(\"lo\", net.UDPAddrFromAddrPort(tt.addr), tt.h)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tctx, done := context.WithCancel(context.Background())\n\t\t\tdefer done()\n\n\t\t\tgo s.Serve(ctx)\n\n\t\t\t// make client calls\n\t\t\td, err := dhcp(ctx)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(d)\n\n\t\t\tdone()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ipxe/http/http.go",
    "content": "// package bhttp is the http server for smee.\npackage http\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n)\n\n// Config is the configuration for the http server.\ntype Config struct {\n\tGitRev         string\n\tStartTime      time.Time\n\tLogger         logr.Logger\n\tTrustedProxies []string\n}\n\n// HandlerMapping is a map of routes to http.HandlerFuncs.\ntype HandlerMapping map[string]http.HandlerFunc\n\n// ServeHTTP sets up all the HTTP routes using a stdlib mux and starts the http\n// server, which will block. App functionality is instrumented in Prometheus and OpenTelemetry.\nfunc (s *Config) ServeHTTP(ctx context.Context, addr string, handlers HandlerMapping) error {\n\tmux := http.NewServeMux()\n\tfor pattern, handler := range handlers {\n\t\tmux.Handle(otelFuncWrapper(pattern, handler))\n\t}\n\n\tmux.Handle(\"/metrics\", promhttp.Handler())\n\tmux.HandleFunc(\"/healthcheck\", s.serveHealthchecker(s.GitRev, s.StartTime))\n\n\t// wrap the mux with an OpenTelemetry interceptor\n\totelHandler := otelhttp.NewHandler(mux, \"smee-http\")\n\n\t// add X-Forwarded-For support if trusted proxies are configured\n\tvar xffHandler http.Handler\n\tif len(s.TrustedProxies) > 0 {\n\t\txffmw, err := newXFF(xffOptions{\n\t\t\tAllowedSubnets: s.TrustedProxies,\n\t\t})\n\t\tif err != nil {\n\t\t\ts.Logger.Error(err, \"failed to create new xff object\")\n\t\t\tpanic(fmt.Errorf(\"failed to create new xff object: %v\", err))\n\t\t}\n\n\t\txffHandler = xffmw.Handler(&loggingMiddleware{\n\t\t\thandler: otelHandler,\n\t\t\tlog:     s.Logger,\n\t\t})\n\t} else {\n\t\txffHandler = &loggingMiddleware{\n\t\t\thandler: otelHandler,\n\t\t\tlog:     s.Logger,\n\t\t}\n\t}\n\n\tserver := http.Server{\n\t\tAddr:    addr,\n\t\tHandler: xffHandler,\n\n\t\t// Mitigate Slowloris attacks. 30 seconds is based on Apache's recommended 20-40\n\t\t// recommendation. Smee doesn't really have many headers so 20s should be plenty of time.\n\t\t// https://en.wikipedia.org/wiki/Slowloris_(computer_security)\n\t\tReadHeaderTimeout: 20 * time.Second,\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\ts.Logger.Info(\"shutting down http server\")\n\t\t_ = server.Shutdown(ctx)\n\t}()\n\tif err := server.ListenAndServe(); err != nil {\n\t\tif errors.Is(err, http.ErrServerClosed) {\n\t\t\treturn nil\n\t\t}\n\t\ts.Logger.Error(err, \"listen and serve http\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *Config) serveHealthchecker(rev string, start time.Time) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tres := struct {\n\t\t\tGitRev     string  `json:\"git_rev\"`\n\t\t\tUptime     float64 `json:\"uptime\"`\n\t\t\tGoroutines int     `json:\"goroutines\"`\n\t\t}{\n\t\t\tGitRev:     rev,\n\t\t\tUptime:     time.Since(start).Seconds(),\n\t\t\tGoroutines: runtime.NumGoroutine(),\n\t\t}\n\t\tif err := json.NewEncoder(w).Encode(&res); err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\ts.Logger.Error(err, \"marshaling healthcheck json\")\n\t\t}\n\t}\n}\n\n// otelFuncWrapper takes a route and an http handler function, wraps the function\n// with otelhttp, and returns the route again and http.Handler all set for mux.Handle().\nfunc otelFuncWrapper(route string, h func(w http.ResponseWriter, req *http.Request)) (string, http.Handler) {\n\treturn route, otelhttp.WithRouteTag(route, http.HandlerFunc(h))\n}\n"
  },
  {
    "path": "internal/ipxe/http/middleware.go",
    "content": "package http\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n)\n\ntype loggingMiddleware struct {\n\thandler http.Handler\n\tlog     logr.Logger\n}\n\n// ServeHTTP implements http.Handler and add logging before and after the request.\nfunc (h *loggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tvar (\n\t\tstart  = time.Now()\n\t\tmethod = req.Method\n\t\turi    = req.RequestURI\n\t\tclient = clientIP(req.RemoteAddr)\n\t)\n\n\tlog := uri != \"/metrics\"\n\n\tres := &responseWriter{ResponseWriter: w}\n\th.handler.ServeHTTP(res, req) // process the request\n\n\t// The \"X-Global-Logging\" header allows all registered HTTP handlers to disable this global logging\n\t// by setting the header to any non empty string. This is useful for handlers that handle partial content of\n\t// larger file. The ISO handler, for example.\n\tr := res.Header().Get(\"X-Global-Logging\")\n\n\tif log && r == \"\" {\n\t\th.log.Info(\"response\", \"method\", method, \"uri\", uri, \"client\", client, \"duration\", time.Since(start), \"status\", res.statusCode)\n\t}\n}\n\ntype responseWriter struct {\n\thttp.ResponseWriter\n\tstatusCode int\n}\n\nfunc (w *responseWriter) Write(b []byte) (int, error) {\n\tif w.statusCode == 0 {\n\t\tw.statusCode = 200\n\t}\n\tn, err := w.ResponseWriter.Write(b)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed writing response: %w\", err)\n\t}\n\n\treturn n, nil\n}\n\nfunc (w *responseWriter) WriteHeader(code int) {\n\tif w.statusCode == 0 {\n\t\tw.statusCode = code\n\t}\n\tw.ResponseWriter.WriteHeader(code)\n}\n\nfunc clientIP(str string) string {\n\thost, _, err := net.SplitHostPort(str)\n\tif err != nil {\n\t\treturn \"?\"\n\t}\n\n\treturn host\n}\n"
  },
  {
    "path": "internal/ipxe/http/xff.go",
    "content": "/*\nhttps://github.com/sebest/xff\nCopyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\npackage http\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// xffOptions is a configuration container to setup the XFF middleware.\ntype xffOptions struct {\n\t// AllowedSubnets is a list of Subnets from which we will accept the\n\t// X-Forwarded-For header.\n\t// If this list is empty we will accept every Subnets (default).\n\tAllowedSubnets []string\n\t// Debugging flag adds additional output to debug server side XFF issues.\n\tDebug bool\n}\n\n// xff http handler.\ntype xff struct {\n\t// Set to true if all IPs or Subnets are allowed.\n\tallowAll bool\n\t// List of IP subnets that are allowed.\n\tallowedMasks []net.IPNet\n}\n\n// New creates a new XFF handler with the provided options.\nfunc newXFF(options xffOptions) (*xff, error) {\n\tallowedMasks, err := toMasks(options.AllowedSubnets)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\txff := &xff{\n\t\tallowAll:     len(options.AllowedSubnets) == 0,\n\t\tallowedMasks: allowedMasks,\n\t}\n\n\treturn xff, nil\n}\n\n// Handler updates RemoteAdd from X-Fowarded-For Headers.\nfunc (xff *xff) Handler(h http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tr.RemoteAddr = getRemoteAddrIfAllowed(r, xff.allowed)\n\t\th.ServeHTTP(w, r)\n\t})\n}\n\n// getRemoteAddrIfAllowed parses the given request, resolves the X-Forwarded-For header\n// and returns the resolved remote address if allowed.\nfunc getRemoteAddrIfAllowed(r *http.Request, allowed func(sip string) bool) string {\n\tif xffh := r.Header.Get(\"X-Forwarded-For\"); xffh != \"\" {\n\t\tif sip, sport, err := net.SplitHostPort(r.RemoteAddr); err == nil && sip != \"\" {\n\t\t\tif allowed(sip) {\n\t\t\t\tif xip := parse(xffh, allowed); xip != \"\" {\n\t\t\t\t\treturn net.JoinHostPort(xip, sport)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn r.RemoteAddr\n}\n\n// parse parses the value of the X-Forwarded-For Header and returns the IP address.\nfunc parse(ipList string, allowed func(string) bool) string {\n\tips := strings.Split(ipList, \",\")\n\tif len(ips) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// simple case of only 1 proxy\n\tif len(ips) == 1 {\n\t\tip := strings.TrimSpace(ips[0])\n\t\tif net.ParseIP(ip) != nil {\n\t\t\treturn ip\n\t\t}\n\t\treturn \"\"\n\t}\n\n\t// multiple proxies\n\t// common form of X-F-F is: client, proxy1, proxy2, ... proxyN-1\n\t// so we verify backwards and return the first unallowed/untrusted proxy\n\tlastIP := \"\"\n\tfor i := len(ips) - 1; i >= 0; i-- {\n\t\tip := strings.TrimSpace(ips[i])\n\t\tif net.ParseIP(ip) == nil {\n\t\t\tbreak\n\t\t}\n\t\tlastIP = ip\n\t\tif !allowed(ip) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn lastIP\n}\n\n// converts a list of subnets' string to a list of net.IPNet.\nfunc toMasks(ips []string) (masks []net.IPNet, err error) {\n\tfor _, cidr := range ips {\n\t\tvar network *net.IPNet\n\t\t_, network, err = net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tmasks = append(masks, *network)\n\t}\n\treturn\n}\n\n// checks that the IP is allowed.\nfunc (xff *xff) allowed(sip string) bool {\n\tif xff.allowAll {\n\t\treturn true\n\t} else if ip := net.ParseIP(sip); ip != nil && ipInMasks(ip, xff.allowedMasks) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// checks if a net.IP is in a list of net.IPNet.\nfunc ipInMasks(ip net.IP, masks []net.IPNet) bool {\n\tfor _, mask := range masks {\n\t\tif mask.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/ipxe/http/xff_test.go",
    "content": "/*\nhttps://github.com/sebest/xff\nCopyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\npackage http\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParse_none(t *testing.T) {\n\tres := parse(\"\", nil)\n\tassert.Equal(t, \"\", res)\n}\n\nfunc allowAll(string) bool { return true }\n\nfunc TestParse_localhost(t *testing.T) {\n\tres := parse(\"127.0.0.1\", allowAll)\n\tassert.Equal(t, \"127.0.0.1\", res)\n}\n\nfunc TestParse_invalid(t *testing.T) {\n\tres := parse(\"invalid\", allowAll)\n\tassert.Equal(t, \"\", res)\n}\n\nfunc TestParse_invalid_sioux(t *testing.T) {\n\tres := parse(\"123#1#2#3\", allowAll)\n\tassert.Equal(t, \"\", res)\n}\n\nfunc TestParse_invalid_private_lookalike(t *testing.T) {\n\tres := parse(\"102.3.2.1\", allowAll)\n\tassert.Equal(t, \"102.3.2.1\", res)\n}\n\nfunc TestParse_valid(t *testing.T) {\n\tres := parse(\"68.45.152.220\", allowAll)\n\tassert.Equal(t, \"68.45.152.220\", res)\n}\n\nfunc TestParse_multi_first(t *testing.T) {\n\tres := parse(\"12.13.14.15, 68.45.152.220\", allowAll)\n\tassert.Equal(t, \"12.13.14.15\", res)\n}\n\nfunc TestParse_multi_with_invalid(t *testing.T) {\n\tres := parse(\"invalid, 190.57.149.90\", allowAll)\n\tassert.Equal(t, \"190.57.149.90\", res)\n}\n\nfunc TestParse_multi_with_invalid2(t *testing.T) {\n\tres := parse(\"190.57.149.90, invalid\", allowAll)\n\tassert.Equal(t, \"\", res)\n}\n\nfunc TestParse_multi_with_invalid_sioux(t *testing.T) {\n\tres := parse(\"190.57.149.90, 123#1#2#3\", allowAll)\n\tassert.Equal(t, \"\", res)\n}\n\nfunc TestParse_ipv6_with_port(t *testing.T) {\n\tres := parse(\"2604:2000:71a9:bf00:f178:a500:9a2d:670d\", allowAll)\n\tassert.Equal(t, \"2604:2000:71a9:bf00:f178:a500:9a2d:670d\", res)\n}\n\nfunc TestToMasks_empty(t *testing.T) {\n\tips := []string{}\n\tmasks, err := toMasks(ips)\n\tassert.Empty(t, masks)\n\tassert.Nil(t, err)\n}\n\nfunc TestToMasks(t *testing.T) {\n\tips := []string{\"127.0.0.1/32\", \"10.0.0.0/8\"}\n\tmasks, err := toMasks(ips)\n\t_, ipnet1, _ := net.ParseCIDR(\"127.0.0.1/32\")\n\t_, ipnet2, _ := net.ParseCIDR(\"10.0.0.0/8\")\n\tassert.Equal(t, []net.IPNet{*ipnet1, *ipnet2}, masks)\n\tassert.Nil(t, err)\n}\n\nfunc TestToMasks_error(t *testing.T) {\n\tips := []string{\"error\"}\n\tmasks, err := toMasks(ips)\n\tassert.Empty(t, masks)\n\tassert.Equal(t, &net.ParseError{Type: \"CIDR address\", Text: \"error\"}, err)\n}\n\nfunc TestAllowed_all(t *testing.T) {\n\tm, _ := newXFF(xffOptions{\n\t\tAllowedSubnets: []string{},\n\t})\n\tassert.True(t, m.allowed(\"127.0.0.1\"))\n}\n\nfunc TestAllowed_yes(t *testing.T) {\n\tm, _ := newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.0/16\"},\n\t})\n\tassert.True(t, m.allowed(\"127.0.0.1\"))\n\n\tm, _ = newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.1/32\"},\n\t})\n\tassert.True(t, m.allowed(\"127.0.0.1\"))\n}\n\nfunc TestAllowed_no(t *testing.T) {\n\tm, _ := newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.0/16\"},\n\t})\n\tassert.False(t, m.allowed(\"127.1.0.1\"))\n\n\tm, _ = newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.1/32\"},\n\t})\n\tassert.False(t, m.allowed(\"127.0.0.2\"))\n}\n\nfunc TestParseUnallowedMidway(t *testing.T) {\n\tm, _ := newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.0/16\"},\n\t})\n\tres := parse(\"1.1.1.1, 8.8.8.8, 127.0.0.1, 127.0.0.2\", m.allowed)\n\tassert.Equal(t, \"8.8.8.8\", res)\n}\n\nfunc TestParseMany(t *testing.T) {\n\tm, _ := newXFF(xffOptions{\n\t\tAllowedSubnets: []string{\"127.0.0.0/16\"},\n\t})\n\tres := parse(\"1.1.1.1, 127.0.0.1, 127.0.0.2, 127.0.0.3\", m.allowed)\n\tassert.Equal(t, \"1.1.1.1\", res)\n}\n"
  },
  {
    "path": "internal/ipxe/script/auto.go",
    "content": "package script\n\nimport (\n\t\"bytes\"\n\t\"text/template\"\n)\n\nfunc GenerateTemplate(d any, script string) (string, error) {\n\tt := template.New(\"auto.ipxe\")\n\tt, err := t.Parse(script)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuffer := new(bytes.Buffer)\n\tif err := t.Execute(buffer, d); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buffer.String(), nil\n}\n"
  },
  {
    "path": "internal/ipxe/script/auto_test.go",
    "content": "package script\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestGenerateTemplate(t *testing.T) {\n\ttests := map[string]struct {\n\t\th       Hook\n\t\tscript  string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t\"no vlan\": {\n\t\t\th: Hook{\n\t\t\t\tArch:              \"x86_64\",\n\t\t\t\tTinkGRPCAuthority: \"1.2.3.4:42113\",\n\t\t\t\tTinkerbellTLS:     false,\n\t\t\t\tWorkerID:          \"3c:ec:ef:4c:4f:54\",\n\t\t\t\tSyslogHost:        \"1.2.3.4\",\n\t\t\t\tDownloadURL:       \"http://location:8080/to/kernel/and/initrd\",\n\t\t\t\tFacility:          \"onprem\",\n\t\t\t\tExtraKernelParams: []string{\"tink_worker_image=quay.io/tinkerbell/tink-worker:v0.8.0\", \"tinkerbell=packet\"},\n\t\t\t\tHWAddr:            \"3c:ec:ef:4c:4f:54\",\n\t\t\t\tRetries:           10,\n\t\t\t\tRetryDelay:        3,\n\t\t\t},\n\t\t\tscript: HookScript,\n\t\t\twant: `#!ipxe\n\necho Loading the Tinkerbell Hook iPXE script...\n\nset arch x86_64\nset download-url http://location:8080/to/kernel/and/initrd\nset kernel vmlinuz-${arch}\nset initrd initramfs-${arch}\nset retries:int32 10\nset retry_delay:int32 3\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/${kernel} tink_worker_image=quay.io/tinkerbell/tink-worker:v0.8.0 tinkerbell=packet \\\nfacility=onprem syslog_host=1.2.3.4 grpc_authority=1.2.3.4:42113 tinkerbell_tls=false tinkerbell_insecure_tls=false worker_id=3c:ec:ef:4c:4f:54 hw_addr=3c:ec:ef:4c:4f:54 \\\nmodules=loop,squashfs,sd-mod,usb-storage intel_iommu=on iommu=pt initrd=initramfs-${arch} console=tty0 console=ttyS1,115200 && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/${initrd} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`,\n\t\t},\n\t\t\"with vlan\": {\n\t\t\th: Hook{\n\t\t\t\tArch:              \"x86_64\",\n\t\t\t\tTinkGRPCAuthority: \"1.2.3.4:42113\",\n\t\t\t\tTinkerbellTLS:     false,\n\t\t\t\tWorkerID:          \"3c:ec:ef:4c:4f:54\",\n\t\t\t\tSyslogHost:        \"1.2.3.4\",\n\t\t\t\tDownloadURL:       \"http://location:8080/to/kernel/and/initrd\",\n\t\t\t\tFacility:          \"onprem\",\n\t\t\t\tExtraKernelParams: []string{\"tink_worker_image=quay.io/tinkerbell/tink-worker:v0.8.0\", \"tinkerbell=packet\"},\n\t\t\t\tHWAddr:            \"3c:ec:ef:4c:4f:54\",\n\t\t\t\tVLANID:            \"16\",\n\t\t\t\tRetries:           10,\n\t\t\t\tRetryDelay:        3,\n\t\t\t},\n\t\t\tscript: HookScript,\n\t\t\twant: `#!ipxe\n\necho Loading the Tinkerbell Hook iPXE script...\n\nset arch x86_64\nset download-url http://location:8080/to/kernel/and/initrd\nset kernel vmlinuz-${arch}\nset initrd initramfs-${arch}\nset retries:int32 10\nset retry_delay:int32 3\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/${kernel} vlan_id=16 tink_worker_image=quay.io/tinkerbell/tink-worker:v0.8.0 tinkerbell=packet \\\nfacility=onprem syslog_host=1.2.3.4 grpc_authority=1.2.3.4:42113 tinkerbell_tls=false tinkerbell_insecure_tls=false worker_id=3c:ec:ef:4c:4f:54 hw_addr=3c:ec:ef:4c:4f:54 \\\nmodules=loop,squashfs,sd-mod,usb-storage intel_iommu=on iommu=pt initrd=initramfs-${arch} console=tty0 console=ttyS1,115200 && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/${initrd} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`,\n\t\t},\n\t\t\"parse error\": {\n\t\t\th:       Hook{},\n\t\t\tscript:  \"bad {{ }\",\n\t\t\twantErr: true,\n\t\t},\n\t\t\"execute error\": {\n\t\t\th:       Hook{},\n\t\t\tscript:  \"{{ .A }}\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := GenerateTemplate(tt.h, tt.script)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Auto.autoDotIPXE() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif diff := cmp.Diff(got, tt.want); diff != \"\" {\n\t\t\t\tt.Errorf(\"Auto.autoDotIPXE() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ipxe/script/custom.go",
    "content": "package script\n\nimport \"net/url\"\n\n// CustomScript is the template for the custom script.\n// It will either chain to a URL or execute an iPXE script.\nvar CustomScript = `#!ipxe\n\necho Loading custom Tinkerbell iPXE script...\n\n{{- if .Chain }}\nchain --autofree {{ .Chain }}\n{{- else }}\n{{ .Script }}\n{{- end }}\n`\n\n// Custom holds either a URL to chain to or a script to execute.\n// There is no validation of the script.\ntype Custom struct {\n\tChain  *url.URL\n\tScript string\n}\n"
  },
  {
    "path": "internal/ipxe/script/hook.go",
    "content": "package script\n\n// HookScript is the default iPXE script for loading Hook.\nvar HookScript = `#!ipxe\n\necho Loading the Tinkerbell Hook iPXE script...\n{{- if .TraceID }}\necho Debug TraceID: {{ .TraceID }}\n{{- end }}\n\nset arch {{ .Arch }}\nset download-url {{ .DownloadURL }}\nset kernel {{ if .Kernel }}{{ .Kernel }}{{ else }}vmlinuz-${arch}{{ end }}\nset initrd {{ if .Initrd }}{{ .Initrd }}{{ else }}initramfs-${arch}{{ end }}\nset retries:int32 {{ .Retries }}\nset retry_delay:int32 {{ .RetryDelay }}\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/${kernel} {{- if ne .VLANID \"\" }} vlan_id={{ .VLANID }} {{- end }} {{- range .ExtraKernelParams}} {{.}} {{- end}} \\\nfacility={{ .Facility }} syslog_host={{ .SyslogHost }} grpc_authority={{ .TinkGRPCAuthority }} tinkerbell_tls={{ .TinkerbellTLS }} tinkerbell_insecure_tls={{ .TinkerbellInsecureTLS }} worker_id={{ .WorkerID }} hw_addr={{ .HWAddr }} \\\nmodules=loop,squashfs,sd-mod,usb-storage intel_iommu=on iommu=pt initrd=initramfs-${arch} console=tty0 console=ttyS1,115200 && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/${initrd} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`\n\n// Hook holds the values used to generate the iPXE script that loads the Hook OS.\ntype Hook struct {\n\tArch                  string   // example x86_64\n\tConsole               string   // example ttyS1,115200\n\tDownloadURL           string   // example https://location:8080/to/kernel/and/initrd\n\tExtraKernelParams     []string // example tink_worker_image=quay.io/tinkerbell/tink-worker:v0.8.0\n\tFacility              string\n\tHWAddr                string // example 3c:ec:ef:4c:4f:54\n\tSyslogHost            string\n\tTinkerbellTLS         bool\n\tTinkerbellInsecureTLS bool\n\tTinkGRPCAuthority     string // example 192.168.2.111:42113\n\tTraceID               string\n\tVLANID                string // string number between 1-4095\n\tWorkerID              string // example 3c:ec:ef:4c:4f:54 or worker1\n\tRetries               int    // number of retries to attempt when fetching kernel and initrd files\n\tRetryDelay            int    // number of seconds to wait between retries\n\tKernel                string // name of the kernel file\n\tInitrd                string // name of the initrd file\n}\n"
  },
  {
    "path": "internal/ipxe/script/ipxe.go",
    "content": "package script\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/handler\"\n\t\"github.com/tinkerbell/smee/internal/metric\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\ntype Handler struct {\n\tLogger                logr.Logger\n\tBackend               handler.BackendReader\n\tOSIEURL               string\n\tExtraKernelParams     []string\n\tPublicSyslogFQDN      string\n\tTinkServerTLS         bool\n\tTinkServerInsecureTLS bool\n\tTinkServerGRPCAddr    string\n\tIPXEScriptRetries     int\n\tIPXEScriptRetryDelay  int\n\tStaticIPXEEnabled     bool\n}\n\ntype data struct {\n\tAllowNetboot  bool // If true, the client will be provided netboot options in the DHCP offer/ack.\n\tConsole       string\n\tMACAddress    net.HardwareAddr\n\tArch          string\n\tVLANID        string\n\tWorkflowID    string\n\tFacility      string\n\tIPXEScript    string\n\tIPXEScriptURL *url.URL\n\tOSIE          OSIE\n}\n\n// OSIE or OS Installation Environment is the data about where the OSIE parts are located.\ntype OSIE struct {\n\t// BaseURL is the URL where the OSIE parts are located.\n\tBaseURL *url.URL\n\t// Kernel is the name of the kernel file.\n\tKernel string\n\t// Initrd is the name of the initrd file.\n\tInitrd string\n}\n\n// getByMac uses the handler.BackendReader to get the (hardware) data and then\n// translates it to the script.Data struct.\nfunc getByMac(ctx context.Context, mac net.HardwareAddr, br handler.BackendReader) (data, error) {\n\tif br == nil {\n\t\treturn data{}, errors.New(\"backend is nil\")\n\t}\n\td, n, err := br.GetByMac(ctx, mac)\n\tif err != nil {\n\t\treturn data{}, err\n\t}\n\n\treturn data{\n\t\tAllowNetboot:  n.AllowNetboot,\n\t\tConsole:       \"\",\n\t\tMACAddress:    d.MACAddress,\n\t\tArch:          d.Arch,\n\t\tVLANID:        d.VLANID,\n\t\tWorkflowID:    d.MACAddress.String(),\n\t\tFacility:      n.Facility,\n\t\tIPXEScript:    n.IPXEScript,\n\t\tIPXEScriptURL: n.IPXEScriptURL,\n\t\tOSIE:          OSIE(n.OSIE),\n\t}, nil\n}\n\nfunc getByIP(ctx context.Context, ip net.IP, br handler.BackendReader) (data, error) {\n\tif br == nil {\n\t\treturn data{}, errors.New(\"backend is nil\")\n\t}\n\td, n, err := br.GetByIP(ctx, ip)\n\tif err != nil {\n\t\treturn data{}, err\n\t}\n\n\treturn data{\n\t\tAllowNetboot:  n.AllowNetboot,\n\t\tConsole:       \"\",\n\t\tMACAddress:    d.MACAddress,\n\t\tArch:          d.Arch,\n\t\tVLANID:        d.VLANID,\n\t\tWorkflowID:    d.MACAddress.String(),\n\t\tFacility:      n.Facility,\n\t\tIPXEScript:    n.IPXEScript,\n\t\tIPXEScriptURL: n.IPXEScriptURL,\n\t\tOSIE:          OSIE(n.OSIE),\n\t}, nil\n}\n\n// HandlerFunc returns a http.HandlerFunc that serves the ipxe script.\n// It is expected that the request path is /<mac address>/auto.ipxe.\nfunc (h *Handler) HandlerFunc() http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif path.Base(r.URL.Path) != \"auto.ipxe\" {\n\t\t\th.Logger.Info(\"URL path not supported\", \"path\", r.URL.Path)\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\n\t\t\treturn\n\t\t}\n\t\tlabels := prometheus.Labels{\"from\": \"http\", \"op\": \"file\"}\n\t\tmetric.JobsTotal.With(labels).Inc()\n\t\tmetric.JobsInProgress.With(labels).Inc()\n\t\tdefer metric.JobsInProgress.With(labels).Dec()\n\t\ttimer := prometheus.NewTimer(metric.JobDuration.With(labels))\n\t\tdefer timer.ObserveDuration()\n\n\t\tctx := r.Context()\n\n\t\t// Should we serve a custom ipxe script?\n\t\t// This gates serving PXE file by\n\t\t// 1. the existence of a hardware record in tink server\n\t\t// AND\n\t\t// 2. the network.interfaces[].netboot.allow_pxe value, in the tink server hardware record, equal to true\n\t\t// This allows serving custom ipxe scripts, starting up into OSIE or other installation environments\n\t\t// without a tink workflow present.\n\n\t\t// Try to get the MAC address from the URL path, if not available get the source IP address.\n\t\tif ha, err := getMAC(r.URL.Path); err == nil {\n\t\t\thw, err := getByMac(ctx, ha, h.Backend)\n\t\t\tif err != nil && h.StaticIPXEEnabled {\n\t\t\t\th.Logger.Info(\"serving static ipxe script\", \"mac\", ha, \"error\", err)\n\t\t\t\th.serveStaticIPXEScript(w)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil || !hw.AllowNetboot {\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\th.Logger.Info(\"the hardware data for this machine, or lack there of, does not allow it to pxe\", \"client\", ha, \"error\", err)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t\th.serveBootScript(ctx, w, path.Base(r.URL.Path), hw)\n\t\t\treturn\n\t\t}\n\t\tif ip, err := getIP(r.RemoteAddr); err == nil {\n\t\t\thw, err := getByIP(ctx, ip, h.Backend)\n\t\t\tif err != nil && h.StaticIPXEEnabled {\n\t\t\t\th.Logger.Info(\"serving static ipxe script\", \"client\", r.RemoteAddr, \"error\", err)\n\t\t\t\th.serveStaticIPXEScript(w)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil || !hw.AllowNetboot {\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\th.Logger.Info(\"the hardware data for this machine, or lack there of, does not allow it to pxe\", \"client\", r.RemoteAddr, \"error\", err)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t\th.serveBootScript(ctx, w, path.Base(r.URL.Path), hw)\n\t\t\treturn\n\t\t}\n\n\t\t// If we get here, we were unable to get the MAC address from the URL path or the source IP address.\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\th.Logger.Info(\"unable to get the MAC address from the URL path or the source IP address\", \"client\", r.RemoteAddr, \"urlPath\", r.URL.Path)\n\t}\n}\n\nfunc (h *Handler) serveStaticIPXEScript(w http.ResponseWriter) {\n\t// Serve static iPXE script.\n\tauto := Hook{\n\t\tDownloadURL:       h.OSIEURL,\n\t\tExtraKernelParams: h.ExtraKernelParams,\n\t\tSyslogHost:        h.PublicSyslogFQDN,\n\t\tTinkerbellTLS:     h.TinkServerTLS,\n\t\tTinkGRPCAuthority: h.TinkServerGRPCAddr,\n\t}\n\tscript, err := GenerateTemplate(auto, StaticScript)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\th.Logger.Error(err, \"error generating the static ipxe script\")\n\t\treturn\n\t}\n\tif _, err := w.Write([]byte(script)); err != nil {\n\t\th.Logger.Error(err, \"unable to send the static ipxe script\")\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc getIP(remoteAddr string) (net.IP, error) {\n\thost, _, err := net.SplitHostPort(remoteAddr)\n\tif err != nil {\n\t\treturn net.IP{}, fmt.Errorf(\"error parsing client address: %w: client: %v\", err, remoteAddr)\n\t}\n\tip := net.ParseIP(host)\n\n\treturn ip, nil\n}\n\nfunc getMAC(urlPath string) (net.HardwareAddr, error) {\n\tmac := path.Base(path.Dir(urlPath))\n\tha, err := net.ParseMAC(mac)\n\tif err != nil {\n\t\treturn net.HardwareAddr{}, fmt.Errorf(\"URL path not supported, the second to last element in the URL path must be a valid mac address, err: %w\", err)\n\t}\n\n\treturn ha, nil\n}\n\nfunc (h *Handler) serveBootScript(ctx context.Context, w http.ResponseWriter, name string, hw data) {\n\tspan := trace.SpanFromContext(ctx)\n\tspan.SetAttributes(attribute.String(\"smee.script_name\", name))\n\tvar script []byte\n\t// check if the custom script should be used\n\tif hw.IPXEScriptURL != nil || hw.IPXEScript != \"\" {\n\t\tname = \"custom.ipxe\"\n\t}\n\tswitch name {\n\tcase \"auto.ipxe\":\n\t\ts, err := h.defaultScript(span, hw)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\th.Logger.Error(err, \"error with default ipxe script\", \"script\", name)\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\treturn\n\t\t}\n\t\tscript = []byte(s)\n\tcase \"custom.ipxe\":\n\t\tcs, err := h.customScript(hw)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\th.Logger.Error(err, \"error with custom ipxe script\", \"script\", name)\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\t\treturn\n\t\t}\n\t\tscript = []byte(cs)\n\tdefault:\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\terr := fmt.Errorf(\"boot script %q not found\", name)\n\t\th.Logger.Error(err, \"boot script not found\", \"script\", name)\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn\n\t}\n\tspan.SetAttributes(attribute.String(\"ipxe-script\", string(script)))\n\n\tif _, err := w.Write(script); err != nil {\n\t\th.Logger.Error(err, \"unable to write boot script\", \"script\", name)\n\t\tspan.SetStatus(codes.Error, err.Error())\n\n\t\treturn\n\t}\n}\n\nfunc (h *Handler) defaultScript(span trace.Span, hw data) (string, error) {\n\tmac := hw.MACAddress\n\tarch := hw.Arch\n\tif arch == \"\" {\n\t\tarch = \"x86_64\"\n\t}\n\t// The worker ID will default to the mac address or use the one specified.\n\twID := mac.String()\n\tif hw.WorkflowID != \"\" {\n\t\twID = hw.WorkflowID\n\t}\n\n\tauto := Hook{\n\t\tArch:                  arch,\n\t\tConsole:               \"\",\n\t\tDownloadURL:           h.OSIEURL,\n\t\tExtraKernelParams:     h.ExtraKernelParams,\n\t\tFacility:              hw.Facility,\n\t\tHWAddr:                mac.String(),\n\t\tSyslogHost:            h.PublicSyslogFQDN,\n\t\tTinkerbellTLS:         h.TinkServerTLS,\n\t\tTinkerbellInsecureTLS: h.TinkServerInsecureTLS,\n\t\tTinkGRPCAuthority:     h.TinkServerGRPCAddr,\n\t\tVLANID:                hw.VLANID,\n\t\tWorkerID:              wID,\n\t\tRetries:               h.IPXEScriptRetries,\n\t\tRetryDelay:            h.IPXEScriptRetryDelay,\n\t}\n\tif hw.OSIE.BaseURL != nil && hw.OSIE.BaseURL.String() != \"\" {\n\t\tauto.DownloadURL = hw.OSIE.BaseURL.String()\n\t}\n\tif hw.OSIE.Kernel != \"\" {\n\t\tauto.Kernel = hw.OSIE.Kernel\n\t}\n\tif hw.OSIE.Initrd != \"\" {\n\t\tauto.Initrd = hw.OSIE.Initrd\n\t}\n\tif sc := span.SpanContext(); sc.IsSampled() {\n\t\tauto.TraceID = sc.TraceID().String()\n\t}\n\n\treturn GenerateTemplate(auto, HookScript)\n}\n\n// customScript returns the custom script or chain URL if defined in the hardware data otherwise an error.\nfunc (h *Handler) customScript(hw data) (string, error) {\n\tif chain := hw.IPXEScriptURL; chain != nil && chain.String() != \"\" {\n\t\tif chain.Scheme != \"http\" && chain.Scheme != \"https\" {\n\t\t\treturn \"\", fmt.Errorf(\"invalid URL scheme: %v\", chain.Scheme)\n\t\t}\n\t\tc := Custom{Chain: chain}\n\t\treturn GenerateTemplate(c, CustomScript)\n\t}\n\tif script := hw.IPXEScript; script != \"\" {\n\t\tc := Custom{Script: script}\n\t\treturn GenerateTemplate(c, CustomScript)\n\t}\n\n\treturn \"\", errors.New(\"no custom script or chain defined in the hardware data\")\n}\n"
  },
  {
    "path": "internal/ipxe/script/ipxe_test.go",
    "content": "package script\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/smee/internal/metric\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\nfunc TestCustomScript(t *testing.T) {\n\ttests := map[string]struct {\n\t\tipxeURL    string\n\t\tipxeScript string\n\t\twant       string\n\t\tshouldErr  bool\n\t}{\n\t\t\"got script\":         {want: \"#!ipxe\\n\\necho Loading custom Tinkerbell iPXE script...\\n#!ipxe\\nautoboot\\n\", ipxeScript: \"#!ipxe\\nautoboot\"},\n\t\t\"got url\":            {want: \"#!ipxe\\n\\necho Loading custom Tinkerbell iPXE script...\\nchain --autofree https://boot.netboot.xyz\\n\", ipxeURL: \"https://boot.netboot.xyz\"},\n\t\t\"invalid URL prefix\": {want: \"\", ipxeURL: \"invalid\", shouldErr: true},\n\t\t\"invalid URL\":        {want: \"\", ipxeURL: \"http://invalid.:123.com\", shouldErr: true},\n\t\t\"no script or url\":   {want: \"\", shouldErr: true},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\th := &Handler{}\n\t\t\tu, err := url.Parse(tt.ipxeURL)\n\t\t\tif err != nil && !tt.shouldErr {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\td := data{MACAddress: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}, IPXEScript: tt.ipxeScript, IPXEScriptURL: u}\n\t\t\tgot, err := h.customScript(d)\n\t\t\tif err != nil && !tt.shouldErr {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultScript(t *testing.T) {\n\tone := `#!ipxe\n\necho Loading the Tinkerbell Hook iPXE script...\n\nset arch x86_64\nset download-url http://127.1.1.1\nset kernel vmlinuz-${arch}\nset initrd initramfs-${arch}\nset retries:int32 10\nset retry_delay:int32 3\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/${kernel} vlan_id=1234 \\\nfacility=onprem syslog_host= grpc_authority= tinkerbell_tls=false tinkerbell_insecure_tls=false worker_id=00:01:02:03:04:05 hw_addr=00:01:02:03:04:05 \\\nmodules=loop,squashfs,sd-mod,usb-storage intel_iommu=on iommu=pt initrd=initramfs-${arch} console=tty0 console=ttyS1,115200 && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/${initrd} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`\n\ttests := map[string]struct {\n\t\twant string\n\t}{\n\t\t\"success with defaults\": {want: one},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\th := &Handler{\n\t\t\t\tOSIEURL:              \"http://127.1.1.1\",\n\t\t\t\tIPXEScriptRetries:    10,\n\t\t\t\tIPXEScriptRetryDelay: 3,\n\t\t\t}\n\t\t\td := data{MACAddress: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}, VLANID: \"1234\", Facility: \"onprem\", Arch: \"x86_64\"}\n\t\t\tsp := trace.SpanFromContext(context.Background())\n\t\t\tgot, err := h.defaultScript(sp, d)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Log(got)\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStaticScript(t *testing.T) {\n\twant := `#!ipxe\n\necho Loading the static Tinkerbell iPXE script...\n\nset arch ${buildarch}\n# Tinkerbell only supports 64 bit archectures.\n# The build architecture does not necessarily represent the architecture of the machine on which iPXE is running.\n# https://ipxe.org/cfg/buildarch\niseq ${arch} i386 && set arch x86_64 ||\niseq ${arch} arm32 && set arch aarch64 ||\niseq ${arch} arm64 && set arch aarch64 ||\nset download-url http://127.0.0.1\nset retries:int32 0\nset retry_delay:int32 0\n\nset worker_id ${mac}\nset grpc_authority 127.0.0.1:42113\nset syslog_host 127.1.1.1\nset tinkerbell_tls false\n\necho worker_id=${mac}\necho grpc_authority=127.0.0.1:42113\necho syslog_host=127.1.1.1\necho tinkerbell_tls=false\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/vmlinuz-${arch} \\\nsyslog_host=${syslog_host} grpc_authority=${grpc_authority} tinkerbell_tls=${tinkerbell_tls} worker_id=${worker_id} hw_addr=${mac} \\\nconsole=tty1 console=tty2 console=ttyAMA0,115200 console=ttyAMA1,115200 console=ttyS0,115200 console=ttyS1,115200 k=v k2=v2 \\\nintel_iommu=on iommu=pt k=v k2=v2 initrd=initramfs-${arch} && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/initramfs-${arch} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`\n\tmetric.Init()\n\th := &Handler{\n\t\tOSIEURL:            \"http://127.0.0.1\",\n\t\tExtraKernelParams:  []string{\"k=v\", \"k2=v2\"},\n\t\tPublicSyslogFQDN:   \"127.1.1.1\",\n\t\tTinkServerTLS:      false,\n\t\tTinkServerGRPCAddr: \"127.0.0.1:42113\",\n\t\tStaticIPXEEnabled:  true,\n\t}\n\thf := h.HandlerFunc()\n\twriter := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/auto.ipxe\", nil)\n\thf(writer, req)\n\tif writer.Code != 200 {\n\t\tt.Errorf(\"expected status code 200, got %d\", writer.Code)\n\t}\n\tif diff := cmp.Diff(writer.Body.String(), want); diff != \"\" {\n\t\tt.Fatalf(\"expected custom script, got %s\", diff)\n\t}\n}\n"
  },
  {
    "path": "internal/ipxe/script/static.go",
    "content": "package script\n\n// StaticScript is the iPXE script used when in the auto-proxy mode.\n// It is built to be generic enough for all hardware to use.\nvar StaticScript = `#!ipxe\n\necho Loading the static Tinkerbell iPXE script...\n\nset arch ${buildarch}\n# Tinkerbell only supports 64 bit archectures.\n# The build architecture does not necessarily represent the architecture of the machine on which iPXE is running.\n# https://ipxe.org/cfg/buildarch\niseq ${arch} i386 && set arch x86_64 ||\niseq ${arch} arm32 && set arch aarch64 ||\niseq ${arch} arm64 && set arch aarch64 ||\nset download-url {{ .DownloadURL }}\nset retries:int32 {{ .Retries }}\nset retry_delay:int32 {{ .RetryDelay }}\n\nset worker_id ${mac}\nset grpc_authority {{ .TinkGRPCAuthority }}\nset syslog_host {{ .SyslogHost }}\nset tinkerbell_tls {{ .TinkerbellTLS }}\n\necho worker_id=${mac}\necho grpc_authority={{ .TinkGRPCAuthority }}\necho syslog_host={{ .SyslogHost }}\necho tinkerbell_tls={{ .TinkerbellTLS }}\n\nset idx:int32 0\n:retry_kernel\nkernel ${download-url}/vmlinuz-${arch} \\\nsyslog_host=${syslog_host} grpc_authority=${grpc_authority} tinkerbell_tls=${tinkerbell_tls} worker_id=${worker_id} hw_addr=${mac} \\\nconsole=tty1 console=tty2 console=ttyAMA0,115200 console=ttyAMA1,115200 console=ttyS0,115200 console=ttyS1,115200 {{- range .ExtraKernelParams}} {{.}} {{- end}} \\\nintel_iommu=on iommu=pt {{- range .ExtraKernelParams}} {{.}} {{- end}} initrd=initramfs-${arch} && goto download_initrd || iseq ${idx} ${retries} && goto kernel-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_kernel\n\n:download_initrd\nset idx:int32 0\n:retry_initrd\ninitrd ${download-url}/initramfs-${arch} && goto boot || iseq ${idx} ${retries} && goto initrd-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_initrd\n\n:boot\nset idx:int32 0\n:retry_boot\nboot || iseq ${idx} ${retries} && goto boot-error || inc idx && echo retry in ${retry_delay} seconds ; sleep ${retry_delay} ; goto retry_boot\n\n:kernel-error\necho Failed to load kernel\nimgfree\nexit\n\n:initrd-error\necho Failed to load initrd\nimgfree\nexit\n\n:boot-error\necho Failed to boot\nimgfree\nexit\n`\n"
  },
  {
    "path": "internal/iso/internal/LICENSE",
    "content": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "internal/iso/internal/acsii.go",
    "content": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage internal\n\n// EqualFold is [strings.EqualFold], ASCII only. It reports whether s and t\n// are equal, ASCII-case-insensitively.\nfunc EqualFold(s, t string) bool {\n\tif len(s) != len(t) {\n\t\treturn false\n\t}\n\tfor i := 0; i < len(s); i++ {\n\t\tif lower(s[i]) != lower(t[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// lower returns the ASCII lowercase version of b.\nfunc lower(b byte) byte {\n\tif 'A' <= b && b <= 'Z' {\n\t\treturn b + ('a' - 'A')\n\t}\n\treturn b\n}\n\n// IsPrint returns whether s is ASCII and printable according to\n// https://tools.ietf.org/html/rfc20#section-4.2.\nfunc IsPrint(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < ' ' || s[i] > '~' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/iso/internal/acsii_test.go",
    "content": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage internal\n\nimport \"testing\"\n\nfunc TestEqualFold(t *testing.T) {\n\tvar tests = []struct {\n\t\tname string\n\t\ta, b string\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"simple match\",\n\t\t\ta:    \"CHUNKED\",\n\t\t\tb:    \"chunked\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"same string\",\n\t\t\ta:    \"chunked\",\n\t\t\tb:    \"chunked\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Unicode Kelvin symbol\",\n\t\t\ta:    \"chunKed\", // This \"K\" is 'KELVIN SIGN' (\\u212A)\n\t\t\tb:    \"chunked\",\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := EqualFold(tt.a, tt.b); got != tt.want {\n\t\t\t\tt.Errorf(\"AsciiEqualFold(%q,%q): got %v want %v\", tt.a, tt.b, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsPrint(t *testing.T) {\n\tvar tests = []struct {\n\t\tname string\n\t\tin   string\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ASCII low\",\n\t\t\tin:   \"This is a space: ' '\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ASCII high\",\n\t\t\tin:   \"This is a tilde: '~'\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ASCII low non-print\",\n\t\t\tin:   \"This is a unit separator: \\x1F\",\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Ascii high non-print\",\n\t\t\tin:   \"This is a Delete: \\x7F\",\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Unicode letter\",\n\t\t\tin:   \"Today it's 280K outside: it's freezing!\", // This \"K\" is 'KELVIN SIGN' (\\u212A)\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Unicode emoji\",\n\t\t\tin:   \"Gophers like 🧀\",\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := IsPrint(tt.in); got != tt.want {\n\t\t\t\tt.Errorf(\"IsASCIIPrint(%q): got %v want %v\", tt.in, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/iso/internal/context.go",
    "content": "package internal\n\nimport \"context\"\n\ntype patchCtxKeyType string\n\nconst isoPatchCtxKey patchCtxKeyType = \"iso-patch\"\n\nfunc WithPatch(ctx context.Context, patch []byte) context.Context {\n\treturn context.WithValue(ctx, isoPatchCtxKey, patch)\n}\n\nfunc GetPatch(ctx context.Context) []byte {\n\tpatch, ok := ctx.Value(isoPatchCtxKey).([]byte)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn patch\n}\n"
  },
  {
    "path": "internal/iso/internal/reverseproxy.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// HTTP reverse proxy handler\n\npackage internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/net/http/httpguts\"\n)\n\n// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].\ntype ProxyRequest struct {\n\t// In is the request received by the proxy.\n\t// The Rewrite function must not modify In.\n\tIn *http.Request\n\n\t// Out is the request which will be sent by the proxy.\n\t// The Rewrite function may modify or replace this request.\n\t// Hop-by-hop headers are removed from this request\n\t// before Rewrite is called.\n\tOut *http.Request\n}\n\n// SetURL routes the outbound request to the scheme, host, and base path\n// provided in target. If the target's path is \"/base\" and the incoming\n// request was for \"/dir\", the target request will be for \"/base/dir\".\n//\n// SetURL rewrites the outbound Host header to match the target's host.\n// To preserve the inbound request's Host header (the default behavior\n// of [NewSingleHostReverseProxy]):\n//\n//\trewriteFunc := func(r *httputil.ProxyRequest) {\n//\t\tr.SetURL(url)\n//\t\tr.Out.Host = r.In.Host\n//\t}\nfunc (r *ProxyRequest) SetURL(target *url.URL) {\n\trewriteRequestURL(r.Out, target)\n\tr.Out.Host = \"\"\n}\n\n// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and\n// X-Forwarded-Proto headers of the outbound request.\n//\n//   - The X-Forwarded-For header is set to the client IP address.\n//   - The X-Forwarded-Host header is set to the host name requested\n//     by the client.\n//   - The X-Forwarded-Proto header is set to \"http\" or \"https\", depending\n//     on whether the inbound request was made on a TLS-enabled connection.\n//\n// If the outbound request contains an existing X-Forwarded-For header,\n// SetXForwarded appends the client IP address to it. To append to the\n// inbound request's X-Forwarded-For header (the default behavior of\n// [ReverseProxy] when using a Director function), copy the header\n// from the inbound request before calling SetXForwarded:\n//\n//\trewriteFunc := func(r *httputil.ProxyRequest) {\n//\t\tr.Out.Header[\"X-Forwarded-For\"] = r.In.Header[\"X-Forwarded-For\"]\n//\t\tr.SetXForwarded()\n//\t}\nfunc (r *ProxyRequest) SetXForwarded() {\n\tclientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)\n\tif err == nil {\n\t\tprior := r.Out.Header[\"X-Forwarded-For\"]\n\t\tif len(prior) > 0 {\n\t\t\tclientIP = strings.Join(prior, \", \") + \", \" + clientIP\n\t\t}\n\t\tr.Out.Header.Set(\"X-Forwarded-For\", clientIP)\n\t} else {\n\t\tr.Out.Header.Del(\"X-Forwarded-For\")\n\t}\n\tr.Out.Header.Set(\"X-Forwarded-Host\", r.In.Host)\n\tif r.In.TLS == nil {\n\t\tr.Out.Header.Set(\"X-Forwarded-Proto\", \"http\")\n\t} else {\n\t\tr.Out.Header.Set(\"X-Forwarded-Proto\", \"https\")\n\t}\n}\n\n// ReverseProxy is an HTTP Handler that takes an incoming request and\n// sends it to another server, proxying the response back to the\n// client.\n//\n// 1xx responses are forwarded to the client if the underlying\n// transport supports ClientTrace.Got1xxResponse.\ntype ReverseProxy struct {\n\t// Rewrite must be a function which modifies\n\t// the request into a new request to be sent\n\t// using Transport. Its response is then copied\n\t// back to the original client unmodified.\n\t// Rewrite must not access the provided ProxyRequest\n\t// or its contents after returning.\n\t//\n\t// The Forwarded, X-Forwarded, X-Forwarded-Host,\n\t// and X-Forwarded-Proto headers are removed from the\n\t// outbound request before Rewrite is called. See also\n\t// the ProxyRequest.SetXForwarded method.\n\t//\n\t// Unparsable query parameters are removed from the\n\t// outbound request before Rewrite is called.\n\t// The Rewrite function may copy the inbound URL's\n\t// RawQuery to the outbound URL to preserve the original\n\t// parameter string. Note that this can lead to security\n\t// issues if the proxy's interpretation of query parameters\n\t// does not match that of the downstream server.\n\t//\n\t// At most one of Rewrite or Director may be set.\n\tRewrite func(*ProxyRequest)\n\n\t// Director is a function which modifies\n\t// the request into a new request to be sent\n\t// using Transport. Its response is then copied\n\t// back to the original client unmodified.\n\t// Director must not access the provided Request\n\t// after returning.\n\t//\n\t// By default, the X-Forwarded-For header is set to the\n\t// value of the client IP address. If an X-Forwarded-For\n\t// header already exists, the client IP is appended to the\n\t// existing values. As a special case, if the header\n\t// exists in the Request.Header map but has a nil value\n\t// (such as when set by the Director func), the X-Forwarded-For\n\t// header is not modified.\n\t//\n\t// To prevent IP spoofing, be sure to delete any pre-existing\n\t// X-Forwarded-For header coming from the client or\n\t// an untrusted proxy.\n\t//\n\t// Hop-by-hop headers are removed from the request after\n\t// Director returns, which can remove headers added by\n\t// Director. Use a Rewrite function instead to ensure\n\t// modifications to the request are preserved.\n\t//\n\t// Unparsable query parameters are removed from the outbound\n\t// request if Request.Form is set after Director returns.\n\t//\n\t// At most one of Rewrite or Director may be set.\n\tDirector func(*http.Request)\n\n\t// The transport used to perform proxy requests.\n\t// If nil, http.DefaultTransport is used.\n\tTransport http.RoundTripper\n\n\t// FlushInterval specifies the flush interval\n\t// to flush to the client while copying the\n\t// response body.\n\t// If zero, no periodic flushing is done.\n\t// A negative value means to flush immediately\n\t// after each write to the client.\n\t// The FlushInterval is ignored when ReverseProxy\n\t// recognizes a response as a streaming response, or\n\t// if its ContentLength is -1; for such responses, writes\n\t// are flushed to the client immediately.\n\tFlushInterval time.Duration\n\n\t// ErrorLog specifies an optional logger for errors\n\t// that occur when attempting to proxy the request.\n\t// If nil, logging is done via the log package's standard logger.\n\tErrorLog *log.Logger\n\n\t// BufferPool optionally specifies a buffer pool to\n\t// get byte slices for use by io.CopyBuffer when\n\t// copying HTTP response bodies.\n\tBufferPool BufferPool\n\n\t// ModifyResponse is an optional function that modifies the\n\t// Response from the backend. It is called if the backend\n\t// returns a response at all, with any HTTP status code.\n\t// If the backend is unreachable, the optional ErrorHandler is\n\t// called without any call to ModifyResponse.\n\t//\n\t// If ModifyResponse returns an error, ErrorHandler is called\n\t// with its error value. If ErrorHandler is nil, its default\n\t// implementation is used.\n\tModifyResponse func(*http.Response) error\n\n\t// ErrorHandler is an optional function that handles errors\n\t// reaching the backend or errors from ModifyResponse.\n\t//\n\t// If nil, the default is to log the provided error and return\n\t// a 502 Status Bad Gateway response.\n\tErrorHandler func(http.ResponseWriter, *http.Request, error)\n\n\t// CopyBuffer is an optional function for handling the copying of the\n\t// response body. If nil, an internal implementation is used.\n\tCopyBuffer CopyBuffer\n}\n\n// A BufferPool is an interface for getting and returning temporary\n// byte slices for use by [io.CopyBuffer].\ntype BufferPool interface {\n\tGet() []byte\n\tPut([]byte)\n}\n\ntype CopyBuffer interface {\n\tCopy(ctx context.Context, dst io.Writer, src io.Reader, buf []byte) (int64, error)\n}\n\nfunc singleJoiningSlash(a, b string) string {\n\taslash := strings.HasSuffix(a, \"/\")\n\tbslash := strings.HasPrefix(b, \"/\")\n\tswitch {\n\tcase aslash && bslash:\n\t\treturn a + b[1:]\n\tcase !aslash && !bslash:\n\t\treturn a + \"/\" + b\n\t}\n\treturn a + b\n}\n\nfunc joinURLPath(a, b *url.URL) (path, rawpath string) {\n\tif a.RawPath == \"\" && b.RawPath == \"\" {\n\t\treturn singleJoiningSlash(a.Path, b.Path), \"\"\n\t}\n\t// Same as singleJoiningSlash, but uses EscapedPath to determine\n\t// whether a slash should be added\n\tapath := a.EscapedPath()\n\tbpath := b.EscapedPath()\n\n\taslash := strings.HasSuffix(apath, \"/\")\n\tbslash := strings.HasPrefix(bpath, \"/\")\n\n\tswitch {\n\tcase aslash && bslash:\n\t\treturn a.Path + b.Path[1:], apath + bpath[1:]\n\tcase !aslash && !bslash:\n\t\treturn a.Path + \"/\" + b.Path, apath + \"/\" + bpath\n\t}\n\treturn a.Path + b.Path, apath + bpath\n}\n\n// NewSingleHostReverseProxy returns a new [ReverseProxy] that routes\n// URLs to the scheme, host, and base path provided in target. If the\n// target's path is \"/base\" and the incoming request was for \"/dir\",\n// the target request will be for /base/dir.\n//\n// NewSingleHostReverseProxy does not rewrite the Host header.\n//\n// To customize the ReverseProxy behavior beyond what\n// NewSingleHostReverseProxy provides, use ReverseProxy directly\n// with a Rewrite function. The ProxyRequest SetURL method\n// may be used to route the outbound request. (Note that SetURL,\n// unlike NewSingleHostReverseProxy, rewrites the Host header\n// of the outbound request by default.)\n//\n//\tproxy := &ReverseProxy{\n//\t\tRewrite: func(r *ProxyRequest) {\n//\t\t\tr.SetURL(target)\n//\t\t\tr.Out.Host = r.In.Host // if desired\n//\t\t},\n//\t}\nfunc NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {\n\tdirector := func(req *http.Request) {\n\t\trewriteRequestURL(req, target)\n\t}\n\treturn &ReverseProxy{Director: director}\n}\n\nfunc rewriteRequestURL(req *http.Request, target *url.URL) {\n\ttargetQuery := target.RawQuery\n\treq.URL.Scheme = target.Scheme\n\treq.URL.Host = target.Host\n\treq.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)\n\tif targetQuery == \"\" || req.URL.RawQuery == \"\" {\n\t\treq.URL.RawQuery = targetQuery + req.URL.RawQuery\n\t} else {\n\t\treq.URL.RawQuery = targetQuery + \"&\" + req.URL.RawQuery\n\t}\n}\n\nfunc copyHeader(dst, src http.Header) {\n\tfor k, vv := range src {\n\t\tfor _, v := range vv {\n\t\t\tdst.Add(k, v)\n\t\t}\n\t}\n}\n\n// Hop-by-hop headers. These are removed when sent to the backend.\n// As of RFC 7230, hop-by-hop headers are required to appear in the\n// Connection header field. These are the headers defined by the\n// obsoleted RFC 2616 (section 13.5.1) and are used for backward\n// compatibility.\nvar hopHeaders = []string{\n\t\"Connection\",\n\t\"Proxy-Connection\", // non-standard but still sent by libcurl and rejected by e.g. google\n\t\"Keep-Alive\",\n\t\"Proxy-Authenticate\",\n\t\"Proxy-Authorization\",\n\t\"Te\",      // canonicalized version of \"TE\"\n\t\"Trailer\", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522\n\t\"Transfer-Encoding\",\n\t\"Upgrade\",\n}\n\nfunc (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {\n\tp.logf(\"http: proxy error: %v\", err)\n\trw.WriteHeader(http.StatusBadGateway)\n}\n\nfunc (p *ReverseProxy) getErrorHandler() func(http.ResponseWriter, *http.Request, error) {\n\tif p.ErrorHandler != nil {\n\t\treturn p.ErrorHandler\n\t}\n\treturn p.defaultErrorHandler\n}\n\n// modifyResponse conditionally runs the optional ModifyResponse hook\n// and reports whether the request should proceed.\nfunc (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response, req *http.Request) bool {\n\tif p.ModifyResponse == nil {\n\t\treturn true\n\t}\n\tif err := p.ModifyResponse(res); err != nil {\n\t\tres.Body.Close()\n\t\tp.getErrorHandler()(rw, req, err)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\n\ttransport := p.Transport\n\tif transport == nil {\n\t\ttransport = http.DefaultTransport\n\t}\n\n\tctx := req.Context()\n\tif ctx.Done() != nil {\n\t\t// CloseNotifier predates context.Context, and has been\n\t\t// entirely superseded by it. If the request contains\n\t\t// a Context that carries a cancellation signal, don't\n\t\t// bother spinning up a goroutine to watch the CloseNotify\n\t\t// channel (if any).\n\t\t//\n\t\t// If the request Context has a nil Done channel (which\n\t\t// means it is either context.Background, or a custom\n\t\t// Context implementation with no cancellation signal),\n\t\t// then consult the CloseNotifier if available.\n\t} else if cn, ok := rw.(http.CloseNotifier); ok {\n\t\tvar cancel context.CancelFunc\n\t\tctx, cancel = context.WithCancel(ctx)\n\t\tdefer cancel()\n\t\tnotifyChan := cn.CloseNotify()\n\t\tgo func() {\n\t\t\tselect {\n\t\t\tcase <-notifyChan:\n\t\t\t\tcancel()\n\t\t\tcase <-ctx.Done():\n\t\t\t}\n\t\t}()\n\t}\n\n\toutreq := req.Clone(ctx)\n\tif req.ContentLength == 0 {\n\t\toutreq.Body = nil // Issue 16036: nil Body for http.Transport retries\n\t}\n\tif outreq.Body != nil {\n\t\t// Reading from the request body after returning from a handler is not\n\t\t// allowed, and the RoundTrip goroutine that reads the Body can outlive\n\t\t// this handler. This can lead to a crash if the handler panics (see\n\t\t// Issue 46866). Although calling Close doesn't guarantee there isn't\n\t\t// any Read in flight after the handle returns, in practice it's safe to\n\t\t// read after closing it.\n\t\tdefer outreq.Body.Close()\n\t}\n\tif outreq.Header == nil {\n\t\toutreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate\n\t}\n\n\tif (p.Director != nil) == (p.Rewrite != nil) {\n\t\tp.getErrorHandler()(rw, req, errors.New(\"ReverseProxy must have exactly one of Director or Rewrite set\"))\n\t\treturn\n\t}\n\n\tif p.Director != nil {\n\t\tp.Director(outreq)\n\t\tif outreq.Form != nil {\n\t\t\toutreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)\n\t\t}\n\t}\n\toutreq.Close = false\n\n\treqUpType := upgradeType(outreq.Header)\n\tif !IsPrint(reqUpType) {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"client tried to switch to invalid protocol %q\", reqUpType))\n\t\treturn\n\t}\n\tremoveHopByHopHeaders(outreq.Header)\n\n\t// Issue 21096: tell backend applications that care about trailer support\n\t// that we support trailers. (We do, but we don't go out of our way to\n\t// advertise that unless the incoming client request thought it was worth\n\t// mentioning.) Note that we look at req.Header, not outreq.Header, since\n\t// the latter has passed through removeHopByHopHeaders.\n\tif httpguts.HeaderValuesContainsToken(req.Header[\"Te\"], \"trailers\") {\n\t\toutreq.Header.Set(\"Te\", \"trailers\")\n\t}\n\n\t// After stripping all the hop-by-hop connection headers above, add back any\n\t// necessary for protocol upgrades, such as for websockets.\n\tif reqUpType != \"\" {\n\t\toutreq.Header.Set(\"Connection\", \"Upgrade\")\n\t\toutreq.Header.Set(\"Upgrade\", reqUpType)\n\t}\n\n\tif p.Rewrite != nil {\n\t\t// Strip client-provided forwarding headers.\n\t\t// The Rewrite func may use SetXForwarded to set new values\n\t\t// for these or copy the previous values from the inbound request.\n\t\toutreq.Header.Del(\"Forwarded\")\n\t\toutreq.Header.Del(\"X-Forwarded-For\")\n\t\toutreq.Header.Del(\"X-Forwarded-Host\")\n\t\toutreq.Header.Del(\"X-Forwarded-Proto\")\n\n\t\t// Remove unparsable query parameters from the outbound request.\n\t\toutreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)\n\n\t\tpr := &ProxyRequest{\n\t\t\tIn:  req,\n\t\t\tOut: outreq,\n\t\t}\n\t\tp.Rewrite(pr)\n\t\toutreq = pr.Out\n\t} else {\n\t\tif clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {\n\t\t\t// If we aren't the first proxy retain prior\n\t\t\t// X-Forwarded-For information as a comma+space\n\t\t\t// separated list and fold multiple headers into one.\n\t\t\tprior, ok := outreq.Header[\"X-Forwarded-For\"]\n\t\t\tomit := ok && prior == nil // Issue 38079: nil now means don't populate the header\n\t\t\tif len(prior) > 0 {\n\t\t\t\tclientIP = strings.Join(prior, \", \") + \", \" + clientIP\n\t\t\t}\n\t\t\tif !omit {\n\t\t\t\toutreq.Header.Set(\"X-Forwarded-For\", clientIP)\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, ok := outreq.Header[\"User-Agent\"]; !ok {\n\t\t// If the outbound request doesn't have a User-Agent header set,\n\t\t// don't send the default Go HTTP client User-Agent.\n\t\toutreq.Header.Set(\"User-Agent\", \"\")\n\t}\n\n\tvar (\n\t\troundTripMutex sync.Mutex\n\t\troundTripDone  bool\n\t)\n\ttrace := &httptrace.ClientTrace{\n\t\tGot1xxResponse: func(code int, header textproto.MIMEHeader) error {\n\t\t\troundTripMutex.Lock()\n\t\t\tdefer roundTripMutex.Unlock()\n\t\t\tif roundTripDone {\n\t\t\t\t// If RoundTrip has returned, don't try to further modify\n\t\t\t\t// the ResponseWriter's header map.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\th := rw.Header()\n\t\t\tcopyHeader(h, http.Header(header))\n\t\t\trw.WriteHeader(code)\n\n\t\t\t// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses\n\t\t\tclear(h)\n\t\t\treturn nil\n\t\t},\n\t}\n\toutreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace))\n\n\tres, err := transport.RoundTrip(outreq)\n\troundTripMutex.Lock()\n\troundTripDone = true\n\troundTripMutex.Unlock()\n\tif err != nil {\n\t\tp.getErrorHandler()(rw, outreq, err)\n\t\treturn\n\t}\n\n\t// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)\n\tif res.StatusCode == http.StatusSwitchingProtocols {\n\t\tif !p.modifyResponse(rw, res, outreq) {\n\t\t\treturn\n\t\t}\n\t\tp.handleUpgradeResponse(rw, outreq, res)\n\t\treturn\n\t}\n\n\tremoveHopByHopHeaders(res.Header)\n\n\tif !p.modifyResponse(rw, res, outreq) {\n\t\treturn\n\t}\n\n\tcopyHeader(rw.Header(), res.Header)\n\n\t// The \"Trailer\" header isn't included in the Transport's response,\n\t// at least for *http.Transport. Build it up from Trailer.\n\tannouncedTrailers := len(res.Trailer)\n\tif announcedTrailers > 0 {\n\t\ttrailerKeys := make([]string, 0, len(res.Trailer))\n\t\tfor k := range res.Trailer {\n\t\t\ttrailerKeys = append(trailerKeys, k)\n\t\t}\n\t\trw.Header().Add(\"Trailer\", strings.Join(trailerKeys, \", \"))\n\t}\n\n\trw.WriteHeader(res.StatusCode)\n\n\tvar resContext context.Context\n\tif res.Request != nil {\n\t\tresContext = res.Request.Context()\n\t}\n\terr = p.copyResponse(resContext, rw, res.Body, p.flushInterval(res))\n\tif err != nil {\n\t\tdefer res.Body.Close()\n\t\t// Since we're streaming the response, if we run into an error all we can do\n\t\t// is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler\n\t\t// on read error while copying body.\n\t\tif !shouldPanicOnCopyError(req) {\n\t\t\tp.logf(\"suppressing panic for copyResponse error in test; copy error: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tpanic(http.ErrAbortHandler)\n\t}\n\tres.Body.Close() // close now, instead of defer, to populate res.Trailer\n\n\tif len(res.Trailer) > 0 {\n\t\t// Force chunking if we saw a response trailer.\n\t\t// This prevents net/http from calculating the length for short\n\t\t// bodies and adding a Content-Length.\n\t\thttp.NewResponseController(rw).Flush()\n\t}\n\n\tif len(res.Trailer) == announcedTrailers {\n\t\tcopyHeader(rw.Header(), res.Trailer)\n\t\treturn\n\t}\n\n\tfor k, vv := range res.Trailer {\n\t\tk = http.TrailerPrefix + k\n\t\tfor _, v := range vv {\n\t\t\trw.Header().Add(k, v)\n\t\t}\n\t}\n}\n\nvar inOurTests bool // whether we're in our own tests\n\n// shouldPanicOnCopyError reports whether the reverse proxy should\n// panic with http.ErrAbortHandler. This is the right thing to do by\n// default, but Go 1.10 and earlier did not, so existing unit tests\n// weren't expecting panics. Only panic in our own tests, or when\n// running under the HTTP server.\nfunc shouldPanicOnCopyError(req *http.Request) bool {\n\tif inOurTests {\n\t\t// Our tests know to handle this panic.\n\t\treturn true\n\t}\n\tif req.Context().Value(http.ServerContextKey) != nil {\n\t\t// We seem to be running under an HTTP server, so\n\t\t// it'll recover the panic.\n\t\treturn true\n\t}\n\t// Otherwise act like Go 1.10 and earlier to not break\n\t// existing tests.\n\treturn false\n}\n\n// removeHopByHopHeaders removes hop-by-hop headers.\nfunc removeHopByHopHeaders(h http.Header) {\n\t// RFC 7230, section 6.1: Remove headers listed in the \"Connection\" header.\n\tfor _, f := range h[\"Connection\"] {\n\t\tfor _, sf := range strings.Split(f, \",\") {\n\t\t\tif sf = textproto.TrimString(sf); sf != \"\" {\n\t\t\t\th.Del(sf)\n\t\t\t}\n\t\t}\n\t}\n\t// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.\n\t// This behavior is superseded by the RFC 7230 Connection header, but\n\t// preserve it for backwards compatibility.\n\tfor _, f := range hopHeaders {\n\t\th.Del(f)\n\t}\n}\n\n// flushInterval returns the p.FlushInterval value, conditionally\n// overriding its value for a specific request/response.\nfunc (p *ReverseProxy) flushInterval(res *http.Response) time.Duration {\n\tresCT := res.Header.Get(\"Content-Type\")\n\n\t// For Server-Sent Events responses, flush immediately.\n\t// The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-stream\n\tif baseCT, _, _ := mime.ParseMediaType(resCT); baseCT == \"text/event-stream\" {\n\t\treturn -1 // negative means immediately\n\t}\n\n\t// We might have the case of streaming for which Content-Length might be unset.\n\tif res.ContentLength == -1 {\n\t\treturn -1\n\t}\n\n\treturn p.FlushInterval\n}\n\nfunc (p *ReverseProxy) copyResponse(ctx context.Context, dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {\n\tvar w io.Writer = dst\n\n\tif flushInterval != 0 {\n\t\tmlw := &maxLatencyWriter{\n\t\t\tdst:     dst,\n\t\t\tflush:   http.NewResponseController(dst).Flush,\n\t\t\tlatency: flushInterval,\n\t\t}\n\t\tdefer mlw.stop()\n\n\t\t// set up initial timer so headers get flushed even if body writes are delayed\n\t\tmlw.flushPending = true\n\t\tmlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)\n\n\t\tw = mlw\n\t}\n\n\tvar buf []byte\n\tif p.BufferPool != nil {\n\t\tbuf = p.BufferPool.Get()\n\t\tdefer p.BufferPool.Put(buf)\n\t}\n\n\tvar err error\n\tif p.CopyBuffer != nil {\n\t\t_, err = p.CopyBuffer.Copy(ctx, w, src, buf)\n\t} else {\n\t\t_, err = p.copyBuffer(w, src, buf)\n\t}\n\n\treturn err\n}\n\n// copyBuffer returns any write errors or non-EOF read errors, and the amount\n// of bytes written.\nfunc (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {\n\tif len(buf) == 0 {\n\t\tbuf = make([]byte, 32*1024)\n\t}\n\tvar written int64\n\tfor {\n\t\tnr, rerr := src.Read(buf)\n\t\tif rerr != nil && rerr != io.EOF && rerr != context.Canceled {\n\t\t\tp.logf(\"httputil: ReverseProxy read error during body copy: %v\", rerr)\n\t\t}\n\t\tif nr > 0 {\n\t\t\tnw, werr := dst.Write(buf[:nr])\n\t\t\tif nw > 0 {\n\t\t\t\twritten += int64(nw)\n\t\t\t}\n\t\t\tif werr != nil {\n\t\t\t\treturn written, werr\n\t\t\t}\n\t\t\tif nr != nw {\n\t\t\t\treturn written, io.ErrShortWrite\n\t\t\t}\n\t\t}\n\t\tif rerr != nil {\n\t\t\tif rerr == io.EOF {\n\t\t\t\trerr = nil\n\t\t\t}\n\t\t\treturn written, rerr\n\t\t}\n\t}\n}\n\nfunc (p *ReverseProxy) logf(format string, args ...any) {\n\tif p.ErrorLog != nil {\n\t\tp.ErrorLog.Printf(format, args...)\n\t} else {\n\t\tlog.Printf(format, args...)\n\t}\n}\n\ntype maxLatencyWriter struct {\n\tdst     io.Writer\n\tflush   func() error\n\tlatency time.Duration // non-zero; negative means to flush immediately\n\n\tmu           sync.Mutex // protects t, flushPending, and dst.Flush\n\tt            *time.Timer\n\tflushPending bool\n}\n\nfunc (m *maxLatencyWriter) Write(p []byte) (n int, err error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tn, err = m.dst.Write(p)\n\tif m.latency < 0 {\n\t\tm.flush()\n\t\treturn\n\t}\n\tif m.flushPending {\n\t\treturn\n\t}\n\tif m.t == nil {\n\t\tm.t = time.AfterFunc(m.latency, m.delayedFlush)\n\t} else {\n\t\tm.t.Reset(m.latency)\n\t}\n\tm.flushPending = true\n\treturn\n}\n\nfunc (m *maxLatencyWriter) delayedFlush() {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tif !m.flushPending { // if stop was called but AfterFunc already started this goroutine\n\t\treturn\n\t}\n\tm.flush()\n\tm.flushPending = false\n}\n\nfunc (m *maxLatencyWriter) stop() {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.flushPending = false\n\tif m.t != nil {\n\t\tm.t.Stop()\n\t}\n}\n\nfunc upgradeType(h http.Header) string {\n\tif !httpguts.HeaderValuesContainsToken(h[\"Connection\"], \"Upgrade\") {\n\t\treturn \"\"\n\t}\n\treturn h.Get(\"Upgrade\")\n}\n\nfunc (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {\n\treqUpType := upgradeType(req.Header)\n\tresUpType := upgradeType(res.Header)\n\tif !IsPrint(resUpType) { // We know reqUpType is ASCII, it's checked by the caller.\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"backend tried to switch to invalid protocol %q\", resUpType))\n\t}\n\tif !EqualFold(reqUpType, resUpType) {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"backend tried to switch protocol %q when %q was requested\", resUpType, reqUpType))\n\t\treturn\n\t}\n\n\tbackConn, ok := res.Body.(io.ReadWriteCloser)\n\tif !ok {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"internal error: 101 switching protocols response with non-writable body\"))\n\t\treturn\n\t}\n\n\trc := http.NewResponseController(rw)\n\tconn, brw, hijackErr := rc.Hijack()\n\tif errors.Is(hijackErr, http.ErrNotSupported) {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"can't switch protocols using non-Hijacker ResponseWriter type %T\", rw))\n\t\treturn\n\t}\n\n\tbackConnCloseCh := make(chan bool)\n\tgo func() {\n\t\t// Ensure that the cancellation of a request closes the backend.\n\t\t// See issue https://golang.org/issue/35559.\n\t\tselect {\n\t\tcase <-req.Context().Done():\n\t\tcase <-backConnCloseCh:\n\t\t}\n\t\tbackConn.Close()\n\t}()\n\tdefer close(backConnCloseCh)\n\n\tif hijackErr != nil {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"Hijack failed on protocol switch: %v\", hijackErr))\n\t\treturn\n\t}\n\tdefer conn.Close()\n\n\tcopyHeader(rw.Header(), res.Header)\n\n\tres.Header = rw.Header()\n\tres.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above\n\tif err := res.Write(brw); err != nil {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"response write: %v\", err))\n\t\treturn\n\t}\n\tif err := brw.Flush(); err != nil {\n\t\tp.getErrorHandler()(rw, req, fmt.Errorf(\"response flush: %v\", err))\n\t\treturn\n\t}\n\terrc := make(chan error, 1)\n\tspc := switchProtocolCopier{user: conn, backend: backConn}\n\tgo spc.copyToBackend(errc)\n\tgo spc.copyFromBackend(errc)\n\t<-errc\n}\n\n// switchProtocolCopier exists so goroutines proxying data back and\n// forth have nice names in stacks.\ntype switchProtocolCopier struct {\n\tuser, backend io.ReadWriter\n}\n\nfunc (c switchProtocolCopier) copyFromBackend(errc chan<- error) {\n\t_, err := io.Copy(c.user, c.backend)\n\terrc <- err\n}\n\nfunc (c switchProtocolCopier) copyToBackend(errc chan<- error) {\n\t_, err := io.Copy(c.backend, c.user)\n\terrc <- err\n}\n\nfunc cleanQueryParams(s string) string {\n\treencode := func(s string) string {\n\t\tv, _ := url.ParseQuery(s)\n\t\treturn v.Encode()\n\t}\n\tfor i := 0; i < len(s); {\n\t\tswitch s[i] {\n\t\tcase ';':\n\t\t\treturn reencode(s)\n\t\tcase '%':\n\t\t\tif i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {\n\t\t\t\treturn reencode(s)\n\t\t\t}\n\t\t\ti += 3\n\t\tdefault:\n\t\t\ti++\n\t\t}\n\t}\n\treturn s\n}\n\nfunc ishex(c byte) bool {\n\tswitch {\n\tcase '0' <= c && c <= '9':\n\t\treturn true\n\tcase 'a' <= c && c <= 'f':\n\t\treturn true\n\tcase 'A' <= c && c <= 'F':\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/iso/internal/reverseproxy_test.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Reverse proxy tests.\n\npackage internal\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst fakeHopHeader = \"X-Fake-Hop-Header-For-Test\"\n\nfunc init() {\n\tinOurTests = true\n\thopHeaders = append(hopHeaders, fakeHopHeader)\n}\n\nfunc TestReverseProxy(t *testing.T) {\n\tconst backendResponse = \"I am the backend\"\n\tconst backendStatus = 404\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method == \"GET\" && r.FormValue(\"mode\") == \"hangup\" {\n\t\t\tc, _, _ := w.(http.Hijacker).Hijack()\n\t\t\tc.Close()\n\t\t\treturn\n\t\t}\n\t\tif len(r.TransferEncoding) > 0 {\n\t\t\tt.Errorf(\"backend got unexpected TransferEncoding: %v\", r.TransferEncoding)\n\t\t}\n\t\tif r.Header.Get(\"X-Forwarded-For\") == \"\" {\n\t\t\tt.Errorf(\"didn't get X-Forwarded-For header\")\n\t\t}\n\t\tif c := r.Header.Get(\"Connection\"); c != \"\" {\n\t\t\tt.Errorf(\"handler got Connection header value %q\", c)\n\t\t}\n\t\tif c := r.Header.Get(\"Te\"); c != \"trailers\" {\n\t\t\tt.Errorf(\"handler got Te header value %q; want 'trailers'\", c)\n\t\t}\n\t\tif c := r.Header.Get(\"Upgrade\"); c != \"\" {\n\t\t\tt.Errorf(\"handler got Upgrade header value %q\", c)\n\t\t}\n\t\tif c := r.Header.Get(\"Proxy-Connection\"); c != \"\" {\n\t\t\tt.Errorf(\"handler got Proxy-Connection header value %q\", c)\n\t\t}\n\t\tif g, e := r.Host, \"some-name\"; g != e {\n\t\t\tt.Errorf(\"backend got Host header %q, want %q\", g, e)\n\t\t}\n\t\tw.Header().Set(\"Trailers\", \"not a special header field name\")\n\t\tw.Header().Set(\"Trailer\", \"X-Trailer\")\n\t\tw.Header().Set(\"X-Foo\", \"bar\")\n\t\tw.Header().Set(\"Upgrade\", \"foo\")\n\t\tw.Header().Set(fakeHopHeader, \"foo\")\n\t\tw.Header().Add(\"X-Multi-Value\", \"foo\")\n\t\tw.Header().Add(\"X-Multi-Value\", \"bar\")\n\t\thttp.SetCookie(w, &http.Cookie{Name: \"flavor\", Value: \"chocolateChip\"})\n\t\tw.WriteHeader(backendStatus)\n\t\tw.Write([]byte(backendResponse))\n\t\tw.Header().Set(\"X-Trailer\", \"trailer_value\")\n\t\tw.Header().Set(http.TrailerPrefix+\"X-Unannounced-Trailer\", \"unannounced_trailer_value\")\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Host = \"some-name\"\n\tgetReq.Header.Set(\"Connection\", \"close, TE\")\n\tgetReq.Header.Add(\"Te\", \"foo\")\n\tgetReq.Header.Add(\"Te\", \"bar, trailers\")\n\tgetReq.Header.Set(\"Proxy-Connection\", \"should be deleted\")\n\tgetReq.Header.Set(\"Upgrade\", \"foo\")\n\tgetReq.Close = true\n\tres, err := frontendClient.Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tif g, e := res.StatusCode, backendStatus; g != e {\n\t\tt.Errorf(\"got res.StatusCode %d; expected %d\", g, e)\n\t}\n\tif g, e := res.Header.Get(\"X-Foo\"), \"bar\"; g != e {\n\t\tt.Errorf(\"got X-Foo %q; expected %q\", g, e)\n\t}\n\tif c := res.Header.Get(fakeHopHeader); c != \"\" {\n\t\tt.Errorf(\"got %s header value %q\", fakeHopHeader, c)\n\t}\n\tif g, e := res.Header.Get(\"Trailers\"), \"not a special header field name\"; g != e {\n\t\tt.Errorf(\"header Trailers = %q; want %q\", g, e)\n\t}\n\tif g, e := len(res.Header[\"X-Multi-Value\"]), 2; g != e {\n\t\tt.Errorf(\"got %d X-Multi-Value header values; expected %d\", g, e)\n\t}\n\tif g, e := len(res.Header[\"Set-Cookie\"]), 1; g != e {\n\t\tt.Fatalf(\"got %d SetCookies, want %d\", g, e)\n\t}\n\tif g, e := res.Trailer, (http.Header{\"X-Trailer\": nil}); !reflect.DeepEqual(g, e) {\n\t\tt.Errorf(\"before reading body, Trailer = %#v; want %#v\", g, e)\n\t}\n\tif cookie := res.Cookies()[0]; cookie.Name != \"flavor\" {\n\t\tt.Errorf(\"unexpected cookie %q\", cookie.Name)\n\t}\n\tbodyBytes, _ := io.ReadAll(res.Body)\n\tif g, e := string(bodyBytes), backendResponse; g != e {\n\t\tt.Errorf(\"got body %q; expected %q\", g, e)\n\t}\n\tif g, e := res.Trailer.Get(\"X-Trailer\"), \"trailer_value\"; g != e {\n\t\tt.Errorf(\"Trailer(X-Trailer) = %q ; want %q\", g, e)\n\t}\n\tif g, e := res.Trailer.Get(\"X-Unannounced-Trailer\"), \"unannounced_trailer_value\"; g != e {\n\t\tt.Errorf(\"Trailer(X-Unannounced-Trailer) = %q ; want %q\", g, e)\n\t}\n\n\t// Test that a backend failing to be reached or one which doesn't return\n\t// a response results in a StatusBadGateway.\n\tgetReq, _ = http.NewRequest(\"GET\", frontend.URL+\"/?mode=hangup\", nil)\n\tgetReq.Close = true\n\tres, err = frontendClient.Do(getReq)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tres.Body.Close()\n\tif res.StatusCode != http.StatusBadGateway {\n\t\tt.Errorf(\"request to bad proxy = %v; want 502 StatusBadGateway\", res.Status)\n\t}\n\n}\n\n// Issue 16875: remove any proxied headers mentioned in the \"Connection\"\n// header value.\nfunc TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {\n\tconst fakeConnectionToken = \"X-Fake-Connection-Token\"\n\tconst backendResponse = \"I am the backend\"\n\n\t// someConnHeader is some arbitrary header to be declared as a hop-by-hop header\n\t// in the Request's Connection header.\n\tconst someConnHeader = \"X-Some-Conn-Header\"\n\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif c := r.Header.Get(\"Connection\"); c != \"\" {\n\t\t\tt.Errorf(\"handler got header %q = %q; want empty\", \"Connection\", c)\n\t\t}\n\t\tif c := r.Header.Get(fakeConnectionToken); c != \"\" {\n\t\t\tt.Errorf(\"handler got header %q = %q; want empty\", fakeConnectionToken, c)\n\t\t}\n\t\tif c := r.Header.Get(someConnHeader); c != \"\" {\n\t\t\tt.Errorf(\"handler got header %q = %q; want empty\", someConnHeader, c)\n\t\t}\n\t\tw.Header().Add(\"Connection\", \"Upgrade, \"+fakeConnectionToken)\n\t\tw.Header().Add(\"Connection\", someConnHeader)\n\t\tw.Header().Set(someConnHeader, \"should be deleted\")\n\t\tw.Header().Set(fakeConnectionToken, \"should be deleted\")\n\t\tio.WriteString(w, backendResponse)\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tproxyHandler.ServeHTTP(w, r)\n\t\tif c := r.Header.Get(someConnHeader); c != \"should be deleted\" {\n\t\t\tt.Errorf(\"handler modified header %q = %q; want %q\", someConnHeader, c, \"should be deleted\")\n\t\t}\n\t\tif c := r.Header.Get(fakeConnectionToken); c != \"should be deleted\" {\n\t\t\tt.Errorf(\"handler modified header %q = %q; want %q\", fakeConnectionToken, c, \"should be deleted\")\n\t\t}\n\t\tc := r.Header[\"Connection\"]\n\t\tvar cf []string\n\t\tfor _, f := range c {\n\t\t\tfor _, sf := range strings.Split(f, \",\") {\n\t\t\t\tif sf = strings.TrimSpace(sf); sf != \"\" {\n\t\t\t\t\tcf = append(cf, sf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tslices.Sort(cf)\n\t\texpectedValues := []string{\"Upgrade\", someConnHeader, fakeConnectionToken}\n\t\tslices.Sort(expectedValues)\n\t\tif !reflect.DeepEqual(cf, expectedValues) {\n\t\t\tt.Errorf(\"handler modified header %q = %q; want %q\", \"Connection\", cf, expectedValues)\n\t\t}\n\t}))\n\tdefer frontend.Close()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Header.Add(\"Connection\", \"Upgrade, \"+fakeConnectionToken)\n\tgetReq.Header.Add(\"Connection\", someConnHeader)\n\tgetReq.Header.Set(someConnHeader, \"should be deleted\")\n\tgetReq.Header.Set(fakeConnectionToken, \"should be deleted\")\n\tres, err := frontend.Client().Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tbodyBytes, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"reading body: %v\", err)\n\t}\n\tif got, want := string(bodyBytes), backendResponse; got != want {\n\t\tt.Errorf(\"got body %q; want %q\", got, want)\n\t}\n\tif c := res.Header.Get(\"Connection\"); c != \"\" {\n\t\tt.Errorf(\"handler got header %q = %q; want empty\", \"Connection\", c)\n\t}\n\tif c := res.Header.Get(someConnHeader); c != \"\" {\n\t\tt.Errorf(\"handler got header %q = %q; want empty\", someConnHeader, c)\n\t}\n\tif c := res.Header.Get(fakeConnectionToken); c != \"\" {\n\t\tt.Errorf(\"handler got header %q = %q; want empty\", fakeConnectionToken, c)\n\t}\n}\n\nfunc TestReverseProxyStripEmptyConnection(t *testing.T) {\n\t// See Issue 46313.\n\tconst backendResponse = \"I am the backend\"\n\n\t// someConnHeader is some arbitrary header to be declared as a hop-by-hop header\n\t// in the Request's Connection header.\n\tconst someConnHeader = \"X-Some-Conn-Header\"\n\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif c := r.Header.Values(\"Connection\"); len(c) != 0 {\n\t\t\tt.Errorf(\"handler got header %q = %v; want empty\", \"Connection\", c)\n\t\t}\n\t\tif c := r.Header.Get(someConnHeader); c != \"\" {\n\t\t\tt.Errorf(\"handler got header %q = %q; want empty\", someConnHeader, c)\n\t\t}\n\t\tw.Header().Add(\"Connection\", \"\")\n\t\tw.Header().Add(\"Connection\", someConnHeader)\n\t\tw.Header().Set(someConnHeader, \"should be deleted\")\n\t\tio.WriteString(w, backendResponse)\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tproxyHandler.ServeHTTP(w, r)\n\t\tif c := r.Header.Get(someConnHeader); c != \"should be deleted\" {\n\t\t\tt.Errorf(\"handler modified header %q = %q; want %q\", someConnHeader, c, \"should be deleted\")\n\t\t}\n\t}))\n\tdefer frontend.Close()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Header.Add(\"Connection\", \"\")\n\tgetReq.Header.Add(\"Connection\", someConnHeader)\n\tgetReq.Header.Set(someConnHeader, \"should be deleted\")\n\tres, err := frontend.Client().Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tbodyBytes, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"reading body: %v\", err)\n\t}\n\tif got, want := string(bodyBytes), backendResponse; got != want {\n\t\tt.Errorf(\"got body %q; want %q\", got, want)\n\t}\n\tif c := res.Header.Get(\"Connection\"); c != \"\" {\n\t\tt.Errorf(\"handler got header %q = %q; want empty\", \"Connection\", c)\n\t}\n\tif c := res.Header.Get(someConnHeader); c != \"\" {\n\t\tt.Errorf(\"handler got header %q = %q; want empty\", someConnHeader, c)\n\t}\n}\n\nfunc TestXForwardedFor(t *testing.T) {\n\tconst prevForwardedFor = \"client ip\"\n\tconst backendResponse = \"I am the backend\"\n\tconst backendStatus = 404\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Header.Get(\"X-Forwarded-For\") == \"\" {\n\t\t\tt.Errorf(\"didn't get X-Forwarded-For header\")\n\t\t}\n\t\tif !strings.Contains(r.Header.Get(\"X-Forwarded-For\"), prevForwardedFor) {\n\t\t\tt.Errorf(\"X-Forwarded-For didn't contain prior data\")\n\t\t}\n\t\tw.WriteHeader(backendStatus)\n\t\tw.Write([]byte(backendResponse))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Header.Set(\"Connection\", \"close\")\n\tgetReq.Header.Set(\"X-Forwarded-For\", prevForwardedFor)\n\tgetReq.Close = true\n\tres, err := frontend.Client().Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tif g, e := res.StatusCode, backendStatus; g != e {\n\t\tt.Errorf(\"got res.StatusCode %d; expected %d\", g, e)\n\t}\n\tbodyBytes, _ := io.ReadAll(res.Body)\n\tif g, e := string(bodyBytes), backendResponse; g != e {\n\t\tt.Errorf(\"got body %q; expected %q\", g, e)\n\t}\n}\n\n// Issue 38079: don't append to X-Forwarded-For if it's present but nil\nfunc TestXForwardedFor_Omit(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif v := r.Header.Get(\"X-Forwarded-For\"); v != \"\" {\n\t\t\tt.Errorf(\"got X-Forwarded-For header: %q\", v)\n\t\t}\n\t\tw.Write([]byte(\"hi\"))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\toldDirector := proxyHandler.Director\n\tproxyHandler.Director = func(r *http.Request) {\n\t\tr.Header[\"X-Forwarded-For\"] = nil\n\t\toldDirector(r)\n\t}\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Host = \"some-name\"\n\tgetReq.Close = true\n\tres, err := frontend.Client().Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tres.Body.Close()\n}\n\nfunc TestReverseProxyRewriteStripsForwarded(t *testing.T) {\n\theaders := []string{\n\t\t\"Forwarded\",\n\t\t\"X-Forwarded-For\",\n\t\t\"X-Forwarded-Host\",\n\t\t\"X-Forwarded-Proto\",\n\t}\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfor _, h := range headers {\n\t\t\tif v := r.Header.Get(h); v != \"\" {\n\t\t\t\tt.Errorf(\"got %v header: %q\", h, v)\n\t\t\t}\n\t\t}\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := &ReverseProxy{\n\t\tRewrite: func(r *ProxyRequest) {\n\t\t\tr.SetURL(backendURL)\n\t\t},\n\t}\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgetReq.Host = \"some-name\"\n\tgetReq.Close = true\n\tfor _, h := range headers {\n\t\tgetReq.Header.Set(h, \"x\")\n\t}\n\tres, err := frontend.Client().Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tres.Body.Close()\n}\n\nvar proxyQueryTests = []struct {\n\tbaseSuffix string // suffix to add to backend URL\n\treqSuffix  string // suffix to add to frontend's request URL\n\twant       string // what backend should see for final request URL (without ?)\n}{\n\t{\"\", \"\", \"\"},\n\t{\"?sta=tic\", \"?us=er\", \"sta=tic&us=er\"},\n\t{\"\", \"?us=er\", \"us=er\"},\n\t{\"?sta=tic\", \"\", \"sta=tic\"},\n}\n\nfunc TestReverseProxyQuery(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"X-Got-Query\", r.URL.RawQuery)\n\t\tw.Write([]byte(\"hi\"))\n\t}))\n\tdefer backend.Close()\n\n\tfor i, tt := range proxyQueryTests {\n\t\tbackendURL, err := url.Parse(backend.URL + tt.baseSuffix)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tfrontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL))\n\t\treq, _ := http.NewRequest(\"GET\", frontend.URL+tt.reqSuffix, nil)\n\t\treq.Close = true\n\t\tres, err := frontend.Client().Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%d. Get: %v\", i, err)\n\t\t}\n\t\tif g, e := res.Header.Get(\"X-Got-Query\"), tt.want; g != e {\n\t\t\tt.Errorf(\"%d. got query %q; expected %q\", i, g, e)\n\t\t}\n\t\tres.Body.Close()\n\t\tfrontend.Close()\n\t}\n}\n\nfunc TestReverseProxyFlushInterval(t *testing.T) {\n\tconst expected = \"hi\"\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(expected))\n\t}))\n\tdefer backend.Close()\n\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.FlushInterval = time.Microsecond\n\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\treq.Close = true\n\tres, err := frontend.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tif bodyBytes, _ := io.ReadAll(res.Body); string(bodyBytes) != expected {\n\t\tt.Errorf(\"got body %q; expected %q\", bodyBytes, expected)\n\t}\n}\n\ntype mockFlusher struct {\n\thttp.ResponseWriter\n\tflushed bool\n}\n\nfunc (m *mockFlusher) Flush() {\n\tm.flushed = true\n}\n\ntype wrappedRW struct {\n\thttp.ResponseWriter\n}\n\nfunc (w *wrappedRW) Unwrap() http.ResponseWriter {\n\treturn w.ResponseWriter\n}\n\nfunc TestReverseProxyResponseControllerFlushInterval(t *testing.T) {\n\tconst expected = \"hi\"\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(expected))\n\t}))\n\tdefer backend.Close()\n\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmf := &mockFlusher{}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.FlushInterval = -1 // flush immediately\n\tproxyWithMiddleware := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tmf.ResponseWriter = w\n\t\tw = &wrappedRW{mf}\n\t\tproxyHandler.ServeHTTP(w, r)\n\t})\n\n\tfrontend := httptest.NewServer(proxyWithMiddleware)\n\tdefer frontend.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\treq.Close = true\n\tres, err := frontend.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tif bodyBytes, _ := io.ReadAll(res.Body); string(bodyBytes) != expected {\n\t\tt.Errorf(\"got body %q; expected %q\", bodyBytes, expected)\n\t}\n\tif !mf.flushed {\n\t\tt.Errorf(\"response writer was not flushed\")\n\t}\n}\n\nfunc TestReverseProxyFlushIntervalHeaders(t *testing.T) {\n\tconst expected = \"hi\"\n\tstopCh := make(chan struct{})\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Add(\"MyHeader\", expected)\n\t\tw.WriteHeader(200)\n\t\tw.(http.Flusher).Flush()\n\t\t<-stopCh\n\t}))\n\tdefer backend.Close()\n\tdefer close(stopCh)\n\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.FlushInterval = time.Microsecond\n\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\treq.Close = true\n\n\tctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)\n\tdefer cancel()\n\treq = req.WithContext(ctx)\n\n\tres, err := frontend.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\n\tif res.Header.Get(\"MyHeader\") != expected {\n\t\tt.Errorf(\"got header %q; expected %q\", res.Header.Get(\"MyHeader\"), expected)\n\t}\n}\n\nfunc TestReverseProxyCancellation(t *testing.T) {\n\tconst backendResponse = \"I am the backend\"\n\n\treqInFlight := make(chan struct{})\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tclose(reqInFlight) // cause the client to cancel its request\n\n\t\tselect {\n\t\tcase <-time.After(10 * time.Second):\n\t\t\t// Note: this should only happen in broken implementations, and the\n\t\t\t// closenotify case should be instantaneous.\n\t\t\tt.Error(\"Handler never saw CloseNotify\")\n\t\t\treturn\n\t\tcase <-w.(http.CloseNotifier).CloseNotify():\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(backendResponse))\n\t}))\n\n\tdefer backend.Close()\n\n\tbackend.Config.ErrorLog = log.New(io.Discard, \"\", 0)\n\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\n\t// Discards errors of the form:\n\t// http: proxy error: read tcp 127.0.0.1:44643: use of closed network connection\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0)\n\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tgo func() {\n\t\t<-reqInFlight\n\t\tfrontendClient.Transport.(*http.Transport).CancelRequest(getReq)\n\t}()\n\tres, err := frontendClient.Do(getReq)\n\tif res != nil {\n\t\tt.Errorf(\"got response %v; want nil\", res.Status)\n\t}\n\tif err == nil {\n\t\t// This should be an error like:\n\t\t// Get \"http://127.0.0.1:58079\": read tcp 127.0.0.1:58079:\n\t\t//    use of closed network connection\n\t\tt.Error(\"Server.Client().Do() returned nil error; want non-nil error\")\n\t}\n}\n\nfunc req(t *testing.T, v string) *http.Request {\n\treq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn req\n}\n\n// Issue 12344\nfunc TestNilBody(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"hi\"))\n\t}))\n\tdefer backend.Close()\n\n\tfrontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tbackURL, _ := url.Parse(backend.URL)\n\t\trp := NewSingleHostReverseProxy(backURL)\n\t\tr := req(t, \"GET / HTTP/1.0\\r\\n\\r\\n\")\n\t\tr.Body = nil // this accidentally worked in Go 1.4 and below, so keep it working\n\t\trp.ServeHTTP(w, r)\n\t}))\n\tdefer frontend.Close()\n\n\tres, err := http.Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\tslurp, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(slurp) != \"hi\" {\n\t\tt.Errorf(\"Got %q; want %q\", slurp, \"hi\")\n\t}\n}\n\n// Issue 15524\nfunc TestUserAgentHeader(t *testing.T) {\n\tvar gotUA string\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tgotUA = r.Header.Get(\"User-Agent\")\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tproxyHandler := new(ReverseProxy)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tproxyHandler.Director = func(req *http.Request) {\n\t\treq.URL = backendURL\n\t}\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tfor _, sentUA := range []string{\"explicit UA\", \"\"} {\n\t\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\t\tgetReq.Header.Set(\"User-Agent\", sentUA)\n\t\tgetReq.Close = true\n\t\tres, err := frontendClient.Do(getReq)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Get: %v\", err)\n\t\t}\n\t\tres.Body.Close()\n\t\tif got, want := gotUA, sentUA; got != want {\n\t\t\tt.Errorf(\"got forwarded User-Agent %q, want %q\", got, want)\n\t\t}\n\t}\n}\n\ntype bufferPool struct {\n\tget func() []byte\n\tput func([]byte)\n}\n\nfunc (bp bufferPool) Get() []byte  { return bp.get() }\nfunc (bp bufferPool) Put(v []byte) { bp.put(v) }\n\nfunc TestReverseProxyGetPutBuffer(t *testing.T) {\n\tconst msg = \"hi\"\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tio.WriteString(w, msg)\n\t}))\n\tdefer backend.Close()\n\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar (\n\t\tmu  sync.Mutex\n\t\tlog []string\n\t)\n\taddLog := func(event string) {\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\t\tlog = append(log, event)\n\t}\n\trp := NewSingleHostReverseProxy(backendURL)\n\tconst size = 1234\n\trp.BufferPool = bufferPool{\n\t\tget: func() []byte {\n\t\t\taddLog(\"getBuf\")\n\t\t\treturn make([]byte, size)\n\t\t},\n\t\tput: func(p []byte) {\n\t\t\taddLog(\"putBuf-\" + strconv.Itoa(len(p)))\n\t\t},\n\t}\n\tfrontend := httptest.NewServer(rp)\n\tdefer frontend.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\treq.Close = true\n\tres, err := frontend.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tslurp, err := io.ReadAll(res.Body)\n\tres.Body.Close()\n\tif err != nil {\n\t\tt.Fatalf(\"reading body: %v\", err)\n\t}\n\tif string(slurp) != msg {\n\t\tt.Errorf(\"msg = %q; want %q\", slurp, msg)\n\t}\n\twantLog := []string{\"getBuf\", \"putBuf-\" + strconv.Itoa(size)}\n\tmu.Lock()\n\tdefer mu.Unlock()\n\tif !reflect.DeepEqual(log, wantLog) {\n\t\tt.Errorf(\"Log events = %q; want %q\", log, wantLog)\n\t}\n}\n\nfunc TestReverseProxy_Post(t *testing.T) {\n\tconst backendResponse = \"I am the backend\"\n\tconst backendStatus = 200\n\tvar requestBody = bytes.Repeat([]byte(\"a\"), 1<<20)\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tslurp, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Backend body read = %v\", err)\n\t\t}\n\t\tif len(slurp) != len(requestBody) {\n\t\t\tt.Errorf(\"Backend read %d request body bytes; want %d\", len(slurp), len(requestBody))\n\t\t}\n\t\tif !bytes.Equal(slurp, requestBody) {\n\t\t\tt.Error(\"Backend read wrong request body.\") // 1MB; omitting details\n\t\t}\n\t\tw.Write([]byte(backendResponse))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tpostReq, _ := http.NewRequest(\"POST\", frontend.URL, bytes.NewReader(requestBody))\n\tres, err := frontend.Client().Do(postReq)\n\tif err != nil {\n\t\tt.Fatalf(\"Do: %v\", err)\n\t}\n\tif g, e := res.StatusCode, backendStatus; g != e {\n\t\tt.Errorf(\"got res.StatusCode %d; expected %d\", g, e)\n\t}\n\tbodyBytes, _ := io.ReadAll(res.Body)\n\tif g, e := string(bodyBytes), backendResponse; g != e {\n\t\tt.Errorf(\"got body %q; expected %q\", g, e)\n\t}\n}\n\ntype RoundTripperFunc func(*http.Request) (*http.Response, error)\n\nfunc (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn fn(req)\n}\n\n// Issue 16036: send a Request with a nil Body when possible\nfunc TestReverseProxy_NilBody(t *testing.T) {\n\tbackendURL, _ := url.Parse(\"http://fake.tld/\")\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tproxyHandler.Transport = RoundTripperFunc(func(req *http.Request) (*http.Response, error) {\n\t\tif req.Body != nil {\n\t\t\tt.Error(\"Body != nil; want a nil Body\")\n\t\t}\n\t\treturn nil, errors.New(\"done testing the interesting part; so force a 502 Gateway error\")\n\t})\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tres, err := frontend.Client().Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != 502 {\n\t\tt.Errorf(\"status code = %v; want 502 (Gateway Error)\", res.Status)\n\t}\n}\n\n// Issue 33142: always allocate the request headers\nfunc TestReverseProxy_AllocatedHeader(t *testing.T) {\n\tproxyHandler := new(ReverseProxy)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tproxyHandler.Director = func(*http.Request) {}     // noop\n\tproxyHandler.Transport = RoundTripperFunc(func(req *http.Request) (*http.Response, error) {\n\t\tif req.Header == nil {\n\t\t\tt.Error(\"Header == nil; want a non-nil Header\")\n\t\t}\n\t\treturn nil, errors.New(\"done testing the interesting part; so force a 502 Gateway error\")\n\t})\n\n\tproxyHandler.ServeHTTP(httptest.NewRecorder(), &http.Request{\n\t\tMethod:     \"GET\",\n\t\tURL:        &url.URL{Scheme: \"http\", Host: \"fake.tld\", Path: \"/\"},\n\t\tProto:      \"HTTP/1.0\",\n\t\tProtoMajor: 1,\n\t})\n}\n\n// Issue 14237. Test ModifyResponse and that an error from it\n// causes the proxy to return StatusBadGateway, or StatusOK otherwise.\nfunc TestReverseProxyModifyResponse(t *testing.T) {\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Add(\"X-Hit-Mod\", fmt.Sprintf(\"%v\", r.URL.Path == \"/mod\"))\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\trproxy := NewSingleHostReverseProxy(rpURL)\n\trproxy.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\trproxy.ModifyResponse = func(resp *http.Response) error {\n\t\tif resp.Header.Get(\"X-Hit-Mod\") != \"true\" {\n\t\t\treturn fmt.Errorf(\"tried to by-pass proxy\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tfrontendProxy := httptest.NewServer(rproxy)\n\tdefer frontendProxy.Close()\n\n\ttests := []struct {\n\t\turl      string\n\t\twantCode int\n\t}{\n\t\t{frontendProxy.URL + \"/mod\", http.StatusOK},\n\t\t{frontendProxy.URL + \"/schedule\", http.StatusBadGateway},\n\t}\n\n\tfor i, tt := range tests {\n\t\tresp, err := http.Get(tt.url)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to reach proxy: %v\", err)\n\t\t}\n\t\tif g, e := resp.StatusCode, tt.wantCode; g != e {\n\t\t\tt.Errorf(\"#%d: got res.StatusCode %d; expected %d\", i, g, e)\n\t\t}\n\t\tresp.Body.Close()\n\t}\n}\n\ntype failingRoundTripper struct{}\n\nfunc (failingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {\n\treturn nil, errors.New(\"some error\")\n}\n\ntype staticResponseRoundTripper struct{ res *http.Response }\n\nfunc (rt staticResponseRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {\n\treturn rt.res, nil\n}\n\nfunc TestReverseProxyErrorHandler(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\twantCode       int\n\t\terrorHandler   func(http.ResponseWriter, *http.Request, error)\n\t\ttransport      http.RoundTripper // defaults to failingRoundTripper\n\t\tmodifyResponse func(*http.Response) error\n\t}{\n\t\t{\n\t\t\tname:     \"default\",\n\t\t\twantCode: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\tname:         \"errorhandler\",\n\t\t\twantCode:     http.StatusTeapot,\n\t\t\terrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },\n\t\t},\n\t\t{\n\t\t\tname: \"modifyresponse_noerr\",\n\t\t\ttransport: staticResponseRoundTripper{\n\t\t\t\t&http.Response{StatusCode: 345, Body: http.NoBody},\n\t\t\t},\n\t\t\tmodifyResponse: func(res *http.Response) error {\n\t\t\t\tres.StatusCode++\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\terrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },\n\t\t\twantCode:     346,\n\t\t},\n\t\t{\n\t\t\tname: \"modifyresponse_err\",\n\t\t\ttransport: staticResponseRoundTripper{\n\t\t\t\t&http.Response{StatusCode: 345, Body: http.NoBody},\n\t\t\t},\n\t\t\tmodifyResponse: func(res *http.Response) error {\n\t\t\t\tres.StatusCode++\n\t\t\t\treturn errors.New(\"some error to trigger errorHandler\")\n\t\t\t},\n\t\t\terrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },\n\t\t\twantCode:     http.StatusTeapot,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget := &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   \"dummy.tld\",\n\t\t\t\tPath:   \"/\",\n\t\t\t}\n\t\t\trproxy := NewSingleHostReverseProxy(target)\n\t\t\trproxy.Transport = tt.transport\n\t\t\trproxy.ModifyResponse = tt.modifyResponse\n\t\t\tif rproxy.Transport == nil {\n\t\t\t\trproxy.Transport = failingRoundTripper{}\n\t\t\t}\n\t\t\trproxy.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\t\t\tif tt.errorHandler != nil {\n\t\t\t\trproxy.ErrorHandler = tt.errorHandler\n\t\t\t}\n\t\t\tfrontendProxy := httptest.NewServer(rproxy)\n\t\t\tdefer frontendProxy.Close()\n\n\t\t\tresp, err := http.Get(frontendProxy.URL + \"/test\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to reach proxy: %v\", err)\n\t\t\t}\n\t\t\tif g, e := resp.StatusCode, tt.wantCode; g != e {\n\t\t\t\tt.Errorf(\"got res.StatusCode %d; expected %d\", g, e)\n\t\t\t}\n\t\t\tresp.Body.Close()\n\t\t})\n\t}\n}\n\n// Issue 16659: log errors from short read\nfunc TestReverseProxy_CopyBuffer(t *testing.T) {\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tout := \"this call was relayed by the reverse proxy\"\n\t\t// Coerce a wrong content length to induce io.UnexpectedEOF\n\t\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(out)*2))\n\t\tfmt.Fprintln(w, out)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, err := url.Parse(backendServer.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar proxyLog bytes.Buffer\n\trproxy := NewSingleHostReverseProxy(rpURL)\n\trproxy.ErrorLog = log.New(&proxyLog, \"\", log.Lshortfile)\n\tdonec := make(chan bool, 1)\n\tfrontendProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() { donec <- true }()\n\t\trproxy.ServeHTTP(w, r)\n\t}))\n\tdefer frontendProxy.Close()\n\n\tif _, err = frontendProxy.Client().Get(frontendProxy.URL); err == nil {\n\t\tt.Fatalf(\"want non-nil error\")\n\t}\n\t// The race detector complains about the proxyLog usage in logf in copyBuffer\n\t// and our usage below with proxyLog.Bytes() so we're explicitly using a\n\t// channel to ensure that the ReverseProxy's ServeHTTP is done before we\n\t// continue after Get.\n\t<-donec\n\n\texpected := []string{\n\t\t\"EOF\",\n\t\t\"read\",\n\t}\n\tfor _, phrase := range expected {\n\t\tif !bytes.Contains(proxyLog.Bytes(), []byte(phrase)) {\n\t\t\tt.Errorf(\"expected log to contain phrase %q\", phrase)\n\t\t}\n\t}\n}\n\ntype staticTransport struct {\n\tres *http.Response\n}\n\nfunc (t *staticTransport) RoundTrip(r *http.Request) (*http.Response, error) {\n\treturn t.res, nil\n}\n\nfunc BenchmarkServeHTTP(b *testing.B) {\n\tres := &http.Response{\n\t\tStatusCode: 200,\n\t\tBody:       io.NopCloser(strings.NewReader(\"\")),\n\t}\n\tproxy := &ReverseProxy{\n\t\tDirector:  func(*http.Request) {},\n\t\tTransport: &staticTransport{res},\n\t}\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tproxy.ServeHTTP(w, r)\n\t}\n}\n\nfunc TestServeHTTPDeepCopy(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello Gopher!\"))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype result struct {\n\t\tbefore, after string\n\t}\n\n\tresultChan := make(chan result, 1)\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tfrontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tbefore := r.URL.String()\n\t\tproxyHandler.ServeHTTP(w, r)\n\t\tafter := r.URL.String()\n\t\tresultChan <- result{before: before, after: after}\n\t}))\n\tdefer frontend.Close()\n\n\twant := result{before: \"/\", after: \"/\"}\n\n\tres, err := frontend.Client().Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"Do: %v\", err)\n\t}\n\tres.Body.Close()\n\n\tgot := <-resultChan\n\tif got != want {\n\t\tt.Errorf(\"got = %+v; want = %+v\", got, want)\n\t}\n}\n\n// Issue 18327: verify we always do a deep copy of the Request.Header map\n// before any mutations.\nfunc TestClonesRequestHeaders(t *testing.T) {\n\tlog.SetOutput(io.Discard)\n\tdefer log.SetOutput(os.Stderr)\n\treq, _ := http.NewRequest(\"GET\", \"http://foo.tld/\", nil)\n\treq.RemoteAddr = \"1.2.3.4:56789\"\n\trp := &ReverseProxy{\n\t\tDirector: func(req *http.Request) {\n\t\t\treq.Header.Set(\"From-Director\", \"1\")\n\t\t},\n\t\tTransport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {\n\t\t\tif v := req.Header.Get(\"From-Director\"); v != \"1\" {\n\t\t\t\tt.Errorf(\"From-Directory value = %q; want 1\", v)\n\t\t\t}\n\t\t\treturn nil, io.EOF\n\t\t}),\n\t}\n\trp.ServeHTTP(httptest.NewRecorder(), req)\n\n\tfor _, h := range []string{\n\t\t\"From-Director\",\n\t\t\"X-Forwarded-For\",\n\t} {\n\t\tif req.Header.Get(h) != \"\" {\n\t\t\tt.Errorf(\"%v header mutation modified caller's request\", h)\n\t\t}\n\t}\n}\n\ntype roundTripperFunc func(req *http.Request) (*http.Response, error)\n\nfunc (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn fn(req)\n}\n\nfunc TestModifyResponseClosesBody(t *testing.T) {\n\treq, _ := http.NewRequest(\"GET\", \"http://foo.tld/\", nil)\n\treq.RemoteAddr = \"1.2.3.4:56789\"\n\tcloseCheck := new(checkCloser)\n\tlogBuf := new(strings.Builder)\n\toutErr := errors.New(\"ModifyResponse error\")\n\trp := &ReverseProxy{\n\t\tDirector: func(req *http.Request) {},\n\t\tTransport: &staticTransport{&http.Response{\n\t\t\tStatusCode: 200,\n\t\t\tBody:       closeCheck,\n\t\t}},\n\t\tErrorLog: log.New(logBuf, \"\", 0),\n\t\tModifyResponse: func(*http.Response) error {\n\t\t\treturn outErr\n\t\t},\n\t}\n\trec := httptest.NewRecorder()\n\trp.ServeHTTP(rec, req)\n\tres := rec.Result()\n\tif g, e := res.StatusCode, http.StatusBadGateway; g != e {\n\t\tt.Errorf(\"got res.StatusCode %d; expected %d\", g, e)\n\t}\n\tif !closeCheck.closed {\n\t\tt.Errorf(\"body should have been closed\")\n\t}\n\tif g, e := logBuf.String(), outErr.Error(); !strings.Contains(g, e) {\n\t\tt.Errorf(\"ErrorLog %q does not contain %q\", g, e)\n\t}\n}\n\ntype checkCloser struct {\n\tclosed bool\n}\n\nfunc (cc *checkCloser) Close() error {\n\tcc.closed = true\n\treturn nil\n}\n\nfunc (cc *checkCloser) Read(b []byte) (int, error) {\n\treturn len(b), nil\n}\n\n// Issue 23643: panic on body copy error\nfunc TestReverseProxy_PanicBodyError(t *testing.T) {\n\tlog.SetOutput(io.Discard)\n\tdefer log.SetOutput(os.Stderr)\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tout := \"this call was relayed by the reverse proxy\"\n\t\t// Coerce a wrong content length to induce io.ErrUnexpectedEOF\n\t\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(out)*2))\n\t\tfmt.Fprintln(w, out)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, err := url.Parse(backendServer.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trproxy := NewSingleHostReverseProxy(rpURL)\n\n\t// Ensure that the handler panics when the body read encounters an\n\t// io.ErrUnexpectedEOF\n\tdefer func() {\n\t\terr := recover()\n\t\tif err == nil {\n\t\t\tt.Fatal(\"handler should have panicked\")\n\t\t}\n\t\tif err != http.ErrAbortHandler {\n\t\t\tt.Fatal(\"expected ErrAbortHandler, got\", err)\n\t\t}\n\t}()\n\treq, _ := http.NewRequest(\"GET\", \"http://foo.tld/\", nil)\n\trproxy.ServeHTTP(httptest.NewRecorder(), req)\n}\n\n/* Commented out because `neverEnding` is not available and not something I want to copy in.\n// Issue #46866: panic without closing incoming request body causes a panic\nfunc TestReverseProxy_PanicClosesIncomingBody(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tout := \"this call was relayed by the reverse proxy\"\n\t\t// Coerce a wrong content length to induce io.ErrUnexpectedEOF\n\t\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(out)*2))\n\t\tfmt.Fprintln(w, out)\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 10; j++ {\n\t\t\t\tconst reqLen = 6 * 1024 * 1024\n\t\t\t\treq, _ := http.NewRequest(\"POST\", frontend.URL, &io.LimitedReader{R: neverEnding('x'), N: reqLen})\n\t\t\t\treq.ContentLength = reqLen\n\t\t\t\tresp, _ := frontendClient.Transport.RoundTrip(req)\n\t\t\t\tif resp != nil {\n\t\t\t\t\tio.Copy(io.Discard, resp.Body)\n\t\t\t\t\tresp.Body.Close()\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n}\n*/\n\nfunc TestSelectFlushInterval(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tp    *ReverseProxy\n\t\tres  *http.Response\n\t\twant time.Duration\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tres:  &http.Response{},\n\t\t\tp:    &ReverseProxy{FlushInterval: 123},\n\t\t\twant: 123,\n\t\t},\n\t\t{\n\t\t\tname: \"server-sent events overrides non-zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Content-Type\": {\"text/event-stream\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 123},\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"server-sent events overrides zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Content-Type\": {\"text/event-stream\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 0},\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"server-sent events with media-type parameters overrides non-zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Content-Type\": {\"text/event-stream;charset=utf-8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 123},\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"server-sent events with media-type parameters overrides zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Content-Type\": {\"text/event-stream;charset=utf-8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 0},\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"Content-Length: -1, overrides non-zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tContentLength: -1,\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 123},\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"Content-Length: -1, overrides zero\",\n\t\t\tres: &http.Response{\n\t\t\t\tContentLength: -1,\n\t\t\t},\n\t\t\tp:    &ReverseProxy{FlushInterval: 0},\n\t\t\twant: -1,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.p.flushInterval(tt.res)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"flushLatency = %v; want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReverseProxyWebSocket(t *testing.T) {\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif upgradeType(r.Header) != \"websocket\" {\n\t\t\tt.Error(\"unexpected backend request\")\n\t\t\thttp.Error(w, \"unexpected request\", 400)\n\t\t\treturn\n\t\t}\n\t\tc, _, err := w.(http.Hijacker).Hijack()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tdefer c.Close()\n\t\tio.WriteString(c, \"HTTP/1.1 101 Switching Protocols\\r\\nConnection: upgrade\\r\\nUpgrade: WebSocket\\r\\n\\r\\n\")\n\t\tbs := bufio.NewScanner(c)\n\t\tif !bs.Scan() {\n\t\t\tt.Errorf(\"backend failed to read line from client: %v\", bs.Err())\n\t\t\treturn\n\t\t}\n\t\tfmt.Fprintf(c, \"backend got %q\\n\", bs.Text())\n\t}))\n\tdefer backendServer.Close()\n\n\tbackURL, _ := url.Parse(backendServer.URL)\n\trproxy := NewSingleHostReverseProxy(backURL)\n\trproxy.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\trproxy.ModifyResponse = func(res *http.Response) error {\n\t\tres.Header.Add(\"X-Modified\", \"true\")\n\t\treturn nil\n\t}\n\n\thandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\trw.Header().Set(\"X-Header\", \"X-Value\")\n\t\trproxy.ServeHTTP(rw, req)\n\t\tif got, want := rw.Header().Get(\"X-Modified\"), \"true\"; got != want {\n\t\t\tt.Errorf(\"response writer X-Modified header = %q; want %q\", got, want)\n\t\t}\n\t})\n\n\tfrontendProxy := httptest.NewServer(handler)\n\tdefer frontendProxy.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontendProxy.URL, nil)\n\treq.Header.Set(\"Connection\", \"Upgrade\")\n\treq.Header.Set(\"Upgrade\", \"websocket\")\n\n\tc := frontendProxy.Client()\n\tres, err := c.Do(req)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif res.StatusCode != 101 {\n\t\tt.Fatalf(\"status = %v; want 101\", res.Status)\n\t}\n\n\tgot := res.Header.Get(\"X-Header\")\n\twant := \"X-Value\"\n\tif got != want {\n\t\tt.Errorf(\"Header(XHeader) = %q; want %q\", got, want)\n\t}\n\n\tif !EqualFold(upgradeType(res.Header), \"websocket\") {\n\t\tt.Fatalf(\"not websocket upgrade; got %#v\", res.Header)\n\t}\n\trwc, ok := res.Body.(io.ReadWriteCloser)\n\tif !ok {\n\t\tt.Fatalf(\"response body is of type %T; does not implement ReadWriteCloser\", res.Body)\n\t}\n\tdefer rwc.Close()\n\n\tif got, want := res.Header.Get(\"X-Modified\"), \"true\"; got != want {\n\t\tt.Errorf(\"response X-Modified header = %q; want %q\", got, want)\n\t}\n\n\tio.WriteString(rwc, \"Hello\\n\")\n\tbs := bufio.NewScanner(rwc)\n\tif !bs.Scan() {\n\t\tt.Fatalf(\"Scan: %v\", bs.Err())\n\t}\n\tgot = bs.Text()\n\twant = `backend got \"Hello\"`\n\tif got != want {\n\t\tt.Errorf(\"got %#q, want %#q\", got, want)\n\t}\n}\n\nfunc TestReverseProxyWebSocketCancellation(t *testing.T) {\n\tn := 5\n\ttriggerCancelCh := make(chan bool, n)\n\tnthResponse := func(i int) string {\n\t\treturn fmt.Sprintf(\"backend response #%d\\n\", i)\n\t}\n\tterminalMsg := \"final message\"\n\n\tcst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif g, ws := upgradeType(r.Header), \"websocket\"; g != ws {\n\t\t\tt.Errorf(\"Unexpected upgrade type %q, want %q\", g, ws)\n\t\t\thttp.Error(w, \"Unexpected request\", 400)\n\t\t\treturn\n\t\t}\n\t\tconn, bufrw, err := w.(http.Hijacker).Hijack()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\n\t\tupgradeMsg := \"HTTP/1.1 101 Switching Protocols\\r\\nConnection: upgrade\\r\\nUpgrade: WebSocket\\r\\n\\r\\n\"\n\t\tif _, err := io.WriteString(conn, upgradeMsg); err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif _, _, err := bufrw.ReadLine(); err != nil {\n\t\t\tt.Errorf(\"Failed to read line from client: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfor i := 0; i < n; i++ {\n\t\t\tif _, err := bufrw.WriteString(nthResponse(i)); err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-triggerCancelCh:\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"Writing response #%d failed: %v\", i, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbufrw.Flush()\n\t\t\ttime.Sleep(time.Second)\n\t\t}\n\t\tif _, err := bufrw.WriteString(terminalMsg); err != nil {\n\t\t\tselect {\n\t\t\tcase <-triggerCancelCh:\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"Failed to write terminal message: %v\", err)\n\t\t\t}\n\t\t}\n\t\tbufrw.Flush()\n\t}))\n\tdefer cst.Close()\n\n\tbackendURL, _ := url.Parse(cst.URL)\n\trproxy := NewSingleHostReverseProxy(backendURL)\n\trproxy.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\trproxy.ModifyResponse = func(res *http.Response) error {\n\t\tres.Header.Add(\"X-Modified\", \"true\")\n\t\treturn nil\n\t}\n\n\thandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\trw.Header().Set(\"X-Header\", \"X-Value\")\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tgo func() {\n\t\t\t<-triggerCancelCh\n\t\t\tcancel()\n\t\t}()\n\t\trproxy.ServeHTTP(rw, req.WithContext(ctx))\n\t})\n\n\tfrontendProxy := httptest.NewServer(handler)\n\tdefer frontendProxy.Close()\n\n\treq, _ := http.NewRequest(\"GET\", frontendProxy.URL, nil)\n\treq.Header.Set(\"Connection\", \"Upgrade\")\n\treq.Header.Set(\"Upgrade\", \"websocket\")\n\n\tres, err := frontendProxy.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Dialing to frontend proxy: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tif g, w := res.StatusCode, 101; g != w {\n\t\tt.Fatalf(\"Switching protocols failed, got: %d, want: %d\", g, w)\n\t}\n\n\tif g, w := res.Header.Get(\"X-Header\"), \"X-Value\"; g != w {\n\t\tt.Errorf(\"X-Header mismatch\\n\\tgot:  %q\\n\\twant: %q\", g, w)\n\t}\n\n\tif g, w := upgradeType(res.Header), \"websocket\"; !EqualFold(g, w) {\n\t\tt.Fatalf(\"Upgrade header mismatch\\n\\tgot:  %q\\n\\twant: %q\", g, w)\n\t}\n\n\trwc, ok := res.Body.(io.ReadWriteCloser)\n\tif !ok {\n\t\tt.Fatalf(\"Response body type mismatch, got %T, want io.ReadWriteCloser\", res.Body)\n\t}\n\n\tif got, want := res.Header.Get(\"X-Modified\"), \"true\"; got != want {\n\t\tt.Errorf(\"response X-Modified header = %q; want %q\", got, want)\n\t}\n\n\tif _, err := io.WriteString(rwc, \"Hello\\n\"); err != nil {\n\t\tt.Fatalf(\"Failed to write first message: %v\", err)\n\t}\n\n\t// Read loop.\n\n\tbr := bufio.NewReader(rwc)\n\tfor {\n\t\tline, err := br.ReadString('\\n')\n\t\tswitch {\n\t\tcase line == terminalMsg: // this case before \"err == io.EOF\"\n\t\t\tt.Fatalf(\"The websocket request was not canceled, unfortunately!\")\n\n\t\tcase err == io.EOF:\n\t\t\treturn\n\n\t\tcase err != nil:\n\t\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\n\t\tcase line == nthResponse(0): // We've gotten the first response back\n\t\t\t// Let's trigger a cancel.\n\t\t\tclose(triggerCancelCh)\n\t\t}\n\t}\n}\n\nfunc TestUnannouncedTrailer(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.(http.Flusher).Flush()\n\t\tw.Header().Set(http.TrailerPrefix+\"X-Unannounced-Trailer\", \"unannounced_trailer_value\")\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tres, err := frontendClient.Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\n\tio.ReadAll(res.Body)\n\n\tif g, w := res.Trailer.Get(\"X-Unannounced-Trailer\"), \"unannounced_trailer_value\"; g != w {\n\t\tt.Errorf(\"Trailer(X-Unannounced-Trailer) = %q; want %q\", g, w)\n\t}\n\n}\n\nfunc TestSetURL(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(r.Host))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := &ReverseProxy{\n\t\tRewrite: func(r *ProxyRequest) {\n\t\t\tr.SetURL(backendURL)\n\t\t},\n\t}\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tres, err := frontendClient.Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"Reading body: %v\", err)\n\t}\n\n\tif got, want := string(body), backendURL.Host; got != want {\n\t\tt.Errorf(\"backend got Host %q, want %q\", got, want)\n\t}\n}\n\nfunc TestSingleJoinSlash(t *testing.T) {\n\ttests := []struct {\n\t\tslasha   string\n\t\tslashb   string\n\t\texpected string\n\t}{\n\t\t{\"https://www.google.com/\", \"/favicon.ico\", \"https://www.google.com/favicon.ico\"},\n\t\t{\"https://www.google.com\", \"/favicon.ico\", \"https://www.google.com/favicon.ico\"},\n\t\t{\"https://www.google.com\", \"favicon.ico\", \"https://www.google.com/favicon.ico\"},\n\t\t{\"https://www.google.com\", \"\", \"https://www.google.com/\"},\n\t\t{\"\", \"favicon.ico\", \"/favicon.ico\"},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := singleJoiningSlash(tt.slasha, tt.slashb); got != tt.expected {\n\t\t\tt.Errorf(\"singleJoiningSlash(%q,%q) want %q got %q\",\n\t\t\t\ttt.slasha,\n\t\t\t\ttt.slashb,\n\t\t\t\ttt.expected,\n\t\t\t\tgot)\n\t\t}\n\t}\n}\n\nfunc TestJoinURLPath(t *testing.T) {\n\ttests := []struct {\n\t\ta        *url.URL\n\t\tb        *url.URL\n\t\twantPath string\n\t\twantRaw  string\n\t}{\n\t\t{&url.URL{Path: \"/a/b\"}, &url.URL{Path: \"/c\"}, \"/a/b/c\", \"\"},\n\t\t{&url.URL{Path: \"/a/b\", RawPath: \"badpath\"}, &url.URL{Path: \"c\"}, \"/a/b/c\", \"/a/b/c\"},\n\t\t{&url.URL{Path: \"/a/b\", RawPath: \"/a%2Fb\"}, &url.URL{Path: \"/c\"}, \"/a/b/c\", \"/a%2Fb/c\"},\n\t\t{&url.URL{Path: \"/a/b\", RawPath: \"/a%2Fb\"}, &url.URL{Path: \"/c\"}, \"/a/b/c\", \"/a%2Fb/c\"},\n\t\t{&url.URL{Path: \"/a/b/\", RawPath: \"/a%2Fb%2F\"}, &url.URL{Path: \"c\"}, \"/a/b//c\", \"/a%2Fb%2F/c\"},\n\t\t{&url.URL{Path: \"/a/b/\", RawPath: \"/a%2Fb/\"}, &url.URL{Path: \"/c/d\", RawPath: \"/c%2Fd\"}, \"/a/b/c/d\", \"/a%2Fb/c%2Fd\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tp, rp := joinURLPath(tt.a, tt.b)\n\t\tif p != tt.wantPath || rp != tt.wantRaw {\n\t\t\tt.Errorf(\"joinURLPath(URL(%q,%q),URL(%q,%q)) want (%q,%q) got (%q,%q)\",\n\t\t\t\ttt.a.Path, tt.a.RawPath,\n\t\t\t\ttt.b.Path, tt.b.RawPath,\n\t\t\t\ttt.wantPath, tt.wantRaw,\n\t\t\t\tp, rp)\n\t\t}\n\t}\n}\n\nfunc TestReverseProxyRewriteReplacesOut(t *testing.T) {\n\tconst content = \"response_content\"\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(content))\n\t}))\n\tdefer backend.Close()\n\tproxyHandler := &ReverseProxy{\n\t\tRewrite: func(r *ProxyRequest) {\n\t\t\tr.Out, _ = http.NewRequest(\"GET\", backend.URL, nil)\n\t\t},\n\t}\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tres, err := frontend.Client().Get(frontend.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\tdefer res.Body.Close()\n\tbody, _ := io.ReadAll(res.Body)\n\tif got, want := string(body), content; got != want {\n\t\tt.Errorf(\"got response %q, want %q\", got, want)\n\t}\n}\n\nfunc Test1xxHeadersNotModifiedAfterRoundTrip(t *testing.T) {\n\t// https://go.dev/issue/65123: We use httptrace.Got1xxResponse to capture 1xx responses\n\t// and proxy them. httptrace handlers can execute after RoundTrip returns, in particular\n\t// after experiencing connection errors. When this happens, we shouldn't modify the\n\t// ResponseWriter headers after ReverseProxy.ServeHTTP returns.\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tw.WriteHeader(103)\n\t\t}\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\n\trw := &testResponseWriter{}\n\tfunc() {\n\t\t// Cancel the request (and cause RoundTrip to return) immediately upon\n\t\t// seeing a 1xx response.\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\t\tctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{\n\t\t\tGot1xxResponse: func(code int, header textproto.MIMEHeader) error {\n\t\t\t\tcancel()\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\n\t\treq, _ := http.NewRequestWithContext(ctx, \"GET\", \"http://go.dev/\", nil)\n\t\tproxyHandler.ServeHTTP(rw, req)\n\t}()\n\t// Trigger data race while iterating over response headers.\n\t// When run with -race, this causes the condition in https://go.dev/issue/65123 often\n\t// enough to detect reliably.\n\tfor _ = range rw.Header() {\n\t}\n}\n\nfunc Test1xxResponses(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\th := w.Header()\n\t\th.Add(\"Link\", \"</style.css>; rel=preload; as=style\")\n\t\th.Add(\"Link\", \"</script.js>; rel=preload; as=script\")\n\t\tw.WriteHeader(http.StatusEarlyHints)\n\n\t\th.Add(\"Link\", \"</foo.js>; rel=preload; as=script\")\n\t\tw.WriteHeader(http.StatusProcessing)\n\n\t\tw.Write([]byte(\"Hello\"))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := NewSingleHostReverseProxy(backendURL)\n\tproxyHandler.ErrorLog = log.New(io.Discard, \"\", 0) // quiet for tests\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\tfrontendClient := frontend.Client()\n\n\tcheckLinkHeaders := func(t *testing.T, expected, got []string) {\n\t\tt.Helper()\n\n\t\tif len(expected) != len(got) {\n\t\t\tt.Errorf(\"Expected %d link headers; got %d\", len(expected), len(got))\n\t\t}\n\n\t\tfor i := range expected {\n\t\t\tif i >= len(got) {\n\t\t\t\tt.Errorf(\"Expected %q link header; got nothing\", expected[i])\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif expected[i] != got[i] {\n\t\t\t\tt.Errorf(\"Expected %q link header; got %q\", expected[i], got[i])\n\t\t\t}\n\t\t}\n\t}\n\n\tvar respCounter uint8\n\ttrace := &httptrace.ClientTrace{\n\t\tGot1xxResponse: func(code int, header textproto.MIMEHeader) error {\n\t\t\tswitch code {\n\t\t\tcase http.StatusEarlyHints:\n\t\t\t\tcheckLinkHeaders(t, []string{\"</style.css>; rel=preload; as=style\", \"</script.js>; rel=preload; as=script\"}, header[\"Link\"])\n\t\t\tcase http.StatusProcessing:\n\t\t\t\tcheckLinkHeaders(t, []string{\"</style.css>; rel=preload; as=style\", \"</script.js>; rel=preload; as=script\", \"</foo.js>; rel=preload; as=script\"}, header[\"Link\"])\n\t\t\tdefault:\n\t\t\t\tt.Error(\"Unexpected 1xx response\")\n\t\t\t}\n\n\t\t\trespCounter++\n\n\t\t\treturn nil\n\t\t},\n\t}\n\treq, _ := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), \"GET\", frontend.URL, nil)\n\n\tres, err := frontendClient.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Get: %v\", err)\n\t}\n\n\tdefer res.Body.Close()\n\n\tif respCounter != 2 {\n\t\tt.Errorf(\"Expected 2 1xx responses; got %d\", respCounter)\n\t}\n\tcheckLinkHeaders(t, []string{\"</style.css>; rel=preload; as=style\", \"</script.js>; rel=preload; as=script\", \"</foo.js>; rel=preload; as=script\"}, res.Header[\"Link\"])\n\n\tbody, _ := io.ReadAll(res.Body)\n\tif string(body) != \"Hello\" {\n\t\tt.Errorf(\"Read body %q; want Hello\", body)\n\t}\n}\n\nconst (\n\ttestWantsCleanQuery = true\n\ttestWantsRawQuery   = false\n)\n\nfunc TestReverseProxyQueryParameterSmugglingDirectorDoesNotParseForm(t *testing.T) {\n\ttestReverseProxyQueryParameterSmuggling(t, testWantsRawQuery, func(u *url.URL) *ReverseProxy {\n\t\tproxyHandler := NewSingleHostReverseProxy(u)\n\t\toldDirector := proxyHandler.Director\n\t\tproxyHandler.Director = func(r *http.Request) {\n\t\t\toldDirector(r)\n\t\t}\n\t\treturn proxyHandler\n\t})\n}\n\nfunc TestReverseProxyQueryParameterSmugglingDirectorParsesForm(t *testing.T) {\n\ttestReverseProxyQueryParameterSmuggling(t, testWantsCleanQuery, func(u *url.URL) *ReverseProxy {\n\t\tproxyHandler := NewSingleHostReverseProxy(u)\n\t\toldDirector := proxyHandler.Director\n\t\tproxyHandler.Director = func(r *http.Request) {\n\t\t\t// Parsing the form causes ReverseProxy to remove unparsable\n\t\t\t// query parameters before forwarding.\n\t\t\tr.FormValue(\"a\")\n\t\t\toldDirector(r)\n\t\t}\n\t\treturn proxyHandler\n\t})\n}\n\nfunc TestReverseProxyQueryParameterSmugglingRewrite(t *testing.T) {\n\ttestReverseProxyQueryParameterSmuggling(t, testWantsCleanQuery, func(u *url.URL) *ReverseProxy {\n\t\treturn &ReverseProxy{\n\t\t\tRewrite: func(r *ProxyRequest) {\n\t\t\t\tr.SetURL(u)\n\t\t\t},\n\t\t}\n\t})\n}\n\nfunc TestReverseProxyQueryParameterSmugglingRewritePreservesRawQuery(t *testing.T) {\n\ttestReverseProxyQueryParameterSmuggling(t, testWantsRawQuery, func(u *url.URL) *ReverseProxy {\n\t\treturn &ReverseProxy{\n\t\t\tRewrite: func(r *ProxyRequest) {\n\t\t\t\tr.SetURL(u)\n\t\t\t\tr.Out.URL.RawQuery = r.In.URL.RawQuery\n\t\t\t},\n\t\t}\n\t})\n}\n\nfunc testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuery bool, newProxy func(*url.URL) *ReverseProxy) {\n\tconst content = \"response_content\"\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(r.URL.RawQuery))\n\t}))\n\tdefer backend.Close()\n\tbackendURL, err := url.Parse(backend.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tproxyHandler := newProxy(backendURL)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\t// Don't spam output with logs of queries containing semicolons.\n\tbackend.Config.ErrorLog = log.New(io.Discard, \"\", 0)\n\tfrontend.Config.ErrorLog = log.New(io.Discard, \"\", 0)\n\n\tfor _, test := range []struct {\n\t\trawQuery   string\n\t\tcleanQuery string\n\t}{{\n\t\trawQuery:   \"a=1&a=2;b=3\",\n\t\tcleanQuery: \"a=1\",\n\t}, {\n\t\trawQuery:   \"a=1&a=%zz&b=3\",\n\t\tcleanQuery: \"a=1&b=3\",\n\t}} {\n\t\tres, err := frontend.Client().Get(frontend.URL + \"?\" + test.rawQuery)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Get: %v\", err)\n\t\t}\n\t\tdefer res.Body.Close()\n\t\tbody, _ := io.ReadAll(res.Body)\n\t\twantQuery := test.rawQuery\n\t\tif wantCleanQuery {\n\t\t\twantQuery = test.cleanQuery\n\t\t}\n\t\tif got, want := string(body), wantQuery; got != want {\n\t\t\tt.Errorf(\"proxy forwarded raw query %q as %q, want %q\", test.rawQuery, got, want)\n\t\t}\n\t}\n}\n\ntype testResponseWriter struct {\n\th           http.Header\n\twriteHeader func(int)\n\twrite       func([]byte) (int, error)\n}\n\nfunc (rw *testResponseWriter) Header() http.Header {\n\tif rw.h == nil {\n\t\trw.h = make(http.Header)\n\t}\n\treturn rw.h\n}\n\nfunc (rw *testResponseWriter) WriteHeader(statusCode int) {\n\tif rw.writeHeader != nil {\n\t\trw.writeHeader(statusCode)\n\t}\n}\n\nfunc (rw *testResponseWriter) Write(p []byte) (int, error) {\n\tif rw.write != nil {\n\t\treturn rw.write(p)\n\t}\n\treturn len(p), nil\n}\n"
  },
  {
    "path": "internal/iso/ipam.go",
    "content": "package iso\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nfunc parseIPAM(d *data.DHCP) string {\n\tif d == nil {\n\t\treturn \"\"\n\t}\n\t// return format is ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>\n\tipam := make([]string, 9)\n\tipam[0] = func() string {\n\t\tm := d.MACAddress.String()\n\n\t\treturn strings.ReplaceAll(m, \":\", \"-\")\n\t}()\n\tipam[1] = func() string {\n\t\tif d.VLANID != \"\" {\n\t\t\treturn d.VLANID\n\t\t}\n\t\treturn \"\"\n\t}()\n\tipam[2] = func() string {\n\t\tif d.IPAddress.Compare(netip.Addr{}) != 0 {\n\t\t\treturn d.IPAddress.String()\n\t\t}\n\t\treturn \"\"\n\t}()\n\tipam[3] = func() string {\n\t\tif d.SubnetMask != nil {\n\t\t\treturn net.IP(d.SubnetMask).String()\n\t\t}\n\t\treturn \"\"\n\t}()\n\tipam[4] = func() string {\n\t\tif d.DefaultGateway.Compare(netip.Addr{}) != 0 {\n\t\t\treturn d.DefaultGateway.String()\n\t\t}\n\t\treturn \"\"\n\t}()\n\tipam[5] = d.Hostname\n\tipam[6] = func() string {\n\t\tvar nameservers []string\n\t\tfor _, e := range d.NameServers {\n\t\t\tnameservers = append(nameservers, e.String())\n\t\t}\n\t\tif len(nameservers) > 0 {\n\t\t\treturn strings.Join(nameservers, \",\")\n\t\t}\n\n\t\treturn \"\"\n\t}()\n\tipam[7] = func() string {\n\t\tif len(d.DomainSearch) > 0 {\n\t\t\treturn strings.Join(d.DomainSearch, \",\")\n\t\t}\n\n\t\treturn \"\"\n\t}()\n\tipam[8] = func() string {\n\t\tvar ntp []string\n\t\tfor _, e := range d.NTPServers {\n\t\t\tntp = append(ntp, e.String())\n\t\t}\n\t\tif len(ntp) > 0 {\n\t\t\treturn strings.Join(ntp, \",\")\n\t\t}\n\n\t\treturn \"\"\n\t}()\n\n\treturn fmt.Sprintf(\"ipam=%s\", strings.Join(ipam, \":\"))\n}\n"
  },
  {
    "path": "internal/iso/ipam_test.go",
    "content": "package iso\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nfunc TestParseIPAM(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput *data.DHCP\n\t\twant  string\n\t}{\n\t\t\"empty\": {},\n\t\t\"only MAC\": {\n\t\t\tinput: &data.DHCP{MACAddress: net.HardwareAddr{0xde, 0xed, 0xbe, 0xef, 0xfe, 0xed}},\n\t\t\twant:  \"ipam=de-ed-be-ef-fe-ed::::::::\",\n\t\t},\n\t\t\"everything\": {\n\t\t\tinput: &data.DHCP{\n\t\t\t\tMACAddress:     net.HardwareAddr{0xde, 0xed, 0xbe, 0xef, 0xfe, 0xed},\n\t\t\t\tIPAddress:      netip.AddrFrom4([4]byte{127, 0, 0, 1}),\n\t\t\t\tSubnetMask:     net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\tDefaultGateway: netip.AddrFrom4([4]byte{127, 0, 0, 2}),\n\t\t\t\tNameServers:    []net.IP{{1, 1, 1, 1}, {4, 4, 4, 4}},\n\t\t\t\tHostname:       \"myhost\",\n\t\t\t\tNTPServers:     []net.IP{{129, 6, 15, 28}, {129, 6, 15, 29}},\n\t\t\t\tDomainSearch:   []string{\"example.com\", \"example.org\"},\n\t\t\t\tVLANID:         \"400\",\n\t\t\t},\n\t\t\twant: \"ipam=de-ed-be-ef-fe-ed:400:127.0.0.1:255.255.255.0:127.0.0.2:myhost:1.1.1.1,4.4.4.4:example.com,example.org:129.6.15.28,129.6.15.29\",\n\t\t},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := parseIPAM(tt.input)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Fatalf(\"diff: %v\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/iso/iso.go",
    "content": "package iso\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tapierrors \"k8s.io/apimachinery/pkg/api/errors\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n\t\"github.com/tinkerbell/smee/internal/iso/internal\"\n)\n\nconst (\n\tdefaultConsoles = \"console=ttyAMA0 console=ttyS0 console=tty0 console=tty1 console=ttyS1\"\n)\n\n// BackendReader is an interface that defines the method to read data from a backend.\ntype BackendReader interface {\n\t// Read data (from a backend) based on a mac address\n\t// and return DHCP headers and options, including netboot info.\n\tGetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error)\n}\n\n// Handler is a struct that contains the necessary fields to patch an ISO file with\n// relevant information for the Tink worker.\ntype Handler struct {\n\tBackend           BackendReader\n\tExtraKernelParams []string\n\tLogger            logr.Logger\n\t// MagicString is the string pattern that will be matched\n\t// in the source iso before patching. The field can be set\n\t// during build time by setting this field.\n\t// Ref: https://github.com/tinkerbell/hook/blob/main/linuxkit-templates/hook.template.yaml\n\tMagicString string\n\t// SourceISO is the source url where the unmodified iso lives.\n\t// It must be a valid url.URL{} object and must have a url.URL{}.Scheme of HTTP or HTTPS.\n\tSourceISO          string\n\tSyslog             string\n\tTinkServerTLS      bool\n\tTinkServerGRPCAddr string\n\tStaticIPAMEnabled  bool\n\t// parsedURL derives a url.URL from the SourceISO field.\n\t// It needed for validation of SourceISO and easier modification.\n\tparsedURL       *url.URL\n\tmagicStrPadding []byte\n}\n\n// HandlerFunc returns a reverse proxy HTTP handler function that performs ISO patching.\nfunc (h *Handler) HandlerFunc() (http.HandlerFunc, error) {\n\ttarget, err := url.Parse(h.SourceISO)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th.parsedURL = target\n\n\tproxy := &internal.ReverseProxy{\n\t\tRewrite: func(r *internal.ProxyRequest) {\n\t\t\tr.SetURL(target)\n\t\t},\n\t}\n\n\tproxy.Transport = h\n\tproxy.FlushInterval = -1\n\tproxy.CopyBuffer = h\n\n\th.magicStrPadding = bytes.Repeat([]byte{' '}, len(h.MagicString))\n\n\treturn proxy.ServeHTTP, nil\n}\n\n// Copy implements the internal.CopyBuffer interface.\n// This implementation allows us to inspect and patch content on its way to the client without buffering the entire response\n// in memory. This allows memory use to be constant regardless of the size of the response.\nfunc (h *Handler) Copy(ctx context.Context, dst io.Writer, src io.Reader, buf []byte) (int64, error) {\n\tif len(buf) == 0 {\n\t\tbuf = make([]byte, 32*1024)\n\t}\n\tvar written int64\n\tfor {\n\t\tnr, rerr := src.Read(buf)\n\t\tif rerr != nil && rerr != io.EOF && rerr != context.Canceled { //nolint: errorlint // going to defer to the stdlib on this one.\n\t\t\th.Logger.Info(\"httputil: ReverseProxy read error during body copy: %v\", rerr)\n\t\t}\n\t\tif nr > 0 {\n\t\t\t// This is the patching check and handling.\n\t\t\tb := buf[:nr]\n\t\t\ti := bytes.Index(b, []byte(h.MagicString))\n\t\t\tif i != -1 {\n\t\t\t\tdup := make([]byte, len(b))\n\t\t\t\tcopy(dup, b)\n\t\t\t\tcopy(dup[i:], h.magicStrPadding)\n\t\t\t\tcopy(dup[i:], internal.GetPatch(ctx))\n\t\t\t\tb = dup\n\t\t\t}\n\t\t\tnw, werr := dst.Write(b)\n\t\t\tif nw > 0 {\n\t\t\t\twritten += int64(nw)\n\t\t\t}\n\t\t\tif werr != nil {\n\t\t\t\treturn written, werr\n\t\t\t}\n\t\t\tif nr != nw {\n\t\t\t\treturn written, io.ErrShortWrite\n\t\t\t}\n\t\t}\n\t\tif rerr != nil {\n\t\t\tif rerr == io.EOF {\n\t\t\t\trerr = nil\n\t\t\t}\n\t\t\treturn written, rerr\n\t\t}\n\t}\n}\n\n// RoundTrip is a method on the Handler struct that implements the http.RoundTripper interface.\n// This method is called by the internal.NewSingleHostReverseProxy to handle the incoming request.\n// The method is responsible for validating the incoming request and getting the source ISO.\nfunc (h *Handler) RoundTrip(req *http.Request) (*http.Response, error) {\n\tlog := h.Logger.WithValues(\"method\", req.Method, \"inboundURI\", req.RequestURI, \"remoteAddr\", req.RemoteAddr)\n\tlog.V(1).Info(\"starting the ISO patching HTTP handler\")\n\n\tif filepath.Ext(req.URL.Path) != \".iso\" {\n\t\tlog.Info(\"extension not supported, only supported extension is '.iso'\")\n\t\treturn &http.Response{\n\t\t\tStatus:     fmt.Sprintf(\"%d %s\", http.StatusNotFound, http.StatusText(http.StatusNotFound)),\n\t\t\tStatusCode: http.StatusNotFound,\n\t\t\tBody:       http.NoBody,\n\t\t\tRequest:    req,\n\t\t}, nil\n\t}\n\n\t// The incoming request url is expected to have the mac address present.\n\t// Fetch the mac and validate if there's a hardware object\n\t// associated with the mac.\n\t//\n\t// We serve the iso only if this validation passes.\n\tha, err := getMAC(req.URL.Path)\n\tif err != nil {\n\t\tlog.Info(\"unable to parse mac address in the URL path\", \"error\", err)\n\t\treturn &http.Response{\n\t\t\tStatus:     fmt.Sprintf(\"%d %s\", http.StatusBadRequest, http.StatusText(http.StatusBadRequest)),\n\t\t\tStatusCode: http.StatusBadRequest,\n\t\t\tBody:       http.NoBody,\n\t\t\tRequest:    req,\n\t\t}, nil\n\t}\n\n\tfac, dhcpData, err := h.getFacility(req.Context(), ha, h.Backend)\n\tif err != nil {\n\t\tlog.Info(\"unable to get the hardware object\", \"error\", err, \"mac\", ha)\n\t\tif apierrors.IsNotFound(err) {\n\t\t\treturn &http.Response{\n\t\t\t\tStatus:     fmt.Sprintf(\"%d %s\", http.StatusNotFound, http.StatusText(http.StatusNotFound)),\n\t\t\t\tStatusCode: http.StatusNotFound,\n\t\t\t\tBody:       http.NoBody,\n\t\t\t\tRequest:    req,\n\t\t\t}, nil\n\t\t}\n\t\treturn &http.Response{\n\t\t\tStatus:     fmt.Sprintf(\"%d %s\", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)),\n\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\tBody:       http.NoBody,\n\t\t\tRequest:    req,\n\t\t}, nil\n\t}\n\t// The hardware object doesn't contain a dedicated field for consoles right now and\n\t// historically the facility is used as a way to define consoles on a per Hardware basis.\n\tvar consoles string\n\tswitch {\n\tcase fac != \"\" && strings.Contains(fac, \"console=\"):\n\t\tconsoles = fmt.Sprintf(\"facility=%s\", fac)\n\tcase fac != \"\":\n\t\tconsoles = fmt.Sprintf(\"facility=%s %s\", fac, defaultConsoles)\n\tdefault:\n\t\tconsoles = defaultConsoles\n\t}\n\t// The patch is added to the request context so that it can be used in the Copy method.\n\treq = req.WithContext(internal.WithPatch(req.Context(), []byte(h.constructPatch(consoles, ha.String(), dhcpData))))\n\n\t// The internal.NewSingleHostReverseProxy takes the incoming request url and adds the path to the target (h.SourceISO).\n\t// This function is more than a pass through proxy. The MAC address in the url path is required to do hardware lookups using the backend reader\n\t// and is not used when making http calls to the target (h.SourceISO). All valid requests are passed through to the target.\n\treq.URL.Path = h.parsedURL.Path\n\tlog = log.WithValues(\"outboundURL\", req.URL.String())\n\n\t// RoundTripper needs a Transport to execute a HTTP transaction\n\t// For our use case the default transport will suffice.\n\tresp, err := http.DefaultTransport.RoundTrip(req)\n\tif err != nil {\n\t\tlog.Error(err, \"issue getting the source ISO\", \"sourceIso\", h.SourceISO)\n\t\treturn nil, err\n\t}\n\t// by setting this header we are telling the logging middleware to not log its default log message.\n\t// we do this because there are a lot of partial content requests and it allow this handler to take care of logging.\n\tresp.Header.Set(\"X-Global-Logging\", \"false\")\n\n\tif resp.StatusCode == http.StatusPartialContent {\n\t\t// 0.002% of the time we log a 206 request message.\n\t\t// In testing, it was observed that about 3000 HTTP 206 requests are made per ISO mount.\n\t\t// 0.002% gives us about 5 - 10, log messages per ISO mount.\n\t\t// We're optimizing for showing \"enough\" log messages so that progress can be observed.\n\t\tif p := randomPercentage(100000); p < 0.002 {\n\t\t\tlog.Info(\"206 status code response\", \"sourceIso\", h.SourceISO, \"status\", resp.Status)\n\t\t}\n\t} else {\n\t\tlog.Info(\"response received\", \"sourceIso\", h.SourceISO, \"status\", resp.Status)\n\t}\n\n\tlog.V(1).Info(\"roundtrip complete\")\n\n\treturn resp, nil\n}\n\nfunc (h *Handler) constructPatch(console, mac string, d *data.DHCP) string {\n\tsyslogHost := fmt.Sprintf(\"syslog_host=%s\", h.Syslog)\n\tgrpcAuthority := fmt.Sprintf(\"grpc_authority=%s\", h.TinkServerGRPCAddr)\n\ttinkerbellTLS := fmt.Sprintf(\"tinkerbell_tls=%v\", h.TinkServerTLS)\n\tworkerID := fmt.Sprintf(\"worker_id=%s\", mac)\n\tvlanID := func() string {\n\t\tif d != nil && d.VLANID != \"\" {\n\t\t\treturn fmt.Sprintf(\"vlan_id=%s\", d.VLANID)\n\t\t}\n\t\treturn \"\"\n\t}()\n\thwAddr := fmt.Sprintf(\"hw_addr=%s\", mac)\n\tall := []string{strings.Join(h.ExtraKernelParams, \" \"), console, vlanID, hwAddr, syslogHost, grpcAuthority, tinkerbellTLS, workerID}\n\tif h.StaticIPAMEnabled {\n\t\tall = append(all, parseIPAM(d))\n\t}\n\n\treturn strings.Join(all, \" \")\n}\n\nfunc getMAC(urlPath string) (net.HardwareAddr, error) {\n\tmac := path.Base(path.Dir(urlPath))\n\thw, err := net.ParseMAC(mac)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse URL path: %s , the second to last element in the URL path must be a valid mac address, err: %w\", urlPath, err)\n\t}\n\n\treturn hw, nil\n}\n\nfunc (h *Handler) getFacility(ctx context.Context, mac net.HardwareAddr, br BackendReader) (string, *data.DHCP, error) {\n\tif br == nil {\n\t\treturn \"\", nil, errors.New(\"backend is nil\")\n\t}\n\n\td, n, err := br.GetByMac(ctx, mac)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\treturn n.Facility, d, nil\n}\n\nfunc randomPercentage(precision int64) float64 {\n\trandom, err := rand.Int(rand.Reader, big.NewInt(precision))\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\treturn float64(random.Int64()) / float64(precision)\n}\n"
  },
  {
    "path": "internal/iso/iso_test.go",
    "content": "package iso\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\tdiskfs \"github.com/diskfs/go-diskfs\"\n\t\"github.com/diskfs/go-diskfs/disk\"\n\t\"github.com/diskfs/go-diskfs/filesystem\"\n\t\"github.com/diskfs/go-diskfs/filesystem/iso9660\"\n\t\"github.com/go-logr/logr\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nconst magicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit021bmpdb9ctrc87x2ymc8e7icu4ffi15x1hah9iyaiz38ckyap8hwx2vt5rm44ixv4hau8iw718q5yd019um5dt2xpqqa2rjtdypzr5v1gun8un110hhwp8cex7pqrh2ivh0ynpm4zkkwc8wcn367zyethzy7q8hzudyeyzx3cgmxqbkh825gcak7kxzjbgjajwizryv7ec1xm2h0hh7pz29qmvtgfjj1vphpgq1zcbiiehv52wrjy9yq473d9t1rvryy6929nk435hfx55du3ih05kn5tju3vijreru1p6knc988d4gfdz28eragvryq5x8aibe5trxd0t6t7jwxkde34v6pj1khmp50k6qqj3nzgcfzabtgqkmeqhdedbvwf3byfdma4nkv3rcxugaj2d0ru30pa2fqadjqrtjnv8bu52xzxv7irbhyvygygxu1nt5z4fh9w1vwbdcmagep26d298zknykf2e88kumt59ab7nq79d8amnhhvbexgh48e8qc61vq2e9qkihzt1twk1ijfgw70nwizai15iqyted2dt9gfmf2gg7amzufre79hwqkddc1cd935ywacnkrnak6r7xzcz7zbmq3kt04u2hg1iuupid8rt4nyrju51e6uejb2ruu36g9aibmz3hnmvazptu8x5tyxk820g2cdpxjdij766bt2n3djur7v623a2v44juyfgz80ekgfb9hkibpxh3zgknw8a34t4jifhf116x15cei9hwch0fye3xyq0acuym8uhitu5evc4rag3ui0fny3qg4kju7zkfyy8hwh537urd5uixkzwu5bdvafz4jmv7imypj543xg5em8jk8cgk7c4504xdd5e4e71ihaumt6u5u2t1w7um92fepzae8p0vq93wdrd1756npu1pziiur1payc7kmdwyxg3hj5n4phxbc29x0tcddamjrwt260b0w`\n\nfunc TestReqPathInvalid(t *testing.T) {\n\ttests := map[string]struct {\n\t\tisoURL     string\n\t\tstatusCode int\n\t}{\n\t\t\"invalid URL prefix\": {isoURL: \"invalid\", statusCode: http.StatusNotFound},\n\t\t\"invalid URL\":        {isoURL: \"http://invalid.:123/hook.iso\", statusCode: http.StatusBadRequest},\n\t\t\"no script or url\":   {isoURL: \"http://10.10.10.10:8080/aa:aa:aa:aa:aa:aa/invalid.iso\", statusCode: http.StatusInternalServerError},\n\t}\n\tfor name, tt := range tests {\n\t\tu, _ := url.Parse(tt.isoURL)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\th := &Handler{\n\t\t\t\tparsedURL: u,\n\t\t\t}\n\t\t\treq := http.Request{\n\t\t\t\tMethod: http.MethodGet,\n\t\t\t\tURL:    u,\n\t\t\t}\n\n\t\t\tgot, err := h.RoundTrip(&req)\n\t\t\tgot.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got.StatusCode != tt.statusCode {\n\t\t\t\tt.Fatalf(\"got response status code: %d, want status code: %d\", got.StatusCode, tt.statusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCreateISO(t *testing.T) {\n\tt.Skip(\"Unskip this test to create a new ISO file\")\n\tgrubCfg := `set timeout=0\nset gfxpayload=text\nmenuentry 'LinuxKit ISO Image' {\n        linuxefi /kernel 464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit021bmpdb9ctrc87x2ymc8e7icu4ffi15x1hah9iyaiz38ckyap8hwx2vt5rm44ixv4hau8iw718q5yd019um5dt2xpqqa2rjtdypzr5v1gun8un110hhwp8cex7pqrh2ivh0ynpm4zkkwc8wcn367zyethzy7q8hzudyeyzx3cgmxqbkh825gcak7kxzjbgjajwizryv7ec1xm2h0hh7pz29qmvtgfjj1vphpgq1zcbiiehv52wrjy9yq473d9t1rvryy6929nk435hfx55du3ih05kn5tju3vijreru1p6knc988d4gfdz28eragvryq5x8aibe5trxd0t6t7jwxkde34v6pj1khmp50k6qqj3nzgcfzabtgqkmeqhdedbvwf3byfdma4nkv3rcxugaj2d0ru30pa2fqadjqrtjnv8bu52xzxv7irbhyvygygxu1nt5z4fh9w1vwbdcmagep26d298zknykf2e88kumt59ab7nq79d8amnhhvbexgh48e8qc61vq2e9qkihzt1twk1ijfgw70nwizai15iqyted2dt9gfmf2gg7amzufre79hwqkddc1cd935ywacnkrnak6r7xzcz7zbmq3kt04u2hg1iuupid8rt4nyrju51e6uejb2ruu36g9aibmz3hnmvazptu8x5tyxk820g2cdpxjdij766bt2n3djur7v623a2v44juyfgz80ekgfb9hkibpxh3zgknw8a34t4jifhf116x15cei9hwch0fye3xyq0acuym8uhitu5evc4rag3ui0fny3qg4kju7zkfyy8hwh537urd5uixkzwu5bdvafz4jmv7imypj543xg5em8jk8cgk7c4504xdd5e4e71ihaumt6u5u2t1w7um92fepzae8p0vq93wdrd1756npu1pziiur1payc7kmdwyxg3hj5n4phxbc29x0tcddamjrwt260b0w text\n        initrdefi /initrd.img\n}\n`\n\tif err := os.Remove(\"testdata/output.iso\"); err != nil && !os.IsNotExist(err) {\n\t\tt.Fatal(err)\n\t}\n\tvar diskSize int64 = 51200 // 50Kb\n\tmydisk, err := diskfs.Create(\"./testdata/output.iso\", diskSize, diskfs.SectorSizeDefault)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer mydisk.Close()\n\n\t// the following line is required for an ISO, which may have logical block sizes\n\t// only of 2048, 4096, 8192\n\tmydisk.LogicalBlocksize = 2048\n\tfspec := disk.FilesystemSpec{Partition: 0, FSType: filesystem.TypeISO9660, VolumeLabel: \"label\"}\n\tfs, err := mydisk.CreateFilesystem(fspec)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := fs.Mkdir(\"EFI/BOOT\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\trw, err := fs.OpenFile(\"EFI/BOOT/grub.cfg\", os.O_CREATE|os.O_RDWR)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcontent := []byte(grubCfg)\n\t_, err = rw.Write(content)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tiso, ok := fs.(*iso9660.FileSystem)\n\tif !ok {\n\t\tt.Fatal(fmt.Errorf(\"not an iso9660 filesystem\"))\n\t}\n\terr = iso.Finalize(iso9660.FinalizeOptions{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestPatching(t *testing.T) {\n\t// create a small ISO file with the magic string\n\t// serve ISO with a http server\n\t// patch the ISO file\n\t// mount the ISO file and check if the magic string was patched\n\n\t// If anything changes here the space padding will be different. Be sure to update it accordingly.\n\twantGrubCfg := `set timeout=0\nset gfxpayload=text\nmenuentry 'LinuxKit ISO Image' {\n        linuxefi /kernel  facility=test console=ttyAMA0 console=ttyS0 console=tty0 console=tty1 console=ttyS1  hw_addr=de:ed:be:ef:fe:ed syslog_host=127.0.0.1:514 grpc_authority=127.0.0.1:42113 tinkerbell_tls=false worker_id=de:ed:be:ef:fe:ed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        text\n        initrdefi /initrd.img\n}`\n\t// This expects that testdata/output.iso exists. Run the TestCreateISO test to create it.\n\n\t// serve it with a http server\n\ths := httptest.NewServer(http.FileServer(http.Dir(\"./testdata\")))\n\tdefer hs.Close()\n\n\t// patch the ISO file\n\tu := hs.URL + \"/output.iso\"\n\tparsedURL, err := url.Parse(u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\th := &Handler{\n\t\tLogger:             logr.Discard(),\n\t\tBackend:            &mockBackend{},\n\t\tSourceISO:          u,\n\t\tExtraKernelParams:  []string{},\n\t\tSyslog:             \"127.0.0.1:514\",\n\t\tTinkServerTLS:      false,\n\t\tTinkServerGRPCAddr: \"127.0.0.1:42113\",\n\t\tparsedURL:          parsedURL,\n\t\tMagicString:        magicString,\n\t}\n\th.magicStrPadding = bytes.Repeat([]byte{' '}, len(h.MagicString))\n\t// for debugging enable a logger\n\t// h.Logger = logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))\n\n\thf, err := h.HandlerFunc()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tw := httptest.NewRecorder()\n\thf.ServeHTTP(w, httptest.NewRequest(http.MethodGet, \"/iso/de:ed:be:ef:fe:ed/output.iso\", nil))\n\n\tres := w.Result()\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"got status code: %d, want status code: %d\", res.StatusCode, http.StatusOK)\n\t}\n\n\tisoContents, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tidx := bytes.Index(isoContents, []byte(`set timeout=0`))\n\tif idx == -1 {\n\t\tt.Fatalf(\"could not find the expected grub.cfg contents in the ISO\")\n\t}\n\tcontents := isoContents[idx : idx+len(wantGrubCfg)]\n\n\tif diff := cmp.Diff(wantGrubCfg, string(contents)); diff != \"\" {\n\t\tt.Fatalf(\"patched grub.cfg contents don't match expected: %v\", diff)\n\t}\n}\n\ntype mockBackend struct{}\n\nfunc (m *mockBackend) GetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {\n\td := &data.DHCP{}\n\tn := &data.Netboot{\n\t\tFacility: \"test\",\n\t}\n\treturn d, n, nil\n}\n\nfunc (m *mockBackend) GetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboot, error) {\n\td := &data.DHCP{}\n\tn := &data.Netboot{\n\t\tFacility: \"test\",\n\t}\n\treturn d, n, nil\n}\n"
  },
  {
    "path": "internal/metric/metric.go",
    "content": "package metric\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\tDHCPTotal *prometheus.CounterVec\n\n\tDiscoverDuration    prometheus.ObserverVec\n\tHardwareDiscovers   *prometheus.CounterVec\n\tDiscoversInProgress *prometheus.GaugeVec\n\n\tJobDuration    prometheus.ObserverVec\n\tJobsTotal      *prometheus.CounterVec\n\tJobsInProgress *prometheus.GaugeVec\n)\n\nfunc Init() {\n\tDHCPTotal = promauto.NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"dhcp_total\",\n\t\tHelp: \"Number of DHCP Requests handled.\",\n\t}, []string{\"op\", \"type\", \"giaddr\"})\n\n\tlabelValues := []prometheus.Labels{\n\t\t{\"op\": \"recv\", \"type\": \"DHCPACK\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPDECLINE\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPDISCOVER\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPINFORM\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPNAK\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPOFFER\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPRELEASE\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"recv\", \"type\": \"DHCPREQUEST\", \"giaddr\": \"0.0.0.0\"},\n\t\t{\"op\": \"send\", \"type\": \"DHCPOFFER\", \"giaddr\": \"0.0.0.0\"},\n\t}\n\tinitCounterLabels(DHCPTotal, labelValues)\n\n\tlabelValues = []prometheus.Labels{\n\t\t{\"from\": \"dhcp\"},\n\t\t{\"from\": \"ip\"},\n\t}\n\n\tDiscoverDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{\n\t\tName:    \"discover_duration_seconds\",\n\t\tHelp:    \"Duration taken to get a response for a newly discovered request.\",\n\t\tBuckets: prometheus.LinearBuckets(.01, .05, 10),\n\t}, []string{\"from\"})\n\tHardwareDiscovers = promauto.NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"discover_total\",\n\t\tHelp: \"Number of discover requests requested.\",\n\t}, []string{\"from\"})\n\tDiscoversInProgress = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tName: \"discover_in_progress\",\n\t\tHelp: \"Number of discover requests that have yet to receive a response.\",\n\t}, []string{\"from\"})\n\n\tinitObserverLabels(DiscoverDuration, labelValues)\n\tinitCounterLabels(HardwareDiscovers, labelValues)\n\tinitGaugeLabels(DiscoversInProgress, labelValues)\n\n\tJobDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{\n\t\tName:    \"jobs_duration_seconds\",\n\t\tHelp:    \"Duration taken for a job to complete.\",\n\t\tBuckets: prometheus.LinearBuckets(.01, .05, 10),\n\t}, []string{\"from\", \"op\"})\n\tJobsTotal = promauto.NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"jobs_total\",\n\t\tHelp: \"Number of jobs.\",\n\t}, []string{\"from\", \"op\"})\n\tJobsInProgress = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tName: \"jobs_in_progress\",\n\t\tHelp: \"Number of jobs waiting to complete.\",\n\t}, []string{\"from\", \"op\"})\n\n\tlabelValues = []prometheus.Labels{\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPACK\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPDECLINE\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPDISCOVER\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPINFORM\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPNAK\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPOFFER\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPRELEASE\"},\n\t\t{\"from\": \"dhcp\", \"op\": \"DHCPREQUEST\"},\n\t\t{\"from\": \"http\", \"op\": \"file\"},\n\t\t{\"from\": \"http\", \"op\": \"hardware-components\"},\n\t\t{\"from\": \"http\", \"op\": \"phone-home\"},\n\t\t{\"from\": \"http\", \"op\": \"problem\"},\n\t\t{\"from\": \"http\", \"op\": \"event\"},\n\t\t{\"from\": \"tftp\", \"op\": \"read\"},\n\t}\n\n\tinitObserverLabels(JobDuration, labelValues)\n\tinitCounterLabels(JobsTotal, labelValues)\n\tinitGaugeLabels(JobsInProgress, labelValues)\n}\n\nfunc initCounterLabels(m *prometheus.CounterVec, l []prometheus.Labels) {\n\tfor _, labels := range l {\n\t\tm.With(labels)\n\t}\n}\n\nfunc initGaugeLabels(m *prometheus.GaugeVec, l []prometheus.Labels) {\n\tfor _, labels := range l {\n\t\tm.With(labels)\n\t}\n}\n\nfunc initObserverLabels(m prometheus.ObserverVec, l []prometheus.Labels) {\n\tfor _, labels := range l {\n\t\tm.With(labels)\n\t}\n}\n"
  },
  {
    "path": "internal/otel/otel.go",
    "content": "/*\nhttps://github.com/equinix-labs/otel-init-go\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage otel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.4.0\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n)\n\n// SimpleCarrier is an abstraction for handling traceparent propagation\n// that needs a type that implements the propagation.TextMapCarrier().\n// This is the simplest possible implementation that is a little fragile\n// but since we're not doing anything else with it, it's fine for this.\ntype SimpleCarrier map[string]string\n\n// Get implements the otel interface for propagation.\nfunc (otp SimpleCarrier) Get(key string) string {\n\treturn otp[key]\n}\n\n// Set implements the otel interface for propagation.\nfunc (otp SimpleCarrier) Set(key, value string) {\n\totp[key] = value\n}\n\n// Keys implements the otel interface for propagation.\nfunc (otp SimpleCarrier) Keys() []string {\n\tout := []string{}\n\tfor k := range otp {\n\t\tout = append(out, k)\n\t}\n\treturn out\n}\n\n// Clear implements the otel interface for propagation.\nfunc (otp SimpleCarrier) Clear() {\n\tfor k := range otp {\n\t\tdelete(otp, k)\n\t}\n}\n\n// TraceparentStringFromContext gets the current trace from the context and\n// returns a W3C traceparent string. Depends on global OTel TextMapPropagator.\nfunc TraceparentStringFromContext(ctx context.Context) string {\n\tcarrier := SimpleCarrier{}\n\tprop := otel.GetTextMapPropagator()\n\tprop.Inject(ctx, carrier)\n\treturn carrier.Get(\"traceparent\")\n}\n\n// ContextWithEnvTraceparent is a helper that looks for the the TRACEPARENT\n// environment variable and if it's set, it grabs the traceparent and\n// adds it to the context it returns. When there is no envvar or it's\n// empty, the original context is returned unmodified.\n// Depends on global OTel TextMapPropagator.\nfunc ContextWithEnvTraceparent(ctx context.Context) context.Context {\n\ttraceparent := os.Getenv(\"TRACEPARENT\")\n\tif traceparent != \"\" {\n\t\treturn ContextWithTraceparentString(ctx, traceparent)\n\t}\n\treturn ctx\n}\n\n// ContextWithTraceparentString takes a W3C traceparent string, uses the otel\n// carrier code to get it into a context it returns ready to go.\n// Depends on global OTel TextMapPropagator.\nfunc ContextWithTraceparentString(ctx context.Context, traceparent string) context.Context {\n\tcarrier := SimpleCarrier{}\n\tcarrier.Set(\"traceparent\", traceparent)\n\tprop := otel.GetTextMapPropagator()\n\treturn prop.Extract(ctx, carrier)\n}\n\n// Config holds the typed values of configuration read from the environment.\n// It is public mainly to make testing easier and most users should never\n// use it directly.\ntype Config struct {\n\tServicename string `json:\"service_name\"`\n\tEndpoint    string `json:\"endpoint\"`\n\tInsecure    bool   `json:\"insecure\"`\n\tLogger      logr.Logger\n}\n\n// Init sets up the OpenTelemetry plumbing so it's ready to use.\n// It requires a context.Context and returns context and a func() that encapuslates clean shutdown.\nfunc Init(ctx context.Context, c Config) (context.Context, context.CancelFunc, error) {\n\tif c.Endpoint != \"\" {\n\t\treturn c.initTracing(ctx)\n\t}\n\n\t// no configuration, nothing to do, the calling code is inert\n\t// config is available in the returned context (for test/debug)\n\treturn ctx, func() {}, nil\n}\n\nfunc (c Config) initTracing(ctx context.Context) (context.Context, context.CancelFunc, error) {\n\t// set the service name that will show up in tracing UIs\n\tresAttrs := resource.WithAttributes(semconv.ServiceNameKey.String(c.Servicename))\n\tres, err := resource.New(ctx, resAttrs)\n\tif err != nil {\n\t\treturn ctx, nil, fmt.Errorf(\"failed to create OpenTelemetry service name resource: %w\", err)\n\t}\n\n\tretryPolicy := `{\n\t\t\"methodConfig\": [{\n\t\t\t\"retryPolicy\": {\n\t\t\t\t\"MaxAttempts\": 1000,\n\t\t\t\t\"InitialBackoff\": \".01s\",\n\t\t\t\t\"MaxBackoff\": \".01s\",\n\t\t\t\t\"BackoffMultiplier\": 1.0,\n\t\t\t\t\"RetryableStatusCodes\": [ \"UNAVAILABLE\" ]\n\t\t\t}\n\t\t}]\n\t}`\n\n\tgrpcOpts := []otlptracegrpc.Option{\n\t\totlptracegrpc.WithEndpoint(c.Endpoint),\n\t\totlptracegrpc.WithDialOption(grpc.WithDefaultServiceConfig(retryPolicy)),\n\t\totlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{\n\t\t\tEnabled:         true,\n\t\t\tInitialInterval: time.Second * 5,\n\t\t\tMaxInterval:     time.Second * 30,\n\t\t\tMaxElapsedTime:  time.Minute * 5,\n\t\t}),\n\t}\n\tif c.Insecure {\n\t\tgrpcOpts = append(grpcOpts, otlptracegrpc.WithInsecure())\n\t} else {\n\t\tcreds := credentials.NewClientTLSFromCert(nil, \"\")\n\t\tgrpcOpts = append(grpcOpts, otlptracegrpc.WithTLSCredentials(creds))\n\t}\n\t// TODO: add TLS client cert auth\n\n\texporter, err := otlptracegrpc.New(context.Background(), grpcOpts...)\n\tif err != nil {\n\t\treturn ctx, nil, fmt.Errorf(\"failed to configure OTLP exporter: %w\", err)\n\t}\n\n\t// TODO: more configuration opportunities here\n\tbsp := sdktrace.NewBatchSpanProcessor(exporter)\n\ttracerProvider := sdktrace.NewTracerProvider(\n\t\tsdktrace.WithResource(res),\n\t\tsdktrace.WithSpanProcessor(bsp),\n\t)\n\n\t// set global propagator to tracecontext (the default is no-op).\n\tprop := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})\n\totel.SetTextMapPropagator(prop)\n\n\t// inject the tracer into the otel globals, start background goroutines\n\totel.SetTracerProvider(tracerProvider)\n\n\t// logger\n\totel.SetLogger(c.Logger)\n\n\t// set a custom error handler so that we can use our own logger\n\totel.SetErrorHandler(c)\n\n\t// the public function will wrap this in its own shutdown function\n\treturn ctx, func() {\n\t\tctx1, done := context.WithTimeout(context.Background(), 5*time.Second)\n\t\terr = tracerProvider.Shutdown(ctx1)\n\t\tif err != nil {\n\t\t\tc.Logger.Info(\"shutdown of OpenTelemetry tracerProvider failed: %s\", err)\n\t\t}\n\t\tdone()\n\n\t\tctx2, done := context.WithTimeout(context.Background(), 5*time.Second)\n\t\terr = exporter.Shutdown(ctx2)\n\t\tif err != nil {\n\t\t\tc.Logger.Info(\"shutdown of OpenTelemetry OTLP exporter failed: %s\", err)\n\t\t}\n\t\tdone()\n\t}, nil\n}\n\nfunc (c Config) Handle(err error) {\n\tif err != nil {\n\t\tc.Logger.Info(\"OpenTelemetry error\", \"err\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/syslog/facility_string.go",
    "content": "// Code generated by \"stringer -type=facility -output=facility_string.go\"; DO NOT EDIT.\n\npackage syslog\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[kern-0]\n\t_ = x[user-1]\n\t_ = x[mail-2]\n\t_ = x[daemon-3]\n\t_ = x[auth-4]\n\t_ = x[syslog-5]\n\t_ = x[lpr-6]\n\t_ = x[news-7]\n\t_ = x[uucp-8]\n\t_ = x[clock-9]\n\t_ = x[authpriv-10]\n\t_ = x[ftp-11]\n\t_ = x[ntp-12]\n\t_ = x[audit-13]\n\t_ = x[alert-14]\n\t_ = x[cron-15]\n\t_ = x[local0-16]\n\t_ = x[local1-17]\n\t_ = x[local2-18]\n\t_ = x[local3-19]\n\t_ = x[local4-20]\n\t_ = x[local5-21]\n\t_ = x[local6-22]\n\t_ = x[local7-23]\n}\n\nconst _facility_name = \"kernusermaildaemonauthsysloglprnewsuucpclockauthprivftpntpauditalertcronlocal0local1local2local3local4local5local6local7\"\n\nvar _facility_index = [...]uint8{0, 4, 8, 12, 18, 22, 28, 31, 35, 39, 44, 52, 55, 58, 63, 68, 72, 78, 84, 90, 96, 102, 108, 114, 120}\n\nfunc (i facility) String() string {\n\tif i >= facility(len(_facility_index)-1) {\n\t\treturn \"facility(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _facility_name[_facility_index[i]:_facility_index[i+1]]\n}\n"
  },
  {
    "path": "internal/syslog/message.go",
    "content": "package syslog\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n)\n\n//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=facility -output=facility_string.go\ntype facility byte\n\nconst (\n\tkern facility = iota\n\tuser\n\tmail\n\tdaemon\n\tauth\n\tsyslog\n\tlpr\n\tnews\n\tuucp\n\tclock\n\tauthpriv\n\tftp\n\tntp\n\taudit\n\talert\n\tcron\n\tlocal0\n\tlocal1\n\tlocal2\n\tlocal3\n\tlocal4\n\tlocal5\n\tlocal6\n\tlocal7\n)\n\n//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=severity -output=severity_string.go\ntype severity byte\n\nconst (\n\tEMERG severity = iota\n\tALERT\n\tCRIT\n\tERR\n\tWARNING\n\tNOTICE\n\tINFO\n\tDEBUG\n)\n\ntype message struct {\n\tbuf  [2048]byte\n\tsize int\n\ttime time.Time\n\thost net.IP\n\n\t// parsed fields\n\tpriority byte\n\thostname []byte\n\tapp      []byte\n\tprocid   []byte\n\tmsgid    []byte\n\tmsg      []byte\n}\n\nfunc (m *message) Facility() facility {\n\treturn facility(m.priority / 8)\n}\n\nfunc (m *message) Host() string {\n\treturn m.host.String()\n}\n\nfunc (m *message) Severity() severity {\n\treturn severity(m.priority % 8)\n}\n\nvar msgCleanup = strings.NewReplacer([]string{\"\\b\", \"\"}...)\n\nfunc (m *message) String() string {\n\tif m.msg == nil {\n\t\treturn fmt.Sprintf(\"host=%s syslog=%q\", m.host, m.buf[:m.size])\n\t}\n\n\tfields := make([]string, 0, 7)\n\n\t// fields = append(fields, fmt.Sprintf(\"ptr=%p\", m))\n\t// fields = append(fields, \"time=\" + m.time.Format(time.RFC3339))\n\n\tif m.hostname != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"host=%s\", m.hostname))\n\t} else {\n\t\tfields = append(fields, \"host=\"+m.host.String())\n\t}\n\n\tfields = append(fields, \"facility=\"+m.Facility().String())\n\tfields = append(fields, \"severity=\"+m.Severity().String())\n\n\tif m.app != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"app-name=%s\", m.app))\n\t}\n\n\tif m.procid != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"procid=%s\", m.procid))\n\t}\n\n\tif m.msgid != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"msgid=%s\", m.msgid))\n\t}\n\n\tfields = append(fields, fmt.Sprintf(\"msg=%q\", msgCleanup.Replace(string(m.msg))))\n\n\treturn strings.Join(fields, \" \")\n}\n\nfunc (m *message) Timestamp() time.Time {\n\treturn m.time\n}\n\nfunc (m *message) correctLegacyTime(t time.Time) {\n\tt = t.AddDate(m.time.Year(), 0, 0)\n\n\toffset := m.time.Sub(t) //nolint:ifshort // erroneous warning. offset is used below\n\tif offset < 0 {\n\t\toffset = -offset\n\t}\n\n\tif hoursOff := (offset - (offset % time.Hour)) / time.Hour; hoursOff > 1 {\n\t\tt = t.Add(hoursOff)\n\t}\n\n\tm.time = t\n}\n\nfunc (m *message) parse() bool {\n\tif !m.parsePriority() {\n\t\treturn false\n\t}\n\tif !m.parseVersion() {\n\t\treturn m.parseLegacyHeader()\n\t}\n\tif !m.parseHeader() {\n\t\treturn false\n\t}\n\n\treturn m.parseStructuredData()\n}\n\nfunc (m *message) parseHeader() bool {\n\t// TIMESTAMP HOSTNAME APP-NAME PROCID MSGID MSG\n\tparts := bytes.SplitN(m.msg, []byte{' '}, 6)\n\n\tif len(parts) != 6 || !m.parseTimestamp(parts[0]) {\n\t\treturn false\n\t}\n\tm.hostname = ignoreNil(parts[1])\n\tm.app = ignoreNil(parts[2])\n\tm.procid = ignoreNil(parts[3])\n\tm.msgid = ignoreNil(parts[4])\n\tm.msg = parts[5]\n\n\treturn true\n}\n\nfunc (m *message) parseStructuredData() bool {\n\tif len(m.msg) >= 2 && m.msg[0] == '-' && m.msg[1] == ' ' {\n\t\tm.msg = m.msg[2:]\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (m *message) parseLegacyHeader() bool {\n\tconst (\n\t\tlayout  = time.Stamp\n\t\ttimeLen = len(layout)\n\t)\n\tif len(m.msg) <= timeLen || m.msg[timeLen] != ' ' {\n\t\tgoto parseHostname // too short or missing expected space after timestamp\n\t}\n\n\tif t, err := time.Parse(layout, string(m.msg[:timeLen])); err != nil {\n\t\tgoto parseHostname // doesn't match the expected layout\n\t} else if !t.IsZero() { // if zero, ignore and use the current time\n\t\tm.correctLegacyTime(t)\n\t}\n\tm.msg = m.msg[timeLen+1:]\n\nparseHostname:\n\tm.hostname = nil\n\n\tm.parseLegacyTag()\n\n\tm.trimSeverityPrefix()\n\tm.trimTimePrefix()\n\tm.trimCarriageReturns()\n\n\treturn true\n}\n\nfunc (m *message) parseLegacyTag() {\n\tb := m.msg\n\n\tfor i, c := range b {\n\t\tif c >= '0' && c <= '9' {\n\t\t\tcontinue\n\t\t}\n\t\tif c >= 'a' && c <= 'z' {\n\t\t\tcontinue\n\t\t}\n\t\tif c >= 'A' && c <= 'Z' {\n\t\t\tcontinue\n\t\t}\n\t\tif c == '-' || c == '_' || c == '/' || c == '.' {\n\t\t\tcontinue\n\t\t}\n\t\tif c == '[' {\n\t\t\tm.app, b = b[:i], b[i:]\n\n\t\t\tgoto parsePid\n\t\t}\n\t\tm.app, b = b[:i], b[i:]\n\n\t\tgoto trimColon\n\t}\n\tm.app = nil\n\tm.procid = nil\n\n\treturn\n\nparsePid:\n\tif i := bytes.IndexByte(b[1:], ']'); i != -1 {\n\t\tm.procid = b[1 : 1+i]\n\t\tb = b[1+i+1:]\n\t} else {\n\t\tm.procid = nil\n\t}\n\ntrimColon:\n\tm.msg = bytes.TrimPrefix(b, []byte{':', ' '})\n}\n\nfunc (m *message) parsePriority() bool {\n\tif m.size < 3 || m.buf[0] != '<' {\n\t\treturn false\n\t}\n\tvar pri byte\n\tfor i, c := range m.buf[1:5] {\n\t\tif c == '>' {\n\t\t\tm.priority = pri\n\t\t\tm.msg = m.buf[1+i+1 : m.size]\n\n\t\t\treturn true\n\t\t}\n\t\tif c < '0' || c > '9' {\n\t\t\treturn false\n\t\t}\n\t\tpri = pri*10 + c - '0'\n\t}\n\n\treturn false\n}\n\nfunc (m *message) parseTimestamp(b []byte) bool {\n\tif ignoreNil(b) == nil {\n\t\treturn true // NILVALUE\n\t}\n\n\tconst (\n\t\tlayout  = \"2006-01-02T15:04:05.999999Z07:00\"\n\t\ttimeLen = len(layout)\n\t)\n\tif len(b) > timeLen {\n\t\treturn false // too long\n\t}\n\tt, err := time.Parse(layout, string(b))\n\tif err != nil {\n\t\treturn false\n\t}\n\tm.time = t\n\n\treturn true\n}\n\nfunc (m *message) parseVersion() bool {\n\tif len(m.msg) < 2 {\n\t\treturn false // too short\n\t}\n\tif m.msg[1] != ' ' {\n\t\treturn false // missing space after version\n\t}\n\tif m.msg[0] != '1' {\n\t\treturn false // we only support version 1\n\t}\n\tm.msg = m.msg[2:]\n\n\treturn true\n}\n\nfunc (m *message) reset() {\n\tm.priority = 0\n\tm.hostname = nil\n\tm.app = nil\n\tm.procid = nil\n\tm.msgid = nil\n\tm.msg = nil\n}\n\nfunc (m *message) trimSeverityPrefix() {\n\tprefix := []byte(m.Severity().String() + \": \")\n\tm.msg = bytes.TrimPrefix(m.msg, prefix)\n}\n\nfunc (m *message) trimTimePrefix() {\n\tm.msg = bytes.TrimPrefix(m.msg, []byte(m.time.Format(\"2006-01-02 15:04:05 \")))\n}\n\nfunc (m *message) trimCarriageReturns() {\n\tif len(m.msg) > 0 && m.msg[0] == '\\r' {\n\t\tm.msg = m.msg[1:]\n\t}\n\t// m.msg = bytes.Replace(m.msg, \"\\r\", \"(CR)\", -1)\n}\n\nfunc ignoreNil(b []byte) []byte {\n\tif len(b) == 1 && b[0] == '-' {\n\t\treturn nil\n\t}\n\n\treturn b\n}\n"
  },
  {
    "path": "internal/syslog/receiver.go",
    "content": "package syslog\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n)\n\nvar syslogMessagePool = sync.Pool{\n\tNew: func() interface{} { return new(message) },\n}\n\ntype Receiver struct {\n\tc     *net.UDPConn\n\tparse chan *message\n\tdone  chan struct{}\n\terr   error\n\n\tLogger logr.Logger\n}\n\nfunc StartReceiver(ctx context.Context, logger logr.Logger, laddr string, parsers int) error {\n\tif parsers < 1 {\n\t\tparsers = 1\n\t}\n\n\taddr, err := net.ResolveUDPAddr(\"udp4\", laddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"resolve syslog udp listen address: %w\", err)\n\t}\n\n\tc, err := net.ListenUDP(\"udp4\", addr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"listen on syslog udp address: %w\", err)\n\t}\n\n\ts := &Receiver{\n\t\tc:      c,\n\t\tparse:  make(chan *message, parsers),\n\t\tdone:   make(chan struct{}),\n\t\tLogger: logger,\n\t}\n\n\tfor i := 0; i < parsers; i++ {\n\t\tgo s.runParser()\n\t}\n\tgo s.run(ctx)\n\n\treturn nil\n}\n\nfunc (r *Receiver) Done() <-chan struct{} {\n\treturn r.done\n}\n\nfunc (r *Receiver) Err() error {\n\treturn r.err\n}\n\nfunc (r *Receiver) cleanup() {\n\tr.c.Close()\n\n\tclose(r.parse)\n\tclose(r.done)\n}\n\nfunc (r *Receiver) run(ctx context.Context) {\n\tvar msg *message\n\tdefer func() {\n\t\tif msg != nil {\n\t\t\tsyslogMessagePool.Put(msg)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tr.cleanup()\n\t}()\n\n\tfor {\n\t\tif msg == nil {\n\t\t\tvar ok bool\n\t\t\tmsg, ok = syslogMessagePool.Get().(*message)\n\t\t\tif !ok {\n\t\t\t\tr.Logger.Error(errors.New(\"error type asserting pool item into message\"), \"error type asserting pool item into message\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tn, from, err := r.c.ReadFromUDP(msg.buf[:])\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"error reading udp message: %w\", err)\n\t\t\tif _, ok := err.(net.Error); ok {\n\t\t\t\tr.Logger.Error(err, \"error reading udp message\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.err = err\n\n\t\t\treturn\n\t\t}\n\t\tmsg.time = time.Now().UTC()\n\t\tmsg.host = from.IP\n\t\tmsg.size = n\n\t\tr.parse <- msg\n\t\tmsg = nil\n\t}\n}\n\nfunc parse(m *message) map[string]interface{} {\n\tstructured := make(map[string]interface{})\n\tif m.Facility().String() != \"\" {\n\t\tstructured[\"facility\"] = m.Facility().String()\n\t}\n\tif m.Severity().String() != \"\" {\n\t\tstructured[\"severity\"] = m.Severity().String()\n\t}\n\tif string(m.hostname) != \"\" {\n\t\tstructured[\"hostname\"] = string(m.hostname)\n\t}\n\tif string(m.app) != \"\" {\n\t\tstructured[\"app-name\"] = string(m.app)\n\t}\n\tif string(m.procid) != \"\" {\n\t\tstructured[\"procid\"] = string(m.procid)\n\t}\n\tif string(m.msgid) != \"\" {\n\t\tstructured[\"msgid\"] = string(m.msgid)\n\t}\n\tif string(m.msg) != \"\" {\n\t\tif strings.HasPrefix(string(m.msg), \"{\") {\n\t\t\tvar j map[string]interface{}\n\t\t\tif err := json.Unmarshal(m.msg, &j); err == nil {\n\t\t\t\tstructured[\"msg\"] = j\n\t\t\t}\n\t\t} else {\n\t\t\tstructured[\"msg\"] = string(m.msg)\n\t\t}\n\t}\n\tstructured[\"host\"] = m.host.String()\n\n\treturn structured\n}\n\nfunc (r *Receiver) runParser() {\n\tfor m := range r.parse {\n\t\tif m.parse() {\n\t\t\tstructured := parse(m)\n\t\t\tsl := r.Logger.WithValues(\"msg\", structured)\n\t\t\tif m.Severity() == DEBUG {\n\t\t\t\tsl.V(1).Info(\"msg\")\n\t\t\t} else {\n\t\t\t\tsl.Info(\"msg\")\n\t\t\t}\n\t\t} else {\n\t\t\tr.Logger.V(1).Info(\"msg\", \"msg\", m)\n\t\t}\n\t\tm.reset()\n\t\tsyslogMessagePool.Put(m)\n\t}\n}\n"
  },
  {
    "path": "internal/syslog/severity_string.go",
    "content": "// Code generated by \"stringer -type=severity -output=severity_string.go\"; DO NOT EDIT.\n\npackage syslog\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[EMERG-0]\n\t_ = x[ALERT-1]\n\t_ = x[CRIT-2]\n\t_ = x[ERR-3]\n\t_ = x[WARNING-4]\n\t_ = x[NOTICE-5]\n\t_ = x[INFO-6]\n\t_ = x[DEBUG-7]\n}\n\nconst _severity_name = \"EMERGALERTCRITERRWARNINGNOTICEINFODEBUG\"\n\nvar _severity_index = [...]uint8{0, 5, 10, 14, 17, 24, 30, 34, 39}\n\nfunc (i severity) String() string {\n\tif i >= severity(len(_severity_index)-1) {\n\t\treturn \"severity(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _severity_name[_severity_index[i]:_severity_index[i+1]]\n}\n"
  },
  {
    "path": "lint.mk",
    "content": "# BEGIN: lint-install github.com/tinkerbell/smee\n# http://github.com/tinkerbell/lint-install\n\n.PHONY: lint\nlint: _lint  ## Run linting\n\nLINT_ARCH := $(shell uname -m)\nLINT_OS := $(shell uname)\nLINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]')\nLINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))\n\n# shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation\nifeq ($(LINT_OS),Darwin)\n\tifeq ($(LINT_ARCH),arm64)\n\t\tLINT_ARCH=x86_64\n\tendif\nendif\n\nLINTERS :=\nFIXERS :=\n\nGOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml\nGOLANGCI_LINT_VERSION ?= v2.2.1\nGOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)\n$(GOLANGCI_LINT_BIN):\n\tmkdir -p $(LINT_ROOT)/out/linters\n\trm -rf $(LINT_ROOT)/out/linters/golangci-lint-*\n\tcurl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION)\n\tmv $(LINT_ROOT)/out/linters/golangci-lint $@\n\nLINTERS += golangci-lint-lint\ngolangci-lint-lint: $(GOLANGCI_LINT_BIN)\n\tfind . -name go.mod -execdir sh -c '\"$(GOLANGCI_LINT_BIN)\" run -c \"$(GOLANGCI_LINT_CONFIG)\"' '{}' '+'\n\nFIXERS += golangci-lint-fix\ngolangci-lint-fix: $(GOLANGCI_LINT_BIN)\n\tfind . -name go.mod -execdir \"$(GOLANGCI_LINT_BIN)\" run -c \"$(GOLANGCI_LINT_CONFIG)\" --fix \\;\n\n.PHONY: _lint $(LINTERS)\n_lint: $(LINTERS)\n\n.PHONY: fix $(FIXERS)\nfix: $(FIXERS)\n\n# END: lint-install github.com/tinkerbell/smee\n"
  },
  {
    "path": "rules.mk",
    "content": "# Only use the recipes defined in these makefiles\nMAKEFLAGS += --no-builtin-rules\n.SUFFIXES:\n# Delete target files if there's an error\n# This avoids a failure to then skip building on next run if the output is created by shell redirection for example\n# Not really necessary for now, but just good to have already if it becomes necessary later.\n.DELETE_ON_ERROR:\n# Treat the whole recipe as a one shell script/invocation instead of one-per-line\n.ONESHELL:\n# Use bash instead of plain sh\nSHELL := bash\n.SHELLFLAGS := -o pipefail -euc\n\n# Runnable tools\nGO\t\t\t?= go\nGOIMPORTS\t:= $(GO) run golang.org/x/tools/cmd/goimports@latest\n\n.PHONY: all smee crosscompile dc image gen run test\n\nCGO_ENABLED := 0\nexport CGO_ENABLED\n\nGitRev := $(shell git rev-parse --short HEAD)\n\ncrossbinaries := cmd/smee/smee-linux-amd64 cmd/smee/smee-linux-arm64\ncmd/smee/smee-linux-amd64: FLAGS=GOARCH=amd64\ncmd/smee/smee-linux-arm64: FLAGS=GOARCH=arm64\ncmd/smee/smee-linux-amd64 cmd/smee/smee-linux-arm64: smee\n\t${FLAGS} GOOS=linux go build -ldflags=\"-X main.GitRev=${GitRev}\" -o $@ ./cmd/smee/\n\ngenerated_go_files := \\\n\tinternal/syslog/facility_string.go \\\n\tinternal/syslog/severity_string.go \\\n\n# go generate\ngo_generate: $(generated_go_files)\n$(filter %_string.go,$(generated_go_files)):\ninternal/syslog/facility_string.go: internal/syslog/message.go\ninternal/syslog/severity_string.go: internal/syslog/message.go\n$(generated_go_files):\n\tgo generate -run=\"$(@F)\" ./...\n\t$(GOIMPORTS) -w $@\n\ncmd/smee/smee: internal/syslog/facility_string.go internal/syslog/severity_string.go cleanup\n\tgo build -v -ldflags=\"-X main.GitRev=${GitRev}\" -o $@ ./cmd/smee/\n\ncleanup:\n\trm -f cmd/smee/smee cmd/smee/smee-linux-amd64 cmd/smee/smee-linux-arm64"
  },
  {
    "path": "test/Dockerfile",
    "content": "FROM alpine:3.14\nEXPOSE 67 69\n\nRUN apk add --update --upgrade --no-cache net-tools busybox tftp-hpa curl tcpdump\n\nCOPY busybox-udhcpc-script.sh /busybox-udhcpc-script.sh\nCOPY extract-traceparent-from-opt43.sh /extract-traceparent-from-opt43.sh\nCOPY test-smee.sh /test-smee.sh\n\nENTRYPOINT /test-smee.sh\n"
  },
  {
    "path": "test/busybox-udhcpc-script.sh",
    "content": "#!/bin/sh\n# instead of messing with the actual interface configuration\n# this just dumps the environment variables to a file and stdout\n\nenv | grep -v '^[A-Z]' | sort | tee /tmp/dhcpoffer-vars.sh\n"
  },
  {
    "path": "test/extract-traceparent-from-opt43.sh",
    "content": "#!/bin/sh\n# shellcheck shell=dash\n\n# extract_traceparent_from_opt43 takes a hex string from busybox udhcpc's opt43\n# and extracts sub-option 69 which is where we stuff the traceparent in binary,\n# which busybox helpfully gives us in a hex string as $opt43\n#\n# PXE_DISCOVERY_CONTROL is 060108 (option 6, 1 byte long, value 8)\n# traceparent is 451a (type 69, 26 bytes, value is tp)\n#\n# The DHCP spec says nothing about ordering and smee can be observed to serve\n# the types in a different order on different runs, so the option has to be\n# fully parsed to get the right data.\n#\n# this would be way easier in perl/python but this needs to work in dash\n# and with busybox shell tools\n#\n# takes 1 argument, usually $opt43\n# sets $opt43x69 to the hex traceparent\n# exports $TRACEPARENT to the W3C-formatted traceparent string\nextract_traceparent_from_opt43() {\n\tlocal hexdata strlen offset\n\thexdata=$1\n\tshift\n\topt43x69=\"\" # in case the global is still set, empty it\n\tstrlen=$(echo -n \"$hexdata\" | wc -c)\n\toffset=1 # cut(1) uses offsets starting at 1\n\n\twhile [ \"$offset\" -lt \"$strlen\" ]; do\n\t\t# extract the type number, 1 byte\n\t\tlocal type_end htype type\n\t\ttype_end=$((offset + 1))\n\t\thtype=$(echo -n \"$hexdata\" | cut -c \"${offset}-${type_end}\")\n\t\ttype=$(printf '%d' \"0x$htype\")\n\n\t\t# extract the value length, 1 byte\n\t\tlocal len_start len_end hlen len\n\t\tlen_start=$((offset + 2))\n\t\tlen_end=$((offset + 3))\n\t\thlen=$(echo -n \"$hexdata\" | cut -c \"${len_start}-${len_end}\")\n\t\tlen=$(printf '%d' \"0x$hlen\")\n\n\t\t# calculate value offsets\n\t\tlocal bov eov\n\t\tbov=$((offset + 4))        # beginning of value\n\t\teov=$((bov + len * 2 - 1)) # end of value\n\n\t\tif [ \"$type\" -eq 69 ]; then\n\t\t\t# set global to the full tp hex data\n\t\t\topt43x69=$(echo -n \"$hexdata\" | cut -c \"${bov}-${eov}\")\n\n\t\t\t# break out the sections of the traceparent to make a proper W3C tp string\n\t\t\tlocal ver trace_id span_id flags\n\t\t\tver=$(echo -n \"$opt43x69\" | cut -c \"1-2\")       # 1 byte\n\t\t\ttrace_id=$(echo -n \"$opt43x69\" | cut -c \"3-34\") # 16 bytes\n\t\t\tspan_id=$(echo -n \"$opt43x69\" | cut -c \"35-50\") # 8 bytes\n\t\t\tflags=$(echo -n \"$opt43x69\" | cut -c \"51-53\")   # 1 byte\n\n\t\t\t# set TRACEPARENT to the W3C-formatted string\n\t\t\texport TRACEPARENT=\"${ver}-${trace_id}-${span_id}-${flags}\"\n\t\tfi\n\n\t\t# add to the offset:\n\t\t# 4 characters for type and len e.g. 0601 (type 6, length 1)\n\t\t# len (is bytes) * 2 (bc hex) = chars of offset e.g. 08 (value is 8, 2 chars in hex)\n\t\toffset=$((4 + offset + len * 2))\n\t\tlocal next\n\t\tnext=$(echo -n \"$hexdata\" | cut -c \"${offset}-$((offset + 1))\")\n\n\t\t# opt43 always ends with 0xff so if the next byte is ff it's the end for sure\n\t\tif [ \"$next\" = \"ff\" ]; then\n\t\t\tbreak\n\t\tfi\n\tdone\n}\n"
  },
  {
    "path": "test/hardware.yaml",
    "content": "---\n02:00:00:00:00:ff:\n  ipAddress: \"192.168.99.43\"\n  subnetMask: \"255.255.255.0\"\n  defaultGateway: \"192.168.99.1\"\n  nameServers:\n    - \"8.8.8.8\"\n  hostname: \"smee-test-client\"\n  domainName: \"example.com\"\n  broadcastAddress: \"192.168.2.255\"\n  ntpServers:\n    - \"132.163.96.2\"\n  leaseTime: 86400\n  domainSearch:\n    - \"example.com\"\n  netboot:\n    allowPxe: true\n"
  },
  {
    "path": "test/otel-collector.yaml",
    "content": "# opentelemetry-collector is a proxy for telemetry events.\n#\n# This configuration is set up for use in smee development.\n# With collector in debug mode every trace is printed to the console\n# so you can see traces without any complex tooling. There are also\n# examples below for how to send to Lightstep and Honeycomb.\n\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: \"0.0.0.0:4317\"\n\nprocessors:\n  batch:\n\nexporters:\n  # set to debug and your traces will get printed to the console spammily\n  logging:\n    logLevel: debug\n  # Lightstep: set & export LIGHTSTEP_TOKEN and enable below\n  otlp/1:\n    endpoint: \"ingest.lightstep.com:443\"\n    headers:\n      \"lightstep-access-token\": \"${LIGHTSTEP_TOKEN}\"\n  # Honeycomb: set & export HONEYCOMB_TEAM to the auth token, and set/export\n  # HONEYCOMB_DATASET to the dataset name you want to use, then enable below\n  otlp/2:\n    endpoint: \"api.honeycomb.io:443\"\n    headers:\n      \"x-honeycomb-team\": \"${HONEYCOMB_TEAM}\"\n      \"x-honeycomb-dataset\": \"${HONEYCOMB_DATASET}\"\n\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      # only enable logging by default\n      exporters: [logging]\n      # Lightstep:\n      # exporters: [logging, otlp/1]\n      # Honeycomb:\n      # exporters: [logging, otlp/2]\n"
  },
  {
    "path": "test/start-smee.sh",
    "content": "#!/bin/sh\n# the docker-compose overrides the smee container's ENTRYPOINT\n# with this script so it's a little easier to debug things\n#\n# configuration environment variables are provided by docker-compose\n\n# for example, to see the DHCP packets coming from the DHCP client\n# container, uncomment these.\n# apk update && apk add --no-cache tcpdump\n# tcpdump -nvvei eth0 port 67 or port 68 &\n# or just apk add tcpdump then run this in another terminal:\n#     docker exec -ti smee_smee_1 tcpdump -nvvei eth0 port 67 or port 68\n\n# start smee and explicitly bind DHCP to broadcast address otherwise\n# smee will start up fine but not see the DHCP requests\n# TODO: probably move smee to just use the envvars for otel\n/usr/bin/smee &\n\nsleep 100000\n"
  },
  {
    "path": "test/test-smee.sh",
    "content": "#!/bin/sh\n# shellcheck shell=dash disable=SC1091,SC2154\n\n# useful for debugging sometimes\n# tcpdump -ni eth0 &\n# alternatively, only show DHCP and pretty print the packets\n# tcpdump -nvvei eth0 port 67 or port 68 &\n\nsleep_at_start=3\necho \"starting DHCP in $sleep_at_start seconds\"\nsleep $sleep_at_start\n\n# busybox udhcpc will happily set arbitrary DHCP options and is easy\n# to configure with a custom setup script to call on DHCPOFFER\n#\n# dummy setup script for -s is copied in by Dockerfile\n# -q tells udhcpc to exit after getting a lease, otherwise it will keep generating new traces\n# opt60 (-V PXEClient) pretend to be an Intel PXE client. required to be noticed by smee\n# opt93 (-x 0x5d) set to 0 for \"Intel x86PC\" platform, required by smee\n# opt94 (-x 0x5e) set to 0 for \"UNDI\" firmware type, required by smee\n# opt97 (-x 0x61) sets the client guid (https://datatracker.ietf.org/doc/html/rfc4578#section-2.3)\n#              first 8 octets should be zeroes to make smee happy (Intel PXE does this)\n#              ID: 4a525bd43517df7f8b4799c18d (randomly generated and hard-coded here)\nbusybox udhcpc \\\n\t-q \\\n\t-s /busybox-udhcpc-script.sh \\\n\t-V PXEClient \\\n\t-x 0x5d:0000 \\\n\t-x 0x5e:0000 \\\n\t-x 0x61:000000004a525bd43517df7f8b4799c18d\n\n# set boot_file variable ahead of sourcing dhcpoffer-vars.sh to please the linter\nboot_file=\"\"\n\n# the busybox script writes the DHCP variables to /tmp/dhcpoffer-vars.sh\n# shellcheck disable=SC1091\n. /tmp/dhcpoffer-vars.sh\n\n# smee sets 2 values in option 43, check out dhcp/pxe.go\n# these can come in out of order so we have to look for the traceparent's\n# id and length which is always 0x451a\n# busybox udhcpc helpfully returns options in hex\n# option43 ordering is not guaranteed, at least not in this implementation\n. extract-traceparent-from-opt43.sh     # load a function to do the parsing\nextract_traceparent_from_opt43 \"$opt43\" # parse the value, exports TRACEPARENT\necho \"got traceparent $TRACEPARENT from opt43 value $opt43\"\n# write it to the shell profile.d for easy loading\necho \"export TRACEPARENT=$TRACEPARENT\" >/etc/profile.d/smee-traceparent.sh\n\n# fetch / from the server with the traceparent set\ntp_header=\"Traceparent: $TRACEPARENT\"\ncurl -H \"$tp_header\" http://192.168.99.42/auto.ipxe\n# TODO: test opportunity here: validate the returned traceparent matches the one in boot_file\n\n# boot_file is set by the DHCP envvars\n# OTEL in Smee is enabled by default.\ntftp 192.168.99.42 -c get \"${boot_file}\"\n\n# sleep a long time so you can enter the container with\n# docker exec -ti smee_client_1 /bin/sh\nsleep 30000\n"
  }
]