Compare commits

..

35 Commits

Author SHA1 Message Date
Owen
c5d5fcedd9 Make sure to process version first
Former-commit-id: f0309857b9
2025-12-11 19:37:51 -05:00
Owen
13c0a082b5 Update cicd
Former-commit-id: 5a6fcadf91
2025-12-11 16:40:55 -05:00
Owen
48962d4b65 Make cicd create draft
Former-commit-id: 6e31d3dcd5
2025-12-11 16:09:34 -05:00
Owen
c469707986 Update cicd to use right username
Former-commit-id: 41e6324c79
2025-12-11 16:04:25 -05:00
Owen
13c40f6b2c Update cicd
Former-commit-id: 5757e8dca8
2025-12-11 16:00:20 -05:00
Owen
6071be0d08 Merge branch 'dev'
Former-commit-id: 382515a85c
2025-12-11 15:40:51 -05:00
Owen
4b269782ea Update iss
Former-commit-id: 5da2198b35
2025-12-11 12:29:43 -05:00
Owen
518bf0e36a Update link
Former-commit-id: 14f7682be5
2025-12-10 21:03:17 -05:00
Owen
c80bb9740a Update readme
Former-commit-id: 0d27206b28
2025-12-10 16:23:58 -05:00
Owen
3ceef1ef74 Small adjustments
Former-commit-id: df1c2c18e0
2025-12-10 14:06:36 -05:00
Owen
acb0b4a9a5 Fix ipv6 connectivity
Former-commit-id: 61065def17
2025-12-10 10:34:30 -05:00
Owen
29aa68ecf7 Fix docker ignore
Former-commit-id: f24add4f72
2025-12-08 14:05:48 -05:00
Owen
50a97b19d1 Use explicit newt version not local
Former-commit-id: 630b55008b
2025-12-08 14:00:59 -05:00
Owen
229ce7504f Merge branch 'dev'
Former-commit-id: ead73ca1c5
2025-12-08 12:12:47 -05:00
Owen
b4f3619aff Update test
Former-commit-id: 25644db2f3
2025-12-08 12:12:35 -05:00
Owen Schwartz
e77a4fbd66 Merge pull request #57 from fosrl/dev
Add robust client connectivity support

Former-commit-id: 1f15ecc4b2
2025-12-08 12:05:52 -05:00
Owen
f8f368a981 Update readme
Former-commit-id: 1687099c52
2025-12-07 21:25:24 -05:00
Owen
153b986100 Adapt args to work on windows
Former-commit-id: 7546fc82ac
2025-12-07 17:44:10 -05:00
Owen
1c47c0981c Fix small bugs =
Former-commit-id: 02c838eb86
2025-12-07 12:05:27 -05:00
Owen
defd85e118 Add site name
Former-commit-id: 2a60de4f1f
2025-12-07 10:52:22 -05:00
Owen
ec1085f5f7 Merge branch 'main' into dev
Former-commit-id: b2814dc157
2025-12-06 21:09:44 -05:00
Owen Schwartz
a39e6d4f2b Merge pull request #54 from fosrl/dependabot/go_modules/prod-minor-updates-dd7da38a6b
Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in the prod-minor-updates group

Former-commit-id: a9efe7c91a
2025-12-06 12:01:08 -05:00
Owen Schwartz
4875835024 Merge pull request #55 from fosrl/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6

Former-commit-id: 50b9dae88c
2025-12-06 12:00:51 -05:00
Owen Schwartz
f5a74c36f8 Merge pull request #56 from fosrl/dependabot/docker/minor-updates-60be0b6e22
Bump alpine from 3.22 to 3.23 in the minor-updates group

Former-commit-id: 84257a094e
2025-12-06 12:00:38 -05:00
dependabot[bot]
3e24a77625 Bump alpine from 3.22 to 3.23 in the minor-updates group
Bumps the minor-updates group with 1 update: alpine.


Updates `alpine` from 3.22 to 3.23

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: a9423b01e6
2025-12-03 20:24:29 +00:00
dependabot[bot]
534631fb27 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: bd9e8857bf
2025-11-24 20:46:59 +00:00
dependabot[bot]
f93f73f541 Bump golang.org/x/crypto in the prod-minor-updates group
Bumps the prod-minor-updates group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 0fc7f22f1a
2025-11-19 20:29:21 +00:00
Owen Schwartz
b87f90c211 Merge pull request #48 from fosrl/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5

Former-commit-id: f7f072b919
2025-11-14 10:28:22 -05:00
Owen Schwartz
3e0cefa3dc Merge pull request #53 from fosrl/dependabot/go_modules/prod-minor-updates-aca723b595
Bump the prod-minor-updates group across 1 directory with 2 updates

Former-commit-id: 616ab62bc1
2025-11-14 10:27:44 -05:00
Owen Schwartz
2fe3359ae8 Merge pull request #52 from fosrl/copilot/fix-system-path-removal
Fix Windows uninstaller deleting entire System PATH instead of OLM entry

Former-commit-id: 0398e68788
2025-11-14 10:27:30 -05:00
dependabot[bot]
0aa8f07be3 Bump the prod-minor-updates group across 1 directory with 2 updates
Bumps the prod-minor-updates group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.44.0)

Updates `golang.org/x/sys` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/sys/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: e061955e90
2025-11-11 20:19:38 +00:00
copilot-swe-agent[bot]
36d47a7331 Refactor PATH removal to use TStringList for more robust parsing
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>

Former-commit-id: 91f0230d21
2025-11-11 01:46:51 +00:00
copilot-swe-agent[bot]
10fa5acb0b Fix Windows PATH removal issue by implementing custom uninstall procedure
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>

Former-commit-id: 1168f5541c
2025-11-11 01:44:40 +00:00
copilot-swe-agent[bot]
e3a679609f Initial plan
Former-commit-id: d910034ea1
2025-11-11 01:41:18 +00:00
dependabot[bot]
b7a04dc511 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 33e28ead68
2025-10-27 21:16:53 +00:00
21 changed files with 1163 additions and 472 deletions

View File

@@ -1,9 +1,9 @@
.gitignore
.dockerignore
olm
*.json
README.md
Makefile
public/
LICENSE
CONTRIBUTING.md
CONTRIBUTING.md
bin/

View File

@@ -1,60 +1,613 @@
name: CI/CD Pipeline
permissions:
contents: write # gh-release
packages: write # GHCR push
id-token: write # Keyless-Signatures & Attestations
attestations: write # actions/attest-build-provenance
security-events: write # upload-sarif
actions: read
on:
push:
tags:
- "*"
push:
tags:
- "*"
workflow_dispatch:
inputs:
version:
description: "SemVer version to release (e.g., 1.2.3, no leading 'v')"
required: true
type: string
publish_latest:
description: "Also publish the 'latest' image tag"
required: true
type: boolean
default: false
publish_minor:
description: "Also publish the 'major.minor' image tag (e.g., 1.2)"
required: true
type: boolean
default: false
target_branch:
description: "Branch to tag"
required: false
default: "main"
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}
cancel-in-progress: true
jobs:
release:
name: Build and Release
runs-on: amd64-runner
prepare:
if: github.event_name == 'workflow_dispatch'
name: Prepare release (create tag)
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Validate version input
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if ! [[ "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "Invalid version: $INPUT_VERSION (expected X.Y.Z or X.Y.Z-rc.N)" >&2
exit 1
fi
- name: Create and push tag
shell: bash
env:
TARGET_BRANCH: ${{ inputs.target_branch }}
VERSION: ${{ inputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch --prune origin
git checkout "$TARGET_BRANCH"
git pull --ff-only origin "$TARGET_BRANCH"
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "Tag $VERSION already exists" >&2
exit 1
fi
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "refs/tags/$VERSION"
release:
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.actor != 'github-actions[bot]') }}
name: Build and Release
runs-on: ubuntu-24.04
timeout-minutes: 120
env:
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Capture created timestamp
run: echo "IMAGE_CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
shell: bash
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Set up 1.2.0 Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: 1.25
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Update version in main.go
run: |
TAG=${{ env.TAG }}
if [ -f main.go ]; then
sed -i 's/version_replaceme/'"$TAG"'/' main.go
echo "Updated main.go with version $TAG"
else
echo "main.go not found"
fi
- name: Build and push Docker images
run: |
TAG=${{ env.TAG }}
make docker-build-release tag=$TAG
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build binaries
run: |
make go-build-release
- name: Normalize image names to lowercase
run: |
set -euo pipefail
echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV"
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
shell: bash
- name: Upload artifacts from /bin
uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/
- name: Extract tag name
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "TAG=${INPUT_VERSION}" >> $GITHUB_ENV
else
echo "TAG=${{ github.ref_name }}" >> $GITHUB_ENV
fi
shell: bash
- name: Validate pushed tag format (no leading 'v')
if: ${{ github.event_name == 'push' }}
shell: bash
env:
TAG_GOT: ${{ env.TAG }}
run: |
set -euo pipefail
if [[ "$TAG_GOT" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "Tag OK: $TAG_GOT"
exit 0
fi
echo "ERROR: Tag '$TAG_GOT' is not allowed. Use 'X.Y.Z' or 'X.Y.Z-rc.N' (no leading 'v')." >&2
exit 1
- name: Wait for tag to be visible (dispatch only)
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
set -euo pipefail
for i in {1..90}; do
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -qE "refs/tags/${TAG}$"; then
echo "Tag ${TAG} is visible on origin"; exit 0
fi
echo "Tag not yet visible, retrying... ($i/90)"
sleep 2
done
echo "Tag ${TAG} not visible after waiting"; exit 1
shell: bash
- name: Update version in main.go
run: |
TAG=${{ env.TAG }}
if [ -f main.go ]; then
sed -i 's/version_replaceme/'"$TAG"'/' main.go
echo "Updated main.go with version $TAG"
else
echo "main.go not found"
fi
- name: Ensure repository is at the tagged commit (dispatch only)
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
set -euo pipefail
git fetch --tags --force
git checkout "refs/tags/${TAG}"
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
shell: bash
- name: Detect release candidate (rc)
run: |
set -euo pipefail
if [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version-file: go.mod
- name: Resolve publish-latest flag
env:
EVENT_NAME: ${{ github.event_name }}
PL_INPUT: ${{ inputs.publish_latest }}
PL_VAR: ${{ vars.PUBLISH_LATEST }}
run: |
set -euo pipefail
val="false"
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ "${PL_INPUT}" = "true" ]; then val="true"; fi
else
if [ "${PL_VAR}" = "true" ]; then val="true"; fi
fi
echo "PUBLISH_LATEST=$val" >> $GITHUB_ENV
shell: bash
- name: Resolve publish-minor flag
env:
EVENT_NAME: ${{ github.event_name }}
PM_INPUT: ${{ inputs.publish_minor }}
PM_VAR: ${{ vars.PUBLISH_MINOR }}
run: |
set -euo pipefail
val="false"
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ "${PM_INPUT}" = "true" ]; then val="true"; fi
else
if [ "${PM_VAR}" = "true" ]; then val="true"; fi
fi
echo "PUBLISH_MINOR=$val" >> $GITHUB_ENV
shell: bash
- name: Cache Go modules
if: ${{ hashFiles('**/go.sum') != '' }}
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Go vet & test
if: ${{ hashFiles('**/go.mod') != '' }}
run: |
go version
go vet ./...
go test ./... -race -covermode=atomic
shell: bash
- name: Resolve license fallback
run: echo "IMAGE_LICENSE=${{ github.event.repository.license.spdx_id || 'NOASSERTION' }}" >> $GITHUB_ENV
shell: bash
- name: Resolve registries list (GHCR always, Docker Hub only if creds)
shell: bash
run: |
set -euo pipefail
images="${GHCR_IMAGE}"
if [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ] && [ -n "${{ secrets.DOCKER_HUB_USERNAME }}" ]; then
images="${images}\n${DOCKERHUB_IMAGE}"
fi
{
echo 'IMAGE_LIST<<EOF'
echo -e "$images"
echo 'EOF'
} >> "$GITHUB_ENV"
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ env.IMAGE_LIST }}
tags: |
type=semver,pattern={{version}},value=${{ env.TAG }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.TAG }},enable=${{ env.PUBLISH_MINOR == 'true' && env.IS_RC != 'true' }}
type=raw,value=latest,enable=${{ env.PUBLISH_LATEST == 'true' && env.IS_RC != 'true' }}
flavor: |
latest=false
labels: |
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.version=${{ env.TAG }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.documentation=${{ github.event.repository.html_url }}
org.opencontainers.image.description=${{ github.event.repository.description }}
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
org.opencontainers.image.created=${{ env.IMAGE_CREATED }}
org.opencontainers.image.ref.name=${{ env.TAG }}
org.opencontainers.image.authors=${{ github.repository_owner }}
- name: Echo build config (non-secret)
shell: bash
env:
IMAGE_TITLE: ${{ github.event.repository.name }}
IMAGE_VERSION: ${{ env.TAG }}
IMAGE_REVISION: ${{ github.sha }}
IMAGE_SOURCE_URL: ${{ github.event.repository.html_url }}
IMAGE_URL: ${{ github.event.repository.html_url }}
IMAGE_DESCRIPTION: ${{ github.event.repository.description }}
IMAGE_LICENSE: ${{ env.IMAGE_LICENSE }}
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USERNAME }}
REPO: ${{ github.repository }}
OWNER: ${{ github.repository_owner }}
WORKFLOW_REF: ${{ github.workflow_ref }}
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
echo "=== OCI Label Values ==="
echo "org.opencontainers.image.title=${IMAGE_TITLE}"
echo "org.opencontainers.image.version=${IMAGE_VERSION}"
echo "org.opencontainers.image.revision=${IMAGE_REVISION}"
echo "org.opencontainers.image.source=${IMAGE_SOURCE_URL}"
echo "org.opencontainers.image.url=${IMAGE_URL}"
echo "org.opencontainers.image.description=${IMAGE_DESCRIPTION}"
echo "org.opencontainers.image.licenses=${IMAGE_LICENSE}"
echo
echo "=== Images ==="
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE}"
echo "GHCR_IMAGE=${GHCR_IMAGE}"
echo "DOCKER_HUB_USERNAME=${DOCKER_HUB_USER}"
echo
echo "=== GitHub Kontext ==="
echo "repository=${REPO}"
echo "owner=${OWNER}"
echo "workflow_ref=${WORKFLOW_REF}"
echo "ref=${REF}"
echo "ref_name=${REF_NAME}"
echo "run_url=${RUN_URL}"
echo
echo "=== docker/metadata-action outputs (Tags/Labels), raw ==="
echo "::group::tags"
echo "${{ steps.meta.outputs.tags }}"
echo "::endgroup::"
echo "::group::labels"
echo "${{ steps.meta.outputs.labels }}"
echo "::endgroup::"
- name: Build and push (Docker Hub + GHCR)
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ github.repository }}
cache-to: type=gha,mode=max,scope=${{ github.repository }}
provenance: mode=max
sbom: true
- name: Compute image digest refs
run: |
echo "DIGEST=${{ steps.build.outputs.digest }}" >> $GITHUB_ENV
echo "GHCR_REF=$GHCR_IMAGE@${{ steps.build.outputs.digest }}" >> $GITHUB_ENV
echo "DH_REF=$DOCKERHUB_IMAGE@${{ steps.build.outputs.digest }}" >> $GITHUB_ENV
echo "Built digest: ${{ steps.build.outputs.digest }}"
shell: bash
- name: Attest build provenance (GHCR)
id: attest-ghcr
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
show-summary: true
- name: Attest build provenance (Docker Hub)
continue-on-error: true
id: attest-dh
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: index.docker.io/fosrl/${{ github.event.repository.name }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
show-summary: true
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: 'v3.0.2'
- name: Sanity check cosign private key
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null
shell: bash
- name: Sign GHCR image (digest) with key (recursive)
env:
COSIGN_YES: "true"
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
echo "Signing ${GHCR_REF} (digest) recursively with provided key"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${GHCR_REF}"
echo "Waiting 30 seconds for signatures to propagate..."
shell: bash
- name: Generate SBOM (SPDX JSON)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
image-ref: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
format: spdx-json
output: sbom.spdx.json
- name: Validate SBOM JSON
run: jq -e . sbom.spdx.json >/dev/null
shell: bash
- name: Minify SBOM JSON (optional hardening)
run: jq -c . sbom.spdx.json > sbom.min.json && mv sbom.min.json sbom.spdx.json
shell: bash
- name: Create SBOM attestation (GHCR, private key)
env:
COSIGN_YES: "true"
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
cosign attest \
--key env://COSIGN_PRIVATE_KEY \
--type spdxjson \
--predicate sbom.spdx.json \
"${GHCR_REF}"
shell: bash
- name: Create SBOM attestation (Docker Hub, private key)
continue-on-error: true
env:
COSIGN_YES: "true"
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_DOCKER_MEDIA_TYPES: "1"
run: |
set -euo pipefail
cosign attest \
--key env://COSIGN_PRIVATE_KEY \
--type spdxjson \
--predicate sbom.spdx.json \
"${DH_REF}"
shell: bash
- name: Keyless sign & verify GHCR digest (OIDC)
env:
COSIGN_YES: "true"
WORKFLOW_REF: ${{ github.workflow_ref }} # owner/repo/.github/workflows/<file>@refs/tags/<tag>
ISSUER: https://token.actions.githubusercontent.com
run: |
set -euo pipefail
echo "Keyless signing ${GHCR_REF}"
cosign sign --rekor-url https://rekor.sigstore.dev --recursive "${GHCR_REF}"
echo "Verify keyless (OIDC) signature policy on ${GHCR_REF}"
cosign verify \
--certificate-oidc-issuer "${ISSUER}" \
--certificate-identity "https://github.com/${WORKFLOW_REF}" \
"${GHCR_REF}" -o text
shell: bash
- name: Sign Docker Hub image (digest) with key (recursive)
continue-on-error: true
env:
COSIGN_YES: "true"
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_DOCKER_MEDIA_TYPES: "1"
run: |
set -euo pipefail
echo "Signing ${DH_REF} (digest) recursively with provided key (Docker media types fallback)"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${DH_REF}"
shell: bash
- name: Keyless sign & verify Docker Hub digest (OIDC)
continue-on-error: true
env:
COSIGN_YES: "true"
ISSUER: https://token.actions.githubusercontent.com
COSIGN_DOCKER_MEDIA_TYPES: "1"
run: |
set -euo pipefail
echo "Keyless signing ${DH_REF} (force public-good Rekor)"
cosign sign --rekor-url https://rekor.sigstore.dev --recursive "${DH_REF}"
echo "Keyless verify via Rekor (strict identity)"
if ! cosign verify \
--rekor-url https://rekor.sigstore.dev \
--certificate-oidc-issuer "${ISSUER}" \
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
"${DH_REF}" -o text; then
echo "Rekor verify failed — retry offline bundle verify (no Rekor)"
if ! cosign verify \
--offline \
--certificate-oidc-issuer "${ISSUER}" \
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
"${DH_REF}" -o text; then
echo "Offline bundle verify failed — ignore tlog (TEMP for debugging)"
cosign verify \
--insecure-ignore-tlog=true \
--certificate-oidc-issuer "${ISSUER}" \
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
"${DH_REF}" -o text || true
fi
fi
- name: Verify signature (public key) GHCR digest + tag
env:
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
run: |
set -euo pipefail
TAG_VAR="${TAG}"
echo "Verifying (digest) ${GHCR_REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "$GHCR_REF" -o text
echo "Verifying (tag) $GHCR_IMAGE:$TAG_VAR"
cosign verify --key env://COSIGN_PUBLIC_KEY "$GHCR_IMAGE:$TAG_VAR" -o text
shell: bash
- name: Verify SBOM attestation (GHCR)
env:
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
run: cosign verify-attestation --key env://COSIGN_PUBLIC_KEY --type spdxjson "$GHCR_REF" -o text
shell: bash
- name: Verify SLSA provenance (GHCR)
env:
ISSUER: https://token.actions.githubusercontent.com
WFREF: ${{ github.workflow_ref }}
run: |
set -euo pipefail
# (optional) show which predicate types are present to aid debugging
cosign download attestation "$GHCR_REF" \
| jq -r '.payload | @base64d | fromjson | .predicateType' | sort -u || true
# Verify the SLSA v1 provenance attestation (predicate URL)
cosign verify-attestation \
--type 'https://slsa.dev/provenance/v1' \
--certificate-oidc-issuer "$ISSUER" \
--certificate-identity "https://github.com/${WFREF}" \
--rekor-url https://rekor.sigstore.dev \
"$GHCR_REF" -o text
shell: bash
- name: Verify signature (public key) Docker Hub digest
continue-on-error: true
env:
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_DOCKER_MEDIA_TYPES: "1"
run: |
set -euo pipefail
echo "Verifying (digest) ${DH_REF} with Docker media types"
cosign verify --key env://COSIGN_PUBLIC_KEY "${DH_REF}" -o text
shell: bash
- name: Verify signature (public key) Docker Hub tag
continue-on-error: true
env:
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_DOCKER_MEDIA_TYPES: "1"
run: |
set -euo pipefail
echo "Verifying (tag) $DOCKERHUB_IMAGE:$TAG with Docker media types"
cosign verify --key env://COSIGN_PUBLIC_KEY "$DOCKERHUB_IMAGE:$TAG" -o text
shell: bash
# - name: Trivy scan (GHCR image)
# id: trivy
# uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
# with:
# image-ref: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
# format: sarif
# output: trivy-ghcr.sarif
# ignore-unfixed: true
# vuln-type: os,library
# severity: CRITICAL,HIGH
# exit-code: ${{ (vars.TRIVY_FAIL || '0') }}
# - name: Upload SARIF
# if: ${{ always() && hashFiles('trivy-ghcr.sarif') != '' }}
# uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
# with:
# sarif_file: trivy-ghcr.sarif
# category: Image Vulnerability Scan
# - name: Build binaries
# env:
# CGO_ENABLED: "0"
# GOFLAGS: "-trimpath"
# run: |
# set -euo pipefail
# TAG_VAR="${TAG}"
# make go-build-release tag=$TAG_VAR
# shell: bash
# - name: Create GitHub Release
# uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
# with:
# tag_name: ${{ env.TAG }}
# generate_release_notes: true
# prerelease: ${{ env.IS_RC == 'true' }}
# files: |
# bin/*
# fail_on_unmatched_files: true
# draft: true
# body: |
# ## Container Images
# - GHCR: `${{ env.GHCR_REF }}`
# - Docker Hub: `${{ env.DH_REF || 'N/A' }}`
# **Digest:** `${{ steps.build.outputs.digest }}`

View File

@@ -11,7 +11,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Clone fosrl/newt
uses: actions/checkout@v6
with:
repository: fosrl/newt
path: ../newt
- name: Set up Go
uses: actions/setup-go@v6

306
API.md Normal file
View File

@@ -0,0 +1,306 @@
## HTTP API
Olm can be controlled with an embedded HTTP server when using `--enable-http`. This allows you to start it as a daemon and trigger it with the following endpoints. The API can listen on either a TCP address or a Unix socket/Windows named pipe.
### Socket vs TCP
By default, when `--enable-http` is used, Olm listens on a TCP address (configured via `--http-addr`, default `:9452`). Alternatively, Olm can listen on a Unix socket (Linux/macOS) or Windows named pipe for local-only communication with better security.
**Unix Socket (Linux/macOS):**
- Socket path example: `/var/run/olm/olm.sock`
- The directory is created automatically if it doesn't exist
- Socket permissions are set to `0666` to allow access
- Existing socket files are automatically removed on startup
- Socket file is cleaned up when Olm stops
**Windows Named Pipe:**
- Pipe path example: `\\.\pipe\olm`
- If the path doesn't start with `\`, it's automatically prefixed with `\\.\pipe\`
- Security descriptor grants full access to Everyone and the current owner
- Named pipes are automatically cleaned up by Windows
**Connecting to the Socket:**
```bash
# Linux/macOS - using curl with Unix socket
curl --unix-socket /var/run/olm/olm.sock http://localhost/status
---
### POST /connect
Initiates a new connection request to a Pangolin server.
**Request Body:**
```json
{
"id": "string",
"secret": "string",
"endpoint": "string",
"userToken": "string",
"mtu": 1280,
"dns": "8.8.8.8",
"dnsProxyIP": "string",
"upstreamDNS": ["8.8.8.8:53", "1.1.1.1:53"],
"interfaceName": "olm",
"holepunch": false,
"tlsClientCert": "string",
"pingInterval": "3s",
"pingTimeout": "5s",
"orgId": "string"
}
```
**Required Fields:**
- `id`: Olm ID generated by Pangolin
- `secret`: Authentication secret for the Olm ID
- `endpoint`: Target Pangolin endpoint URL
**Optional Fields:**
- `userToken`: User authentication token
- `mtu`: MTU for the internal WireGuard interface (default: 1280)
- `dns`: DNS server to use for resolving the endpoint
- `dnsProxyIP`: DNS proxy IP address
- `upstreamDNS`: Array of upstream DNS servers
- `interfaceName`: Name of the WireGuard interface (default: olm)
- `holepunch`: Enable NAT hole punching (default: false)
- `tlsClientCert`: TLS client certificate
- `pingInterval`: Interval for pinging the server (default: 3s)
- `pingTimeout`: Timeout for each ping (default: 5s)
- `orgId`: Organization ID to connect to
**Response:**
- **Status Code:** `202 Accepted`
- **Content-Type:** `application/json`
```json
{
"status": "connection request accepted"
}
```
**Error Responses:**
- `405 Method Not Allowed` - Non-POST requests
- `400 Bad Request` - Invalid JSON or missing required fields
- `409 Conflict` - Already connected to a server (disconnect first)
---
### GET /status
Returns the current connection status, registration state, and peer information.
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"connected": true,
"registered": true,
"terminated": false,
"version": "1.0.0",
"agent": "olm",
"orgId": "org_123",
"peers": {
"10": {
"siteId": 10,
"name": "Site A",
"connected": true,
"rtt": 145338339,
"lastSeen": "2025-08-13T14:39:17.208334428-07:00",
"endpoint": "p.fosrl.io:21820",
"isRelay": true,
"peerAddress": "100.89.128.5",
"holepunchConnected": false
},
"8": {
"siteId": 8,
"name": "Site B",
"connected": false,
"rtt": 0,
"lastSeen": "2025-08-13T14:39:19.663823645-07:00",
"endpoint": "p.fosrl.io:21820",
"isRelay": true,
"peerAddress": "100.89.128.10",
"holepunchConnected": false
}
},
"networkSettings": {
"tunnelIP": "100.89.128.3/20"
}
}
```
**Fields:**
- `connected`: Boolean indicating if connected to Pangolin
- `registered`: Boolean indicating if registered with the server
- `terminated`: Boolean indicating if the connection was terminated
- `version`: Olm version string
- `agent`: Agent identifier
- `orgId`: Current organization ID
- `peers`: Map of peer statuses by site ID
- `siteId`: Peer site identifier
- `name`: Site name
- `connected`: Boolean peer connection state
- `rtt`: Peer round-trip time (integer, nanoseconds)
- `lastSeen`: Last time peer was seen (RFC3339 timestamp)
- `endpoint`: Peer endpoint address
- `isRelay`: Whether the peer is relayed (true) or direct (false)
- `peerAddress`: Peer's IP address in the tunnel
- `holepunchConnected`: Whether holepunch connection is established
- `networkSettings`: Current network configuration including tunnel IP
**Error Responses:**
- `405 Method Not Allowed` - Non-GET requests
---
### POST /disconnect
Disconnects from the current Pangolin server and tears down the WireGuard tunnel.
**Request Body:** None required
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"status": "disconnect initiated"
}
```
**Error Responses:**
- `405 Method Not Allowed` - Non-POST requests
- `409 Conflict` - Not currently connected to a server
---
### POST /switch-org
Switches to a different organization while maintaining the connection.
**Request Body:**
```json
{
"orgId": "string"
}
```
**Required Fields:**
- `orgId`: The organization ID to switch to
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"status": "org switch request accepted"
}
```
**Error Responses:**
- `405 Method Not Allowed` - Non-POST requests
- `400 Bad Request` - Invalid JSON or missing orgId field
- `500 Internal Server Error` - Org switch failed
---
### POST /exit
Initiates a graceful shutdown of the Olm process.
**Request Body:** None required
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"status": "shutdown initiated"
}
```
**Note:** The response is sent before shutdown begins. There is a 100ms delay before the actual shutdown to ensure the response is delivered.
**Error Responses:**
- `405 Method Not Allowed` - Non-POST requests
---
### GET /health
Simple health check endpoint to verify the API server is running.
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"status": "ok"
}
```
**Error Responses:**
- `405 Method Not Allowed` - Non-GET requests
---
## Usage Examples
### Connect to a peer
```bash
curl -X POST http://localhost:9452/connect \
-H "Content-Type: application/json" \
-d '{
"id": "31frd0uzbjvp721",
"secret": "h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6",
"endpoint": "https://example.com"
}'
```
### Connect with additional options
```bash
curl -X POST http://localhost:9452/connect \
-H "Content-Type: application/json" \
-d '{
"id": "31frd0uzbjvp721",
"secret": "h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6",
"endpoint": "https://example.com",
"mtu": 1400,
"holepunch": true,
"pingInterval": "5s"
}'
```
### Check connection status
```bash
curl http://localhost:9452/status
```
### Switch organization
```bash
curl -X POST http://localhost:9452/switch-org \
-H "Content-Type: application/json" \
-d '{"orgId": "org_456"}'
```
### Disconnect from server
```bash
curl -X POST http://localhost:9452/disconnect
```
### Health check
```bash
curl http://localhost:9452/health
```
### Shutdown Olm
```bash
curl -X POST http://localhost:9452/exit
```
### Using Unix socket (Linux/macOS)
```bash
curl --unix-socket /var/run/olm/olm.sock http://localhost/status
curl --unix-socket /var/run/olm/olm.sock -X POST http://localhost/disconnect
```

View File

@@ -16,7 +16,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /olm
# Start a new stage from scratch
FROM alpine:3.22 AS runner
FROM alpine:3.23 AS runner
RUN apk --no-cache add ca-certificates

View File

@@ -1,5 +1,5 @@
all: go-build-release
all: local
docker-build-release:
@if [ -z "$(tag)" ]; then \
@@ -12,15 +12,9 @@ docker-build-release:
local:
CGO_ENABLED=0 go build -o bin/olm
build:
docker build -t fosrl/olm:latest .
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/olm_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/olm_linux_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/olm_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/olm_darwin_amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/olm_windows_amd64.exe
clean:
rm olm
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/olm_windows_amd64.exe

265
README.md
View File

@@ -6,7 +6,7 @@ Olm is a [WireGuard](https://www.wireguard.com/) tunnel client designed to secur
Olm is used with Pangolin and Newt as part of the larger system. See documentation below:
- [Full Documentation](https://docs.pangolin.net)
- [Full Documentation](https://docs.pangolin.net/manage/clients/understanding-clients)
## Key Functions
@@ -18,111 +18,9 @@ Using the Olm ID and a secret, the olm will make HTTP requests to Pangolin to re
When Olm receives WireGuard control messages, it will use the information encoded (endpoint, public key) to bring up a WireGuard tunnel on your computer to a remote Newt. It will ping over the tunnel to ensure the peer is brought up.
## CLI Args
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
- `id`: Olm ID generated by Pangolin to identify the olm.
- `secret`: A unique secret (not shared and kept private) used to authenticate the olm ID with the websocket in order to receive commands.
- `mtu` (optional): MTU for the internal WG interface. Default: 1280
- `dns` (optional): DNS server to use to resolve the endpoint. Default: 8.8.8.8
- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO
- `ping-interval` (optional): Interval for pinging the server. Default: 3s
- `ping-timeout` (optional): Timeout for each ping. Default: 5s
- `interface` (optional): Name of the WireGuard interface. Default: olm
- `enable-http` (optional): Enable HTTP server for receiving connection requests. Default: false
- `http-addr` (optional): HTTP server address (e.g., ':9452'). Default: :9452
- `holepunch` (optional): Enable hole punching. Default: false
## Environment Variables
All CLI arguments can also be set via environment variables:
- `PANGOLIN_ENDPOINT`: Equivalent to `--endpoint`
- `OLM_ID`: Equivalent to `--id`
- `OLM_SECRET`: Equivalent to `--secret`
- `MTU`: Equivalent to `--mtu`
- `DNS`: Equivalent to `--dns`
- `LOG_LEVEL`: Equivalent to `--log-level`
- `INTERFACE`: Equivalent to `--interface`
- `HTTP_ADDR`: Equivalent to `--http-addr`
- `PING_INTERVAL`: Equivalent to `--ping-interval`
- `PING_TIMEOUT`: Equivalent to `--ping-timeout`
- `HOLEPUNCH`: Set to "true" to enable hole punching (equivalent to `--holepunch`)
- `CONFIG_FILE`: Set to the location of a JSON file to load secret values
Examples:
```bash
olm \
--id 31frd0uzbjvp721 \
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
--endpoint https://example.com
```
You can also run it with Docker compose. For example, a service in your `docker-compose.yml` might look like this using environment vars (recommended):
```yaml
services:
olm:
image: fosrl/olm
container_name: olm
restart: unless-stopped
network_mode: host
devices:
- /dev/net/tun:/dev/net/tun
environment:
- PANGOLIN_ENDPOINT=https://example.com
- OLM_ID=31frd0uzbjvp721
- OLM_SECRET=h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
```
You can also pass the CLI args to the container:
```yaml
services:
olm:
image: fosrl/olm
container_name: olm
restart: unless-stopped
network_mode: host
devices:
- /dev/net/tun:/dev/net/tun
command:
- --id 31frd0uzbjvp721
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
- --endpoint https://example.com
```
**Docker Configuration Notes:**
- `network_mode: host` brings the olm network interface to the host system, allowing the WireGuard tunnel to function properly
- `devices: - /dev/net/tun:/dev/net/tun` is required to give the container access to the TUN device for creating WireGuard interfaces
## Loading secrets from files
You can use `CONFIG_FILE` to define a location of a config file to store the credentials between runs.
```
$ cat ~/.config/olm-client/config.json
{
"id": "spmzu8rbpzj1qq6",
"secret": "f6v61mjutwme2kkydbw3fjo227zl60a2tsf5psw9r25hgae3",
"endpoint": "https://app.pangolin.net",
"tlsClientCert": ""
}
```
This file is also written to when newt first starts up. So you do not need to run every time with --id and secret if you have run it once!
Default locations:
- **macOS**: `~/Library/Application Support/olm-client/config.json`
- **Windows**: `%PROGRAMDATA%\olm\olm-client\config.json`
- **Linux/Others**: `~/.config/olm-client/config.json`
## Hole Punching
In the default mode, olm "relays" traffic through Gerbil in the cloud to get down to newt. This is a little more reliable. Support for NAT hole punching is also EXPERIMENTAL right now using the `--holepunch` flag. This will attempt to orchestrate a NAT hole punch between the two sites so that traffic flows directly. This will save data costs and speed. If it fails it should fall back to relaying.
In the default mode, olm uses both relaying through Gerbil and NAT hole punching to connect to newt. If you want to disable hole punching, use the `--disable-holepunch` flag. Hole punching attempts to orchestrate a NAT hole punch between the two sites so that traffic flows directly, which can save data costs and improve speed. If hole punching fails, traffic will fall back to relaying through Gerbil.
Right now, basic NAT hole punching is supported. We plan to add:
@@ -130,169 +28,14 @@ Right now, basic NAT hole punching is supported. We plan to add:
- [ ] UPnP
- [ ] LAN detection
## Windows Service
On Windows, olm has to be installed and run as a Windows service. When running it with the cli args live above it will attempt to install and run the service to function like a cli tool. You can also run the following:
### Service Management Commands
```
# Install the service
olm.exe install
# Start the service
olm.exe start
# Stop the service
olm.exe stop
# Check service status
olm.exe status
# Remove the service
olm.exe remove
# Run in debug mode (console output) with our without id & secret
olm.exe debug
# Show help
olm.exe help
```
Note running the service requires credentials in `%PROGRAMDATA%\olm\olm-client\config.json`.
### Service Configuration
When running as a service, Olm will read configuration from environment variables or you can modify the service to include command-line arguments:
1. Install the service: `olm.exe install`
2. Set the credentials in `%PROGRAMDATA%\olm\olm-client\config.json`. Hint: if you run olm once with --id and --secret this file will be populated!
3. Start the service: `olm.exe start`
### Service Logs
When running as a service, logs are written to:
- Windows Event Log (Application log, source: "OlmWireguardService")
- Log files in: `%PROGRAMDATA%\olm\logs\olm.log`
You can view the Windows Event Log using Event Viewer or PowerShell:
```powershell
Get-EventLog -LogName Application -Source "OlmWireguardService" -Newest 10
```
## HTTP Endpoints
Olm can be controlled with an embedded http server when using `--enable-http`. This allows you to start it as a daemon and trigger it with the following endpoints:
### POST /connect
Initiates a new connection request.
**Request Body:**
```json
{
"id": "string",
"secret": "string",
"endpoint": "string"
}
```
**Required Fields:**
- `id`: Connection identifier
- `secret`: Authentication secret
- `endpoint`: Target endpoint URL
**Response:**
- **Status Code:** `202 Accepted`
- **Content-Type:** `application/json`
```json
{
"status": "connection request accepted"
}
```
**Error Responses:**
- `405 Method Not Allowed` - Non-POST requests
- `400 Bad Request` - Invalid JSON or missing required fields
### GET /status
Returns the current connection status and peer information.
**Response:**
- **Status Code:** `200 OK`
- **Content-Type:** `application/json`
```json
{
"status": "connected",
"connected": true,
"tunnelIP": "100.89.128.3/20",
"version": "version_replaceme",
"peers": {
"10": {
"siteId": 10,
"connected": true,
"rtt": 145338339,
"lastSeen": "2025-08-13T14:39:17.208334428-07:00",
"endpoint": "p.fosrl.io:21820",
"isRelay": true
},
"8": {
"siteId": 8,
"connected": false,
"rtt": 0,
"lastSeen": "2025-08-13T14:39:19.663823645-07:00",
"endpoint": "p.fosrl.io:21820",
"isRelay": true
}
}
}
```
**Fields:**
- `status`: Overall connection status ("connected" or "disconnected")
- `connected`: Boolean connection state
- `tunnelIP`: IP address and subnet of the tunnel (when connected)
- `version`: Olm version string
- `peers`: Map of peer statuses by site ID
- `siteId`: Peer site identifier
- `connected`: Boolean peer connection state
- `rtt`: Peer round-trip time (integer, nanoseconds)
- `lastSeen`: Last time peer was seen (RFC3339 timestamp)
- `endpoint`: Peer endpoint address
- `isRelay`: Whether the peer is relayed (true) or direct (false)
**Error Responses:**
- `405 Method Not Allowed` - Non-GET requests
## Usage Examples
### Connect to a peer
```bash
curl -X POST http://localhost:8080/connect \
-H "Content-Type: application/json" \
-d '{
"id": "31frd0uzbjvp721",
"secret": "h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6",
"endpoint": "https://example.com"
}'
```
### Check connection status
```bash
curl http://localhost:8080/status
```
## Build
### Binary
Make sure to have Go 1.23.1 installed.
Make sure to have Go 1.25 installed.
```bash
make local
make
```
## Licensing

View File

@@ -38,6 +38,7 @@ type SwitchOrgRequest struct {
// PeerStatus represents the status of a peer connection
type PeerStatus struct {
SiteID int `json:"siteId"`
Name string `json:"name"`
Connected bool `json:"connected"`
RTT time.Duration `json:"rtt"`
LastSeen time.Time `json:"lastSeen"`
@@ -170,6 +171,26 @@ func (s *API) Stop() error {
return nil
}
func (s *API) AddPeerStatus(siteID int, siteName string, connected bool, rtt time.Duration, endpoint string, isRelay bool) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
status, exists := s.peerStatuses[siteID]
if !exists {
status = &PeerStatus{
SiteID: siteID,
}
s.peerStatuses[siteID] = status
}
status.Name = siteName
status.Connected = connected
status.RTT = rtt
status.LastSeen = time.Now()
status.Endpoint = endpoint
status.IsRelay = isRelay
}
// UpdatePeerStatus updates the status of a peer including endpoint and relay info
func (s *API) UpdatePeerStatus(siteID int, connected bool, rtt time.Duration, endpoint string, isRelay bool) {
s.statusMu.Lock()

View File

@@ -40,10 +40,10 @@ type OlmConfig struct {
PingTimeout string `json:"pingTimeout"`
// Advanced
Holepunch bool `json:"holepunch"`
TlsClientCert string `json:"tlsClientCert"`
OverrideDNS bool `json:"overrideDNS"`
DisableRelay bool `json:"disableRelay"`
DisableHolepunch bool `json:"disableHolepunch"`
TlsClientCert string `json:"tlsClientCert"`
OverrideDNS bool `json:"overrideDNS"`
DisableRelay bool `json:"disableRelay"`
// DoNotCreateNewClient bool `json:"doNotCreateNewClient"`
// Parsed values (not in JSON)
@@ -78,16 +78,16 @@ func DefaultConfig() *OlmConfig {
}
config := &OlmConfig{
MTU: 1280,
DNS: "8.8.8.8",
UpstreamDNS: []string{"8.8.8.8:53"},
LogLevel: "INFO",
InterfaceName: "olm",
EnableAPI: false,
SocketPath: socketPath,
PingInterval: "3s",
PingTimeout: "5s",
Holepunch: false,
MTU: 1280,
DNS: "8.8.8.8",
UpstreamDNS: []string{"8.8.8.8:53"},
LogLevel: "INFO",
InterfaceName: "olm",
EnableAPI: false,
SocketPath: socketPath,
PingInterval: "3s",
PingTimeout: "5s",
DisableHolepunch: false,
// DoNotCreateNewClient: false,
sources: make(map[string]string),
}
@@ -103,7 +103,7 @@ func DefaultConfig() *OlmConfig {
config.sources["socketPath"] = string(SourceDefault)
config.sources["pingInterval"] = string(SourceDefault)
config.sources["pingTimeout"] = string(SourceDefault)
config.sources["holepunch"] = string(SourceDefault)
config.sources["disableHolepunch"] = string(SourceDefault)
config.sources["overrideDNS"] = string(SourceDefault)
config.sources["disableRelay"] = string(SourceDefault)
// config.sources["doNotCreateNewClient"] = string(SourceDefault)
@@ -253,9 +253,9 @@ func loadConfigFromEnv(config *OlmConfig) {
config.SocketPath = val
config.sources["socketPath"] = string(SourceEnv)
}
if val := os.Getenv("HOLEPUNCH"); val == "true" {
config.Holepunch = true
config.sources["holepunch"] = string(SourceEnv)
if val := os.Getenv("DISABLE_HOLEPUNCH"); val == "true" {
config.DisableHolepunch = true
config.sources["disableHolepunch"] = string(SourceEnv)
}
if val := os.Getenv("OVERRIDE_DNS"); val == "true" {
config.OverrideDNS = true
@@ -277,24 +277,24 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
// Store original values to detect changes
origValues := map[string]interface{}{
"endpoint": config.Endpoint,
"id": config.ID,
"secret": config.Secret,
"org": config.OrgID,
"userToken": config.UserToken,
"mtu": config.MTU,
"dns": config.DNS,
"upstreamDNS": fmt.Sprintf("%v", config.UpstreamDNS),
"logLevel": config.LogLevel,
"interface": config.InterfaceName,
"httpAddr": config.HTTPAddr,
"socketPath": config.SocketPath,
"pingInterval": config.PingInterval,
"pingTimeout": config.PingTimeout,
"enableApi": config.EnableAPI,
"holepunch": config.Holepunch,
"overrideDNS": config.OverrideDNS,
"disableRelay": config.DisableRelay,
"endpoint": config.Endpoint,
"id": config.ID,
"secret": config.Secret,
"org": config.OrgID,
"userToken": config.UserToken,
"mtu": config.MTU,
"dns": config.DNS,
"upstreamDNS": fmt.Sprintf("%v", config.UpstreamDNS),
"logLevel": config.LogLevel,
"interface": config.InterfaceName,
"httpAddr": config.HTTPAddr,
"socketPath": config.SocketPath,
"pingInterval": config.PingInterval,
"pingTimeout": config.PingTimeout,
"enableApi": config.EnableAPI,
"disableHolepunch": config.DisableHolepunch,
"overrideDNS": config.OverrideDNS,
"disableRelay": config.DisableRelay,
// "doNotCreateNewClient": config.DoNotCreateNewClient,
}
@@ -315,7 +315,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
serviceFlags.StringVar(&config.PingInterval, "ping-interval", config.PingInterval, "Interval for pinging the server")
serviceFlags.StringVar(&config.PingTimeout, "ping-timeout", config.PingTimeout, "Timeout for each ping")
serviceFlags.BoolVar(&config.EnableAPI, "enable-api", config.EnableAPI, "Enable API server for receiving connection requests")
serviceFlags.BoolVar(&config.Holepunch, "holepunch", config.Holepunch, "Enable hole punching")
serviceFlags.BoolVar(&config.DisableHolepunch, "disable-holepunch", config.DisableHolepunch, "Disable hole punching")
serviceFlags.BoolVar(&config.OverrideDNS, "override-dns", config.OverrideDNS, "Override system DNS settings")
serviceFlags.BoolVar(&config.DisableRelay, "disable-relay", config.DisableRelay, "Disable relay connections")
// serviceFlags.BoolVar(&config.DoNotCreateNewClient, "do-not-create-new-client", config.DoNotCreateNewClient, "Do not create new client")
@@ -384,8 +384,8 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
if config.EnableAPI != origValues["enableApi"].(bool) {
config.sources["enableApi"] = string(SourceCLI)
}
if config.Holepunch != origValues["holepunch"].(bool) {
config.sources["holepunch"] = string(SourceCLI)
if config.DisableHolepunch != origValues["disableHolepunch"].(bool) {
config.sources["disableHolepunch"] = string(SourceCLI)
}
if config.OverrideDNS != origValues["overrideDNS"].(bool) {
config.sources["overrideDNS"] = string(SourceCLI)
@@ -505,9 +505,9 @@ func mergeConfigs(dest, src *OlmConfig) {
dest.EnableAPI = src.EnableAPI
dest.sources["enableApi"] = string(SourceFile)
}
if src.Holepunch {
dest.Holepunch = src.Holepunch
dest.sources["holepunch"] = string(SourceFile)
if src.DisableHolepunch {
dest.DisableHolepunch = src.DisableHolepunch
dest.sources["disableHolepunch"] = string(SourceFile)
}
if src.OverrideDNS {
dest.OverrideDNS = src.OverrideDNS
@@ -604,7 +604,7 @@ func (c *OlmConfig) ShowConfig() {
// Advanced
fmt.Println("\nAdvanced:")
fmt.Printf(" holepunch = %v [%s]\n", c.Holepunch, getSource("holepunch"))
fmt.Printf(" disable-holepunch = %v [%s]\n", c.DisableHolepunch, getSource("disableHolepunch"))
fmt.Printf(" override-dns = %v [%s]\n", c.OverrideDNS, getSource("overrideDNS"))
fmt.Printf(" disable-relay = %v [%s]\n", c.DisableRelay, getSource("disableRelay"))
// fmt.Printf(" do-not-create-new-client = %v [%s]\n", c.DoNotCreateNewClient, getSource("doNotCreateNewClient"))

11
go.mod
View File

@@ -4,23 +4,23 @@ go 1.25
require (
github.com/Microsoft/go-winio v0.6.2
github.com/fosrl/newt v0.0.0
github.com/fosrl/newt v0.0.0-20251208171729-6d7985689552
github.com/godbus/dbus/v5 v5.2.0
github.com/gorilla/websocket v1.5.3
github.com/miekg/dns v1.1.68
github.com/vishvananda/netlink v1.3.1
golang.org/x/sys v0.38.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
golang.zx2c4.com/wireguard/windows v0.5.3
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
software.sslmate.com/src/go-pkcs12 v0.6.0
)
require (
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
@@ -28,6 +28,5 @@ require (
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
)
replace github.com/fosrl/newt => ../newt

6
go.sum
View File

@@ -1,5 +1,7 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/fosrl/newt v0.0.0-20251208171729-6d7985689552 h1:51pHUtoqQhYPS9OiBDHLgYV44X/CBzR5J7GuWO3izhU=
github.com/fosrl/newt v0.0.0-20251208171729-6d7985689552/go.mod h1:pol958CEs0nQmo/35Ltv0CGksheIKCS2hoNvdTVLEcI=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -14,8 +16,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=

BIN
http.pcap Normal file

Binary file not shown.

View File

@@ -177,7 +177,8 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
// Load configuration from file, env vars, and CLI args
// Priority: CLI args > Env vars > Config file > Defaults
config, showVersion, showConfig, err := LoadConfig(os.Args[1:])
// Use the passed args parameter instead of os.Args[1:] to support Windows service mode
config, showVersion, showConfig, err := LoadConfig(args)
if err != nil {
fmt.Printf("Failed to load configuration: %v\n", err)
return
@@ -235,7 +236,7 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
DNS: config.DNS,
UpstreamDNS: config.UpstreamDNS,
InterfaceName: config.InterfaceName,
Holepunch: config.Holepunch,
Holepunch: !config.DisableHolepunch,
TlsClientCert: config.TlsClientCert,
PingIntervalDuration: config.PingIntervalDuration,
PingTimeoutDuration: config.PingTimeoutDuration,

78
olm.iss
View File

@@ -44,8 +44,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
; The 'DestName' flag ensures that 'olm_windows_amd64.exe' is installed as 'olm.exe'
Source: "C:\Users\Administrator\Downloads\olm_windows_amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion
Source: "C:\Users\Administrator\Downloads\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "Z:\olm_windows_amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion
Source: "Z:\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
@@ -57,13 +57,13 @@ Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
; The 'Path' variable is located under 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'.
; ValueType: expandsz allows for environment variables (like %ProgramFiles%) in the path.
; ValueData: "{olddata};{app}" appends the current application directory to the existing PATH.
; Flags: uninsdeletevalue ensures the entry is removed upon uninstallation.
; Check: IsWin64 ensures this is applied on 64-bit systems, which matches ArchitecturesAllowed.
; Note: Removal during uninstallation is handled by CurUninstallStepChanged procedure in [Code] section.
; Check: NeedsAddPath ensures this is applied only if the path is not already present.
[Registry]
; Add the application's installation directory to the system PATH.
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
Flags: uninsdeletevalue; Check: NeedsAddPath(ExpandConstant('{app}'))
Check: NeedsAddPath(ExpandConstant('{app}'))
[Code]
function NeedsAddPath(Path: string): boolean;
@@ -78,11 +78,75 @@ begin
Result := True;
exit;
end;
// Perform a case-insensitive check to see if the path is already present.
// We add semicolons to prevent partial matches (e.g., matching C:\App in C:\App2).
if Pos(';' + UpperCase(Path) + ';', ';' + UpperCase(OrigPath) + ';') > 0 then
Result := False
else
Result := True;
end;
end;
procedure RemovePathEntry(PathToRemove: string);
var
OrigPath: string;
NewPath: string;
PathList: TStringList;
I: Integer;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path', OrigPath)
then begin
// Path variable doesn't exist, nothing to remove
exit;
end;
// Create a string list to parse the PATH entries
PathList := TStringList.Create;
try
// Split the PATH by semicolons
PathList.Delimiter := ';';
PathList.StrictDelimiter := True;
PathList.DelimitedText := OrigPath;
// Find and remove the matching entry (case-insensitive)
for I := PathList.Count - 1 downto 0 do
begin
if CompareText(Trim(PathList[I]), Trim(PathToRemove)) = 0 then
begin
Log('Found and removing PATH entry: ' + PathList[I]);
PathList.Delete(I);
end;
end;
// Reconstruct the PATH
NewPath := PathList.DelimitedText;
// Write the new PATH back to the registry
if RegWriteExpandStringValue(HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path', NewPath)
then
Log('Successfully removed path entry: ' + PathToRemove)
else
Log('Failed to write modified PATH to registry');
finally
PathList.Free;
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
AppPath: string;
begin
if CurUninstallStep = usUninstall then
begin
// Get the application installation path
AppPath := ExpandConstant('{app}');
Log('Removing PATH entry for: ' + AppPath);
// Remove only our path entry from the system PATH
RemovePathEntry(AppPath);
end;
end;

View File

@@ -417,7 +417,7 @@ func StartTunnel(config TunnelConfig) {
siteEndpoint = site.Endpoint
}
apiServer.UpdatePeerStatus(site.SiteId, false, 0, siteEndpoint, false)
apiServer.AddPeerStatus(site.SiteId, site.Name, false, 0, siteEndpoint, false)
if err := peerManager.AddPeer(site); err != nil {
logger.Error("Failed to add peer: %v", err)
@@ -471,7 +471,7 @@ func StartTunnel(config TunnelConfig) {
// Get existing peer from PeerManager
existingPeer, exists := peerManager.GetPeer(updateData.SiteId)
if !exists {
logger.Error("Peer with site ID %d not found", updateData.SiteId)
logger.Warn("Peer with site ID %d not found", updateData.SiteId)
return
}
@@ -502,6 +502,13 @@ func StartTunnel(config TunnelConfig) {
return
}
// If the endpoint changed, trigger holepunch to refresh NAT mappings
if updateData.Endpoint != "" && updateData.Endpoint != existingPeer.Endpoint {
logger.Info("Endpoint changed for site %d, triggering holepunch to refresh NAT mappings", updateData.SiteId)
holePunchManager.TriggerHolePunch()
holePunchManager.ResetInterval()
}
// Update successful
logger.Info("Successfully updated peer for site %d", updateData.SiteId)
})
@@ -778,23 +785,28 @@ func StartTunnel(config TunnelConfig) {
return
}
// Add exit node to holepunch rotation if we have a holepunch manager
if holePunchManager != nil {
exitNode := holepunch.ExitNode{
Endpoint: handshakeData.ExitNode.Endpoint,
PublicKey: handshakeData.ExitNode.PublicKey,
}
added := holePunchManager.AddExitNode(exitNode)
if added {
logger.Info("Added exit node %s to holepunch rotation for handshake", exitNode.Endpoint)
} else {
logger.Debug("Exit node %s already in holepunch rotation", exitNode.Endpoint)
}
holePunchManager.ResetInterval() // start sending immediately again so we fill in the endpoint on the cloud
// Get existing peer from PeerManager
_, exists := peerManager.GetPeer(handshakeData.SiteId)
if exists {
logger.Warn("Peer with site ID %d already added", handshakeData.SiteId)
return
}
exitNode := holepunch.ExitNode{
Endpoint: handshakeData.ExitNode.Endpoint,
PublicKey: handshakeData.ExitNode.PublicKey,
}
added := holePunchManager.AddExitNode(exitNode)
if added {
logger.Info("Added exit node %s to holepunch rotation for handshake", exitNode.Endpoint)
} else {
logger.Debug("Exit node %s already in holepunch rotation", exitNode.Endpoint)
}
holePunchManager.TriggerHolePunch() // Trigger immediate hole punch attempt
holePunchManager.ResetInterval() // start sending immediately again so we fill in the endpoint on the cloud
// Send handshake acknowledgment back to server with retry
stopPeerSend, _ = olm.SendMessageInterval("olm/wg/server/peer/add", map[string]interface{}{
"siteId": handshakeData.SiteId,
@@ -859,27 +871,25 @@ func StartTunnel(config TunnelConfig) {
})
olm.OnTokenUpdate(func(token string, exitNodes []websocket.ExitNode) {
if holePunchManager != nil {
holePunchManager.SetToken(token)
holePunchManager.SetToken(token)
logger.Debug("Got exit nodes for hole punching: %v", exitNodes)
logger.Debug("Got exit nodes for hole punching: %v", exitNodes)
// Convert websocket.ExitNode to holepunch.ExitNode
hpExitNodes := make([]holepunch.ExitNode, len(exitNodes))
for i, node := range exitNodes {
hpExitNodes[i] = holepunch.ExitNode{
Endpoint: node.Endpoint,
PublicKey: node.PublicKey,
}
// Convert websocket.ExitNode to holepunch.ExitNode
hpExitNodes := make([]holepunch.ExitNode, len(exitNodes))
for i, node := range exitNodes {
hpExitNodes[i] = holepunch.ExitNode{
Endpoint: node.Endpoint,
PublicKey: node.PublicKey,
}
}
logger.Debug("Updated hole punch exit nodes: %v", hpExitNodes)
logger.Debug("Updated hole punch exit nodes: %v", hpExitNodes)
// Start hole punching using the manager
logger.Info("Starting hole punch for %d exit nodes", len(exitNodes))
if err := holePunchManager.StartMultipleExitNodes(hpExitNodes); err != nil {
logger.Warn("Failed to start hole punch: %v", err)
}
// Start hole punching using the manager
logger.Info("Starting hole punch for %d exit nodes", len(exitNodes))
if err := holePunchManager.StartMultipleExitNodes(hpExitNodes); err != nil {
logger.Warn("Failed to start hole punch: %v", err)
}
})

View File

@@ -1,9 +1,6 @@
package olm
import (
"fmt"
"net"
"strings"
"time"
"github.com/fosrl/newt/logger"
@@ -11,33 +8,6 @@ import (
"github.com/fosrl/olm/websocket"
)
// Helper function to format endpoints correctly
func formatEndpoint(endpoint string) string {
if endpoint == "" {
return ""
}
// Check if it's already a valid host:port that SplitHostPort can parse (e.g., [::1]:8080 or 1.2.3.4:8080)
_, _, err := net.SplitHostPort(endpoint)
if err == nil {
return endpoint // Already valid, no change needed
}
// If it failed, it might be our malformed "ipv6:port" string. Let's check and fix it.
lastColon := strings.LastIndex(endpoint, ":")
if lastColon > 0 { // Ensure there is a colon and it's not the first character
hostPart := endpoint[:lastColon]
// Check if the host part is a literal IPv6 address
if ip := net.ParseIP(hostPart); ip != nil && ip.To4() == nil {
// It is! Reformat it with brackets.
portPart := endpoint[lastColon+1:]
return fmt.Sprintf("[%s]:%s", hostPart, portPart)
}
}
// If it's not the specific malformed case, return it as is.
return endpoint
}
func sendPing(olm *websocket.Client) error {
err := olm.SendMessage("olm/ping", map[string]interface{}{
"timestamp": time.Now().Unix(),
@@ -83,16 +53,3 @@ func GetNetworkSettingsJSON() (string, error) {
func GetNetworkSettingsIncrementor() int {
return network.GetIncrementor()
}
// stringSlicesEqual compares two string slices for equality
func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -150,6 +150,8 @@ func (pm *PeerManager) AddPeer(siteConfig SiteConfig) error {
pm.peers[siteConfig.SiteId] = siteConfig
pm.APIServer.AddPeerStatus(siteConfig.SiteId, siteConfig.Name, false, 0, siteConfig.Endpoint, false)
// Perform rapid initial holepunch test (outside of lock to avoid blocking)
// This quickly determines if holepunch is viable and triggers relay if not
go pm.performRapidInitialTest(siteConfig.SiteId, siteConfig.Endpoint)

View File

@@ -191,12 +191,12 @@ func (pm *PeerMonitor) AddPeer(siteID int, endpoint string, holepunchEndpoint st
// update holepunch endpoint for a peer
func (pm *PeerMonitor) UpdateHolepunchEndpoint(siteID int, endpoint string) {
go func() {
time.Sleep(3 * time.Second)
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.holepunchEndpoints[siteID] = endpoint
}()
// Short delay to allow WireGuard peer reconfiguration to complete
// The NAT mapping refresh is handled separately by TriggerHolePunch in olm.go
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.holepunchEndpoints[siteID] = endpoint
logger.Debug("Updated holepunch endpoint for site %d to %s", siteID, endpoint)
}
// RapidTestPeer performs a rapid connectivity test for a newly added peer.
@@ -294,6 +294,12 @@ func (pm *PeerMonitor) RemovePeer(siteID int) {
pm.removePeerUnlocked(siteID)
}
func (pm *PeerMonitor) RemoveHolepunchEndpoint(siteID int) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
delete(pm.holepunchEndpoints, siteID)
}
// Start begins monitoring all peers
func (pm *PeerMonitor) Start() {
pm.mutex.Lock()

View File

@@ -9,6 +9,7 @@ type PeerAction struct {
// UpdatePeerData represents the data needed to update a peer
type SiteConfig struct {
SiteId int `json:"siteId"`
Name string `json:"name,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
RelayEndpoint string `json:"relayEndpoint,omitempty"`
PublicKey string `json:"publicKey,omitempty"`

View File

@@ -99,15 +99,32 @@ func (s *olmService) Execute(args []string, r <-chan svc.ChangeRequest, changes
// Continue with empty args if loading fails
savedArgs = []string{}
}
s.elog.Info(1, fmt.Sprintf("Loaded saved service args: %v", savedArgs))
// Combine service start args with saved args, giving priority to service start args
// Note: When the service is started via SCM, args[0] is the service name
// When started via s.Start(args...), the args passed are exactly what we provide
finalArgs := []string{}
// Check if we have args passed directly to Execute (from s.Start())
if len(args) > 0 {
// Skip the first arg which is typically the service name
if len(args) > 1 {
// The first arg from SCM is the service name, but when we call s.Start(args...),
// the args we pass become args[1:] in Execute. However, if started by SCM without
// args, args[0] will be the service name.
// We need to check if args[0] looks like the service name or a flag
if len(args) == 1 && args[0] == serviceName {
// Only service name, no actual args
s.elog.Info(1, "Only service name in args, checking saved args")
} else if len(args) > 1 && args[0] == serviceName {
// Service name followed by actual args
finalArgs = append(finalArgs, args[1:]...)
s.elog.Info(1, fmt.Sprintf("Using service start parameters (after service name): %v", finalArgs))
} else {
// Args don't start with service name, use them all
// This happens when args are passed via s.Start(args...)
finalArgs = append(finalArgs, args...)
s.elog.Info(1, fmt.Sprintf("Using service start parameters (direct): %v", finalArgs))
}
s.elog.Info(1, fmt.Sprintf("Using service start parameters: %v", finalArgs))
}
// If no service start parameters, use saved args
@@ -116,6 +133,7 @@ func (s *olmService) Execute(args []string, r <-chan svc.ChangeRequest, changes
s.elog.Info(1, fmt.Sprintf("Using saved service args: %v", finalArgs))
}
s.elog.Info(1, fmt.Sprintf("Final args to use: %v", finalArgs))
s.args = finalArgs
// Start the main olm functionality
@@ -325,12 +343,15 @@ func removeService() error {
}
func startService(args []string) error {
// Save the service arguments as backup
if len(args) > 0 {
err := saveServiceArgs(args)
if err != nil {
return fmt.Errorf("failed to save service args: %v", err)
}
fmt.Printf("Starting service with args: %v\n", args)
// Always save the service arguments so they can be loaded on service restart
err := saveServiceArgs(args)
if err != nil {
fmt.Printf("Warning: failed to save service args: %v\n", err)
// Continue anyway, args will still be passed directly
} else {
fmt.Printf("Saved service args to: %s\n", getServiceArgsPath())
}
m, err := mgr.Connect()
@@ -346,6 +367,7 @@ func startService(args []string) error {
defer s.Close()
// Pass arguments directly to the service start call
// Note: These args will appear in Execute() after the service name
err = s.Start(args...)
if err != nil {
return fmt.Errorf("failed to start service: %v", err)

View File

@@ -38,8 +38,9 @@ func IsAuthError(err error) bool {
type TokenResponse struct {
Data struct {
Token string `json:"token"`
ExitNodes []ExitNode `json:"exitNodes"`
Token string `json:"token"`
ExitNodes []ExitNode `json:"exitNodes"`
ServerVersion string `json:"serverVersion"`
} `json:"data"`
Success bool `json:"success"`
Message string `json:"message"`
@@ -348,6 +349,9 @@ func (c *Client) getToken() (string, []ExitNode, error) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
// print out the request for debugging
logger.Debug("Requesting token from %s with body: %s", req.URL.String(), string(jsonData))
// Make the request
client := &http.Client{}
if tlsConfig != nil {