[
  {
    "path": ".dockerignore",
    "content": "*\n\n!cmd\n!internal\n!go.mod\n!LICENSE\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference\n\nversion: 2\nupdates:\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"gomod\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "content": "# Dependency Review Action\n#\n# This Action will scan dependency manifest files that change as part of a Pull Request,\n# surfacing known-vulnerable versions of the packages declared or updated in the PR.\n# Once installed, if the workflow run is marked as required,\n# PRs introducing known-vulnerable packages will be blocked from merging.\n#\n# Source repository: https://github.com/actions/dependency-review-action\nname: 'Dependency Review'\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden the runner (Audit all outbound calls)\n        uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1\n        with:\n          egress-policy: audit\n\n      - name: 'Checkout Repository'\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: 'Dependency Review'\n        uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0\n"
  },
  {
    "path": ".github/workflows/docker-image-release.yaml",
    "content": "name: Build and Publish Release\npermissions:\n  contents: read\n  packages: write\n\non:\n   push:\n     tags: ['*']\n\njobs:\n\n  build:\n\n    runs-on: ubuntu-latest\n    env:\n      GO111MODULE: on\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: '1.26'\n\n      - name: Run Gosec Security Scanner\n        run: |\n          go install github.com/securego/gosec/v2/cmd/gosec@v2.23.0\n          gosec ./...\n\n      - name: Run Go tests\n        run: go test ./...\n\n      - name: Extract tag name\n        id: get_tag\n        run: echo \"VERSION=${GITHUB_REF#refs/tags/}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Install Cosign\n        uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1\n        with:\n          cosign-release: 'v2.6.1'\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push Docker Hub image\n        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n        id: push-dockerhub\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm/v7,linux/arm64\n          push: true\n          build-args: VERSION=${{ steps.get_tag.outputs.VERSION }}\n          tags: |\n            docker.io/wollomatic/socket-proxy:${{ steps.get_tag.outputs.VERSION }}\n            docker.io/wollomatic/socket-proxy:1\n\n      - name: Sign Docker Hub image\n        run: cosign sign --yes --recursive --key env://COSIGN_PRIVATE_KEY docker.io/wollomatic/socket-proxy:${{ steps.get_tag.outputs.VERSION }}@${{ steps.push-dockerhub.outputs.digest }}\n        env:\n          COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}\n          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}\n\n      - name: Build and push GHCR image\n        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n        id: push-ghcr\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm/v7,linux/arm64\n          push: true\n          build-args: VERSION=${{ steps.get_tag.outputs.VERSION }}\n          tags: |\n            ghcr.io/wollomatic/socket-proxy:${{ steps.get_tag.outputs.VERSION }}\n            ghcr.io/wollomatic/socket-proxy:1\n\n      - name: Sign GHCR image\n        run: cosign sign --yes --recursive --key env://COSIGN_PRIVATE_KEY ghcr.io/wollomatic/socket-proxy:${{ steps.get_tag.outputs.VERSION }}@${{ steps.push-ghcr.outputs.digest }}\n        env:\n          COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}\n          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}\n"
  },
  {
    "path": ".github/workflows/docker-image-testing.yaml",
    "content": "name: Build and Publish Testing\npermissions:\n  contents: read\n  packages: write\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - develop\n\njobs:\n\n  build:\n\n    runs-on: ubuntu-latest\n    env:\n      GO111MODULE: on\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: '1.26'\n\n      - name: Run Gosec Security Scanner\n        run: |\n          go install github.com/securego/gosec/v2/cmd/gosec@v2.23.0\n          gosec ./...\n\n      - name: Run Go tests\n        run: go test ./...\n\n#      - name: Install Cosign\n#        uses: sigstore/cosign-installer@v3.10.0\n#        with:\n#          cosign-release: 'v2.6.0'\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push image to Docker Hub and GHCR\n        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n        id: push-all\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm/v7,linux/arm64\n          push: true\n          build-args: VERSION=testing-${{ github.sha }}\n          tags: |\n            docker.io/wollomatic/socket-proxy:testing\n            docker.io/wollomatic/socket-proxy:testing-${{ github.sha }}\n            ghcr.io/wollomatic/socket-proxy:testing\n            ghcr.io/wollomatic/socket-proxy:testing-${{ github.sha }}\n\n#      - name: Build and push Docker Hub image\n#        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n#        id: push-dockerhub\n#        with:\n#          context: .\n#          platforms: linux/amd64,linux/arm/v7,linux/arm64\n#          push: true\n#          build-args: VERSION=testing-${{ github.sha }}\n#          tags: |\n#            docker.io/wollomatic/socket-proxy:testing\n#            docker.io/wollomatic/socket-proxy:testing-${{ github.sha }}\n#\n#      - name: Sign Docker Hub image\n#        run: cosign sign --yes --recursive --key env://COSIGN_PRIVATE_KEY docker.io/wollomatic/socket-proxy:testing-${{ github.sha }}@${{ steps.push-dockerhub.outputs.digest }}\n#        env:\n#          COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}\n#          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}\n#\n#      - name: Build and push GHCR image\n#        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n#        id: push-ghcr\n#        with:\n#          context: .\n#          platforms: linux/amd64,linux/arm/v7,linux/arm64\n#          push: true\n#          build-args: VERSION=testing-${{ github.sha }}\n#          tags: |\n#            ghcr.io/wollomatic/socket-proxy:testing\n#            ghcr.io/wollomatic/socket-proxy:testing-${{ github.sha }}\n#\n#      - name: Sign GHCR image\n#        run: cosign sign --yes --recursive --key env://COSIGN_PRIVATE_KEY ghcr.io/wollomatic/socket-proxy:testing-${{ github.sha }}@${{ steps.push-ghcr.outputs.digest }}\n#        env:\n#          COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}\n#          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\n# JetBrains IDEA\n.idea\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1\nFROM --platform=$BUILDPLATFORM golang:1.26.2-alpine3.23@sha256:f85330846cde1e57ca9ec309382da3b8e6ae3ab943d2739500e08c86393a21b1 AS build\nWORKDIR /application\nCOPY . ./\nARG TARGETOS\nARG TARGETARCH\nARG VERSION\nRUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \\\n    go build -tags=netgo -gcflags=all=-d=checkptr -ldflags=\"-w -s -X 'main.version=${VERSION}'\" -trimpath \\\n    -o / ./...\n\nFROM scratch\nLABEL org.opencontainers.image.source=https://github.com/wollomatic/socket-proxy \\\n      org.opencontainers.image.description=\"A lightweight and secure unix socket proxy\" \\\n      org.opencontainers.image.licenses=MIT\nUSER 65534:65534\nVOLUME /var/run/docker.sock\nEXPOSE 2375\nENTRYPOINT [\"/socket-proxy\"]\nCOPY --from=build ./healthcheck ./socket-proxy /\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Wolfgang Ellsässer (wollomatic)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---\nParts of this project, specifically the file cmd/socket-proxy/bindmount.go and\nthe files in the internal/docker and internal/go-connections folders,\ncontain source code licensed under the Apache License 2.0. See the comments\nin the applicable files for details.\nThe rest of the project is licensed under the MIT License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        https://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   Copyright 2013-2018 Docker, 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       https://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."
  },
  {
    "path": "README.md",
    "content": "# socket-proxy\n\n## Latest image\n- `wollomatic/socket-proxy:1.12.0` / `ghcr.io/wollomatic/socket-proxy:1.12.0`\n- `wollomatic/socket-proxy:1` / `ghcr.io/wollomatic/socket-proxy:1`\n\n> [!IMPORTANT]\n>## Usage with Traefik >= 2.11.31 / >= 3.6.1\n>Due to a change in how Traefik retrieves the Docker API version (traefik/traefik#12256), the Socket-Proxy configuration for Traefik must be updated to allow `HEAD` requests to `/_ping`:\n>\n>      - '-allowHEAD=/_ping'\n>\n>Otherwise, Traefik would fall back to API version 1.51, which would break the Docker provider on older Docker versions.\n\n## About\n`socket-proxy` is a lightweight, secure-by-default unix socket proxy. Although it was created to proxy the docker socket to Traefik, it can also be used for other purposes.\nIt is heavily inspired by [tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy).\n\nAs an additional benefit, socket-proxy can be used to examine the API calls of the client application.\n\nThe advantage over other solutions is the very slim container image (from-scratch-image) without any external dependencies (no OS, no packages, just the Go standard library).\nIt is designed with security in mind, so there are secure defaults and an additional security layer (IP address-based access control) compared to most other solutions.\n\nThe allowlist is configured for each HTTP method separately using the Go regexp syntax, allowing fine-grained control over the allowed HTTP methods. In bridge network mode, each container that uses socket-proxy can be configured with its own allowlist.\n\nThe source code is available on [GitHub: wollomatic/socket-proxy](https://github.com/wollomatic/socket-proxy)\n\n> [!NOTE]\n> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.  \n\n## Getting Started\n\nSome examples can be found in the [wiki](https://github.com/wollomatic/socket-proxy/wiki) and in the `examples` directory of the repo.\n\n### Warning\n\nYou should know what you are doing. Never expose socket-proxy to a public network. It is meant to be used in a secure environment only.\n\n### Installing\n\nThe container image is available on [Docker Hub (wollomatic/socket-proxy)](https://hub.docker.com/r/wollomatic/socket-proxy) \nand on the [GitHub Container Registry (ghcr.io/wollomatic/socket-proxy)](https://github.com/wollomatic/socket-proxy/pkgs/container/socket-proxy).\n\nTo pin one specific version, use the version tag (for example, `wollomatic/socket-proxy:1.11.0` or `ghcr.io/wollomatic/socket-proxy:1.11.0`).\nTo always use the most recent version, use the `1` tag (`wollomatic/socket-proxy:1` or `ghcr.io/wollomatic/socket-proxy:1`). This tag will be valid as long as there is no breaking change in the deployment.\n\nThere may be an additional docker image with the `testing`-tag. This image is only for testing. Likely, documentation for the `testing` image could only be found in the GitHub commit messages. It is not recommended to use the `testing` image in production.\n\nEvery socket-proxy release image is signed with Cosign. The public key is available on [GitHub: wollomatic/socket-proxy/main/cosign.pub](https://raw.githubusercontent.com/wollomatic/socket-proxy/main/cosign.pub) and [https://wollomatic.de/socket-proxy/cosign.pub](https://wollomatic.de/socket-proxy/cosign.pub). For more information, please refer to the [Security Policy](https://github.com/wollomatic/socket-proxy/blob/main/SECURITY.md).\nAs of version 1.6, all multi-arch images are signed.\n\n### Allowing access\n\nBecause of the secure-by-default design, you need to allow every access explicitly.\n\nThis is meant to be an additional layer of security. It does not replace other security measures, such as firewalls, network segmentation, etc. Do not expose socket-proxy to a public network.\n\n#### Setting up the TCP listener\n\nSocket-proxy listens per default only on `127.0.0.1`. Depending on what you need, you may want to set another listener address with the `-listenip` parameter. In almost every use case, `-listenip=0.0.0.0` will be the correct configuration when using socket-proxy in a docker image.\n\n#### Using a unix socket instead of a TCP listener\n\nIf you want to proxy/filter the unix socket to a new unix socket instead to a TCP listener,\nyou need to set the `-proxysocketendpoint` parameter or the `SP_PROXYSOCKETENDPOINT` env variable to the socket path of the new unix socket.\nThis will also disable the TCP listener.\n\nFor example `-proxysocketendpoint=/tmp/filtered-socket.sock`\n\n> [!NOTE]\n> Versions prior to 1.10.0 of socket-proxy set the default file permissions of the Unix socket to 0400, instead of 0600 as stated in the documentation.\n\n#### Setting up the IP address or hostname allowlist\n\nPer default, only `127.0.0.1/32` is allowed to connect to socket-proxy. You may want to set another allowlist with the `-allowfrom` parameter, depending on your needs.\n\nAlternatively, not only IP networks but also hostnames can be configured. So it is now possible to explicitly allow one or more specific hostnames to connect to the proxy, for example, `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`.\n\nUsing the hostname is an easy-to-configure way to have more security. Access to the socket proxy will not even be permitted from the host system.\n\n#### Setting up the allowlist for requests\n\nYou must set up regular expressions for each HTTP method the client application needs access to.\n\nThe name of a parameter should be \"-allow\", followed by the HTTP method name (for example, `-allowGET`). The request will be allowed if that parameter is set and the incoming request matches the method and path matching the regexp. If unset, the corresponding HTTP method is disallowed.\n\nIt is also possible to configure the allowlist via environment variables. The variables are called \"SP_ALLOW_\", followed by the HTTP method (for example, `SP_ALLOW_GET`).\n\nIf both command-line parameter and environment variable are configured for a particular HTTP method, the environment variable is ignored.\n\nUse Go's regexp syntax to create the patterns for these parameters. To avoid insecure configurations, `^` and `$` are added automatically to the start and end of the pattern. Note: invalid regexp results in program termination.\n\nExamples (command-line):\n+ `'-allowGET=/v1\\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.\n+ `'-allowHEAD=.*'` allows all HEAD requests.\n+ `'-allowGET=/version -allowGET=/_ping'` supports using `-allowGET` multiple times\n\nExamples (env variables):\n+ `'SP_ALLOW_GET=\"/v1\\..{1,2}/(version|containers/.*|events.*)\"'` could be used for allowing access to the docker socket for Traefik v2.\n+ `'SP_ALLOW_HEAD=\".*\"'` allows all HEAD requests.\n+ `'SP_ALLOW_GET=\"/version\" SP_ALLOW_GET_2=\"/_ping\"'` supports multiple `SP_ALLOW_GET` entries\n\nFor more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).\n\nAn excellent online regexp tester is [regex101.com](https://regex101.com/).\n\nTo determine which HTTP requests your client application uses, you could switch socket-proxy to debug log level and look at the log output while allowing all requests in a secure environment.\n\n> [!NOTE]\n> Starting with version 1.12.0, the socket-proxy supports using multiple -allow* entries in params, environment, or docker labels.\n\n#### Setting up bind mount restrictions\n\nBy default, socket-proxy does not restrict bind mounts. If you want to add an additional layer of security by restricting which directories can be used as bind mount sources, you can use the `-allowbindmountfrom` parameter or the `SP_ALLOWBINDMOUNTFROM` environment variable.\n\nWhen configured, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. Multiple directories can be specified separated by commas.\n\nFor example:\n+ `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories like `/home/user/data` or `/var/log/app`\n+ `SP_ALLOWBINDMOUNTFROM=\"/app/data,/tmp\"` allows bind mounts from `/app/data` and `/tmp` directories\n\nBind mount restrictions are applied to relevant Docker API endpoints and work with both legacy bind mount syntax (`-v /host/path:/container/path`) and modern mount syntax.\n\n**Note**: This feature only restricts bind mounts. Other mount types (volumes, tmpfs, etc.) are not affected by this restriction.\n\n#### Setting up per-container allowlists\n\nAllowlists for both requests and bind mount restrictions can be specified for particular containers. To do this:\n\n1. Set `-proxycontainername` or the environment variable `SP_PROXYCONTAINERNAME` to the name of the socket proxy container.\n2. Make sure that each container that will use the socket proxy is in a Docker network that the socket proxy container is also in.\n3. Use the same regex syntax for request allowlists and for bind mount restrictions that were discussed earlier, but for labels on each container that will use the socket proxy. Each label name will have the prefix of `socket-proxy.allow.`, with `socket-proxy.allow.bindmountfrom` for bind mount restrictions. For example:\n\n```yaml\nservices:\n  traefik:\n    # [...] see github.com/wollomatic/traefik-hardened for a full example\n    networks:\n      - traefik-servicenet # this is the common traefik network\n      - docker-proxynet    # this should be only restricted to traefik and socket-proxy\n    labels:\n      - 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy\n      - 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy\n      - 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy\n```\n\nWhen this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.\n\n### Container health check\n\nHealth checks are disabled by default. As the socket-proxy container may not be exposed to a public network, a separate health check binary is included in the container image. To activate the health check, the `-allowhealthcheck` parameter or the environment variable `SP_ALLOWHEALTHCHECK=true` must be set. Then, a health check is possible for example with the following docker-compose snippet:\n\n``` compose.yaml\n# [...]\n    healthcheck:\n      test: [\"CMD\", \"./healthcheck\"]\n      interval: 10s\n      timeout: 5s\n      retries: 2\n# [...]\n```\n### Socket watchdog\n\nIn certain circumstances (for example, after a Docker engine update), the socket connection may break, causing the client application to fail. To prevent this, the socket-proxy can be configured to check the socket availability at regular intervals. If the socket is not available, the socket-proxy will be stopped so the container orchestrator can restart it. This feature is disabled by default. To enable it, set the `-watchdoginterval` parameter (or `SP_WATCHDOGINTERVAL` env variable) to the desired interval in seconds and set the `-stoponwatchdog` parameter (or `SP_STOPONWATCHDOG=true`). If `-stoponwatchdog`is not set, the watchdog will only log an error message and continue to run (the problem would still exist in that case).\n\n### Example for proxying the docker socket to Traefik\n\nYou need to know how to install Traefik in this environment. See [wollomatic/traefik2-hardened](https://github.com/wollomatic/traefik2-hardened) for an example.\n\nThe image can be deployed with docker compose:\n\n``` compose.yaml\nservices:\n  dockerproxy:\n    image: wollomatic/socket-proxy:<<version>> # choose most recent image\n    restart: unless-stopped\n    user: \"65534:<<your docker group id>>\"\n    mem_limit: 64M\n    read_only: true\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges\n    command:\n      - '-loglevel=info'\n      - '-listenip=0.0.0.0'\n      - '-allowfrom=traefik' # allow only hostname \"traefik\" to connect\n      - '-allowGET=/v1\\..{1,2}/(version|containers/.*|events.*)'\n      - '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories\n      - '-watchdoginterval=3600' # check once per hour for socket availability\n      - '-stoponwatchdog' # halt program on error and let compose restart it\n      - '-shutdowngracetime=5' # wait 5 seconds before shutting down\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    networks:\n      - docker-proxynet    # NEVER EVER expose this to the public internet!\n                           # this is a private network only for traefik and socket-proxy\n                           # it is not the same as the traefik-servicenet\n\n  traefik:\n    # [...] see github.com/wollomatic/traefik-hardened for a full example\n    depends_on:\n      - dockerproxy\n    networks:\n      - traefik-servicenet # this is the common traefik network\n      - docker-proxynet    # this should be only restricted to traefik and socket-proxy\n  \nnetworks:\n  traefik-servicenet:\n    external: true\n  docker-proxynet:\n    driver: bridge\n    internal: true\n```\n\n### Examining the API calls of the client application\n\nTo log the API calls of the client application, set the log level to `DEBUG` and allow all requests. Then, you can examine the log output to determine which requests the client application makes. Allowing all requests can be done by setting the following parameters:\n```\n- '-loglevel=debug'\n- '-allowGET=.*'\n- '-allowHEAD=.*'\n- '-allowPOST=.*'\n- '-allowPUT=.*'\n- '-allowPATCH=.*'\n- '-allowDELETE=.*'\n- '-allowCONNECT=.*'\n- '-allowTRACE=.*'\n- '-allowOPTIONS=.*'\n```\n\n### all parameters and environment variables\n\nsocket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.\n\n| Parameter                      | Environment Variable             | Default Value          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `-allowfrom`                   | `SP_ALLOWFROM`                   | `127.0.0.1/32`         | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |\n| `-allowbindmountfrom`          | `SP_ALLOWBINDMOUNTFROM`          | (not set)              | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories.                                                                                                                                                                                                                                 |\n| `-allowhealthcheck`            | `SP_ALLOWHEALTHCHECK`            | (not set/false)        | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |\n| `-listenip`                    | `SP_LISTENIP`                    | `127.0.0.1`            | Specifies the IP address the server will bind on. Default is only the internal network.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |\n| `-logjson`                     | `SP_LOGJSON`                     | (not set/false)        | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |\n| `-loglevel`                    | `SP_LOGLEVEL`                    | `INFO`                 | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| `-proxyport`                   | `SP_PROXYPORT`                   | `2375`                 | Defines the TCP port the proxy listens to.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| `-shutdowngracetime`           | `SP_SHUTDOWNGRACETIME`           | `10`                   | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server)                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |\n| `-socketpath`                  | `SP_SOCKETPATH`                  | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |\n| `-stoponwatchdog`              | `SP_STOPONWATCHDOG`              | (not set/false)        | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |\n| `-watchdoginterval`            | `SP_WATCHDOGINTERVAL`            | `0`                    | Check for socket availability every x seconds (disable checks, if not set or value is 0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| `-proxysocketendpoint`         | `SP_PROXYSOCKETENDPOINT`         | (not set)              | Proxy to the given unix socket instead of a TCP port                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |\n| `-proxysocketendpointfilemode` | `SP_PROXYSOCKETENDPOINTFILEMODE` | `0600`                 | Explicitly set the file mode for the filtered unix socket endpoint (only useful with `-proxysocketendpoint`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |\n| `-proxycontainername`          | `SP_PROXYCONTAINERNAME`          | (not set)              | Provides the name of the socket proxy container to enable per-container allowlists specified by Docker container labels (not available with `-proxysocketendpoint`)                                                                                                                                                                                                                                                                                                                                                                                                                                                               |\n\n### Changelog\n\n1.0 - initial release\n\n1.1 - add hostname support for `-allowfrom` parameter\n\n1.2 - reformat logging of allowlist on program start\n\n1.3 - allow multiple, comma-separated hostnames in `-allowfrom` parameter (thanks [@ildyria](https://github.com/ildyria))\n\n1.4 - allow configuration from env variables\n\n1.5 - allow unix socket as proxied/filtered endpoint\n\n1.6 - Cosign: sign a multi-arch container image AND all referenced, discrete images. Image is also available on GHCR.\n\n1.7 - also allow comma-separated CIDRs in `-allowfrom` (not only hostnames as in versions > 1.3)\n\n1.8 - add optional bind mount restrictions (thanks [@powerman](https://github.com/powerman), [@C4tWithShell](https://github.com/C4tWithShell))\n\n1.9 - add IPv6 support to `-listenip` (thanks [@op3](https://github.com/op3))\n\n1.10 - fix socket file mode (thanks [@amanda-wee](https://github.com/amanda-wee)), optimize build actions (thanks [@reneleonhardt](https://github.com/reneleonhardt))\n\n1.11 - add per-container allowlists specified by Docker container labels (thanks [@amanda-wee](https://github.com/amanda-wee))\n\n1.12 - support use of allow* multiple times in env, flag and docker labels (thanks [@qianlongzt](https://github.com/qianlongzt))\n\n## License\n\nParts of this project, specifically the file `cmd/socket-proxy/bindmount.go` and\nthe files in the `internal/docker` and `internal/go-connections` folders,\ncontain source code licensed under the Apache License 2.0. See the comments\nin the applicable files for details.\nThe rest of the project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgements\n\n+ [Chris Wiegman: Protecting Your Docker Socket With Traefik 2](https://chriswiegman.com/2019/11/protecting-your-docker-socket-with-traefik-2/) [@ChrisWiegman](https://github.com/ChrisWiegman)\n+ [tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)\n+ [@justsomescripts](https://github.com/justsomescripts) fix parsing environment variable to configure unix socket\n\n## Alternatives\n\n+ [hectorm/cetusguard](https://github.com/hectorm/cetusguard)\n+ [11notes/docker-socket-proxy](https://github.com/11notes/docker-socket-proxy)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n# Security Policy\n\n## Supported Versions\n\nAs no breaking changes to existing features are planned, only the most recent version is supported.\n\n## Signed Docker Images\n\nThe docker images are signed with cosign. The public key is available in the repository, on [https://wollomatic.de/socket-proxy/cosign.pub](https://wollomatic.de/socket-proxy/cosign.pub) and here:\n```\n- - - -----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYdXlfRbkO6KqPU7Khn1mSjbOIaD3\num421A0NeT1wi840iWNp6MVKyj3tpnAyaQcLgd5/22O+eEHY+5+EHwB+eA==\n- - - -----END PUBLIC KEY-----\n```\n\nThe signature is stored at Docker hub as well. For more information about cosign, see [https://github.com/sigstore/cosign#readme](https://github.com/sigstore/cosign#readme).\n\n## Reporting a Vulnerability\n\nPlease report vulnerabilities to git2026(at)wollomatic.dev\n\nFeel free to encrypt the message if you like:\n[Download PGP public key 9123F130](https://wollomatic.dev/gpg/9123F130.gpg.asc)\n```\n- - - - -----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: Benutzer-ID:\t<security2025(at)wollomatic.de>\nComment: Gültig seit:\t30.12.2021 18:15\nComment: Gültig bis:\t31.12.2025 12:00\nComment: Typ:\t4.096-bit RSA\nComment: Fingerabdruck:\tD57424AC7C262F4B44F45B575586B7A4D15E6CA7\n\n\nmQINBGHN6UwBEADglyuMVQxNfZJ9RU/UA56sxdR/cgt9mNUUNzepQxYXhTJPBrPu\ngnMcy8oJOHla9wjgSz/RWqi/VN29asXYikortnL+iRzzDdCQDZS2ULCR0BBvNpoi\nHgyeSn3xowapCHY44ghekERU+Zv2Kbw6GiYdNhzCmpCt+Du8LxF/tyoUlyJY4uas\nDmdu6ZXp+5rRgXpYSWj2fgeRz15FDEWsHXFC2CuZZSGgcy4paVQrDFlpVDdlV0JX\nktFPDCwF3zcVGSElJjZGAzDDoPb30Mh/ui2NSBElF9iuZk6Rt0h7rVwTOCyJL76d\nJ2mBk5ldf//JRBUfxC5zHlDhAxmsWFSCuCgkK7lvyUYzlG0mBneYVQpjmOEPZscU\nPlNafwxMHBNIkO3B4Y9HWy5dbwAjey4X8gZRTJv4e9O9WoUx41Hdf/UIicCIvGWq\nDJ6Z8iWnqddX/nxb5mWhxb79Tj022wdMjVInn7bbOOwj6lERqsGqQYdEQgTPfMg+\nTswfXnFPwsOdXCw7NmfUAyRS9uam+ThDQbIgKjGgqn2+0pKtd4jPFv/woMN77CWo\no5ZBSd7pF1dTdkmAI0gSapAyjewEsExq73OicYbCIwfTUxvWFNyp2gHPXWFSfAgb\nYvo6GGnmFL6wFE6H9eVi824+pdYnYuE65xB8+3TUu6FKvToJgbjDaObcXwARAQAB\ntBpzZWN1cml0eTIwMjVAd29sbG9tYXRpYy5kZYkCWAQTAQgAQhYhBNV0JKx8Ji9L\nRPRbV1WGt6TRXmynBQJhzelMAhsDBQkHhxjkBQsJCAcCAyICAQYVCgkICwIEFgID\nAQIeBwIXgAAKCRBVhrek0V5sp8gFEACFgXwLwpVjEhAWwGF54MxFxyHNreJ8b0xa\n5OYG8UsSSW6L6SvWOjl+FV4L6OFgUAM22WosvbOfL3NMDt8RVv2RxQ6WcIBaxPq3\nesNm/O2bT/gDCdvqUo2J6hAbeIilTrYXA74cwggCovJN3aTf7P4ieggYPbMi9SoT\nEdGb3Q1TeaCPqEsutroTG0gqG6Ff+gZs6IHCYcpb7+gSomARoxD5Xlmu+rgPgmcT\nI5DERSt6iXiPAsGVaiPePm32aj8gRwLDHmuMEV3UYxdjffLBqM991ZyuYVUmVPQp\nAkMRz3sQ5lZOoV503mDR02761yQxCf8bOxlTpuuvRGV86hlKvce+fj9yvegUBOBJ\nbwtquKhVfxZYkvyL+Nt4jjB8rH8M9UIZjMScaA4NoWjEzBDh6MdRjMMZvB/UpROA\nmPsN5YgzQGnEH04bP07QYfH1cGeIUx6YvT8nwnKj+aMWbHy/hyxPF4RCzveCkC+X\nKlMPTj3/oCw1pf9AigJ2PBZrg9SW4Wtr5BoLEqdVr+Wxm2Um75GPji5C+ZbimDXL\nD3gTxPi81guhkPi51gucrqqhzAHIoRiPAC0rqbO+PPKejJzDfLgrRhWD8hhOAbLK\nwx0eWhMKMU/lZyH7RSCpnOd1lU46pSUfosZBgO0c8DGW54AAFrmc4lyx/uJ2KDDU\nGgliSPDEnLkCDQRhzelMARAAn/EtaUC6/O79fiYWILeGbK8YqBu5u1HhRn4V6ztH\nPW5n4oz1GUVnfUX+/o95jGdCxfrrC+lCF6D9Utvk7vGNMxKfdyM5CFzUeYvgZ6OP\nm+0s9dZRahRh/01jgRVbkojoH0nAeWhLGRjQ20ElwJak3c/Moe+3EjQUrzm7hHuL\nwE7XPiBhYsR3mqq4GgwrXOmm7tDy8ccFVs5kq/8zneaCwr3OMz6aa94zIxvSIKf5\nvMrTKNvDnc2BLIj2IwUSd7OOD0tnBb610pr+rDX/NHA5y03Vw2DbD4uS7TBCYwjm\nY/2YvuRcWu+3jOMteQrDyNzSIKN0V2tgiYt249IzqAosKiWfOazZFDwloglCQSkf\nFeJYZPYfmRTi6slxZvaEWJGMIElBm/yl1fDD96YGqI+CWn45FBzO4hsmXxeOkPqJ\nNInU5vQiV9aFSOaQs9Zo/aw33P4UwqJWLNHJaf++kITuLBU6994wproNWmtxLK2v\nlPet2BWJRrcRgV00cLXyOVwcfG7x3I5d1ohMhQa7NyJTi9XTwWBdy/cd32J8FSPj\n6L0Oyvx5p0+wy9B6exBXNcaQKbqrtetmJ0XG2CBew1CZGr5ARULeTnitB2ma2rTr\nBmiQDWM6kpKgfBn1Ek8XXlj8wEvLuKN+TEADjD9CnRsy61yofOszfI/882hkKGkt\n/bEAEQEAAYkCPAQYAQgAJhYhBNV0JKx8Ji9LRPRbV1WGt6TRXmynBQJhzelMAhsM\nBQkHhxjkAAoJEFWGt6TRXmynEKcP/2v7ds6b9rKD/GMtZgElXYDNbDYAcUoOR9UG\nDf5o7tcZ2gao/dald5YASaBUs1cA1BJG7/cORzyWuwEpzRsNjI2E/tpwN3Ki2B2/\n2oI4rZaxiuh9h+Z46umo0gLlqF9AE+MFb1t+oGoMzkioTo2pC6ce9P68MRP63mGs\nPYFe1ghH56N8giTGHQqafzNHEVr9PGMXgPaQr5C9tWwd37g3BZPY0jQHf3kRa1r3\nAvczeBUnEIkBFZA+CGM1EaE77TlcY7Sh7H035P5xe1y1ehrAtP5Nb4e82WLOgV0K\n4XoiHm+1uOF1kCT1pT5q5l9H9QYvLUJ7+XpGuIt25GtQcd55hU3NFMiAD13gAOPg\n7zO5pz++4jG8x7osDAPjquKoEsTDH2qmWEcF+/5tOit/byqzB/wTCZIxAFNLKdUn\nVihMY7iTlDZMrnXOKDmuyLIsV3TWzddUDv9DOTRH2kdSYdIzMA2gYiLHIb+mb9T/\nCzfsxB8x4pEjtvrWK5vEH5G9tSBfSBTbVJI/mwVUBftkBuJpCrTUknzJJhD6gW4s\nAGx0J/IYKvNwbYErCoOsqM78lZZ20hvKwDCW1jNEZibqiL98yhQhoEymTu9FHShR\nWrjWE3RoPNCEPKwCVSh08Y/bVcUyfkDNKkN3l8lT34TIEUOkzdXD2JLL6cogLpn2\nQ/PCqEw9\n=6UYI\n- - - - -----END PGP PUBLIC KEY BLOCK-----\n```\n-----BEGIN PGP SIGNATURE-----\n\niHUEARYKAB0WIQQX7u5MQzQWc4kIq7Te/gx2oe2rbQUCaaybJwAKCRDe/gx2oe2r\nbYhkAQCRbh/Bn1+/7sFlP2jU9BKfNUkSy/Ss5PA9DpYlHu4SaAD/RJXH70xYb7jW\nt90C39ppKOCFyshcaTWPGWmE5treYQo=\n=er7Z\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "cmd/healthcheck/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\n// main does a health check against the socket-proxy server\n// if the health check fails, the program exits with a non-zero exit code and logs an error\n// if the health check succeeds, the program exits with a zero exit code\n// socket-proxy must be started with the -allowhealthcheck flag\nfunc main() {\n\tresp, err := http.Head(\"http://localhost:55555/health\")\n\tif err != nil {\n\t\tlog.Fatal(\"error doing health check: \", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tlog.Fatal(\"health check failed, got status: \", resp.StatusCode)\n\t}\n}\n"
  },
  {
    "path": "cmd/socket-proxy/bindmount.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n/*\nThe subsets of github.com/docker/docker/api/types/ are licensed under a Apache 2.0 license.\n\nNOTICE regarding this file only:\n\nDocker\nCopyright 2012-2017 Docker, Inc.\n\nThis product includes software developed at Docker, Inc. (https://www.docker.com).\n\nThis product contains software (https://github.com/creack/pty) developed\nby Keith Rarick, licensed under the MIT License.\n\nThe following is courtesy of our legal counsel:\n\n\nUse and transfer of Docker may be subject to certain restrictions by the\nUnited States and other governments.\nIt is your responsibility to ensure that your use and/or transfer does not\nviolate applicable laws.\n\nFor more information, please see https://www.bis.doc.gov\n\nSee also https://www.apache.org/dev/crypto.html and/or seek legal counsel.\n*/\n\n// mountType is the subset of github.com/docker/docker/api/types/mount.Type.\ntype mountType string\n\nconst (\n\t// mountTypeBind is the type for mounting host dir.\n\tmountTypeBind mountType = \"bind\"\n)\n\ntype (\n\t// containerCreateRequest is the subset of github.com/docker/docker/api/types/container.CreateRequest.\n\tcontainerCreateRequest struct {\n\t\tHostConfig *containerHostConfig `json:\"HostConfig,omitempty\"`\n\t}\n\t// containerHostConfig is the subset of github.com/docker/docker/api/types/container.HostConfig.\n\tcontainerHostConfig struct {\n\t\tBinds  []string     // List of volume bindings for this container.\n\t\tMounts []mountMount `json:\",omitempty\"` // Mounts specs used by the container.\n\t}\n\t// swarmServiceSpec is the subset of github.com/docker/docker/api/types/swarm.ServiceSpec.\n\tswarmServiceSpec struct {\n\t\tTaskTemplate swarmTaskSpec `json:\",omitempty\"`\n\t}\n\t// swarmTaskSpec is the subset of github.com/docker/docker/api/types/swarm.TaskSpec.\n\tswarmTaskSpec struct {\n\t\tContainerSpec *swarmContainerSpec `json:\",omitempty\"`\n\t}\n\t// swarmContainerSpec is the subset of github.com/docker/docker/api/types/swarm.ContainerSpec.\n\tswarmContainerSpec struct {\n\t\tMounts []mountMount `json:\",omitempty\"`\n\t}\n\t// mountMount is the subset of github.com/docker/docker/api/types/mount.Mount.\n\tmountMount struct {\n\t\tType mountType `json:\",omitempty\"`\n\t\t// Source specifies the name of the mount. Depending on mount type, this\n\t\t// may be a volume name or a host path, or even ignored.\n\t\t// Source is not supported for tmpfs (must be an empty value)\n\t\tSource string `json:\",omitempty\"`\n\t\tTarget string `json:\",omitempty\"`\n\t}\n)\n\n// checkBindMountRestrictions checks if bind mounts in the request are allowed.\nfunc checkBindMountRestrictions(allowedBindMounts []string, r *http.Request) error {\n\t// Only check if bind mount restrictions are configured\n\tif len(allowedBindMounts) == 0 {\n\t\treturn nil\n\t}\n\n\tif r.Method != http.MethodPost {\n\t\treturn nil\n\t}\n\n\t// Check different API endpoints that can use bind mounts\n\tpathParts := strings.Split(r.URL.Path, \"/\")\n\tswitch {\n\tcase len(pathParts) >= 4 && pathParts[2] == \"containers\" && pathParts[3] == \"create\":\n\t\t// Container creation: /vX.xx/containers/create\n\t\treturn checkContainer(allowedBindMounts, r)\n\tcase len(pathParts) >= 5 && pathParts[2] == \"containers\" && pathParts[4] == \"update\":\n\t\t// Container update: /vX.xx/containers/{id}/update\n\t\treturn checkContainer(allowedBindMounts, r)\n\tcase len(pathParts) >= 4 && pathParts[2] == \"services\" && pathParts[3] == \"create\":\n\t\t// Service creation: /vX.xx/services/create\n\t\treturn checkService(allowedBindMounts, r)\n\tcase len(pathParts) >= 5 && pathParts[2] == \"services\" && pathParts[4] == \"update\":\n\t\t// Service update: /vX.xx/services/{id}/update\n\t\treturn checkService(allowedBindMounts, r)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// checkContainer checks bind mounts in container creation requests.\nfunc checkContainer(allowedBindMounts []string, r *http.Request) error {\n\tbody, err := readAndRestoreBody(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar req containerCreateRequest\n\tif err := json.Unmarshal(body, &req); err != nil {\n\t\tslog.Debug(\"failed to parse container request\", \"error\", err)\n\t\treturn nil // Don't block if we can't parse.\n\t}\n\n\treturn checkHostConfigBindMounts(allowedBindMounts, req.HostConfig)\n}\n\n// checkService checks bind mounts in service creation requests.\nfunc checkService(allowedBindMounts []string, r *http.Request) error {\n\tbody, err := readAndRestoreBody(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar req swarmServiceSpec\n\tif err := json.Unmarshal(body, &req); err != nil {\n\t\tslog.Debug(\"failed to parse service request\", \"error\", err)\n\t\treturn nil // Don't block if we can't parse.\n\t}\n\n\tif req.TaskTemplate.ContainerSpec == nil {\n\t\treturn nil // No container spec, nothing to check.\n\t}\n\treturn checkHostConfigBindMounts(\n\t\tallowedBindMounts,\n\t\t&containerHostConfig{\n\t\t\tMounts: req.TaskTemplate.ContainerSpec.Mounts,\n\t\t},\n\t)\n}\n\n// checkHostConfigBindMounts checks bind mounts in HostConfig.\nfunc checkHostConfigBindMounts(allowedBindMounts []string, hostConfig *containerHostConfig) error {\n\tif hostConfig == nil {\n\t\treturn nil // No HostConfig, nothing to check\n\t}\n\n\t// Check legacy Binds field\n\tfor _, bind := range hostConfig.Binds {\n\t\tif err := validateBindMount(allowedBindMounts, bind); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Check modern Mounts field\n\tfor _, mountItem := range hostConfig.Mounts {\n\t\tif mountItem.Type == mountTypeBind {\n\t\t\tif err := validateBindMountSource(allowedBindMounts, mountItem.Source); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// validateBindMount validates a bind mount string in the format \"source:target:options\".\nfunc validateBindMount(allowedBindMounts []string, bind string) error {\n\tparts := strings.Split(bind, \":\")\n\tif len(parts) < 2 {\n\t\treturn fmt.Errorf(\"invalid bind mount format: %s\", bind)\n\t}\n\treturn validateBindMountSource(allowedBindMounts, parts[0])\n}\n\n// validateBindMountSource checks if the source directory is allowed.\nfunc validateBindMountSource(allowedBindMounts []string, source string) error {\n\t// Skip if source is not an absolute path (i.e. bind mount).\n\tif !strings.HasPrefix(source, \"/\") {\n\t\treturn nil\n\t}\n\n\tsource = filepath.Clean(source) // Clean the path to resolve .. and . components.\n\tfor _, allowedDir := range allowedBindMounts {\n\t\tif allowedDir == \"/\" || source == allowedDir || strings.HasPrefix(source, allowedDir+\"/\") {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"bind mount source directory not allowed: %s\", source)\n}\n\n// readAndRestoreBody reads the request body and restores it for further processing.\nfunc readAndRestoreBody(r *http.Request) ([]byte, error) {\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read request body: %w\", err)\n\t}\n\n\t// Restore the body for further processing\n\tr.Body = io.NopCloser(bytes.NewBuffer(body))\n\treturn body, nil\n}\n"
  },
  {
    "path": "cmd/socket-proxy/bindmount_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"testing\"\n)\n\nfunc skipIfNotUnix(t *testing.T) {\n\tswitch runtime.GOOS {\n\tcase \"linux\", \"darwin\", \"freebsd\", \"openbsd\", \"netbsd\", \"dragonfly\", \"solaris\", \"aix\":\n\t\t// Supported Unix platforms\n\tdefault:\n\t\tt.Skip(\"skipping test: only runs on Unix-like systems\")\n\t}\n}\n\nfunc TestValidateBindMountSource(t *testing.T) {\n\tskipIfNotUnix(t)\n\n\tallowedBindMounts := []string{\"/home\", \"/var/log\"}\n\n\ttests := []struct {\n\t\tname       string\n\t\tsource     string\n\t\tshouldPass bool\n\t}{\n\t\t{\"exact match\", \"/home\", true},\n\t\t{\"subdirectory\", \"/home/user\", true},\n\t\t{\"deep subdirectory\", \"/home/user/data\", true},\n\t\t{\"not allowed\", \"/etc\", false},\n\t\t{\"empty source\", \"\", true},      // empty sources are skipped\n\t\t{\"relative path\", \"home\", true}, // relative paths are skipped\n\t\t{\"var log exact\", \"/var/log\", true},\n\t\t{\"var log subdir\", \"/var/log/app\", true},\n\t\t{\"similar but different\", \"/home2\", false},\n\t\t{\"prefix but not subdir\", \"/home2/user\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateBindMountSource(allowedBindMounts, tt.source)\n\t\t\tif tt.shouldPass && err != nil {\n\t\t\t\tt.Errorf(\"expected %s to pass, but got error: %v\", tt.source, err)\n\t\t\t}\n\t\t\tif !tt.shouldPass && err == nil {\n\t\t\t\tt.Errorf(\"expected %s to fail, but it passed\", tt.source)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsPathAllowed(t *testing.T) {\n\tskipIfNotUnix(t)\n\n\ttests := []struct {\n\t\tname       string\n\t\tpath       string\n\t\tallowedDir string\n\t\texpected   bool\n\t}{\n\t\t{\"exact match\", \"/home\", \"/home\", true},\n\t\t{\"subdirectory\", \"/home/user\", \"/home\", true},\n\t\t{\"deep subdirectory\", \"/home/user/data\", \"/home\", true},\n\t\t{\"not subdirectory\", \"/etc\", \"/home\", false},\n\t\t{\"similar prefix\", \"/home2\", \"/home\", false},\n\t\t{\"parent directory\", \"/\", \"/home\", false},\n\t\t{\"path traversal with ..\", \"/home/user/../..\", \"/home\", false},\n\t\t{\"path traversal to allowed\", \"/home/user/..\", \"/home\", true},\n\t\t{\"path traversal outside\", \"/home/../etc\", \"/home\", false},\n\t\t{\"complex path traversal\", \"/home/user/../../etc\", \"/home\", false},\n\t\t{\"path with dots in name\", \"/home/user.name\", \"/home\", true},\n\t\t{\"path with current dir\", \"/home/./user\", \"/home\", true},\n\t\t{\"root directory exact match\", \"/\", \"/\", true},\n\t\t{\"any path should be allowed when root is allowed\", \"/etc\", \"/\", true},\n\t\t{\"deep path should be allowed when root is allowed\", \"/var/log/app\", \"/\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateBindMountSource([]string{tt.allowedDir}, tt.path)\n\t\t\tif (err == nil) != tt.expected {\n\t\t\t\tt.Errorf(\"isPathAllowed(%s, %s) = %v, expected %v\", tt.path, tt.allowedDir, err, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateBindMount(t *testing.T) {\n\tskipIfNotUnix(t)\n\n\tallowedBindMounts := []string{\"/home\", \"/var/log\"}\n\n\ttests := []struct {\n\t\tname       string\n\t\tbind       string\n\t\tshouldPass bool\n\t}{\n\t\t{\"valid bind\", \"/home/user:/app\", true},\n\t\t{\"invalid format\", \"/home/user\", false},\n\t\t{\"not allowed source\", \"/etc:/app\", false},\n\t\t{\"allowed with options\", \"/home/user:/app:ro\", true},\n\t\t{\"var log bind\", \"/var/log:/logs:ro\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateBindMount(allowedBindMounts, tt.bind)\n\t\t\tif tt.shouldPass && err != nil {\n\t\t\t\tt.Errorf(\"expected %s to pass, but got error: %v\", tt.bind, err)\n\t\t\t}\n\t\t\tif !tt.shouldPass && err == nil {\n\t\t\t\tt.Errorf(\"expected %s to fail, but it passed\", tt.bind)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCheckBindMountRestrictions(t *testing.T) {\n\tskipIfNotUnix(t)\n\n\tallowedBindMounts := []string{\"/home\"}\n\n\ttests := []struct {\n\t\tname       string\n\t\tmethod     string\n\t\tpath       string\n\t\tbody       string\n\t\tshouldPass bool\n\t}{\n\t\t{\n\t\t\tname:       \"GET request should pass\",\n\t\t\tmethod:     \"GET\",\n\t\t\tpath:       \"/v1.40/containers/json\",\n\t\t\tbody:       \"\",\n\t\t\tshouldPass: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"POST to non-container endpoint should pass\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/images/create\",\n\t\t\tbody:       \"\",\n\t\t\tshouldPass: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"container create with allowed bind\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/containers/create\",\n\t\t\tbody:       `{\"HostConfig\":{\"Binds\":[\"/home/user:/app\"]}}`,\n\t\t\tshouldPass: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"container create with disallowed bind\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/containers/create\",\n\t\t\tbody:       `{\"HostConfig\":{\"Binds\":[\"/etc:/app\"]}}`,\n\t\t\tshouldPass: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"path traversal attack\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/containers/create\",\n\t\t\tbody:       `{\"HostConfig\":{\"Binds\":[\"/home/user/../../etc:/app\"]}}`,\n\t\t\tshouldPass: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"container create with no binds\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/containers/create\",\n\t\t\tbody:       `{\"HostConfig\":{}}`,\n\t\t\tshouldPass: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"container update with bind mount\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/containers/abc123/update\",\n\t\t\tbody:       `{\"HostConfig\":{\"Binds\":[\"/home/user:/app\"]}}`,\n\t\t\tshouldPass: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"service create with bind mount\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v1.40/services/create\",\n\t\t\tbody:       `{\"TaskTemplate\":{\"ContainerSpec\":{\"Mounts\":[{\"Type\":\"bind\",\"Source\":\"/etc\",\"Target\":\"/app\"}]}}}`,\n\t\t\tshouldPass: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"v2 API should work too\",\n\t\t\tmethod:     \"POST\",\n\t\t\tpath:       \"/v2.0/containers/create\",\n\t\t\tbody:       `{\"HostConfig\":{\"Binds\":[\"/etc:/app\"]}}`,\n\t\t\tshouldPass: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t\t\t}\n\n\t\t\terr = checkBindMountRestrictions(allowedBindMounts, req)\n\t\t\tif tt.shouldPass && err != nil {\n\t\t\t\tt.Errorf(\"expected request to pass, but got error: %v\", err)\n\t\t\t}\n\t\t\tif !tt.shouldPass && err == nil {\n\t\t\t\tt.Errorf(\"expected request to fail, but it passed\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/socket-proxy/checksocketconnection.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\nconst dialTimeout = 5 // timeout in seconds for the socket connection\n\n// checkSocketAvailability tries to connect to the socket and returns an error if it fails.\nfunc checkSocketAvailability(socketPath string) error {\n\tslog.Debug(\"checking socket availability\", \"origin\", \"checkSocketAvailability\")\n\tconn, err := net.DialTimeout(\"unix\", socketPath, dialTimeout*time.Second)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = conn.Close()\n\tif err != nil {\n\t\tslog.Error(\"error closing socket\", \"origin\", \"checkSocketAvailability\", \"error\", err)\n\t}\n\treturn nil\n}\n\n// startSocketWatchdog starts a watchdog that checks the socket availability every n seconds.\nfunc startSocketWatchdog(socketPath string, interval int64, stopOnWatchdog bool, exitChan chan int) {\n\tticker := time.NewTicker(time.Duration(interval) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tif err := checkSocketAvailability(socketPath); err != nil {\n\t\t\tslog.Error(\"socket is unavailable\", \"origin\", \"watchdog\", \"error\", err)\n\t\t\tif stopOnWatchdog {\n\t\t\t\tslog.Warn(\"stopping socket-proxy because of unavailable socket\", \"origin\", \"watchdog\")\n\t\t\t\texitChan <- 10\n\t\t\t}\n\t\t}\n\t}\n}\n\n// healthCheckServer starts a http server that listens on localhost:55555/health\n// and returns 200 if the socket is available, 503 otherwise.\nfunc healthCheckServer(socketPath string) {\n\thcMux := http.NewServeMux()\n\thcMux.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != http.MethodHead {\n\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\treturn\n\t\t}\n\t\terr := checkSocketAvailability(socketPath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"health check failed\", \"origin\", \"healthcheck\", \"error\", err)\n\t\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\thcSrv := &http.Server{\n\t\tAddr:              \"127.0.0.1:55555\",\n\t\tHandler:           hcMux,\n\t\tReadHeaderTimeout: 5 * time.Second,\n\t\tReadTimeout:       5 * time.Second,\n\t\tWriteTimeout:      5 * time.Second,\n\t}\n\n\tif err := hcSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\tslog.Error(\"healthcheck http server problem\", \"origin\", \"healthcheck\", \"error\", err)\n\t}\n}\n"
  },
  {
    "path": "cmd/socket-proxy/handlehttprequest.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/config\"\n)\n\n// handleHTTPRequest checks if the request is allowed and sends it to the proxy.\n// Otherwise, it returns a \"405 Method Not Allowed\" or a \"403 Forbidden\" error.\n// In case of an error, it returns a 500 Internal Server Error.\nfunc handleHTTPRequest(w http.ResponseWriter, r *http.Request) {\n\tallowList, ok := determineAllowList(r)\n\tif !ok {\n\t\tcommunicateBlockedRequest(w, r, \"forbidden IP\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\tallowed, exists := allowList.AllowedRequests[r.Method]\n\tif !exists { // method not in map -> not allowed\n\t\tcommunicateBlockedRequest(w, r, \"method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tif !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed\n\t\tcommunicateBlockedRequest(w, r, \"path not allowed\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\t// check bind mount restrictions\n\tif err := checkBindMountRestrictions(allowList.AllowedBindMounts, r); err != nil {\n\t\tcommunicateBlockedRequest(w, r, \"bind mount restriction: \"+err.Error(), http.StatusForbidden)\n\t\treturn\n\t}\n\n\t// finally, log and proxy the request\n\tslog.Debug(\"allowed request\", \"method\", r.Method, \"URL\", r.URL, \"client\", r.RemoteAddr) // #nosec G706 - structured logging (slog) safely encodes values\n\tsocketProxy.ServeHTTP(w, r)                                                             // #nosec G704 - Request target is always the specified socket\n}\n\nfunc matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {\n\tfor _, allowedURI := range allowedURIs {\n\t\tif allowedURI.MatchString(requestURI) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// return the relevant allowlist\nfunc determineAllowList(r *http.Request) (config.AllowList, bool) {\n\tif cfg.ProxySocketEndpoint == \"\" { // do not perform this check if we proxy to a unix socket\n\t\t// Get the client IP address from the remote address string\n\t\tclientIPStr, _, err := net.SplitHostPort(r.RemoteAddr)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"cannot get valid IP address from request\", \"reason\", err, \"method\", r.Method, \"URL\", r.URL, \"client\", r.RemoteAddr) // #nosec G706 - structured logging (slog) safely encodes values\n\t\t\treturn config.AllowList{}, false\n\t\t}\n\n\t\t// If applicable, get the non-default allowlist corresponding to the client IP address\n\t\tif cfg.ProxyContainerName != \"\" {\n\t\t\tallowList, found := cfg.AllowLists.FindByIP(clientIPStr)\n\t\t\tif found {\n\t\t\t\treturn allowList, true\n\t\t\t}\n\t\t}\n\n\t\t// Check if client is allowed for the default allowlist:\n\t\tallowedIP, err := isAllowedClient(clientIPStr)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"cannot get valid IP address for client allowlist check\", \"reason\", err, \"method\", r.Method, \"URL\", r.URL, \"client\", r.RemoteAddr) // #nosec G706 - structured logging (slog) safely encodes values\n\t\t}\n\t\tif !allowedIP {\n\t\t\treturn config.AllowList{}, false\n\t\t}\n\t}\n\n\treturn cfg.AllowLists.Default, true\n}\n\n// isAllowedClient checks if the given remote address is allowed to connect to the proxy.\n// The IP address is extracted from a RemoteAddr string (the part before the colon).\nfunc isAllowedClient(clientIPStr string) (bool, error) {\n\t// Parse the IP address\n\tclientIP := net.ParseIP(clientIPStr)\n\tif clientIP == nil {\n\t\treturn false, errors.New(\"invalid IP format\")\n\t}\n\n\tfor _, allowFromItem := range cfg.AllowFrom {\n\n\t\t// first try to handle as an CIDR\n\t\t_, allowedIPNet, err := net.ParseCIDR(allowFromItem)\n\t\tif err == nil {\n\t\t\t// AllowFrom is a valid CIDR, so check if IP address is in allowed network\n\t\t\tif allowedIPNet.Contains(clientIP) {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// AllowFrom is not a valid CIDR, so try to resolve it via DNS\n\t\t// We intentionally do not cache the DNS lookups.\n\t\t// In our use case, the resolver should be a local service, and we don't want to cause DNS caching errors.\n\t\tips, err := net.LookupIP(allowFromItem)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"error looking up allowed client hostname\", \"hostname\", allowFromItem, \"error\", err.Error())\n\t\t}\n\t\tfor _, ip := range ips {\n\t\t\t// Check if the IP address is one of the resolved IPs\n\t\t\tif ip.Equal(clientIP) {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we get here, the IP address is not allowed\n\treturn false, nil\n}\n\n// sendHTTPError sends an HTTP error with the given status code.\nfunc sendHTTPError(w http.ResponseWriter, status int) {\n\thttp.Error(w, http.StatusText(status), status)\n}\n\n// communicateBlockedRequest logs a blocked request and sends a HTTP error.\nfunc communicateBlockedRequest(w http.ResponseWriter, r *http.Request, reason string, status int) {\n\tslog.Warn(\"blocked request\", \"reason\", reason, \"method\", r.Method, \"URL\", r.URL, \"client\", r.RemoteAddr, \"response\", status) // #nosec G706 - structured logging (slog) safely encodes values\n\tsendHTTPError(w, status)\n}\n"
  },
  {
    "path": "cmd/socket-proxy/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/config\"\n)\n\nconst (\n\tprogramURL   = \"github.com/wollomatic/socket-proxy\"\n\tlogAddSource = false // set to true to log the source position (file and line) of the log message\n)\n\nvar (\n\tversion     = \"dev\" // will be overwritten by build system\n\tsocketProxy *httputil.ReverseProxy\n\tcfg         *config.Config\n)\n\nfunc main() {\n\tvar err error\n\tcfg, err = config.InitConfig()\n\tif err != nil {\n\t\tslog.Error(\"error initializing config\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// setup channels for graceful shutdown\n\tinternalQuit := make(chan int, 1)       // send to this channel to invoke graceful shutdown, int is the exit code\n\texternalQuit := make(chan os.Signal, 1) // configure listener for SIGINT and SIGTERM\n\tsignal.Notify(externalQuit, syscall.SIGINT, syscall.SIGTERM)\n\n\t// setup logging\n\tlogOpts := &slog.HandlerOptions{\n\t\tAddSource: logAddSource,\n\t\tLevel:     cfg.LogLevel,\n\t}\n\tvar logger *slog.Logger\n\tif cfg.LogJSON {\n\t\tlogger = slog.New(slog.NewJSONHandler(os.Stdout, logOpts))\n\t} else {\n\t\tlogger = slog.New(slog.NewTextHandler(os.Stdout, logOpts))\n\t}\n\tslog.SetDefault(logger)\n\n\t// setup non-default allowlists\n\tif cfg.ProxySocketEndpoint == \"\" && cfg.ProxyContainerName != \"\" {\n\t\tgo cfg.UpdateAllowLists()\n\t}\n\n\t// print configuration\n\tslog.Info(\"starting socket-proxy\", \"version\", version, \"os\", runtime.GOOS, \"arch\", runtime.GOARCH, \"runtime\", runtime.Version(), \"URL\", programURL)\n\tif cfg.ProxySocketEndpoint == \"\" {\n\t\t// join the cfg.AllowFrom slice to a string to avoid the brackets in the logging (avoid confusion with IPv6 addresses)\n\t\tallowFromString := strings.Join(cfg.AllowFrom, \",\")\n\t\tslog.Info(\"configuration info\", \"socketpath\", cfg.SocketPath, \"listenaddress\", cfg.ListenAddress, \"loglevel\", cfg.LogLevel, \"logjson\", cfg.LogJSON, \"allowfrom\", allowFromString, \"shutdowngracetime\", cfg.ShutdownGraceTime)\n\t} else {\n\t\tslog.Info(\"configuration info\", \"socketpath\", cfg.SocketPath, \"proxysocketendpoint\", cfg.ProxySocketEndpoint, \"proxysocketendpointfilemode\", cfg.ProxySocketEndpointFileMode, \"loglevel\", cfg.LogLevel, \"logjson\", cfg.LogJSON, \"shutdowngracetime\", cfg.ShutdownGraceTime)\n\t\tslog.Info(\"proxysocketendpoint is set, so the TCP listener is deactivated\")\n\t}\n\tif cfg.WatchdogInterval > 0 {\n\t\tslog.Info(\"watchdog enabled\", \"interval\", cfg.WatchdogInterval, \"stoponwatchdog\", cfg.StopOnWatchdog)\n\t} else {\n\t\tslog.Info(\"watchdog disabled\")\n\t}\n\tif len(cfg.ProxyContainerName) > 0 {\n\t\tslog.Info(\"Proxy container name provided\", \"proxycontainername\", cfg.ProxyContainerName)\n\t\tslog.Info(\"per-container allowlists enabled!\")\n\t} else {\n\t\t// we only log this on DEBUG level because providing the socket-proxy container name\n\t\t// enables the use of labels to specify per-container allowlists\n\t\tslog.Debug(\"no proxy container name provided\")\n\t}\n\tcfg.AllowLists.PrintNetworks()\n\n\t// print default request allowlist\n\tcfg.AllowLists.PrintDefault(cfg.LogJSON)\n\n\t// check if the socket is available\n\terr = checkSocketAvailability(cfg.SocketPath)\n\tif err != nil {\n\t\tslog.Error(\"socket not available\", \"error\", err)\n\t\tos.Exit(2)\n\t}\n\n\t// define the reverse proxy\n\tsocketURLDummy, _ := url.Parse(\"http://localhost\") // dummy URL - we use the unix socket\n\tsocketProxy = httputil.NewSingleHostReverseProxy(socketURLDummy)\n\tsocketProxy.Transport = &http.Transport{\n\t\tDialContext: func(_ context.Context, _, _ string) (net.Conn, error) {\n\t\t\treturn net.Dial(\"unix\", cfg.SocketPath)\n\t\t},\n\t}\n\n\tvar l net.Listener\n\tif cfg.ProxySocketEndpoint != \"\" {\n\t\tif _, err = os.Stat(cfg.ProxySocketEndpoint); err == nil {\n\t\t\tslog.Warn(fmt.Sprintf(\"%s already exists, removing existing file\", cfg.ProxySocketEndpoint))\n\t\t\tif err = os.Remove(cfg.ProxySocketEndpoint); err != nil {\n\t\t\t\tslog.Error(\"error removing existing socket file\", \"error\", err)\n\t\t\t\tos.Exit(2)\n\t\t\t}\n\t\t}\n\t\tl, err = net.Listen(\"unix\", cfg.ProxySocketEndpoint)\n\t\tif err != nil {\n\t\t\tslog.Error(\"error creating socket\", \"error\", err)\n\t\t\tos.Exit(2)\n\t\t}\n\t\tif err = os.Chmod(cfg.ProxySocketEndpoint, cfg.ProxySocketEndpointFileMode); err != nil {\n\t\t\tslog.Error(\"error setting socket file permissions\", \"error\", err)\n\t\t\tos.Exit(2)\n\t\t}\n\t} else {\n\t\tl, err = net.Listen(\"tcp\", cfg.ListenAddress)\n\t\tif err != nil {\n\t\t\tslog.Error(\"error listening on address\", \"error\", err)\n\t\t\tos.Exit(2)\n\t\t}\n\t}\n\n\tsrv := &http.Server{ // #nosec G112 -- intentionally do not time out the client\n\t\tHandler: http.HandlerFunc(handleHTTPRequest), // #nosec G112\n\t} // #nosec G112\n\n\t// start the server in a goroutine\n\tgo func() {\n\t\tif err := srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\tslog.Error(\"http server problem\", \"error\", err)\n\t\t\tos.Exit(2)\n\t\t}\n\t}()\n\n\tslog.Info(\"socket-proxy running and listening...\")\n\n\t// start the watchdog if configured\n\tif cfg.WatchdogInterval > 0 {\n\t\tgo startSocketWatchdog(cfg.SocketPath, int64(cfg.WatchdogInterval), cfg.StopOnWatchdog, internalQuit) // #nosec G115 - we validated the integer size in config.go\n\t\tslog.Debug(\"watchdog running\")\n\t}\n\n\t// start the health check server if configured\n\tif cfg.AllowHealthcheck {\n\t\tgo healthCheckServer(cfg.SocketPath)\n\t\tslog.Debug(\"healthcheck ready\")\n\t}\n\n\t// Wait for stop signal\n\texitCode := 0\n\tselect {\n\tcase <-externalQuit:\n\t\tslog.Info(\"received stop signal - shutting down\")\n\tcase value := <-internalQuit:\n\t\tslog.Info(\"received internal shutdown - shutting down\")\n\t\texitCode = value\n\t}\n\t// Try to shut down gracefully\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(int64(cfg.ShutdownGraceTime))*time.Second) // #nosec G115 - we validated the integer size in config.go\n\tdefer cancel()\n\tif err := srv.Shutdown(ctx); err != nil {\n\t\tslog.Warn(\"timeout stopping server\", \"error\", err)\n\t}\n\tslog.Info(\"shutdown finished - exiting\", \"exit code\", exitCode)\n\tos.Exit(exitCode)\n}\n"
  },
  {
    "path": "cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYdXlfRbkO6KqPU7Khn1mSjbOIaD3\num421A0NeT1wi840iWNp6MVKyj3tpnAyaQcLgd5/22O+eEHY+5+EHwB+eA==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "examples/docker-compose/dozzle/compose.yaml",
    "content": "services:\n  dockerproxy:\n    image: wollomatic/socket-proxy:1\n    command:\n      - '-loglevel=info'\n      - '-allowfrom=dozzle' # allow only the small subnet \"docker-proxynet\"\n      - '-listenip=0.0.0.0'\n      - '-allowGET=/v1\\..{2}/(containers/.*|events)|/_ping'\n      - '-allowHEAD=/_ping'\n      - '-watchdoginterval=300'\n      - '-stoponwatchdog'\n      - '-shutdowngracetime=10'\n    restart: unless-stopped\n    read_only: true\n    mem_limit: 64M\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges\n    user: 65534:998 # change gid from 998 to the gid of the docker group on your host\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    networks:\n      - docker-proxynet\n\n  dozzle:\n    image: amir20/dozzle:v10.0 # make sure you use the most recent version\n    user: 65534:65534\n    read_only: true\n    mem_limit: 256M\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges\n    depends_on:\n      - dockerproxy\n    environment:\n      DOZZLE_REMOTE_HOST: tcp://dockerproxy:2375\n#     # add additional configuration here\n#     # for example labels for traefik if needed\n      # or expose the port to the host network:\n#    ports:\n#       - 127.0.0.1:8080:8080 # bind only to the host network\n    networks:\n      - docker-proxynet\n      - dozzle\n\nnetworks:\n  docker-proxynet:\n    internal: true\n    attachable: false\n  dozzle:\n    driver: bridge\n    attachable: false\n"
  },
  {
    "path": "examples/docker-compose/watchtower/compose.yaml",
    "content": "services:\n  dockerproxy:\n    image: wollomatic/socket-proxy:1\n    command:\n      - '-loglevel=info'\n      - '-allowfrom=watchtower' # allow only access from the \"watchtower\" service\n      - '-listenip=0.0.0.0'\n      - '-shutdowngracetime=10'\n      # this whitelists the API endpoints that watchtower needs:\n      - '-allowGET=/v1\\..{2}/(containers/.*|images/.*)'\n      - '-allowPOST=/v1\\..{2}/(containers/.*|images/.*|networks/.*)'\n      - '-allowDELETE=/v1\\..{2}/(containers/.*|images/.*)'\n      - '-allowHEAD=/_ping'\n      # check socket connection every hour and stop the proxy if it fails (will then be restarted by docker):\n      - '-watchdoginterval=3600'\n      - '-stoponwatchdog'\n    restart: unless-stopped\n    read_only: true\n    mem_limit: 64M\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges\n    user: 65534:998 # change gid from 998 to the gid of the docker group on your host\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    labels:\n      - com.centurylinklabs.watchtower.enable=false # if watchtower would try to update the proxy, it would just stop\n    networks:\n      - docker-proxynet\n\n  watchtower:\n    # image: containrrr/watchtower:1.7.1\n    # https://github.com/containrrr/watchtower was archived on December 17, 2025.\n    # https://github.com/nicholas-fedor/watchtower is a maintained fork.\n    image: ghcr.io/nicholas-fedor/watchtower:1.14.2 # the containrrr repo is no longer maintained\n    depends_on:\n      - dockerproxy\n    command:\n      - '--host=tcp://dockerproxy:2375'\n      - '--schedule=0 30 4 * * *'\n      - '--debug'\n      - '--stop-timeout=5m'\n      - '--cleanup'\n    user: 65534:65534\n    read_only: true\n    mem_limit: 256M\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges\n    networks:\n      - docker-proxynet\n      - watchtower\n\nnetworks:\n  docker-proxynet:\n    internal: true\n    attachable: false\n  watchtower:\n    driver: bridge\n    attachable: false\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/wollomatic/socket-proxy\n\ngo 1.26.0\n"
  },
  {
    "path": "internal/config/config.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/container\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/events\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/filters\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/client\"\n)\n\nconst allowedDockerLabelPrefix = \"socket-proxy.allow.\"\n\nconst (\n\tdefaultAllowFrom                   = \"127.0.0.1/32\"         // allowed IPs to connect to the proxy\n\tdefaultAllowHealthcheck            = false                  // allow health check requests (HEAD http://localhost:55555/health)\n\tdefaultLogJSON                     = false                  // if true, log in JSON format\n\tdefaultLogLevel                    = \"INFO\"                 // log level as string\n\tdefaultListenIP                    = \"127.0.0.1\"            // ip address to bind the server to\n\tdefaultProxyPort                   = uint(2375)             // tcp port to listen on\n\tdefaultSocketPath                  = \"/var/run/docker.sock\" // path to the unix socket\n\tdefaultShutdownGraceTime           = uint(10)               // Maximum time in seconds to wait for the server to shut down gracefully\n\tdefaultWatchdogInterval            = uint(0)                // watchdog interval in seconds (0 to disable)\n\tdefaultStopOnWatchdog              = false                  // set to true to stop the program when the socket gets unavailable (otherwise log only)\n\tdefaultProxySocketEndpoint         = \"\"                     // empty string means no socket listener, but regular TCP listener\n\tdefaultProxySocketEndpointFileMode = uint(0o600)            // set the file mode of the unix socket endpoint\n\tdefaultAllowBindMountFrom          = \"\"                     // empty string means no bind mount restrictions\n\tdefaultProxyContainerName          = \"\"                     // socket-proxy Docker container name (empty string disables container labels for allowlists)\n)\n\ntype Config struct {\n\tAllowLists                  *AllowListRegistry\n\tAllowFrom                   []string\n\tAllowHealthcheck            bool\n\tLogJSON                     bool\n\tStopOnWatchdog              bool\n\tShutdownGraceTime           uint\n\tWatchdogInterval            uint\n\tLogLevel                    slog.Level\n\tListenAddress               string\n\tSocketPath                  string\n\tProxySocketEndpoint         string\n\tProxySocketEndpointFileMode os.FileMode\n\tProxyContainerName          string\n}\n\ntype AllowListRegistry struct {\n\tmutex    sync.RWMutex         // mutex to control read/write of byIP\n\tnetworks []string             // names of networks in which socket proxy access is allowed for non-default allowlists\n\tDefault  AllowList            // default allowlist\n\tbyIP     map[string]AllowList // map container IP address to allowlist for that container\n}\n\ntype AllowList struct {\n\tID                string                      // Container ID (empty for the default allowlist)\n\tAllowedRequests   map[string][]*regexp.Regexp // map of request methods to request path regex patterns (no requests allowed if empty)\n\tAllowedBindMounts []string                    // list of from portion of allowed bind mounts (all bind mounts allowed if empty)\n}\n\n// used for list of allowed requests\ntype methodRegex struct {\n\tmethod       string\n\tregexStrings arrayParams\n}\n\nvar supportedHTTPMethods = []string{\n\thttp.MethodGet,\n\thttp.MethodHead,\n\thttp.MethodPost,\n\thttp.MethodPut,\n\thttp.MethodPatch,\n\thttp.MethodDelete,\n\thttp.MethodConnect,\n\thttp.MethodTrace,\n\thttp.MethodOptions,\n}\n\n// InitConfig reads configuration from environment variables and command-line\n// flags, validates the resulting values, and returns the initialized Config.\nfunc InitConfig() (*Config, error) {\n\tvar (\n\t\tcfg                                     Config\n\t\tallowFromString                         string\n\t\tlistenIP                                string\n\t\tproxyPort                               uint\n\t\tlogLevel                                string\n\t\tendpointFileMode                        uint\n\t\tallowBindMountFromString                string\n\t\tdefaultAllowFromValue                   = defaultAllowFrom\n\t\tdefaultAllowHealthcheckValue            = defaultAllowHealthcheck\n\t\tdefaultLogJSONValue                     = defaultLogJSON\n\t\tdefaultListenIPValue                    = defaultListenIP\n\t\tdefaultLogLevelValue                    = defaultLogLevel\n\t\tdefaultProxyPortValue                   = defaultProxyPort\n\t\tdefaultShutdownGraceTimeValue           = defaultShutdownGraceTime\n\t\tdefaultSocketPathValue                  = defaultSocketPath\n\t\tdefaultStopOnWatchdogValue              = defaultStopOnWatchdog\n\t\tdefaultWatchdogIntervalValue            = defaultWatchdogInterval\n\t\tdefaultProxySocketEndpointValue         = defaultProxySocketEndpoint\n\t\tdefaultProxySocketEndpointFileModeValue = defaultProxySocketEndpointFileMode\n\t\tdefaultAllowBindMountFromValue          = defaultAllowBindMountFrom\n\t\tdefaultProxyContainerNameValue          = defaultProxyContainerName\n\t)\n\n\tif val, ok := os.LookupEnv(\"SP_ALLOWFROM\"); ok && val != \"\" {\n\t\tdefaultAllowFromValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_ALLOWHEALTHCHECK\"); ok {\n\t\tif parsedVal, err := strconv.ParseBool(val); err == nil {\n\t\t\tdefaultAllowHealthcheckValue = parsedVal\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_LOGJSON\"); ok {\n\t\tif parsedVal, err := strconv.ParseBool(val); err == nil {\n\t\t\tdefaultLogJSONValue = parsedVal\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_LISTENIP\"); ok && val != \"\" {\n\t\tdefaultListenIPValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_LOGLEVEL\"); ok && val != \"\" {\n\t\tdefaultLogLevelValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_PROXYPORT\"); ok && val != \"\" {\n\t\tif parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil {\n\t\t\tdefaultProxyPortValue = uint(parsedVal)\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_SHUTDOWNGRACETIME\"); ok && val != \"\" {\n\t\tif parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil {\n\t\t\tdefaultShutdownGraceTimeValue = uint(parsedVal)\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_SOCKETPATH\"); ok && val != \"\" {\n\t\tdefaultSocketPathValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_STOPONWATCHDOG\"); ok {\n\t\tif parsedVal, err := strconv.ParseBool(val); err == nil {\n\t\t\tdefaultStopOnWatchdogValue = parsedVal\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_WATCHDOGINTERVAL\"); ok && val != \"\" {\n\t\tif parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil {\n\t\t\tdefaultWatchdogIntervalValue = uint(parsedVal)\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_PROXYSOCKETENDPOINT\"); ok && val != \"\" {\n\t\tdefaultProxySocketEndpointValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_PROXYSOCKETENDPOINTFILEMODE\"); ok {\n\t\tif parsedVal, err := strconv.ParseUint(val, 8, 32); err == nil {\n\t\t\tdefaultProxySocketEndpointFileModeValue = uint(parsedVal)\n\t\t}\n\t}\n\tif val, ok := os.LookupEnv(\"SP_ALLOWBINDMOUNTFROM\"); ok && val != \"\" {\n\t\tdefaultAllowBindMountFromValue = val\n\t}\n\tif val, ok := os.LookupEnv(\"SP_PROXYCONTAINERNAME\"); ok && val != \"\" {\n\t\tdefaultProxyContainerNameValue = val\n\t}\n\n\tmethodAllowLists := newMethodRegexes()\n\n\t// multiple values per method\n\t// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...\n\tallowFromEnv := getAllowFromEnv(os.Environ())\n\tfor i := range methodAllowLists {\n\t\tif val, ok := allowFromEnv[methodAllowLists[i].method]; ok && len(val) > 0 {\n\t\t\tfor _, v := range val {\n\t\t\t\tmethodAllowLists[i].regexStrings = append(methodAllowLists[i].regexStrings, param{value: v, from: fromEnv})\n\t\t\t}\n\t\t}\n\t}\n\n\tflag.StringVar(&allowFromString, \"allowfrom\", defaultAllowFromValue, \"allowed IPs or hostname to connect to the proxy\")\n\tflag.BoolVar(&cfg.AllowHealthcheck, \"allowhealthcheck\", defaultAllowHealthcheckValue, \"allow health check requests (HEAD http://localhost:55555/health)\")\n\tflag.BoolVar(&cfg.LogJSON, \"logjson\", defaultLogJSONValue, \"log in JSON format (otherwise log in plain text\")\n\tflag.StringVar(&listenIP, \"listenip\", defaultListenIPValue, \"ip address to listen on\")\n\tflag.StringVar(&logLevel, \"loglevel\", defaultLogLevelValue, \"set log level: DEBUG, INFO, WARN, ERROR\")\n\tflag.UintVar(&proxyPort, \"proxyport\", defaultProxyPortValue, \"tcp port to listen on\")\n\tflag.UintVar(&cfg.ShutdownGraceTime, \"shutdowngracetime\", defaultShutdownGraceTimeValue, \"maximum time in seconds to wait for the server to shut down gracefully\")\n\tflag.StringVar(&cfg.SocketPath, \"socketpath\", defaultSocketPathValue, \"unix socket path to connect to\")\n\tflag.BoolVar(&cfg.StopOnWatchdog, \"stoponwatchdog\", defaultStopOnWatchdogValue, \"stop the program when the socket gets unavailable (otherwise log only)\")\n\tflag.UintVar(&cfg.WatchdogInterval, \"watchdoginterval\", defaultWatchdogIntervalValue, \"watchdog interval in seconds (0 to disable)\")\n\tflag.StringVar(&cfg.ProxySocketEndpoint, \"proxysocketendpoint\", defaultProxySocketEndpointValue, \"unix socket endpoint (if set, used instead of the TCP listener)\")\n\tflag.UintVar(&endpointFileMode, \"proxysocketendpointfilemode\", defaultProxySocketEndpointFileModeValue, \"set the file mode of the unix socket endpoint\")\n\tflag.StringVar(&allowBindMountFromString, \"allowbindmountfrom\", defaultAllowBindMountFromValue, \"allowed directories for bind mounts (comma-separated)\")\n\tflag.StringVar(&cfg.ProxyContainerName, \"proxycontainername\", defaultProxyContainerNameValue, \"socket-proxy Docker container name\")\n\tfor i := range methodAllowLists {\n\t\tflag.Var(&methodAllowLists[i].regexStrings, \"allow\"+methodAllowLists[i].method, \"regex for \"+methodAllowLists[i].method+\" requests (not set means method is not allowed)\")\n\t}\n\tflag.Parse()\n\n\t// init allowlist registry to configure default allowlist\n\tcfg.AllowLists = &AllowListRegistry{}\n\n\t// parse comma-separeted allowFromString into allowFrom slice\n\tcfg.AllowFrom = strings.Split(allowFromString, \",\")\n\n\t// parse allowBindMountFromString into default allowlist AllowedBindMounts slice and validate\n\tif allowBindMountFromString != \"\" {\n\t\tallowedBindMounts, err := parseAllowedBindMounts(allowBindMountFromString)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcfg.AllowLists.Default.AllowedBindMounts = allowedBindMounts\n\t}\n\n\t// check listenIP and proxyPort\n\tif proxyPort < 1 || proxyPort > 65535 {\n\t\treturn nil, errors.New(\"port number has to be between 1 and 65535\")\n\t}\n\tif cfg.ShutdownGraceTime > math.MaxInt {\n\t\treturn nil, fmt.Errorf(\"shutdowngracetime has to be smaller than %d\", math.MaxInt) // this maximum value has no practical significance\n\t}\n\tif cfg.WatchdogInterval > math.MaxInt {\n\t\treturn nil, fmt.Errorf(\"watchdoginterval has to be smaller than %d\", math.MaxInt) // this maximum value has no practical significance\n\t}\n\tip := net.ParseIP(listenIP)\n\tif ip == nil {\n\t\treturn nil, fmt.Errorf(\"invalid IP \\\"%s\\\" for listenip\", listenIP)\n\t}\n\n\t// Properly format address for both IPv4 and IPv6\n\tif ip.To4() == nil {\n\t\tcfg.ListenAddress = fmt.Sprintf(\"[%s]:%d\", listenIP, proxyPort)\n\t} else {\n\t\tcfg.ListenAddress = fmt.Sprintf(\"%s:%d\", listenIP, proxyPort)\n\t}\n\n\t// parse defaultLogLevel and setup logging handler depending on defaultLogJSON\n\tswitch strings.ToUpper(logLevel) {\n\tcase \"DEBUG\":\n\t\tcfg.LogLevel = slog.LevelDebug\n\tcase \"INFO\":\n\t\tcfg.LogLevel = slog.LevelInfo\n\tcase \"WARN\":\n\t\tcfg.LogLevel = slog.LevelWarn\n\tcase \"ERROR\":\n\t\tcfg.LogLevel = slog.LevelError\n\tdefault:\n\t\treturn nil, errors.New(\"invalid log level \" + logLevel + \": Supported levels are DEBUG, INFO, WARN, ERROR\")\n\t}\n\n\tif endpointFileMode > 0o777 {\n\t\treturn nil, errors.New(\"file mode has to be between 0 and 0o777\")\n\t}\n\tcfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))\n\n\t// compile regexes for default allowed requests\n\tcfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)\n\tfor _, rx := range methodAllowLists {\n\t\tfor _, regexString := range effectiveMethodParams(rx.regexStrings) {\n\t\t\tif regexString.value != \"\" {\n\t\t\t\tlocation := \"\"\n\t\t\t\tswitch regexString.from {\n\t\t\t\tcase fromEnv:\n\t\t\t\t\tlocation = \"env variable\"\n\t\t\t\tcase fromParam:\n\t\t\t\t\tlocation = \"command line parameter\"\n\t\t\t\t}\n\t\t\t\tr, err := compileRegexp(regexString.value, rx.method, location)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)\n\t\t\t}\n\t\t}\n\t}\n\n\t// populate list of socket proxy networks if applicable\n\tif cfg.ProxySocketEndpoint == \"\" && cfg.ProxyContainerName != \"\" {\n\t\tvar err error\n\t\tcfg.AllowLists.networks, err = listSocketProxyNetworks(cfg.SocketPath, cfg.ProxyContainerName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &cfg, nil\n}\n\n// UpdateAllowLists populates the byIP allowlists then keeps them updated\nfunc (cfg *Config) UpdateAllowLists() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tdockerClient, err := client.NewClientWithOpts(\n\t\tclient.WithHost(\"unix://\"+cfg.SocketPath),\n\t\tclient.WithAPIVersionNegotiation(),\n\t)\n\tif err != nil {\n\t\tslog.Error(\"failed to create Docker client\", \"error\", err)\n\t\treturn\n\t}\n\tdefer func(dockerClient *client.Client) {\n\t\terr := dockerClient.Close()\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to close Docker client\", \"error\", err)\n\t\t}\n\t}(dockerClient)\n\n\terr = cfg.AllowLists.initByIP(ctx, dockerClient)\n\tif err != nil {\n\t\tslog.Error(\"failed to initialise non-default allowlists\", \"error\", err)\n\t\treturn\n\t}\n\tslog.Debug(\"initialised non-default allowlists\")\n\n\tfilter := filters.NewArgs()\n\tfilter.Add(\"type\", \"container\")\n\tfilter.Add(\"event\", \"start\")\n\tfilter.Add(\"event\", \"restart\")\n\tfilter.Add(\"event\", \"die\")\n\teventsChan, errChan := dockerClient.Events(ctx, events.ListOptions{Filters: filter})\n\tslog.Debug(\"subscribed to Docker event stream to update allowlists\")\n\n\t// print non-default request allowlists\n\tcfg.AllowLists.PrintByIP(cfg.LogJSON)\n\n\t// handle Docker events to update allowlists\n\tfor {\n\t\tselect {\n\t\tcase event, ok := <-eventsChan:\n\t\t\tif !ok {\n\t\t\t\tslog.Info(\"Docker event stream closed\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tslog.Debug(\"received Docker container event\", \"action\", event.Action, \"id\", event.Actor.ID[:12])\n\t\t\taddedIPs, removedIPs, updateErr := cfg.AllowLists.updateFromEvent(ctx, dockerClient, event)\n\t\t\tif updateErr != nil {\n\t\t\t\tslog.Warn(\"failed to update allowlists from container event\", \"error\", updateErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, ip := range addedIPs {\n\t\t\t\tcfg.AllowLists.mutex.RLock()\n\t\t\t\tallowList, found := cfg.AllowLists.byIP[ip]\n\t\t\t\tcfg.AllowLists.mutex.RUnlock()\n\t\t\t\tif found {\n\t\t\t\t\tallowList.Print(ip, cfg.LogJSON)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, ip := range removedIPs {\n\t\t\t\tslog.Info(\"removed allowlist for container\", \"id\", event.Actor.ID[:12], \"ip\", ip)\n\t\t\t}\n\t\tcase err := <-errChan:\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"received error from Docker event stream\", \"error\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// PrintNetworks prints the allowed networks\nfunc (allowLists *AllowListRegistry) PrintNetworks() {\n\tif len(allowLists.networks) > 0 {\n\t\tslog.Info(\"socket proxy networks detected\", \"socketproxynetworks\", allowLists.networks)\n\t} else {\n\t\t// we only log this on DEBUG level because the socket proxy networks are used for per-container allowlists\n\t\tslog.Debug(\"no socket proxy networks detected\")\n\t}\n}\n\n// PrintDefault prints the default allowlist\nfunc (allowLists *AllowListRegistry) PrintDefault(logJSON bool) {\n\tallowLists.Default.Print(\"\", logJSON)\n}\n\n// PrintByIP prints the non-default allowlists\nfunc (allowLists *AllowListRegistry) PrintByIP(logJSON bool) {\n\tallowLists.mutex.RLock()\n\tdefer allowLists.mutex.RUnlock()\n\tfor ip, allowList := range allowLists.byIP {\n\t\tallowList.Print(ip, logJSON)\n\t}\n}\n\n// FindByIP returns the allowlist corresponding to the given IP address if found\nfunc (allowLists *AllowListRegistry) FindByIP(ip string) (AllowList, bool) {\n\tallowLists.mutex.RLock()\n\tdefer allowLists.mutex.RUnlock()\n\tallowList, found := allowLists.byIP[ip]\n\treturn allowList, found\n}\n\n// initialise allowlist registry byIP allowlists\nfunc (allowLists *AllowListRegistry) initByIP(ctx context.Context, dockerClient *client.Client) error {\n\tfilter := filters.NewArgs()\n\tfor _, network := range allowLists.networks {\n\t\tfilter.Add(\"network\", network)\n\t}\n\tcontainers, err := dockerClient.ContainerList(ctx, container.ListOptions{Filters: filter})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallowLists.mutex.Lock()\n\tdefer allowLists.mutex.Unlock()\n\n\tallowLists.byIP = make(map[string]AllowList)\n\n\tfor _, cntr := range containers {\n\t\tallowedRequests, allowedBindMounts, err := extractLabelData(cntr)\n\t\tif err != nil {\n\t\t\tallowLists.byIP = nil\n\t\t\treturn err\n\t\t}\n\n\t\tif len(allowedRequests) > 0 || len(allowedBindMounts) > 0 {\n\t\t\tfor networkID, cntrNetwork := range cntr.NetworkSettings.Networks {\n\t\t\t\tif slices.Contains(allowLists.networks, networkID) {\n\t\t\t\t\tallowList := AllowList{\n\t\t\t\t\t\tID:                cntr.ID,\n\t\t\t\t\t\tAllowedRequests:   allowedRequests,\n\t\t\t\t\t\tAllowedBindMounts: allowedBindMounts,\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(cntrNetwork.IPAddress) > 0 {\n\t\t\t\t\t\tallowLists.byIP[cntrNetwork.IPAddress] = allowList\n\t\t\t\t\t}\n\t\t\t\t\tif len(cntrNetwork.GlobalIPv6Address) > 0 {\n\t\t\t\t\t\tallowLists.byIP[cntrNetwork.GlobalIPv6Address] = allowList\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// update the allowlist registry based on the Docker event\nfunc (allowLists *AllowListRegistry) updateFromEvent(\n\tctx context.Context, dockerClient *client.Client, event events.Message,\n) ([]string, []string, error) {\n\tcontainerID := event.Actor.ID\n\tvar (\n\t\taddedIPs   []string\n\t\tremovedIPs []string\n\t\terr        error\n\t)\n\n\tswitch event.Action {\n\tcase \"start\", \"restart\":\n\t\taddedIPs, err = allowLists.add(ctx, dockerClient, containerID)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\tcase \"die\":\n\t\tremovedIPs = allowLists.remove(containerID)\n\t}\n\treturn addedIPs, removedIPs, nil\n}\n\n// add the allowlist for the container with the given ID to the allowlist registry\n// if it has at least one socket-proxy allow label and is in a same network as the socket-proxy\nfunc (allowLists *AllowListRegistry) add(\n\tctx context.Context, dockerClient *client.Client, containerID string,\n) ([]string, error) {\n\tfilter := filters.NewArgs()\n\tfilter.Add(\"id\", containerID)\n\tfor _, network := range allowLists.networks {\n\t\tfilter.Add(\"network\", network)\n\t}\n\tcontainers, err := dockerClient.ContainerList(ctx, container.ListOptions{Filters: filter})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(containers) == 0 {\n\t\tslog.Debug(\"container is not in a network with socket-proxy or may have stopped\", \"id\", containerID[:12])\n\t\treturn nil, nil\n\t}\n\tcntr := containers[0]\n\n\tallowedRequests, allowedBindMounts, err := extractLabelData(cntr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ips []string\n\tif len(allowedRequests) > 0 || len(allowedBindMounts) > 0 {\n\t\tallowList := AllowList{\n\t\t\tID:                cntr.ID,\n\t\t\tAllowedRequests:   allowedRequests,\n\t\t\tAllowedBindMounts: allowedBindMounts,\n\t\t}\n\n\t\tallowLists.mutex.Lock()\n\t\tdefer allowLists.mutex.Unlock()\n\n\t\tfor networkID, cntrNetwork := range cntr.NetworkSettings.Networks {\n\t\t\tif slices.Contains(allowLists.networks, networkID) {\n\t\t\t\tipv4Address := cntrNetwork.IPAddress\n\t\t\t\tif len(ipv4Address) > 0 {\n\t\t\t\t\tallowLists.byIP[ipv4Address] = allowList\n\t\t\t\t\tips = append(ips, ipv4Address)\n\t\t\t\t}\n\t\t\t\tipv6Address := cntrNetwork.GlobalIPv6Address\n\t\t\t\tif len(ipv6Address) > 0 {\n\t\t\t\t\tallowLists.byIP[ipv6Address] = allowList\n\t\t\t\t\tips = append(ips, ipv6Address)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ips, nil\n}\n\n// remove allowlists having the given container ID from the allowlist registry\nfunc (allowLists *AllowListRegistry) remove(containerID string) []string {\n\tallowLists.mutex.Lock()\n\tdefer allowLists.mutex.Unlock()\n\n\tvar removedIPs []string\n\tfor ip, allowList := range allowLists.byIP {\n\t\tif allowList.ID == containerID {\n\t\t\tdelete(allowLists.byIP, ip)\n\t\t\tremovedIPs = append(removedIPs, ip)\n\t\t}\n\t}\n\treturn removedIPs\n}\n\n// Print prints the allowlist, including the IP address of the associated container if it is not empty,\n// and in JSON format if logJSON is true\nfunc (allowList AllowList) Print(ip string, logJSON bool) {\n\t// print allowed requests\n\tif logJSON {\n\t\tif ip == \"\" {\n\t\t\tfor method, regex := range allowList.AllowedRequests {\n\t\t\t\tslog.Info(\"configured default request allowlist\", \"method\", method, \"regex\", regex)\n\t\t\t}\n\t\t} else {\n\t\t\tfor method, regex := range allowList.AllowedRequests {\n\t\t\t\tslog.Info(\"configured request allowlist\",\n\t\t\t\t\t\"id\", allowList.ID[:12],\n\t\t\t\t\t\"ip\", ip,\n\t\t\t\t\t\"method\", method,\n\t\t\t\t\t\"regex\", regex,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// don't use slog here, as we want to print the regexes as they are\n\t\t// see https://github.com/wollomatic/socket-proxy/issues/11\n\t\tif ip == \"\" {\n\t\t\tfmt.Printf(\"Default request allowlist:\\n   %-8s %s\\n\", \"Method\", \"Regex\")\n\t\t} else {\n\t\t\tfmt.Printf(\"Request allowlist for %s (%s):\\n   %-8s %s\\n\", allowList.ID[:12], ip, \"Method\", \"Regex\")\n\t\t}\n\t\tfor method, regex := range allowList.AllowedRequests {\n\t\t\tfmt.Printf(\"   %-8s %s\\n\", method, regex)\n\t\t}\n\t}\n\t// print allowed bind mounts\n\tif len(allowList.AllowedBindMounts) > 0 {\n\t\tif ip == \"\" {\n\t\t\tslog.Info(\"Default Docker bind mount restrictions enabled\",\n\t\t\t\t\"allowbindmountfrom\", allowList.AllowedBindMounts,\n\t\t\t)\n\t\t} else {\n\t\t\tslog.Info(\"Docker bind mount restrictions enabled\",\n\t\t\t\t\"allowbindmountfrom\", allowList.AllowedBindMounts,\n\t\t\t\t\"id\", allowList.ID[:12],\n\t\t\t\t\"ip\", ip,\n\t\t\t)\n\t\t}\n\t} else {\n\t\t// we only log this on DEBUG level because bind mount restrictions are a very special use case\n\t\tif ip == \"\" {\n\t\t\tslog.Debug(\"no default Docker bind mount restrictions\")\n\t\t} else {\n\t\t\tslog.Debug(\"no Docker bind mount restrictions\", \"id\", allowList.ID[:12], \"ip\", ip)\n\t\t}\n\t}\n}\n\n// compile allowed requests regex pattern\nfunc compileRegexp(regex, method, configLocation string) (*regexp.Regexp, error) {\n\tr, err := regexp.Compile(\"^\" + regex + \"$\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid regex \\\"%s\\\" for method %s in %s: %w\", regex, method, configLocation, err)\n\t}\n\treturn r, nil\n}\n\n// newMethodRegexes returns one methodRegex entry for each supported HTTP method.\nfunc newMethodRegexes() []methodRegex {\n\tmethods := make([]methodRegex, 0, len(supportedHTTPMethods))\n\tfor _, method := range supportedHTTPMethods {\n\t\tmethods = append(methods, methodRegex{method: method})\n\t}\n\treturn methods\n}\n\n// effectiveMethodParams returns the parameters that should be applied for one\n// HTTP method, preferring command-line values over environment values when both\n// are present.\nfunc effectiveMethodParams(params arrayParams) []param {\n\tif slices.ContainsFunc(params, func(p param) bool { return p.from == fromParam }) {\n\t\treturn slices.DeleteFunc(slices.Clone(params), func(p param) bool { return p.from == fromEnv })\n\t}\n\treturn params\n}\n\n// parse bind mount from string into list of allowed bind mounts\nfunc parseAllowedBindMounts(allowBindMountFromString string) ([]string, error) {\n\tallowedBindMounts := strings.Split(allowBindMountFromString, \",\")\n\tfor i, dir := range allowedBindMounts {\n\t\tif !strings.HasPrefix(dir, \"/\") {\n\t\t\treturn nil, fmt.Errorf(\"bind mount directory must start with /: %q\", dir)\n\t\t}\n\t\tallowedBindMounts[i] = filepath.Clean(dir)\n\t}\n\treturn allowedBindMounts, nil\n}\n\n// return list of docker networks that the socket-proxy container is in\nfunc listSocketProxyNetworks(socketPath, proxyContainerName string) ([]string, error) {\n\tcntr, err := getSocketProxyContainerSummary(socketPath, proxyContainerName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnetworks := make([]string, 0, len(cntr.NetworkSettings.Networks))\n\tfor networkID := range cntr.NetworkSettings.Networks {\n\t\tnetworks = append(networks, networkID)\n\t}\n\treturn networks, nil\n}\n\n// return Docker container summary for the socket proxy container\nfunc getSocketProxyContainerSummary(socketPath, proxyContainerName string) (container.Summary, error) {\n\tconst maxTries = 3\n\n\tdockerClient, err := client.NewClientWithOpts(\n\t\tclient.WithHost(\"unix://\"+socketPath),\n\t\tclient.WithAPIVersionNegotiation(),\n\t)\n\tif err != nil {\n\t\treturn container.Summary{}, err\n\t}\n\tdefer func(dockerClient *client.Client) {\n\t\terr := dockerClient.Close()\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to close Docker client\", \"error\", err)\n\t\t}\n\t}(dockerClient)\n\n\tctx := context.Background()\n\tfilter := filters.NewArgs()\n\tfilter.Add(\"name\", proxyContainerName)\n\tvar containers []container.Summary\n\tfor i := 1; i <= maxTries; i++ {\n\t\tcontainers, err = dockerClient.ContainerList(ctx, container.ListOptions{Filters: filter})\n\t\tif err != nil {\n\t\t\treturn container.Summary{}, err\n\t\t}\n\t\tif len(containers) > 0 {\n\t\t\treturn containers[0], nil\n\t\t}\n\t\tif i < maxTries {\n\t\t\ttime.Sleep(time.Duration(i) * time.Second)\n\t\t}\n\t}\n\treturn container.Summary{}, fmt.Errorf(\"socket-proxy container \\\"%s\\\" was not found after %d attempts; verify the container name is correct and the container is running\", proxyContainerName, maxTries)\n}\n\n// extract Docker container allowlist label data from the container summary\nfunc extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {\n\tallowedRequests := make(map[string][]*regexp.Regexp)\n\tvar allowedBindMounts []string\n\tfor labelName, labelValue := range cntr.Labels {\n\t\tif strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != \"\" {\n\t\t\tallowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))\n\t\t\tmethod, _, _ := strings.Cut(allowSpec, \".\")\n\t\t\tif slices.Contains(supportedHTTPMethods, method) {\n\t\t\t\tr, err := compileRegexp(labelValue, method, \"docker container label\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\t\t\t\tallowedRequests[method] = append(allowedRequests[method], r)\n\t\t\t} else if allowSpec == \"BINDMOUNTFROM\" {\n\t\t\t\tvar err error\n\t\t\t\tallowedBindMounts, err = parseAllowedBindMounts(labelValue)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn allowedRequests, allowedBindMounts, nil\n}\n"
  },
  {
    "path": "internal/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"flag\"\n\t\"math\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/container\"\n)\n\nfunc resetFlagsForTest(t *testing.T, args []string) func() {\n\tt.Helper()\n\n\tprevCommandLine := flag.CommandLine\n\tprevArgs := os.Args\n\n\tflag.CommandLine = flag.NewFlagSet(args[0], flag.ContinueOnError)\n\tflag.CommandLine.SetOutput(os.Stderr)\n\tos.Args = args\n\n\treturn func() {\n\t\tflag.CommandLine = prevCommandLine\n\t\tos.Args = prevArgs\n\t}\n}\n\nfunc Test_extractLabelData(t *testing.T) {\n\ttests := []struct {\n\t\tname string // description of this test case\n\t\t// Named input parameters for target function.\n\t\tcntr    container.Summary\n\t\twant    map[string][]*regexp.Regexp\n\t\twant2   []string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid labels with multiple methods and regexes\",\n\t\t\tcntr: container.Summary{\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"socket-proxy.allow.get.0\": \"regex1\",\n\t\t\t\t\t\"socket-proxy.allow.get.1\": \"regex2\",\n\t\t\t\t\t\"socket-proxy.allow.post\":  \"regex3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string][]*regexp.Regexp{\n\t\t\t\t\"GET\":  {regexp.MustCompile(\"^regex1$\"), regexp.MustCompile(\"^regex2$\")},\n\t\t\t\t\"POST\": {regexp.MustCompile(\"^regex3$\")},\n\t\t\t},\n\t\t\twant2:   nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid regex in label value\",\n\t\t\tcntr: container.Summary{\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"socket-proxy.allow.get\": \"invalid[regex\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twant2:   nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-allow labels are ignored\",\n\t\t\tcntr: container.Summary{\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"socket-proxy.allow.get\": \"regex1\",\n\t\t\t\t\t\"other.label\":            \"value\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string][]*regexp.Regexp{\n\t\t\t\t\"GET\": {regexp.MustCompile(\"^regex1$\")},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got2, gotErr := extractLabelData(tt.cntr)\n\t\t\tif gotErr != nil {\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tt.Errorf(\"extractLabelData() failed: %v\", gotErr)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tt.Fatal(\"extractLabelData() succeeded unexpectedly\")\n\t\t\t}\n\t\t\tif !regexMapsEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"extractLabelData() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got2, tt.want2) {\n\t\t\t\tt.Errorf(\"extractLabelData() = %v, want %v\", got2, tt.want2)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc regexMapsEqual(a, b map[string][]*regexp.Regexp) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor method, aRegexes := range a {\n\t\tbRegexes, ok := b[method]\n\t\tif !ok || len(aRegexes) != len(bRegexes) {\n\t\t\treturn false\n\t\t}\n\t\taRegexStrings := make([]string, 0, len(aRegexes))\n\t\tfor _, ar := range aRegexes {\n\t\t\taRegexStrings = append(aRegexStrings, ar.String())\n\t\t}\n\t\tbRegexStrings := make([]string, 0, len(bRegexes))\n\t\tfor _, br := range bRegexes {\n\t\t\tbRegexStrings = append(bRegexStrings, br.String())\n\t\t}\n\t\tsort.Strings(aRegexStrings)\n\t\tsort.Strings(bRegexStrings)\n\t\tfor i, ar := range aRegexStrings {\n\t\t\tif ar != bRegexStrings[i] {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestInitConfig_AllowMethodFlagOverridesEnv(t *testing.T) {\n\tt.Setenv(\"SP_ALLOW_GET\", \"/from-env\")\n\trestore := resetFlagsForTest(t, []string{\"socket-proxy\", \"-allowGET=/from-flag\"})\n\tdefer restore()\n\n\tcfg, err := InitConfig()\n\tif err != nil {\n\t\tt.Fatalf(\"InitConfig() error = %v\", err)\n\t}\n\n\tregexes := cfg.AllowLists.Default.AllowedRequests[\"GET\"]\n\tif len(regexes) != 1 {\n\t\tt.Fatalf(\"expected 1 GET regex, got %d\", len(regexes))\n\t}\n\tif !regexes[0].MatchString(\"/from-flag\") {\n\t\tt.Fatalf(\"expected GET regex to match /from-flag, got %q\", regexes[0].String())\n\t}\n\tif regexes[0].MatchString(\"/from-env\") {\n\t\tt.Fatalf(\"expected env GET regex to be ignored when flag is present, got %q\", regexes[0].String())\n\t}\n}\n\nfunc TestInitConfig_ShutdownGraceTimeTooLarge(t *testing.T) {\n\trestore := resetFlagsForTest(t, []string{\n\t\t\"socket-proxy\",\n\t\t\"-shutdowngracetime=\" + strconv.FormatUint(uint64(math.MaxInt)+1, 10),\n\t})\n\tdefer restore()\n\n\t_, err := InitConfig()\n\tif err == nil {\n\t\tt.Fatal(\"InitConfig() unexpectedly succeeded\")\n\t}\n}\n\nfunc TestInitConfig_WatchdogIntervalTooLarge(t *testing.T) {\n\trestore := resetFlagsForTest(t, []string{\n\t\t\"socket-proxy\",\n\t\t\"-watchdoginterval=\" + strconv.FormatUint(uint64(math.MaxInt)+1, 10),\n\t})\n\tdefer restore()\n\n\t_, err := InitConfig()\n\tif err == nil {\n\t\tt.Fatal(\"InitConfig() unexpectedly succeeded\")\n\t}\n}\n"
  },
  {
    "path": "internal/config/env.go",
    "content": "package config\n\nimport (\n\t\"strings\"\n)\n\nconst spAllowPrefix = \"SP_ALLOW_\"\n\n// getAllowFromEnv reads allowlist regex strings from environment variables.\n//\n// Environment variables should be of the form\n// like SP_ALLOW_GET, SP_ALLOW_GET_0, SP_ALLOW_GET_1, SP_ALLOW_POST\n// returning a map of method to list of regex strings.\n// like: {\"GET\":[], \"POST\":[]}\nfunc getAllowFromEnv(env []string) map[string][]string {\n\tresult := make(map[string][]string)\n\tfor _, v := range env {\n\t\tif v, ok := strings.CutPrefix(v, spAllowPrefix); ok {\n\t\t\tkey, value, found := strings.Cut(v, \"=\")\n\t\t\tif found {\n\t\t\t\t// optional number suffix after method\n\t\t\t\tmethod, _, _ := strings.Cut(key, \"_\")\n\t\t\t\tresult[method] = append(result[method], value)\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "internal/config/env_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc Test_getAllowFromEnv(t *testing.T) {\n\ttests := []struct {\n\t\tname string // description of this test case\n\t\t// Named input parameters for target function.\n\t\tenv  []string\n\t\twant map[string][]string\n\t}{\n\t\t{\n\t\t\tname: \"single method\",\n\t\t\tenv:  []string{\"SP_ALLOW_GET=/allowed/path\"},\n\t\t\twant: map[string][]string{\"GET\": {\"/allowed/path\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple methods\",\n\t\t\tenv:  []string{\"SP_ALLOW_GET=/get/path\", \"SP_ALLOW_POST=/post/path\"},\n\t\t\twant: map[string][]string{\"GET\": {\"/get/path\"}, \"POST\": {\"/post/path\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple entries for one method\",\n\t\t\tenv:  []string{\"SP_ALLOW_GET=/path/one\", \"SP_ALLOW_GET_1=/path/two\"},\n\t\t\twant: map[string][]string{\"GET\": {\"/path/one\", \"/path/two\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple entries for one method with non-sequential index\",\n\t\t\tenv:  []string{\"SP_ALLOW_GET=/path/one\", \"SP_ALLOW_GET_2=/path/two\"},\n\t\t\twant: map[string][]string{\"GET\": {\"/path/one\", \"/path/two\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"no relevant env vars\",\n\t\t\tenv:  []string{\"OTHER_ENV=some_value\"},\n\t\t\twant: map[string][]string{},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := getAllowFromEnv(tt.env)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"getAllowFromEnv() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/param.go",
    "content": "package config\n\nimport (\n\t\"flag\"\n\t\"strings\"\n)\n\ntype from int\n\nconst (\n\tfromEnv   from = 1\n\tfromParam from = 2\n)\n\ntype param struct {\n\tvalue string\n\tfrom  from\n}\n\ntype arrayParams []param\n\n// ensure that arrayParams implements the flag.Value interface\nvar _ flag.Value = (*arrayParams)(nil)\n\nfunc (a *arrayParams) String() string {\n\tvar values []string\n\tfor _, p := range *a {\n\t\tvalues = append(values, p.value)\n\t}\n\treturn strings.Join(values, \", \")\n}\n\nfunc (a *arrayParams) Set(value string) error {\n\t*a = append(*a, param{value: value, from: fromParam})\n\treturn nil\n}\n"
  },
  {
    "path": "internal/docker/api/common.go",
    "content": "package api\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/common.go\n*/\n\n// Common constants for daemon and client.\nconst (\n\t// DefaultVersion of the current REST API.\n\tDefaultVersion = \"1.51\"\n)\n"
  },
  {
    "path": "internal/docker/api/types/container/container.go",
    "content": "package container\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/container/container.go\n*/\n\n// Summary contains response of Engine API:\n// GET \"/containers/json\"\ntype Summary struct {\n\tID              string `json:\"Id\"`\n\tNames           []string\n\tLabels          map[string]string\n\tNetworkSettings *NetworkSettingsSummary\n}\n"
  },
  {
    "path": "internal/docker/api/types/container/network_settings.go",
    "content": "package container\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/container/network_settings.go\n*/\n\nimport (\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/network\"\n)\n\n// NetworkSettingsSummary provides a summary of container's networks\n// in /containers/json\ntype NetworkSettingsSummary struct {\n\tNetworks map[string]*network.EndpointSettings\n}\n"
  },
  {
    "path": "internal/docker/api/types/container/options.go",
    "content": "package container\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/container/options.go\n*/\n\nimport \"github.com/wollomatic/socket-proxy/internal/docker/api/types/filters\"\n\n// ListOptions holds parameters to list containers with.\ntype ListOptions struct {\n\tFilters filters.Args\n}\n"
  },
  {
    "path": "internal/docker/api/types/error_response.go",
    "content": "package types\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/error_response.go\n*/\n\n// ErrorResponse Represents an error.\n// swagger:model ErrorResponse\ntype ErrorResponse struct {\n\n\t// The error message.\n\t// Required: true\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "internal/docker/api/types/events/events.go",
    "content": "package events\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/events/events.go\n*/\n\nimport \"github.com/wollomatic/socket-proxy/internal/docker/api/types/filters\"\n\n// Type is used for event-types.\ntype Type string\n\n// Action is used for event-actions.\ntype Action string\n\nconst (\n\tActionCreate       Action = \"create\"\n\tActionStart        Action = \"start\"\n\tActionRestart      Action = \"restart\"\n\tActionStop         Action = \"stop\"\n\tActionCheckpoint   Action = \"checkpoint\"\n\tActionPause        Action = \"pause\"\n\tActionUnPause      Action = \"unpause\"\n\tActionAttach       Action = \"attach\"\n\tActionDetach       Action = \"detach\"\n\tActionResize       Action = \"resize\"\n\tActionUpdate       Action = \"update\"\n\tActionRename       Action = \"rename\"\n\tActionKill         Action = \"kill\"\n\tActionDie          Action = \"die\"\n\tActionOOM          Action = \"oom\"\n\tActionDestroy      Action = \"destroy\"\n\tActionRemove       Action = \"remove\"\n\tActionCommit       Action = \"commit\"\n\tActionTop          Action = \"top\"\n\tActionCopy         Action = \"copy\"\n\tActionArchivePath  Action = \"archive-path\"\n\tActionExtractToDir Action = \"extract-to-dir\"\n\tActionExport       Action = \"export\"\n\tActionImport       Action = \"import\"\n\tActionSave         Action = \"save\"\n\tActionLoad         Action = \"load\"\n\tActionTag          Action = \"tag\"\n\tActionUnTag        Action = \"untag\"\n\tActionPush         Action = \"push\"\n\tActionPull         Action = \"pull\"\n\tActionPrune        Action = \"prune\"\n\tActionDelete       Action = \"delete\"\n\tActionEnable       Action = \"enable\"\n\tActionDisable      Action = \"disable\"\n\tActionConnect      Action = \"connect\"\n\tActionDisconnect   Action = \"disconnect\"\n\tActionReload       Action = \"reload\"\n\tActionMount        Action = \"mount\"\n\tActionUnmount      Action = \"unmount\"\n\n\t// ActionExecCreate is the prefix used for exec_create events. These\n\t// event-actions are commonly followed by a colon and space (\": \"),\n\t// and the command that's defined for the exec, for example:\n\t//\n\t//\texec_create: /bin/sh -c 'echo hello'\n\t//\n\t// This is far from ideal; it's a compromise to allow filtering and\n\t// to preserve backward-compatibility.\n\tActionExecCreate Action = \"exec_create\"\n\t// ActionExecStart is the prefix used for exec_create events. These\n\t// event-actions are commonly followed by a colon and space (\": \"),\n\t// and the command that's defined for the exec, for example:\n\t//\n\t//\texec_start: /bin/sh -c 'echo hello'\n\t//\n\t// This is far from ideal; it's a compromise to allow filtering and\n\t// to preserve backward-compatibility.\n\tActionExecStart  Action = \"exec_start\"\n\tActionExecDie    Action = \"exec_die\"\n\tActionExecDetach Action = \"exec_detach\"\n\n\t// ActionHealthStatus is the prefix to use for health_status events.\n\t//\n\t// Health-status events can either have a pre-defined status, in which\n\t// case the \"health_status\" action is followed by a colon, or can be\n\t// \"free-form\", in which case they're followed by the output of the\n\t// health-check output.\n\t//\n\t// This is far from ideal, and a compromise to allow filtering, and\n\t// to preserve backward-compatibility.\n\tActionHealthStatus          Action = \"health_status\"\n\tActionHealthStatusRunning   Action = \"health_status: running\"\n\tActionHealthStatusHealthy   Action = \"health_status: healthy\"\n\tActionHealthStatusUnhealthy Action = \"health_status: unhealthy\"\n)\n\n// Actor describes something that generates events,\n// like a container, or a network, or a volume.\n// It has a defined name and a set of attributes.\n// The container attributes are its labels, other actors\n// can generate these attributes from other properties.\ntype Actor struct {\n\tID         string\n\tAttributes map[string]string\n}\n\n// Message represents the information an event contains\ntype Message struct {\n\tType   Type\n\tAction Action\n\tActor  Actor\n}\n\n// ListOptions holds parameters to filter events with.\ntype ListOptions struct {\n\tFilters filters.Args\n}\n"
  },
  {
    "path": "internal/docker/api/types/filters/errors.go",
    "content": "package filters\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/filters/errors.go\n*/\n\nimport \"fmt\"\n\n// invalidFilter indicates that the provided filter or its value is invalid\ntype invalidFilter struct {\n\tFilter string\n\tValue  []string\n}\n\nfunc (e invalidFilter) Error() string {\n\tmsg := \"invalid filter\"\n\tif e.Filter != \"\" {\n\t\tmsg += \" '\" + e.Filter\n\t\tif e.Value != nil {\n\t\t\tmsg = fmt.Sprintf(\"%s=%s\", msg, e.Value)\n\t\t}\n\t\tmsg += \"'\"\n\t}\n\treturn msg\n}\n\n// InvalidParameter marks this error as ErrInvalidParameter\nfunc (e invalidFilter) InvalidParameter() {}\n"
  },
  {
    "path": "internal/docker/api/types/filters/parse.go",
    "content": "/*\nPackage filters provides tools for encoding a mapping of keys to a set of\nmultiple values.\n\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/filters/parse.go\n*/\npackage filters\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// Args stores a mapping of keys to a set of multiple values.\ntype Args struct {\n\tfields map[string]map[string]bool\n}\n\n// KeyValuePair are used to initialize a new Args\ntype KeyValuePair struct {\n\tKey   string\n\tValue string\n}\n\n// NewArgs returns a new Args populated with the initial args\nfunc NewArgs(initialArgs ...KeyValuePair) Args {\n\targs := Args{fields: map[string]map[string]bool{}}\n\tfor _, arg := range initialArgs {\n\t\targs.Add(arg.Key, arg.Value)\n\t}\n\treturn args\n}\n\n// Keys returns all the keys in list of Args\nfunc (args Args) Keys() []string {\n\tkeys := make([]string, 0, len(args.fields))\n\tfor k := range args.fields {\n\t\tkeys = append(keys, k)\n\t}\n\treturn keys\n}\n\n// MarshalJSON returns a JSON byte representation of the Args\nfunc (args Args) MarshalJSON() ([]byte, error) {\n\tif len(args.fields) == 0 {\n\t\treturn []byte(\"{}\"), nil\n\t}\n\treturn json.Marshal(args.fields)\n}\n\n// ToJSON returns the Args as a JSON encoded string\nfunc ToJSON(a Args) (string, error) {\n\tif a.Len() == 0 {\n\t\treturn \"\", nil\n\t}\n\tbuf, err := json.Marshal(a)\n\treturn string(buf), err\n}\n\n// UnmarshalJSON populates the Args from JSON encode bytes\nfunc (args Args) UnmarshalJSON(raw []byte) error {\n\treturn json.Unmarshal(raw, &args.fields)\n}\n\n// Get returns the list of values associated with the key\nfunc (args Args) Get(key string) []string {\n\tvalues := args.fields[key]\n\tif values == nil {\n\t\treturn make([]string, 0)\n\t}\n\tslice := make([]string, 0, len(values))\n\tfor key := range values {\n\t\tslice = append(slice, key)\n\t}\n\treturn slice\n}\n\n// Add a new value to the set of values\nfunc (args Args) Add(key, value string) {\n\tif _, ok := args.fields[key]; ok {\n\t\targs.fields[key][value] = true\n\t} else {\n\t\targs.fields[key] = map[string]bool{value: true}\n\t}\n}\n\n// Del removes a value from the set\nfunc (args Args) Del(key, value string) {\n\tif _, ok := args.fields[key]; ok {\n\t\tdelete(args.fields[key], value)\n\t\tif len(args.fields[key]) == 0 {\n\t\t\tdelete(args.fields, key)\n\t\t}\n\t}\n}\n\n// Len returns the number of keys in the mapping\nfunc (args Args) Len() int {\n\treturn len(args.fields)\n}\n\n// MatchKVList returns true if all the pairs in sources exist as key=value\n// pairs in the mapping at key, or if there are no values at key.\nfunc (args Args) MatchKVList(key string, sources map[string]string) bool {\n\tfieldValues := args.fields[key]\n\n\t// do not filter if there is no filter set or cannot determine filter\n\tif len(fieldValues) == 0 {\n\t\treturn true\n\t}\n\n\tif len(sources) == 0 {\n\t\treturn false\n\t}\n\n\tfor value := range fieldValues {\n\t\ttestK, testV, hasValue := strings.Cut(value, \"=\")\n\n\t\tv, ok := sources[testK]\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tif hasValue && testV != v {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Match returns true if any of the values at key match the source string\nfunc (args Args) Match(field, source string) bool {\n\tif args.ExactMatch(field, source) {\n\t\treturn true\n\t}\n\n\tfieldValues := args.fields[field]\n\tfor name2match := range fieldValues {\n\t\tmatch, err := regexp.MatchString(name2match, source)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif match {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetBoolOrDefault returns a boolean value of the key if the key is present\n// and is interpretable as a boolean value. Otherwise the default value is returned.\n// Error is not nil only if the filter values are not valid boolean or are conflicting.\nfunc (args Args) GetBoolOrDefault(key string, defaultValue bool) (bool, error) {\n\tfieldValues, ok := args.fields[key]\n\tif !ok {\n\t\treturn defaultValue, nil\n\t}\n\n\tif len(fieldValues) == 0 {\n\t\treturn defaultValue, &invalidFilter{key, nil}\n\t}\n\n\tisFalse := fieldValues[\"0\"] || fieldValues[\"false\"]\n\tisTrue := fieldValues[\"1\"] || fieldValues[\"true\"]\n\tif isFalse == isTrue {\n\t\t// Either no or conflicting truthy/falsy value were provided\n\t\treturn defaultValue, &invalidFilter{key, args.Get(key)}\n\t}\n\treturn isTrue, nil\n}\n\n// ExactMatch returns true if the source matches exactly one of the values.\nfunc (args Args) ExactMatch(key, source string) bool {\n\tfieldValues, ok := args.fields[key]\n\t// do not filter if there is no filter set or cannot determine filter\n\tif !ok || len(fieldValues) == 0 {\n\t\treturn true\n\t}\n\n\t// try to match full name value to avoid O(N) regular expression matching\n\treturn fieldValues[source]\n}\n\n// UniqueExactMatch returns true if there is only one value and the source\n// matches exactly the value.\nfunc (args Args) UniqueExactMatch(key, source string) bool {\n\tfieldValues := args.fields[key]\n\t// do not filter if there is no filter set or cannot determine filter\n\tif len(fieldValues) == 0 {\n\t\treturn true\n\t}\n\tif len(args.fields[key]) != 1 {\n\t\treturn false\n\t}\n\n\t// try to match full name value to avoid O(N) regular expression matching\n\treturn fieldValues[source]\n}\n\n// FuzzyMatch returns true if the source matches exactly one value,  or the\n// source has one of the values as a prefix.\nfunc (args Args) FuzzyMatch(key, source string) bool {\n\tif args.ExactMatch(key, source) {\n\t\treturn true\n\t}\n\n\tfieldValues := args.fields[key]\n\tfor prefix := range fieldValues {\n\t\tif strings.HasPrefix(source, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Contains returns true if the key exists in the mapping\nfunc (args Args) Contains(field string) bool {\n\t_, ok := args.fields[field]\n\treturn ok\n}\n\n// Validate compared the set of accepted keys against the keys in the mapping.\n// An error is returned if any mapping keys are not in the accepted set.\nfunc (args Args) Validate(accepted map[string]bool) error {\n\tfor name := range args.fields {\n\t\tif !accepted[name] {\n\t\t\treturn &invalidFilter{name, nil}\n\t\t}\n\t}\n\treturn nil\n}\n\n// WalkValues iterates over the list of values for a key in the mapping and calls\n// op() for each value. If op returns an error the iteration stops and the\n// error is returned.\nfunc (args Args) WalkValues(field string, op func(value string) error) error {\n\tif _, ok := args.fields[field]; !ok {\n\t\treturn nil\n\t}\n\tfor v := range args.fields[field] {\n\t\tif err := op(v); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Clone returns a copy of args.\nfunc (args Args) Clone() (newArgs Args) {\n\tnewArgs.fields = make(map[string]map[string]bool, len(args.fields))\n\tfor k, m := range args.fields {\n\t\tvar mm map[string]bool\n\t\tif m != nil {\n\t\t\tmm = make(map[string]bool, len(m))\n\t\t\tfor kk, v := range m {\n\t\t\t\tmm[kk] = v\n\t\t\t}\n\t\t}\n\t\tnewArgs.fields[k] = mm\n\t}\n\treturn newArgs\n}\n"
  },
  {
    "path": "internal/docker/api/types/network/endpoint.go",
    "content": "package network\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/network/endpoint.go\n*/\n\n// EndpointSettings stores the network endpoint details\ntype EndpointSettings struct {\n\t// Operational data\n\tNetworkID           string\n\tEndpointID          string\n\tGateway             string\n\tIPAddress           string\n\tIPPrefixLen         int\n\tIPv6Gateway         string\n\tGlobalIPv6Address   string\n\tGlobalIPv6PrefixLen int\n}\n\n// Copy makes a deep copy of `EndpointSettings`\nfunc (es *EndpointSettings) Copy() *EndpointSettings {\n\treturn new(*es)\n}\n"
  },
  {
    "path": "internal/docker/api/types/types.go",
    "content": "package types\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/types.go\n*/\n\n// Ping contains response of Engine API:\n// GET \"/_ping\"\ntype Ping struct {\n\tAPIVersion string\n}\n"
  },
  {
    "path": "internal/docker/api/types/versions/compare.go",
    "content": "package versions\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/api/types/versions/compare.go\n*/\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// compare compares two version strings\n// returns -1 if v1 < v2, 1 if v1 > v2, 0 otherwise.\nfunc compare(v1, v2 string) int {\n\tif v1 == v2 {\n\t\treturn 0\n\t}\n\tvar (\n\t\tcurrTab  = strings.Split(v1, \".\")\n\t\totherTab = strings.Split(v2, \".\")\n\t)\n\n\tmaxVer := len(currTab)\n\tif len(otherTab) > maxVer {\n\t\tmaxVer = len(otherTab)\n\t}\n\tfor i := 0; i < maxVer; i++ {\n\t\tvar currInt, otherInt int\n\n\t\tif len(currTab) > i {\n\t\t\tcurrInt, _ = strconv.Atoi(currTab[i])\n\t\t}\n\t\tif len(otherTab) > i {\n\t\t\totherInt, _ = strconv.Atoi(otherTab[i])\n\t\t}\n\t\tif currInt > otherInt {\n\t\t\treturn 1\n\t\t}\n\t\tif otherInt > currInt {\n\t\t\treturn -1\n\t\t}\n\t}\n\treturn 0\n}\n\n// LessThan checks if a version is less than another\nfunc LessThan(v, other string) bool {\n\treturn compare(v, other) == -1\n}\n"
  },
  {
    "path": "internal/docker/client/client.go",
    "content": "/*\nPackage client is a Go client for the Docker Engine API.\n\nFor more information about the Engine API, see the documentation:\nhttps://docs.docker.com/reference/api/engine/\n\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/client.go\n*/\npackage client\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/versions\"\n\t\"github.com/wollomatic/socket-proxy/internal/go-connections/sockets\"\n)\n\n// DefaultDockerHost defines default host\nconst DefaultDockerHost = \"unix:///var/run/docker.sock\"\n\n// DummyHost is a hostname used for local communication.\nconst DummyHost = \"api.moby.localhost\"\n\n// fallbackAPIVersion is the version to fallback to if API-version negotiation\n// fails. This version is the highest version of the API before API-version\n// negotiation was introduced. If negotiation fails (or no API version was\n// included in the API response), we assume the API server uses the most\n// recent version before negotiation was introduced.\nconst fallbackAPIVersion = \"1.24\"\n\n// Client is the API client that performs all operations\n// against a docker server.\ntype Client struct {\n\t// scheme sets the scheme for the client\n\tscheme string\n\t// host holds the server address to connect to\n\thost string\n\t// proto holds the client protocol i.e. unix.\n\tproto string\n\t// addr holds the client address.\n\taddr string\n\t// basePath holds the path to prepend to the requests.\n\tbasePath string\n\t// client used to send and receive http requests.\n\tclient *http.Client\n\t// version of the server to talk to.\n\tversion string\n\t// userAgent is the User-Agent header to use for HTTP requests. It takes\n\t// precedence over User-Agent headers set in customHTTPHeaders, and other\n\t// header variables. When set to an empty string, the User-Agent header\n\t// is removed, and no header is sent.\n\tuserAgent *string\n\t// custom HTTP headers configured by users.\n\tcustomHTTPHeaders map[string]string\n\n\t// negotiateVersion indicates if the client should automatically negotiate\n\t// the API version to use when making requests. API version negotiation is\n\t// performed on the first request, after which negotiated is set to \"true\"\n\t// so that subsequent requests do not re-negotiate.\n\tnegotiateVersion bool\n\n\t// negotiated indicates that API version negotiation took place\n\tnegotiated atomic.Bool\n\n\t// negotiateLock is used to single-flight the version negotiation process\n\tnegotiateLock sync.Mutex\n\n\t// When the client transport is an *http.Transport (default) we need to do some extra things (like closing idle connections).\n\t// Store the original transport as the http.Client transport will be wrapped with tracing libs.\n\tbaseTransport *http.Transport\n}\n\n// ErrRedirect is the error returned by checkRedirect when the request is non-GET.\nvar ErrRedirect = errors.New(\"unexpected redirect in response\")\n\n// CheckRedirect specifies the policy for dealing with redirect responses. It\n// can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for\n// non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise\n// returns a [http.ErrUseLastResponse], which is special-cased by http.Client\n// to use the last response.\n//\n// Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308)\n// in the client. The client (and by extension API client) can be made to send\n// a request like \"POST /containers//start\" where what would normally be in the\n// name section of the URL is empty. This triggers an HTTP 301 from the daemon.\n//\n// In go 1.8 this 301 is converted to a GET request, and ends up getting\n// a 404 from the daemon. This behavior change manifests in the client in that\n// before, the 301 was not followed and the client did not generate an error,\n// but now results in a message like \"Error response from daemon: page not found\".\nfunc CheckRedirect(_ *http.Request, via []*http.Request) error {\n\tif via[0].Method == http.MethodGet {\n\t\treturn http.ErrUseLastResponse\n\t}\n\treturn ErrRedirect\n}\n\n// NewClientWithOpts initializes a new API client with a default HTTPClient, and\n// default API host and version. It also initializes the custom HTTP headers to\n// add to each request.\nfunc NewClientWithOpts(ops ...Opt) (*Client, error) {\n\thostURL, err := ParseHostURL(DefaultDockerHost)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := defaultHTTPClient(hostURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc := &Client{\n\t\thost:    DefaultDockerHost,\n\t\tversion: api.DefaultVersion,\n\t\tclient:  client,\n\t\tproto:   hostURL.Scheme,\n\t\taddr:    hostURL.Host,\n\t\tscheme:  \"http\",\n\t}\n\n\tfor _, op := range ops {\n\t\tif err := op(c); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif tr, ok := c.client.Transport.(*http.Transport); ok {\n\t\t// Store the base transport\n\t\t// This is used, as an example, to close idle connections when the client is closed\n\t\tc.baseTransport = tr\n\t}\n\n\treturn c, nil\n}\n\nfunc defaultHTTPClient(hostURL *url.URL) (*http.Client, error) {\n\ttransport := &http.Transport{}\n\t// Necessary to prevent long-lived processes using the\n\t// client from leaking connections due to idle connections\n\t// not being released.\n\ttransport.MaxIdleConns = 6\n\ttransport.IdleConnTimeout = 30 * time.Second\n\terr := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &http.Client{\n\t\tTransport:     transport,\n\t\tCheckRedirect: CheckRedirect,\n\t}, nil\n}\n\n// Close the transport used by the client\nfunc (cli *Client) Close() error {\n\tif cli.baseTransport != nil {\n\t\tcli.baseTransport.CloseIdleConnections()\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// checkVersion manually triggers API version negotiation (if configured).\n// This allows for version-dependent code to use the same version as will\n// be negotiated when making the actual requests, and for which cases\n// we cannot do the negotiation lazily.\nfunc (cli *Client) checkVersion(ctx context.Context) error {\n\tif cli.negotiateVersion && !cli.negotiated.Load() {\n\t\t// Ensure exclusive write access to version and negotiated fields\n\t\tcli.negotiateLock.Lock()\n\t\tdefer cli.negotiateLock.Unlock()\n\n\t\t// May have been set during last execution of critical zone\n\t\tif cli.negotiated.Load() {\n\t\t\treturn nil\n\t\t}\n\n\t\tping, err := cli.Ping(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcli.negotiateAPIVersionPing(ping)\n\t}\n\treturn nil\n}\n\n// getAPIPath returns the versioned request path to call the API.\n// It appends the query parameters to the path if they are not empty.\nfunc (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {\n\tvar apiPath string\n\t_ = cli.checkVersion(ctx)\n\tif cli.version != \"\" {\n\t\tapiPath = path.Join(cli.basePath, \"/v\"+strings.TrimPrefix(cli.version, \"v\"), p)\n\t} else {\n\t\tapiPath = path.Join(cli.basePath, p)\n\t}\n\treturn (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String()\n}\n\n// negotiateAPIVersionPing queries the API and updates the version to match the\n// API version from the ping response.\nfunc (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) {\n\t// default to the latest version before versioning headers existed\n\tif pingResponse.APIVersion == \"\" {\n\t\tpingResponse.APIVersion = fallbackAPIVersion\n\t}\n\n\t// if the client is not initialized with a version, start with the latest supported version\n\tif cli.version == \"\" {\n\t\tcli.version = api.DefaultVersion\n\t}\n\n\t// if server version is lower than the client version, downgrade\n\tif versions.LessThan(pingResponse.APIVersion, cli.version) {\n\t\tcli.version = pingResponse.APIVersion\n\t}\n\n\t// Store the results, so that automatic API version negotiation (if enabled)\n\t// won't be performed on the next request.\n\tif cli.negotiateVersion {\n\t\tcli.negotiated.Store(true)\n\t}\n}\n\n// ParseHostURL parses a url string, validates the string is a host url, and\n// returns the parsed URL\nfunc ParseHostURL(host string) (*url.URL, error) {\n\tproto, addr, ok := strings.Cut(host, \"://\")\n\tif !ok || addr == \"\" {\n\t\treturn nil, fmt.Errorf(\"unable to parse docker host `%s`\", host)\n\t}\n\n\tvar basePath string\n\tif proto == \"tcp\" {\n\t\tparsed, err := url.Parse(\"tcp://\" + addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\taddr = parsed.Host\n\t\tbasePath = parsed.Path\n\t}\n\treturn &url.URL{\n\t\tScheme: proto,\n\t\tHost:   addr,\n\t\tPath:   basePath,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/docker/client/container_list.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/container_list.go\n*/\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/url\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/container\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/filters\"\n)\n\n// ContainerList returns the list of containers in the docker host.\nfunc (cli *Client) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) {\n\tquery := url.Values{}\n\n\tif options.Filters.Len() > 0 {\n\t\tfilterJSON, err := filters.ToJSON(options.Filters)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tquery.Set(\"filters\", filterJSON)\n\t}\n\n\tresp, err := cli.get(ctx, \"/containers/json\", query, nil)\n\tdefer ensureReaderClosed(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar containers []container.Summary\n\terr = json.NewDecoder(resp.Body).Decode(&containers)\n\treturn containers, err\n}\n"
  },
  {
    "path": "internal/docker/client/errors.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/errors.go\n*/\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// errConnectionFailed implements an error returned when connection failed.\ntype errConnectionFailed struct {\n\terror\n}\n\n// Error returns a string representation of an errConnectionFailed\nfunc (e errConnectionFailed) Error() string {\n\treturn e.error.Error()\n}\n\nfunc (e errConnectionFailed) Unwrap() error {\n\treturn e.error\n}\n\n// IsErrConnectionFailed returns true if the error is caused by connection failed.\nfunc IsErrConnectionFailed(err error) bool {\n\treturn errors.As(err, &errConnectionFailed{})\n}\n\n// connectionFailed returns an error with host in the error message when connection\n// to docker daemon failed.\nfunc connectionFailed(host string) error {\n\tvar err error\n\tif host == \"\" {\n\t\terr = errors.New(\"cannot connect to the Docker daemon: is the docker daemon running on this host?\")\n\t} else {\n\t\terr = fmt.Errorf(\"cannot connect to the Docker daemon at %s: is the docker daemon running?\", host)\n\t}\n\treturn errConnectionFailed{error: err}\n}\n"
  },
  {
    "path": "internal/docker/client/events.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/events.go\n*/\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/url\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/events\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/filters\"\n)\n\n// Events returns a stream of events in the daemon. It's up to the caller to close the stream\n// by cancelling the context. Once the stream has been completely read an io.EOF error will\n// be sent over the error channel. If an error is sent all processing will be stopped. It's up\n// to the caller to reopen the stream in the event of an error by reinvoking this method.\nfunc (cli *Client) Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) {\n\tmessages := make(chan events.Message)\n\terrs := make(chan error, 1)\n\n\tstarted := make(chan struct{})\n\tgo func() {\n\t\tdefer close(errs)\n\n\t\tquery, err := buildEventsQueryParams(options)\n\t\tif err != nil {\n\t\t\tclose(started)\n\t\t\terrs <- err\n\t\t\treturn\n\t\t}\n\n\t\tresp, err := cli.get(ctx, \"/events\", query, nil)\n\t\tif err != nil {\n\t\t\tclose(started)\n\t\t\terrs <- err\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tdecoder := json.NewDecoder(resp.Body)\n\n\t\tclose(started)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\terrs <- ctx.Err()\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tvar event events.Message\n\t\t\t\tif err := decoder.Decode(&event); err != nil {\n\t\t\t\t\terrs <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase messages <- event:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrs <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\t<-started\n\n\treturn messages, errs\n}\n\nfunc buildEventsQueryParams(options events.ListOptions) (url.Values, error) {\n\tquery := url.Values{}\n\n\tif options.Filters.Len() > 0 {\n\t\tfilterJSON, err := filters.ToJSON(options.Filters)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tquery.Set(\"filters\", filterJSON)\n\t}\n\n\treturn query, nil\n}\n"
  },
  {
    "path": "internal/docker/client/options.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/options.go\n*/\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/go-connections/sockets\"\n)\n\n// Opt is a configuration option to initialize a [Client].\ntype Opt func(*Client) error\n\n// WithHost overrides the client host with the specified one.\nfunc WithHost(host string) Opt {\n\treturn func(c *Client) error {\n\t\thostURL, err := ParseHostURL(host)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.host = host\n\t\tc.proto = hostURL.Scheme\n\t\tc.addr = hostURL.Host\n\t\tc.basePath = hostURL.Path\n\t\tif transport, ok := c.client.Transport.(*http.Transport); ok {\n\t\t\treturn sockets.ConfigureTransport(transport, c.proto, c.addr)\n\t\t}\n\t\treturn fmt.Errorf(\"cannot apply host to transport: %v\", c.client.Transport)\n\t}\n}\n\n// WithAPIVersionNegotiation enables automatic API version negotiation for the client.\n// With this option enabled, the client automatically negotiates the API version\n// to use when making requests. API version negotiation is performed on the first\n// request; subsequent requests do not re-negotiate.\nfunc WithAPIVersionNegotiation() Opt {\n\treturn func(c *Client) error {\n\t\tc.negotiateVersion = true\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/docker/client/ping.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/ping.go\n*/\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"path\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types\"\n)\n\n// Ping pings the server and returns the value of the \"API-Version\" header.\n// It attempts to use a HEAD request on the endpoint, but falls back to GET if\n// HEAD is not supported by the daemon. It ignores internal server errors\n// returned by the API, which may be returned if the daemon is in an unhealthy\n// state, but returns errors for other non-success status codes, failing to\n// connect to the API, or failing to parse the API response.\nfunc (cli *Client) Ping(ctx context.Context) (types.Ping, error) {\n\tvar ping types.Ping\n\n\t// Using cli.buildRequest() + cli.doRequest() instead of cli.sendRequest()\n\t// because ping requests are used during API version negotiation, so we want\n\t// to hit the non-versioned /_ping endpoint, not /v1.xx/_ping\n\treq, err := cli.buildRequest(ctx, http.MethodHead, path.Join(cli.basePath, \"/_ping\"), nil, nil)\n\tif err != nil {\n\t\treturn ping, err\n\t}\n\tresp, err := cli.doRequest(req)\n\tif err != nil {\n\t\tif IsErrConnectionFailed(err) {\n\t\t\treturn ping, err\n\t\t}\n\t\t// We managed to connect, but got some error; continue and try GET request.\n\t} else {\n\t\tdefer ensureReaderClosed(resp)\n\t\tswitch resp.StatusCode {\n\t\tcase http.StatusOK, http.StatusInternalServerError:\n\t\t\t// Server handled the request, so parse the response\n\t\t\treturn parsePingResponse(cli, resp)\n\t\t}\n\t}\n\n\t// HEAD failed; fallback to GET.\n\treq.Method = http.MethodGet\n\tresp, err = cli.doRequest(req)\n\tdefer ensureReaderClosed(resp)\n\tif err != nil {\n\t\treturn ping, err\n\t}\n\treturn parsePingResponse(cli, resp)\n}\n\nfunc parsePingResponse(cli *Client, resp *http.Response) (types.Ping, error) {\n\tif resp == nil {\n\t\treturn types.Ping{}, nil\n\t}\n\n\tvar ping types.Ping\n\tif resp.Header == nil {\n\t\treturn ping, cli.checkResponseErr(resp)\n\t}\n\tping.APIVersion = resp.Header.Get(\"Api-Version\")\n\treturn ping, cli.checkResponseErr(resp)\n}\n"
  },
  {
    "path": "internal/docker/client/request.go",
    "content": "package client\n\n/*\nThis was modified from:\nhttps://github.com/moby/moby/blob/v28.5.1/client/request.go\n*/\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types\"\n\t\"github.com/wollomatic/socket-proxy/internal/docker/api/types/versions\"\n)\n\n// get sends an http request to the docker API using the method GET with a specific Go context.\nfunc (cli *Client) get(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {\n\treturn cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers)\n}\n\nfunc (cli *Client) buildRequest(ctx context.Context, method, path string, body io.Reader, headers http.Header) (*http.Request, error) {\n\treq, err := http.NewRequestWithContext(ctx, method, path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq = cli.addHeaders(req, headers)\n\treq.URL.Scheme = cli.scheme\n\treq.URL.Host = cli.addr\n\n\tif cli.proto == \"unix\" {\n\t\t// Override host header for non-tcp connections.\n\t\treq.Host = DummyHost\n\t}\n\n\tif body != nil && req.Header.Get(\"Content-Type\") == \"\" {\n\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t}\n\treturn req, nil\n}\n\nfunc (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {\n\treq, err := cli.buildRequest(ctx, method, cli.getAPIPath(ctx, path, query), body, headers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := cli.doRequest(req)\n\tswitch {\n\tcase errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):\n\t\treturn nil, err\n\tcase err == nil:\n\t\treturn resp, cli.checkResponseErr(resp)\n\tdefault:\n\t\treturn resp, err\n\t}\n}\n\nfunc (cli *Client) doRequest(req *http.Request) (*http.Response, error) {\n\tresp, err := cli.client.Do(req) // #nosec G704 - Request target is always the specified socket\n\tif err != nil {\n\t\t// Don't decorate context sentinel errors; users may be comparing to\n\t\t// them directly.\n\t\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif uErr, ok := errors.AsType[*url.Error](err); ok {\n\t\t\tif nErr, ok2 := errors.AsType[*net.OpError](uErr.Err); ok2 {\n\t\t\t\tif os.IsPermission(nErr.Err) {\n\t\t\t\t\treturn nil, errConnectionFailed{fmt.Errorf(\"permission denied while trying to connect to the Docker daemon socket at %v: %v\", cli.host, err)}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif nErr, ok := errors.AsType[net.Error](err); ok {\n\t\t\tif nErr.Timeout() {\n\t\t\t\treturn nil, connectionFailed(cli.host)\n\t\t\t}\n\t\t\tif strings.Contains(nErr.Error(), \"connection refused\") || strings.Contains(nErr.Error(), \"dial unix\") {\n\t\t\t\treturn nil, connectionFailed(cli.host)\n\t\t\t}\n\t\t}\n\n\t\treturn nil, errConnectionFailed{fmt.Errorf(\"error during connect: %v\", err)}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (cli *Client) checkResponseErr(serverResp *http.Response) (retErr error) {\n\tif serverResp == nil {\n\t\treturn nil\n\t}\n\tif serverResp.StatusCode >= http.StatusOK && serverResp.StatusCode < http.StatusBadRequest {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tretErr = fmt.Errorf(\"HTTP error %d: %v\", serverResp.StatusCode, retErr)\n\t\t}\n\t}()\n\n\tvar body []byte\n\tvar err error\n\tvar reqURL string\n\tif serverResp.Request != nil {\n\t\treqURL = serverResp.Request.URL.String()\n\t}\n\tstatusMsg := serverResp.Status\n\tif statusMsg == \"\" {\n\t\tstatusMsg = http.StatusText(serverResp.StatusCode)\n\t}\n\tif serverResp.Body != nil {\n\t\tbodyMax := 1 * 1024 * 1024 // 1 MiB\n\t\tbodyR := &io.LimitedReader{\n\t\t\tR: serverResp.Body,\n\t\t\tN: int64(bodyMax),\n\t\t}\n\t\tbody, err = io.ReadAll(bodyR)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif bodyR.N == 0 {\n\t\t\tif reqURL != \"\" {\n\t\t\t\treturn fmt.Errorf(\"request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version\", statusMsg, bodyMax, reqURL)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"request returned %s with a message (> %d bytes); check if the server supports the requested API version\", statusMsg, bodyMax)\n\t\t}\n\t}\n\tif len(body) == 0 {\n\t\tif reqURL != \"\" {\n\t\t\treturn fmt.Errorf(\"request returned %s for API route and version %s, check if the server supports the requested API version\", statusMsg, reqURL)\n\t\t}\n\t\treturn fmt.Errorf(\"request returned %s; check if the server supports the requested API version\", statusMsg)\n\t}\n\n\tvar daemonErr error\n\tif serverResp.Header.Get(\"Content-Type\") == \"application/json\" {\n\t\tvar errorResponse types.ErrorResponse\n\t\tif err := json.Unmarshal(body, &errorResponse); err != nil {\n\t\t\treturn fmt.Errorf(\"error reading JSON: %v\", err)\n\t\t}\n\t\tif errorResponse.Message == \"\" {\n\t\t\t// Error-message is empty, which means that we successfully parsed the\n\t\t\t// JSON-response (no error produced), but it didn't contain an error\n\t\t\t// message. This could either be because the response was empty, or\n\t\t\t// the response was valid JSON, but not with the expected schema\n\t\t\t// ([types.ErrorResponse]).\n\t\t\t//\n\t\t\t// We cannot use \"strict\" JSON handling (json.NewDecoder with DisallowUnknownFields)\n\t\t\t// due to the API using an open schema (we must anticipate fields\n\t\t\t// being added to [types.ErrorResponse] in the future, and not\n\t\t\t// reject those responses.\n\t\t\t//\n\t\t\t// For these cases, we construct an error with the status-code\n\t\t\t// returned, but we could consider returning (a truncated version\n\t\t\t// of) the actual response as-is.\n\n\t\t\tdaemonErr = fmt.Errorf(`API returned a %d (%s) but provided no error-message`,\n\t\t\t\tserverResp.StatusCode,\n\t\t\t\thttp.StatusText(serverResp.StatusCode),\n\t\t\t)\n\t\t} else {\n\t\t\tdaemonErr = errors.New(strings.TrimSpace(errorResponse.Message))\n\t\t}\n\t} else {\n\t\t// Fall back to returning the response as-is for API versions < 1.24\n\t\t// that didn't support JSON error responses, and for situations\n\t\t// where a plain text error is returned. This branch may also catch\n\t\t// situations where a proxy is involved, returning a HTML response.\n\t\tdaemonErr = errors.New(strings.TrimSpace(string(body)))\n\t}\n\treturn fmt.Errorf(\"error response from daemon: %v\", daemonErr)\n}\n\nfunc (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Request {\n\t// Add CLI Config's HTTP Headers BEFORE we set the Docker headers\n\t// then the user can't change OUR headers\n\tfor k, v := range cli.customHTTPHeaders {\n\t\tif versions.LessThan(cli.version, \"1.25\") && http.CanonicalHeaderKey(k) == \"User-Agent\" {\n\t\t\tcontinue\n\t\t}\n\t\treq.Header.Set(k, v)\n\t}\n\n\tfor k, v := range headers {\n\t\treq.Header[http.CanonicalHeaderKey(k)] = v\n\t}\n\n\tif cli.userAgent != nil {\n\t\tif *cli.userAgent == \"\" {\n\t\t\treq.Header.Del(\"User-Agent\")\n\t\t} else {\n\t\t\treq.Header.Set(\"User-Agent\", *cli.userAgent)\n\t\t}\n\t}\n\treturn req\n}\n\nfunc ensureReaderClosed(response *http.Response) {\n\tif response != nil && response.Body != nil {\n\t\t// Drain up to 512 bytes and close the body to let the Transport reuse the connection\n\t\t// see https://github.com/google/go-github/pull/317/files#r57536827\n\n\t\t_, _ = io.CopyN(io.Discard, response.Body, 512)\n\t\t_ = response.Body.Close()\n\t}\n}\n"
  },
  {
    "path": "internal/go-connections/sockets/sockets.go",
    "content": "/*\nPackage sockets provides helper functions to create and configure Unix or TCP sockets.\n\nThis was modified from:\nhttps://github.com/docker/go-connections/blob/v0.6.0/sockets/sockets.go\n*/\npackage sockets\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"syscall\"\n\t\"time\"\n)\n\nconst (\n\tdefaultTimeout        = 10 * time.Second\n\tmaxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)\n)\n\n// ConfigureTransport configures the specified [http.Transport] according to the specified proto\n// and addr.\n//\n// If the proto is unix (using a unix socket to communicate) or npipe the compression is disabled.\n// For other protos, compression is enabled. If you want to manually enable/disable compression,\n// make sure you do it _after_ any subsequent calls to ConfigureTransport is made against the same\n// [http.Transport].\nfunc ConfigureTransport(tr *http.Transport, proto, addr string) error {\n\tif tr.MaxIdleConns == 0 {\n\t\t// prevent long-lived processes from leaking connections\n\t\t// due to idle connections not being released.\n\t\t//\n\t\t// TODO: see if we can also address this from the server side; see: https://github.com/moby/moby/issues/45539\n\t\ttr.MaxIdleConns = 6\n\t\ttr.IdleConnTimeout = 30 * time.Second\n\t}\n\tswitch proto {\n\tcase \"unix\":\n\t\treturn configureUnixTransport(tr, addr)\n\tdefault:\n\t\ttr.Proxy = http.ProxyFromEnvironment\n\t\ttr.DisableCompression = false\n\t\ttr.DialContext = (&net.Dialer{\n\t\t\tTimeout: defaultTimeout,\n\t\t}).DialContext\n\t}\n\treturn nil\n}\n\nfunc configureUnixTransport(tr *http.Transport, addr string) error {\n\tif len(addr) > maxUnixSocketPathSize {\n\t\treturn fmt.Errorf(\"unix socket path %q is too long\", addr)\n\t}\n\t// No need for compression in local communications.\n\ttr.DisableCompression = true\n\tdialer := &net.Dialer{\n\t\tTimeout: defaultTimeout,\n\t}\n\ttr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\treturn dialer.DialContext(ctx, \"unix\", addr)\n\t}\n\treturn nil\n}\n"
  }
]