Full Code of trailofbits/algo for AI

main 3750bac55b63 cached
335 files
946.0 KB
263.6k tokens
247 symbols
1 requests
Download .txt
Showing preview only (1,030K chars total). Download the full file or copy to clipboard to get everything.
Repository: trailofbits/algo
Branch: main
Commit: 3750bac55b63
Files: 335
Total size: 946.0 KB

Directory structure:
gitextract_xk3afjz7/

├── .ansible-lint
├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── actions/
│   │   ├── setup-algo/
│   │   │   └── action.yml
│   │   └── setup-uv/
│   │       └── action.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker-image.yaml
│       ├── integration-tests.yml
│       ├── lint.yml
│       ├── main.yml
│       ├── security.yml
│       ├── smart-tests.yml
│       └── test-effectiveness.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint
├── CLAUDE.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── SECURITY.md
├── algo
├── algo-docker.sh
├── algo-showenv.sh
├── algo.ps1
├── ansible.cfg
├── cloud.yml
├── config.cfg
├── deploy_client.yml
├── destroy.yml
├── docs/
│   ├── aws-credentials.md
│   ├── client-android.md
│   ├── client-apple-ipsec.md
│   ├── client-linux-ipsec.md
│   ├── client-linux-wireguard.md
│   ├── client-linux.md
│   ├── client-macos-wireguard.md
│   ├── client-openwrt-router-wireguard.md
│   ├── client-windows.md
│   ├── cloud-alternative-ingress-ip.md
│   ├── cloud-amazon-ec2.md
│   ├── cloud-azure.md
│   ├── cloud-cloudstack.md
│   ├── cloud-do.md
│   ├── cloud-gce.md
│   ├── cloud-hetzner.md
│   ├── cloud-linode.md
│   ├── cloud-scaleway.md
│   ├── cloud-vultr.md
│   ├── deploy-from-ansible.md
│   ├── deploy-from-cloudshell.md
│   ├── deploy-from-docker.md
│   ├── deploy-from-macos.md
│   ├── deploy-from-script-or-cloud-init-to-localhost.md
│   ├── deploy-from-windows.md
│   ├── deploy-to-ubuntu.md
│   ├── deploy-to-unsupported-cloud.md
│   ├── faq.md
│   ├── firewalls.md
│   ├── index.md
│   └── troubleshooting.md
├── files/
│   └── cloud-init/
│       ├── README.md
│       ├── base.sh
│       ├── base.yml
│       └── sshd_config
├── input.yml
├── install.sh
├── inventory
├── library/
│   ├── gcp_compute_location_info.py
│   ├── lightsail_region_facts.py
│   ├── scaleway_compute.py
│   └── x25519_pubkey.py
├── main.yml
├── playbooks/
│   ├── cloud-post.yml
│   ├── cloud-pre.yml
│   ├── rescue.yml
│   └── tmpfs/
│       ├── linux.yml
│       ├── macos.yml
│       ├── main.yml
│       └── umount.yml
├── pyproject.toml
├── pytest.ini
├── requirements.yml
├── roles/
│   ├── client/
│   │   ├── files/
│   │   │   └── libstrongswan-relax-constraints.conf
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── main.yml
│   │       └── systems/
│   │           ├── CentOS.yml
│   │           ├── Debian.yml
│   │           ├── Fedora.yml
│   │           ├── Ubuntu.yml
│   │           └── main.yml
│   ├── cloud-azure/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   └── deployment.json
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-cloudstack/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-digitalocean/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-ec2/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   └── stack.yaml
│   │   └── tasks/
│   │       ├── cloudformation.yml
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-gce/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-hetzner/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-lightsail/
│   │   ├── files/
│   │   │   └── stack.yaml
│   │   └── tasks/
│   │       ├── cloudformation.yml
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-linode/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-openstack/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       └── main.yml
│   ├── cloud-scaleway/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-vultr/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── common/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── aip/
│   │   │   │   ├── digitalocean.yml
│   │   │   │   ├── main.yml
│   │   │   │   └── placeholder.yml
│   │   │   ├── facts.yml
│   │   │   ├── iptables.yml
│   │   │   ├── main.yml
│   │   │   ├── packages.yml
│   │   │   ├── ubuntu.yml
│   │   │   └── unattended-upgrades.yml
│   │   └── templates/
│   │       ├── 10-algo-lo100.network.j2
│   │       ├── 10periodic.j2
│   │       ├── 50unattended-upgrades.j2
│   │       ├── 99-algo-ipv6-egress.yaml.j2
│   │       ├── rules.v4.j2
│   │       └── rules.v6.j2
│   ├── dns/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   ├── 50-dnscrypt-proxy-unattended-upgrades
│   │   │   └── apparmor.profile.dnscrypt-proxy
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── dns_adblocking.yml
│   │   │   ├── main.yml
│   │   │   └── ubuntu.yml
│   │   └── templates/
│   │       ├── adblock.sh.j2
│   │       ├── dnscrypt-proxy/
│   │       │   ├── cache.toml.j2
│   │       │   ├── filters.toml.j2
│   │       │   ├── global.toml.j2
│   │       │   └── sources.toml.j2
│   │       ├── dnscrypt-proxy.toml.j2
│   │       └── ip-blacklist.txt.j2
│   ├── local/
│   │   └── tasks/
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── privacy/
│   │   ├── README.md
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── advanced_privacy.yml
│   │   │   ├── auto_cleanup.yml
│   │   │   ├── clear_history.yml
│   │   │   ├── log_filtering.yml
│   │   │   ├── log_rotation.yml
│   │   │   └── main.yml
│   │   └── templates/
│   │       ├── 46-privacy-ssh-filter.conf.j2
│   │       ├── 47-privacy-auth-filter.conf.j2
│   │       ├── 48-privacy-kernel-filter.conf.j2
│   │       ├── 49-privacy-vpn-filter.conf.j2
│   │       ├── auth-logrotate.j2
│   │       ├── clear-history-on-logout.sh.j2
│   │       ├── kern-logrotate.j2
│   │       ├── privacy-auto-cleanup.sh.j2
│   │       ├── privacy-log-cleanup.sh.j2
│   │       ├── privacy-logrotate.j2
│   │       ├── privacy-monitor.sh.j2
│   │       ├── privacy-rsyslog.conf.j2
│   │       └── privacy-shutdown-cleanup.service.j2
│   ├── ssh_tunneling/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── templates/
│   │       └── ssh_config.j2
│   ├── strongswan/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── meta/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── client_configs.yml
│   │   │   ├── distribute_keys.yml
│   │   │   ├── ipsec_configuration.yml
│   │   │   ├── main.yml
│   │   │   ├── openssl.yml
│   │   │   └── ubuntu.yml
│   │   └── templates/
│   │       ├── 100-CustomLimitations.conf.j2
│   │       ├── charon.conf.j2
│   │       ├── client_ipsec.conf.j2
│   │       ├── client_ipsec.secrets.j2
│   │       ├── ipsec.conf.j2
│   │       ├── ipsec.secrets.j2
│   │       ├── mobileconfig.j2
│   │       └── strongswan.conf.j2
│   └── wireguard/
│       ├── defaults/
│       │   └── main.yml
│       ├── files/
│       │   └── wireguard.sh
│       ├── handlers/
│       │   └── main.yml
│       ├── tasks/
│       │   ├── keys.yml
│       │   ├── main.yml
│       │   ├── mobileconfig.yml
│       │   └── ubuntu.yml
│       └── templates/
│           ├── client.conf.j2
│           ├── mobileconfig.j2
│           ├── server.conf.j2
│           └── vpn-dict.j2
├── scripts/
│   ├── annotate-test-failure.sh
│   ├── lint.sh
│   ├── list_servers.py
│   ├── test-templates.sh
│   └── track-test-effectiveness.py
├── server.yml
├── tests/
│   ├── README.md
│   ├── conftest.py
│   ├── e2e/
│   │   ├── README.md
│   │   └── test-vpn-connectivity.sh
│   ├── fixtures/
│   │   ├── __init__.py
│   │   └── test_variables.yml
│   ├── integration/
│   │   ├── ansible-service-wrapper.py
│   │   ├── ansible.cfg
│   │   ├── mock-apparmor_status.sh
│   │   ├── mock_modules/
│   │   │   ├── apt.py
│   │   │   ├── command.py
│   │   │   └── shell.py
│   │   ├── test-configs/
│   │   │   ├── .provisioned
│   │   │   └── 10.99.0.10/
│   │   │       ├── .config.yml
│   │   │       ├── ipsec/
│   │   │       │   ├── .pki/
│   │   │       │   │   ├── .rnd
│   │   │       │   │   ├── 10.99.0.10_ca_generated
│   │   │       │   │   ├── cacert.pem
│   │   │       │   │   ├── certs/
│   │   │       │   │   │   ├── 01.pem
│   │   │       │   │   │   ├── 02.pem
│   │   │       │   │   │   ├── 03.pem
│   │   │       │   │   │   ├── 10.99.0.10.crt
│   │   │       │   │   │   ├── 10.99.0.10_crt_generated
│   │   │       │   │   │   ├── testuser1.crt
│   │   │       │   │   │   ├── testuser1_crt_generated
│   │   │       │   │   │   ├── testuser2.crt
│   │   │       │   │   │   └── testuser2_crt_generated
│   │   │       │   │   ├── ecparams/
│   │   │       │   │   │   └── secp384r1.pem
│   │   │       │   │   ├── index.txt
│   │   │       │   │   ├── index.txt.attr
│   │   │       │   │   ├── index.txt.attr.old
│   │   │       │   │   ├── index.txt.old
│   │   │       │   │   ├── openssl.cnf
│   │   │       │   │   ├── private/
│   │   │       │   │   │   ├── .rnd
│   │   │       │   │   │   ├── 10.99.0.10.key
│   │   │       │   │   │   ├── cakey.pem
│   │   │       │   │   │   ├── testuser1.key
│   │   │       │   │   │   ├── testuser1.p12
│   │   │       │   │   │   ├── testuser1_ca.p12
│   │   │       │   │   │   ├── testuser2.key
│   │   │       │   │   │   ├── testuser2.p12
│   │   │       │   │   │   └── testuser2_ca.p12
│   │   │       │   │   ├── public/
│   │   │       │   │   │   ├── testuser1.pub
│   │   │       │   │   │   └── testuser2.pub
│   │   │       │   │   ├── reqs/
│   │   │       │   │   │   ├── 10.99.0.10.req
│   │   │       │   │   │   ├── testuser1.req
│   │   │       │   │   │   └── testuser2.req
│   │   │       │   │   ├── serial
│   │   │       │   │   ├── serial.old
│   │   │       │   │   └── serial_generated
│   │   │       │   ├── apple/
│   │   │       │   │   ├── testuser1.mobileconfig
│   │   │       │   │   └── testuser2.mobileconfig
│   │   │       │   └── manual/
│   │   │       │       ├── cacert.pem
│   │   │       │       ├── testuser1.conf
│   │   │       │       ├── testuser1.p12
│   │   │       │       ├── testuser1.secrets
│   │   │       │       ├── testuser2.conf
│   │   │       │       ├── testuser2.p12
│   │   │       │       └── testuser2.secrets
│   │   │       └── wireguard/
│   │   │           ├── .pki/
│   │   │           │   ├── index.txt
│   │   │           │   ├── preshared/
│   │   │           │   │   ├── 10.99.0.10
│   │   │           │   │   ├── testuser1
│   │   │           │   │   └── testuser2
│   │   │           │   ├── private/
│   │   │           │   │   ├── 10.99.0.10
│   │   │           │   │   ├── testuser1
│   │   │           │   │   └── testuser2
│   │   │           │   └── public/
│   │   │           │       ├── 10.99.0.10
│   │   │           │       ├── testuser1
│   │   │           │       └── testuser2
│   │   │           ├── apple/
│   │   │           │   ├── ios/
│   │   │           │   │   ├── testuser1.mobileconfig
│   │   │           │   │   └── testuser2.mobileconfig
│   │   │           │   └── macos/
│   │   │           │       ├── testuser1.mobileconfig
│   │   │           │       └── testuser2.mobileconfig
│   │   │           ├── testuser1.conf
│   │   │           └── testuser2.conf
│   │   └── test-run.log
│   ├── test-aws-credentials.yml
│   ├── test-local-config.sh
│   ├── test-wireguard-async.yml
│   ├── test-wireguard-fix.yml
│   ├── test-wireguard-real-async.yml
│   ├── test_cloud_init_template.py
│   ├── test_package_preinstall.py
│   ├── unit/
│   │   ├── test_ansible_12_boolean_fix.py
│   │   ├── test_basic_sanity.py
│   │   ├── test_boolean_variables.py
│   │   ├── test_cloud_provider_configs.py
│   │   ├── test_comprehensive_boolean_scan.py
│   │   ├── test_config_validation.py
│   │   ├── test_destroy.py
│   │   ├── test_docker_localhost_deployment.py
│   │   ├── test_double_templating.py
│   │   ├── test_generated_configs.py
│   │   ├── test_iptables_rules.py
│   │   ├── test_lightsail_boto3_fix.py
│   │   ├── test_list_servers.py
│   │   ├── test_openssl_compatibility.py
│   │   ├── test_scaleway_fix.py
│   │   ├── test_strongswan_templates.py
│   │   ├── test_template_rendering.py
│   │   ├── test_user_management.py
│   │   ├── test_wireguard_key_generation.py
│   │   └── test_yaml_jinja2_expressions.py
│   └── validate_jinja2_templates.py
└── users.yml

================================================
FILE CONTENTS
================================================

================================================
FILE: .ansible-lint
================================================
# Ansible-lint configuration
exclude_paths:
  - .cache/
  - .github/
  - tests/
  - files/cloud-init/  # Cloud-init files have special format requirements
  - playbooks/  # These are task files included by other playbooks, not standalone playbooks
  - roles/cloud-ec2/files/  # AWS CloudFormation templates use YAML tags ansible-lint can't parse
  - roles/cloud-lightsail/files/  # AWS CloudFormation templates use YAML tags ansible-lint can't parse

skip_list:
  - 'package-latest'  # Package installs should not use latest - needed for updates
  - 'experimental'  # Experimental rules
  - 'fqcn[action]'  # Use FQCN for module actions - gradual migration
  - 'fqcn[action-core]'  # Use FQCN for builtin actions - gradual migration
  - 'var-naming[no-role-prefix]'  # Variable naming
  - 'var-naming[pattern]'  # Variable naming patterns
  - 'no-free-form'  # Avoid free-form syntax - some legacy usage
  - 'name[casing]'  # Name casing
  - 'yaml[document-start]'  # YAML document start
  - 'role-name'  # Role naming convention - too many cloud-* roles
  - 'no-handler'  # Handler usage - some legitimate non-handler use cases
  - 'name[missing]'  # All tasks should be named - 113 issues to fix (temporary)

# Enable additional rules
enable_list:
  - no-log-password
  - no-same-owner
  - partial-become
  - name[play]  # All plays should be named
  - yaml[new-line-at-end-of-file]  # Files should end with newline
  - jinja[invalid]  # Invalid Jinja2 syntax (catches template errors)
  - jinja[spacing]  # Proper spacing in Jinja2 expressions
  - no-changed-when  # Commands should declare changed_when
  - risky-file-permissions  # File tasks must have explicit mode

verbosity: 1

# Mock custom modules in library/ that ansible-lint can't auto-discover
# These modules exist and work at runtime, but need to be declared for static analysis
mock_modules:
  - gcp_compute_location_info
  - lightsail_region_facts
  - x25519_pubkey
  - scaleway_compute

# vim: ft=yaml


================================================
FILE: .dockerignore
================================================
# Version control and CI
.git/
.github/
.gitignore

# Development environment
.env
.venv/
.ruff_cache/
__pycache__/
*.pyc
*.pyo
*.pyd

# Documentation and metadata
docs/
tests/
README.md
CHANGELOG.md
CONTRIBUTING.md
PULL_REQUEST_TEMPLATE.md
SECURITY.md
logo.png
.travis.yml

# Build artifacts and configs
configs/
Dockerfile
.dockerignore
Vagrantfile

# User configuration (should be bind-mounted)
config.cfg

# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~

# OS generated files
.DS_Store
Thumbs.db


================================================
FILE: .github/FUNDING.yml
================================================
---
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: algovpn
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with a single custom sponsorship URL


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report a problem with Algo
---

**What happened?**


**Environment** (cloud provider, OS, WireGuard or IPsec)


**Output**
```
Paste any error messages or relevant output here
```


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
---
blank_issues_enabled: true
contact_links:
  - name: Troubleshooting Guide
    url: https://trailofbits.github.io/algo/troubleshooting.html
    about: Check common issues and solutions before filing


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/actions/setup-algo/action.yml
================================================
---
name: 'Setup Algo Environment'
description: 'Setup Python, uv, and dependencies for Algo VPN CI'
inputs:
  python-version:
    description: 'Python version to use'
    required: false
    default: '3.11'
  install-shellcheck:
    description: 'Install shellcheck for shell script linting'
    required: false
    default: 'false'
  install-ansible-collections:
    description: 'Install Ansible Galaxy collections'
    required: false
    default: 'false'
runs:
  using: composite
  steps:
    - name: Setup Python
      uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
      with:
        python-version: ${{ inputs.python-version }}

    - name: Setup uv environment
      uses: ./.github/actions/setup-uv

    - name: Install shellcheck
      if: inputs.install-shellcheck == 'true'
      run: sudo apt-get update && sudo apt-get install -y shellcheck
      shell: bash

    - name: Install Ansible collections
      if: inputs.install-ansible-collections == 'true'
      run: uv run ansible-galaxy collection install -r requirements.yml
      shell: bash


================================================
FILE: .github/actions/setup-uv/action.yml
================================================
---
name: 'Setup uv Environment'
description: 'Install uv and sync dependencies for Algo VPN project'
outputs:
  uv-version:
    description: 'The version of uv that was installed'
    value: ${{ steps.setup.outputs.uv-version }}
runs:
  using: composite
  steps:
    - name: Install uv
      id: setup
      uses: astral-sh/setup-uv@1ddb97e5078301c0bec13b38151f8664ed04edc8  # v6
      with:
        enable-cache: true
    - name: Sync dependencies
      run: uv sync
      shell: bash


================================================
FILE: .github/dependabot.yml
================================================
---
version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7
    groups:
      github-actions:
        patterns:
          - "*"

  # Maintain dependencies for Python using uv
  # Using "uv" ecosystem ensures both pyproject.toml AND uv.lock are updated together
  # This prevents Docker build failures from lockfile mismatches
  - package-ecosystem: "uv"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7
    groups:
      python:
        patterns:
          - "*"

  # Maintain Docker base image (python:3.12-alpine)
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7


================================================
FILE: .github/workflows/docker-image.yaml
================================================
---
name: Create and publish a Docker image

'on':
  push:
    branches: ['master']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Set up QEMU
        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a  # v4.0.0

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd  # v4.0.0

      - name: Log in to the Container registry
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2  # v4.0.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf  # v6.0.0
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            # set latest tag for master branch
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}

      - name: Build and push Docker image
        uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294  # v7.0.0
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}


================================================
FILE: .github/workflows/integration-tests.yml
================================================
---
name: Integration Tests

'on':
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - 'main.yml'
      - 'roles/**'
      - 'playbooks/**'
      - 'library/**'
  workflow_dispatch:
  schedule:
    - cron: '0 2 * * 1'  # Weekly on Monday at 2 AM

permissions:
  contents: read

jobs:
  localhost-deployment:
    name: Localhost VPN Deployment Test
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        vpn_type: ['wireguard', 'ipsec', 'both']
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'
          # Note: No pip cache - we use uv for dependency management

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            wireguard \
            wireguard-tools \
            strongswan \
            libstrongswan-standard-plugins \
            dnsmasq \
            qrencode \
            openssl \
            "linux-headers-$(uname -r)" \
            libxml2-utils \
            dnsutils

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Install Python dependencies
        run: uv sync

      - name: Install Ansible collections
        run: uv run ansible-galaxy collection install -r requirements.yml

      - name: Create test configuration
        run: |
          cat > integration-test.cfg << EOF
          users:
            - alice
            - bob
          cloud_providers:
            local:
              server: localhost
              endpoint: 127.0.0.1
          wireguard_enabled: ${{ matrix.vpn_type == 'wireguard' || matrix.vpn_type == 'both' }}
          ipsec_enabled: ${{ matrix.vpn_type == 'ipsec' || matrix.vpn_type == 'both' }}
          dns_adblocking: true
          ssh_tunneling: false
          store_pki: true
          algo_provider: local
          algo_server_name: github-ci-test
          server: localhost
          algo_ssh_port: 22
          CA_password: "test-ca-password-${{ github.run_id }}"
          p12_export_password: "test-p12-password-${{ github.run_id }}"
          tests: true
          no_log: false
          ansible_connection: local
          dns_encryption: true
          algo_dns_adblocking: true
          algo_ssh_tunneling: false
          BetweenClients_DROP: true
          block_smb: true
          block_netbios: true
          pki_in_tmpfs: true
          endpoint: 127.0.0.1
          ssh_port: 4160
          local_service_ip: 172.16.0.1
          local_service_ipv6: "fd00::1"
          EOF

      - name: Run Algo deployment
        run: |
          # Run ansible-playbook via uv - become: true in playbook handles root
          # GitHub runners have passwordless sudo for become escalation
          uv run ansible-playbook main.yml \
            -i "localhost," \
            -c local \
            -e @integration-test.cfg \
            -e "provider=local" \
            -vv

      - name: Verify services are running
        run: |
          # Check WireGuard
          if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
            echo "Checking WireGuard..."
            sudo wg show
            if ! sudo systemctl is-active --quiet wg-quick@wg0; then
              echo "✗ WireGuard service not running"
              exit 1
            fi
            echo "✓ WireGuard is running"
          fi

          # Check StrongSwan (service name is strongswan-starter on Ubuntu 20.04+)
          if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
            echo "Checking StrongSwan..."
            sudo ipsec statusall
            if ! sudo systemctl is-active --quiet strongswan-starter; then
              echo "✗ StrongSwan service not running"
              exit 1
            fi
            echo "✓ StrongSwan is running"
          fi

          # Check dnsmasq
          if ! sudo systemctl is-active --quiet dnsmasq; then
            echo "⚠️  dnsmasq not running (may be expected)"
          else
            echo "✓ dnsmasq is running"
          fi

          # Check dnscrypt-proxy
          if sudo systemctl is-active --quiet dnscrypt-proxy; then
            echo "✓ dnscrypt-proxy is running"
          else
            echo "⚠️  dnscrypt-proxy not running"
          fi

          # DNS health check - verify DNS resolution works
          echo "Testing DNS resolution via local_service_ip (172.16.0.1)..."
          if dig @172.16.0.1 google.com +short +timeout=5 | grep -q .; then
            echo "✓ DNS resolution working"
          else
            echo "⚠️  DNS resolution failed (service may still be starting)"
          fi

      - name: Verify generated configs
        run: |
          echo "Checking generated configuration files..."

          # WireGuard configs
          if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
            for user in alice bob; do
              if [ ! -f "configs/localhost/wireguard/${user}.conf" ]; then
                echo "✗ Missing WireGuard config for ${user}"
                exit 1
              fi
              if [ ! -f "configs/localhost/wireguard/${user}.png" ]; then
                echo "✗ Missing WireGuard QR code for ${user}"
                exit 1
              fi
            done
            echo "✓ All WireGuard configs generated"
          fi

          # IPsec configs (p12 in manual/, mobileconfig in apple/)
          if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
            for user in alice bob; do
              if [ ! -f "configs/localhost/ipsec/manual/${user}.p12" ]; then
                echo "✗ Missing IPsec certificate for ${user}"
                exit 1
              fi
              if [ ! -f "configs/localhost/ipsec/apple/${user}.mobileconfig" ]; then
                echo "✗ Missing IPsec mobile config for ${user}"
                exit 1
              fi
            done
            echo "✓ All IPsec configs generated"
          fi

      - name: Test VPN connectivity
        run: |
          echo "Testing basic VPN connectivity..."

          # Test WireGuard
          if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
            # Get server's WireGuard public key
            SERVER_PUBKEY=$(sudo wg show wg0 public-key)
            echo "Server public key: $SERVER_PUBKEY"

            # Check if interface has peers
            PEER_COUNT=$(sudo wg show wg0 peers | wc -l)
            echo "✓ WireGuard has $PEER_COUNT peer(s) configured"
          fi

          # Test StrongSwan
          if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
            # Check IPsec policies
            sudo ipsec statusall | grep -E "INSTALLED|ESTABLISHED" || echo "No active IPsec connections (expected)"
          fi

      - name: Run E2E VPN connectivity tests
        env:
          VPN_TYPE: ${{ matrix.vpn_type }}
        run: |
          chmod +x tests/e2e/test-vpn-connectivity.sh
          sudo tests/e2e/test-vpn-connectivity.sh "${VPN_TYPE}"

      - name: Collect E2E debug info on failure
        if: failure()
        run: |
          echo "=== E2E Test Debug Information ==="
          echo "=== Network Namespaces ==="
          ip netns list || true
          echo "=== WireGuard Config (alice) ==="
          cat configs/localhost/wireguard/alice.conf 2>/dev/null || echo "Not found"
          echo "=== IPsec Certificates ==="
          ls -la configs/localhost/ipsec/.pki/certs/ 2>/dev/null || echo "Not found"
          echo "=== iptables NAT ==="
          sudo iptables -t nat -L -n -v || true

      - name: Upload configs as artifacts
        if: always()
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: vpn-configs-${{ matrix.vpn_type }}-${{ github.run_id }}
          path: configs/
          retention-days: 7

      - name: Upload logs on failure
        if: failure()
        run: |
          echo "=== Network Interfaces ==="
          ip addr || true
          echo "=== Listening Ports ==="
          sudo ss -tulnp || true
          echo "=== WireGuard Status ==="
          sudo wg show || true
          echo "=== IPsec Status ==="
          sudo ipsec statusall || true
          echo "=== DNS Services ==="
          sudo systemctl status dnscrypt-proxy dnscrypt-proxy.socket dnsmasq --no-pager || true
          echo "=== WireGuard Log ==="
          sudo journalctl -u wg-quick@wg0 -n 50 --no-pager || true
          echo "=== StrongSwan Log ==="
          sudo journalctl -u strongswan -n 50 --no-pager || true
          echo "=== dnscrypt-proxy Log ==="
          sudo journalctl -u dnscrypt-proxy -n 50 --no-pager || true
          echo "=== System Log (last 100 lines) ==="
          sudo journalctl -n 100 --no-pager || true

  docker-build-test:
    name: Docker Image Build Test
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Build Algo Docker image
        run: |
          docker build -t algo:ci-test .

      - name: Test Docker image
        run: |
          # Test that the image can run and show help
          docker run --rm --entrypoint /bin/sh algo:ci-test -c "cd /algo && ./algo --help" || true

          # Test that required binaries exist in the virtual environment
          docker run --rm --entrypoint /bin/sh algo:ci-test -c "cd /algo && uv run which ansible"
          docker run --rm --entrypoint /bin/sh algo:ci-test -c "which python3"
          docker run --rm --entrypoint /bin/sh algo:ci-test -c "which rsync"

      - name: Test Docker config validation
        run: |
          # Create a minimal valid config
          mkdir -p test-data
          cat > test-data/config.cfg << 'EOF'
          users:
            - test-user
          cloud_providers:
            ec2:
              size: t3.micro
              region: us-east-1
          wireguard_enabled: true
          ipsec_enabled: false
          dns_encryption: true
          algo_provider: ec2
          EOF

          # Test that config is readable
          docker run --rm --entrypoint cat -v "$(pwd)/test-data:/data" algo:ci-test /data/config.cfg

          echo "✓ Docker image built and basic tests passed"


================================================
FILE: .github/workflows/lint.yml
================================================
---
name: Lint

'on':
  push:
    branches: [main, master]
  pull_request:

permissions:
  contents: read

jobs:
  ansible-lint:
    name: Ansible linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup Algo environment
        uses: ./.github/actions/setup-algo
        with:
          install-ansible-collections: 'true'

      - name: Run ansible-lint
        run: |
          uv run --with ansible-lint ansible-lint .

      - name: Run playbook dry-run check (catch runtime issues)
        run: |
          # Test main playbook logic without making changes
          # This catches filter warnings, collection issues, and runtime errors
          uv run ansible-playbook main.yml --check --connection=local \
            -e "server_ip=test" \
            -e "server_name=ci-test" \
            -e "IP_subject_alt_name=192.168.1.1" \
            || echo "Dry-run check completed with issues - review output above"

  yaml-lint:
    name: YAML linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Run yamllint
        run: uv run --with yamllint yamllint -c .yamllint .

  jinja2-lint:
    name: Jinja2 template linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Run j2lint
        run: |
          # Lint Jinja2 templates for syntax and style issues
          # Ignored rules (incompatible with Ansible config-file templates):
          #   S3: indentation (dictated by output format, not Jinja style)
          #   S5: tabs (some config formats require them)
          #   S6: whitespace-control delimiters ({%- -%} are standard Ansible)
          #   S7: single-statement-per-line (inline Jinja in config output)
          #   V1: lowercase variables (existing names like IP_subject_alt_name)
          uv run --with j2lint j2lint roles/ --ignore S3 S5 S6 S7 V1

  python-lint:
    name: Python linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup Algo environment
        uses: ./.github/actions/setup-algo

      - name: Run ruff check
        run: |
          # Fast Python linter
          uv run --with ruff ruff check .

      - name: Run ruff format check
        run: |
          # Verify consistent Python formatting
          uv run --with ruff ruff format --check .

  python-types:
    name: Python type checking
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup Algo environment
        uses: ./.github/actions/setup-algo

      - name: Run ty check
        run: |
          # Type checking with ty
          uv run --with ty ty check

  shellcheck:
    name: Shell script linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup Algo environment
        uses: ./.github/actions/setup-algo
        with:
          install-shellcheck: 'true'

      - name: Run shellcheck
        run: |
          # Check all shell scripts, not just algo and install.sh
          find . -type f -name "*.sh" -not -path "./.git/*" -exec shellcheck {} \;

  powershell-lint:
    name: PowerShell script linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Install PowerShell
        run: |
          # Install PowerShell Core
          wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.0/powershell_7.4.0-1.deb_amd64.deb
          sudo dpkg -i powershell_7.4.0-1.deb_amd64.deb
          sudo apt-get install -f

      - name: Install PSScriptAnalyzer
        run: |
          pwsh -Command "Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser"

      - name: Run PowerShell syntax check
        run: |
          # Check syntax by parsing the script
          pwsh -NoProfile -NonInteractive -Command "
            try {
              \$null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Path './algo.ps1' -Raw), [ref]\$null)
              Write-Host '✓ PowerShell syntax check passed'
            } catch {
              Write-Error 'PowerShell syntax error: ' + \$_.Exception.Message
              exit 1
            }
          "

      - name: Run PSScriptAnalyzer
        run: |
          pwsh -Command "
            \$results = Invoke-ScriptAnalyzer -Path './algo.ps1' -Severity Warning,Error
            if (\$results.Count -gt 0) {
              \$results | Format-Table -AutoSize
              exit 1
            } else {
              Write-Host '✓ PSScriptAnalyzer check passed'
            }
          "

  actionlint:
    name: GitHub Actions linting
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Install actionlint
        run: |
          bash <(curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
          sudo mv actionlint /usr/local/bin/

      - name: Run actionlint
        run: |
          actionlint .github/workflows/*.yml

  zizmor:
    name: GitHub Actions security audit
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Install zizmor
        run: |
          pip install zizmor

      - name: Run zizmor
        run: |
          zizmor .github/workflows/


================================================
FILE: .github/workflows/main.yml
================================================
---
name: Main

'on':
  push:
    branches:
      - master
      - main
  workflow_dispatch:

permissions:
  contents: read

jobs:
  syntax-check:
    name: Ansible syntax check
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Check Ansible playbook syntax
        run: uv run ansible-playbook main.yml --syntax-check

  basic-tests:
    name: Basic sanity tests
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Install system dependencies
        run: sudo apt-get update && sudo apt-get install -y shellcheck

      - name: Run basic sanity tests
        run: uv run pytest tests/unit/ -v

  docker-build:
    name: Docker build test
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Build Docker image
        run: docker build -t local/algo:test .

      - name: Test Docker image starts
        run: |
          # Just verify the image can start and show help
          docker run --rm local/algo:test /algo/algo --help

      - name: Run Docker deployment tests
        run: uv run pytest tests/unit/test_docker_localhost_deployment.py -v

  config-generation:
    name: Configuration generation test
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Test configuration generation (local mode)
        run: |
          # Run our simplified config test
          chmod +x tests/test-local-config.sh
          ./tests/test-local-config.sh

  ansible-dry-run:
    name: Ansible dry-run validation
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    permissions:
      contents: read
    strategy:
      matrix:
        provider: [local, ec2, digitalocean, gce]
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Create test configuration for ${{ matrix.provider }}
        run: |
          # Create provider-specific test config
          cat > test-${{ matrix.provider }}.cfg << 'EOF'
          users:
            - testuser
          cloud_providers:
            ${{ matrix.provider }}:
              server: test-server
              size: t3.micro
              image: ubuntu-22.04
              region: us-east-1
          wireguard_enabled: true
          ipsec_enabled: false
          dns_adblocking: false
          ssh_tunneling: false
          store_pki: true
          algo_provider: ${{ matrix.provider }}
          algo_server_name: test-algo-vpn
          server: test-server
          endpoint: 10.0.0.1
          ansible_ssh_user: ubuntu
          ansible_ssh_port: 22
          algo_ssh_port: 4160
          algo_ondemand_cellular: false
          algo_ondemand_wifi: false
          EOF

      - name: Run Ansible check mode for ${{ matrix.provider }}
        run: |
          # Run ansible in check mode to validate playbooks work
          uv run ansible-playbook main.yml \
            -i "localhost," \
            -c local \
            -e @test-${{ matrix.provider }}.cfg \
            -e "provider=${{ matrix.provider }}" \
            --check \
            --diff \
            -vv \
            --skip-tags "facts,tests,local,update-alternatives,cloud_api" || true

          # The || true is because check mode will fail on some tasks
          # but we're looking for syntax/undefined variable errors


================================================
FILE: .github/workflows/security.yml
================================================
---
name: Security

'on':
  push:
    branches: [main, master]
  pull_request:

permissions:
  contents: read

jobs:
  semgrep:
    name: Semgrep SAST
    runs-on: ubuntu-22.04
    container:
      image: semgrep/semgrep@sha256:d3d1be3a3770514d16a6a57b9761575d7536d70f45a5220274f4ec7d55c442b9  # v1.151.0
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Run semgrep
        run: >
          semgrep --config auto
          --exclude-rule dockerfile.security.last-user-is-root.last-user-is-root
          --error --quiet .

  pip-audit:
    name: Python dependency audit
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - name: Setup Algo environment
        uses: ./.github/actions/setup-algo

      - name: Run pip-audit
        run: uv run --with pip-audit pip-audit


================================================
FILE: .github/workflows/smart-tests.yml
================================================
---
name: Smart Test Selection

'on':
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: read

jobs:
  changed-files:
    name: Detect Changed Files
    runs-on: ubuntu-latest
    outputs:
      # Define what tests to run based on changes
      run_syntax_check: ${{ steps.filter.outputs.ansible }}
      run_basic_tests: ${{ steps.filter.outputs.python }}
      run_docker_tests: ${{ steps.filter.outputs.docker }}
      run_config_tests: ${{ steps.filter.outputs.configs }}
      run_template_tests: ${{ steps.filter.outputs.templates }}
      run_lint: ${{ steps.filter.outputs.lint }}
      run_integration: ${{ steps.filter.outputs.integration }}
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false

      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36  # v3.0.2
        id: filter
        with:
          filters: |
            ansible:
              - '**/*.yml'
              - '**/*.yaml'
              - 'main.yml'
              - 'playbooks/**'
              - 'roles/**'
              - 'library/**'
            python:
              - '**/*.py'
              - 'pyproject.toml'
              - 'uv.lock'
              - 'tests/**'
            docker:
              - 'Dockerfile*'
              - '.dockerignore'
              - 'docker-compose*.yml'
            configs:
              - 'config.cfg*'
              - 'roles/**/templates/**'
              - 'roles/**/defaults/**'
            templates:
              - '**/*.j2'
              - 'roles/**/templates/**'
            lint:
              - '**/*.py'
              - '**/*.yml'
              - '**/*.yaml'
              - '**/*.sh'
              - '**/*.j2'
              - '.ansible-lint'
              - '.yamllint'
              - 'pyproject.toml'
            integration:
              - 'main.yml'
              - 'roles/**'
              - 'library/**'
              - 'playbooks/**'

  syntax-check:
    name: Ansible Syntax Check
    needs: changed-files
    if: needs.changed-files.outputs.run_syntax_check == 'true'
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Check Ansible playbook syntax
        run: uv run ansible-playbook main.yml --syntax-check

  basic-tests:
    name: Basic Sanity Tests
    needs: changed-files
    if: needs.changed-files.outputs.run_basic_tests == 'true' || needs.changed-files.outputs.run_template_tests == 'true'
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Install system dependencies
        run: sudo apt-get update && sudo apt-get install -y shellcheck

      - name: Run relevant tests
        env:
          RUN_BASIC_TESTS: ${{ needs.changed-files.outputs.run_basic_tests }}
          RUN_TEMPLATE_TESTS: ${{ needs.changed-files.outputs.run_template_tests }}
        run: |
          # Always run basic sanity
          uv run pytest tests/unit/test_basic_sanity.py -v

          # Run other tests based on what changed
          if [[ "${RUN_BASIC_TESTS}" == "true" ]]; then
            uv run pytest \
              tests/unit/test_config_validation.py \
              tests/unit/test_user_management.py \
              tests/unit/test_openssl_compatibility.py \
              tests/unit/test_cloud_provider_configs.py \
              tests/unit/test_generated_configs.py \
              -v
          fi

          if [[ "${RUN_TEMPLATE_TESTS}" == "true" ]]; then
            uv run pytest tests/unit/test_template_rendering.py -v
          fi

  docker-tests:
    name: Docker Build Test
    needs: changed-files
    if: needs.changed-files.outputs.run_docker_tests == 'true'
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Build Docker image
        run: docker build -t local/algo:test .

      - name: Test Docker image starts
        run: |
          docker run --rm local/algo:test /algo/algo --help

      - name: Run Docker deployment tests
        run: uv run pytest tests/unit/test_docker_localhost_deployment.py -v

  config-tests:
    name: Configuration Tests
    needs: changed-files
    if: needs.changed-files.outputs.run_config_tests == 'true'
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Test configuration generation
        run: |
          chmod +x tests/test-local-config.sh
          ./tests/test-local-config.sh

      - name: Run ansible dry-run tests
        run: |
          # Quick dry-run for local provider only
          cat > test-local.cfg << 'EOF'
          users:
            - testuser
          cloud_providers:
            local:
              server: test-server
          wireguard_enabled: true
          ipsec_enabled: false
          dns_adblocking: false
          ssh_tunneling: false
          algo_provider: local
          algo_server_name: test-algo-vpn
          server: test-server
          endpoint: 10.0.0.1
          EOF

          uv run ansible-playbook main.yml \
            -i "localhost," \
            -c local \
            -e @test-local.cfg \
            -e "provider=local" \
            --check \
            --diff \
            -vv \
            --skip-tags "facts,tests,local,update-alternatives,cloud_api" || true

  lint:
    name: Linting
    needs: changed-files
    if: needs.changed-files.outputs.run_lint == 'true'
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Setup uv environment
        uses: ./.github/actions/setup-uv

      - name: Install ansible dependencies
        run: uv run ansible-galaxy collection install community.crypto

      - name: Run relevant linters
        env:
          RUN_LINT: ${{ needs.changed-files.outputs.run_lint }}
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.sha }}
        run: |
          # Run linters if lint-related files changed
          if [[ "${RUN_LINT}" == "true" ]]; then
            echo "Running linters..."

            # Run Python linter
            uv run --with ruff ruff check .

            # Run YAML linter
            uv run --with yamllint yamllint -c .yamllint .

            # Run Ansible linter
            uv run --with ansible-lint ansible-lint

            # Check Jinja2 templates
            if git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | grep -q '\.j2$'; then
              uv run --with j2lint j2lint roles/ --ignore S3 S5 S6 S7 V1
            fi

            # Check shell scripts if any changed
            if git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | grep -q '\.sh$'; then
              find . -name "*.sh" -type f -not -path "./.git/*" -exec shellcheck {} +
            fi
          fi

  all-tests-required:
    name: All Required Tests
    needs: [syntax-check, basic-tests, docker-tests, config-tests, lint]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Check test results
        env:
          SYNTAX_CHECK_RESULT: ${{ needs.syntax-check.result }}
          BASIC_TESTS_RESULT: ${{ needs.basic-tests.result }}
          DOCKER_TESTS_RESULT: ${{ needs.docker-tests.result }}
          CONFIG_TESTS_RESULT: ${{ needs.config-tests.result }}
          LINT_RESULT: ${{ needs.lint.result }}
        run: |
          # This job ensures all required tests pass
          # It will fail if any dependent job failed
          if [[ "${SYNTAX_CHECK_RESULT}" == "failure" ]] || \
             [[ "${BASIC_TESTS_RESULT}" == "failure" ]] || \
             [[ "${DOCKER_TESTS_RESULT}" == "failure" ]] || \
             [[ "${CONFIG_TESTS_RESULT}" == "failure" ]] || \
             [[ "${LINT_RESULT}" == "failure" ]]; then
            echo "One or more required tests failed"
            exit 1
          fi
          echo "All required tests passed!"

  trigger-integration:
    name: Trigger Integration Tests
    needs: changed-files
    if: |
      needs.changed-files.outputs.run_integration == 'true' &&
      github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    steps:
      - name: Trigger integration tests
        run: |
          echo "Integration tests should be triggered for this PR"
          echo "Changed files indicate potential breaking changes"
          echo "Run workflow manually: .github/workflows/integration-tests.yml"


================================================
FILE: .github/workflows/test-effectiveness.yml
================================================
---
name: Test Effectiveness Tracking

'on':
  schedule:
    - cron: '0 0 * * 0'  # Weekly on Sunday
  workflow_dispatch:  # Allow manual runs

permissions:
  contents: write
  issues: write
  pull-requests: read
  actions: read

jobs:
  track-effectiveness:
    name: Analyze Test Effectiveness
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98  # v5.0.1
        with:
          persist-credentials: true

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: '3.11'

      - name: Analyze test effectiveness
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          python scripts/track-test-effectiveness.py

      - name: Upload metrics
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: test-effectiveness-metrics
          path: .metrics/

      - name: Create issue if tests are ineffective
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          # Check if we need to create an issue
          if grep -q "⚠️" .metrics/test-effectiveness-report.md; then
            # Check if issue already exists
            existing=$(gh issue list --label "test-effectiveness" --state open --json number --jq '.[0].number')

            if [ -z "$existing" ]; then
              gh issue create \
                --title "Test Effectiveness Review Needed" \
                --body-file .metrics/test-effectiveness-report.md \
                --label "test-effectiveness,maintenance"
            else
              # Update existing issue
              gh issue comment "$existing" --body-file .metrics/test-effectiveness-report.md
            fi
          fi

      - name: Commit metrics if changed
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"

          if [[ -n $(git status -s .metrics/) ]]; then
            git add .metrics/
            git commit -m "chore: Update test effectiveness metrics [skip ci]"
            git push
          fi


================================================
FILE: .gitignore
================================================
*.retry
.idea/
configs/*
inventory_users
*.kate-swp
.env/
.venv/
.DS_Store
.vagrant
.ansible/
__pycache__/
*.pyc
algo.egg-info/


================================================
FILE: .pre-commit-config.yaml
================================================
# See https://prek.j178.dev for more information
---
# Apply to all files without committing:
#   prek run --all-files
# Update this file:
#   prek auto-update

repos:
  # Use prek built-in hooks (faster, Rust-native)
  - repo: builtin
    hooks:
      - id: check-yaml
        args: [--allow-multiple-documents]
        exclude: '(files/cloud-init/base\.yml|roles/cloud-.*/files/stack\.yaml)'
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: check-added-large-files
        args: ['--maxkb=500']
      - id: check-merge-conflict
      - id: mixed-line-ending
        args: [--fix=lf]

  # Python linting with ruff (fast, replaces many tools)
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.14.14
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  # YAML linting
  - repo: https://github.com/adrienverge/yamllint
    rev: v1.38.0
    hooks:
      - id: yamllint
        args: [-c=.yamllint]
        exclude: '.git/.*'

  # Shell script linting
  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.11.0.1
    hooks:
      - id: shellcheck
        exclude: '.git/.*'

  # Local hooks that use the project's installed tools
  - repo: local
    hooks:
      - id: ty-check
        name: Python type check
        entry: bash -c 'uv run --with ty ty check'
        language: system
        types: [python]
        pass_filenames: false

      - id: j2lint
        name: Jinja2 template lint
        entry: bash -c 'uv run j2lint roles/ --ignore S3 S5 S6 S7 V1'
        language: system
        files: '\.j2$'
        pass_filenames: false

      - id: ansible-lint
        name: Ansible-lint
        entry: bash -c 'uv run ansible-lint --force-color || echo "Ansible-lint had issues - check output"'
        language: system
        types: [yaml]
        files: \.(yml|yaml)$
        exclude: '^(.git/|.github/|requirements\.yml)'
        pass_filenames: false

      - id: ansible-syntax
        name: Ansible syntax check
        entry: bash -c 'uv run ansible-playbook main.yml --syntax-check'
        language: system
        files: 'main\.yml|server\.yml|users\.yml'
        pass_filenames: false

      - id: semgrep
        name: Semgrep security scan
        entry: >
          bash -c '
          command -v semgrep >/dev/null &&
          semgrep --config auto
          --exclude-rule dockerfile.security.last-user-is-root.last-user-is-root
          --error --quiet --skip-unknown-extensions .
          || echo "semgrep not installed - skipping"'
        language: system
        pass_filenames: false

      - id: actionlint
        name: GitHub Actions lint
        entry: bash -c 'command -v actionlint >/dev/null && actionlint .github/workflows/ || echo "actionlint not installed - skipping"'
        language: system
        files: '^\.github/workflows/.*\.yml$'
        pass_filenames: false

      - id: zizmor
        name: GitHub Actions security audit
        entry: bash -c 'command -v zizmor >/dev/null && zizmor .github/workflows/ || echo "zizmor not installed - skipping"'
        language: system
        files: '^\.github/workflows/.*\.yml$'
        pass_filenames: false

# Configuration for prek

# Files to exclude globally
exclude: |
  (?x)^(
    .env/.*|
    .venv/.*|
    .git/.*|
    __pycache__/.*|
    .*\.egg-info/.*
  )$


================================================
FILE: .yamllint
================================================
---
extends: default

# Cloud-init files must be excluded from normal YAML rules
# The #cloud-config header cannot have a space and cannot have --- document start
ignore: |
  files/cloud-init/
  .env/
  .venv/
  .ansible/
  configs/
  tests/integration/test-configs/

rules:
  line-length:
    max: 160
    level: warning
  comments:
    min-spaces-from-content: 1
  comments-indentation: false
  octal-values:
    forbid-implicit-octal: true
    forbid-explicit-octal: true
  braces:
    max-spaces-inside: 1
  truthy:
    allowed-values: ['true', 'false', 'yes', 'no']


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md - LLM Guidance for Algo VPN

This document provides essential context and guidance for LLMs working on the Algo VPN codebase.

## Project Overview

Algo is an Ansible-based tool that sets up a personal VPN in the cloud. It's designed to be:
- **Security-focused**: Creates hardened VPN servers with minimal attack surface
- **Easy to use**: Automated deployment with sensible defaults
- **Multi-platform**: Supports various cloud providers and operating systems
- **Privacy-preserving**: No logging, minimal data retention

### Core Technologies
- **VPN Protocols**: WireGuard (preferred) and IPsec/IKEv2
- **Configuration Management**: Ansible (v12+)
- **Languages**: Python, YAML, Shell, Jinja2 templates
- **Supported Providers**: AWS, Azure, DigitalOcean, GCP, Vultr, Hetzner, local deployment

### Philosophy
- Stability over features
- Security over convenience
- Clarity over cleverness
- Test everything
- Stay in scope - solve exactly what the issue asks, nothing more
- Test assumptions - run the code before committing
- Resist new dependencies - each one is attack surface and maintenance

## Architecture and Structure

```
algo/
├── main.yml                 # Primary playbook
├── users.yml               # User management playbook
├── server.yml              # Server-specific tasks
├── config.cfg              # Main configuration file
├── pyproject.toml          # Python project configuration and dependencies
├── uv.lock                 # Exact dependency versions lockfile
├── requirements.yml        # Ansible collections
├── roles/                  # Ansible roles
│   ├── common/            # Base system configuration, firewall, hardening
│   ├── wireguard/         # WireGuard VPN setup
│   ├── strongswan/        # IPsec/IKEv2 setup
│   ├── dns/               # DNS configuration (dnscrypt-proxy)
│   └── cloud-*/           # Cloud provider specific roles
├── library/               # Custom Ansible modules
└── tests/unit/            # Python unit tests
```

## Development Workflow

### Quality Gates (MANDATORY)

**All PRs must pass these checks locally before submission.** CI will reject failures:

```bash
# Run the full lint suite (same as CI)
ansible-lint . && yamllint . && ruff check . && shellcheck scripts/*.sh && semgrep --config auto --exclude-rule dockerfile.security.last-user-is-root.last-user-is-root --error --quiet .
ansible-playbook main.yml --syntax-check
ansible-playbook users.yml --syntax-check
pytest tests/unit/ -q
```

Common lint issues to fix before submitting:
- YAML files missing `---` document start markers
- GitHub workflows with unquoted `on:` (must be `'on':`)
- Using `ignore_errors: true` instead of `failed_when: false`
- Jinja2 spacing errors (`{{foo}}` should be `{{ foo }}`)
- Missing `mode:` on file/directory tasks

### Zero-Tolerance Warning Policy

**No warnings are tolerated in CI.** Every linter finding must be either fixed or explicitly allowlisted in the tool's config file (`.ansible-lint`, `pyproject.toml`, etc.).

Why this matters for Algo:
- **Security tool** - VPN misconfigurations silently break privacy guarantees. A "cosmetic" warning today hides a real bug tomorrow.
- **Ansible complexity** - YAML+Jinja2 linting catches real runtime failures (wrong key order breaks `when` evaluation, spacing errors cause template failures). Warnings in Ansible are not style nits.
- **CI signal integrity** - If 30 warnings scroll by on every run, the 31st one (a real regression) goes unnoticed. Zero warnings means every new finding gets human attention.

Resolution order of preference:
1. **Fix it** - Preferred. Most findings have straightforward fixes.
2. **Allowlist in config** - If the rule is wrong for this project, add to `skip_list` with a comment explaining why.
3. **Inline suppress** - Last resort. Use `# noqa: rule-name` with a comment justifying the exception.

Never use `warn_list` in `.ansible-lint` — it exists as a migration tool, not a permanent home. Rules either pass or are explicitly skipped.

### Design Requirements

When adding or modifying features, verify these before requesting review:

1. **Validate inputs early** - Check for empty lists, missing configs, permission mismatches before expensive operations
2. **Explicit file modes** - Always specify `mode:` on file/directory tasks (never rely on umask)
3. **Fail vs warn** - Permission/security issues should fail; optional features can warn
4. **Actionable errors** - Include fix commands in error messages: `"Run: sudo chown -R $USER configs/"`
5. **Follow existing patterns** - Search codebase first: `rg "when:.*localhost" --type yaml`

### Linting Tools

| Tool | Target | Key Rules |
|------|--------|-----------|
| `ansible-lint` | YAML tasks | Use `failed_when` not `ignore_errors`, add `mode:` to files |
| `yamllint` | All YAML | Document start `---`, quote `'on':` in workflows |
| `ruff` | Python | Line length 120, target Python 3.11 |
| `shellcheck` | Shell scripts | Quote variables, use `set -euo pipefail` |
| `semgrep` | All code | SAST scanner, `--config auto`, suppress with `# nosemgrep: rule-id` |

### Git Workflow

1. Create feature branches from `master`
2. Run all linters before pushing
3. Make atomic commits with clear messages
4. Update PR description with test results

### Self-Review Checklist

Before creating a PR, review your own diff:

- [ ] Did I run all linters locally?
- [ ] Did I search for similar patterns in the codebase?
- [ ] Did I add explicit `mode:` to file/directory tasks?
- [ ] Did I validate inputs before expensive operations?
- [ ] Did I update tests if I changed file paths or behavior?
- [ ] Would a reviewer ask "what happens if X is empty/missing?"

## Ansible Pitfalls

### with_items vs loop

`with_items` auto-flattens lists; `loop` does not. **Never mechanically convert:**

```yaml
# WRONG - treats list as single item, creates file named "['alice', 'bob']"
loop:
  - "{{ users }}"

# CORRECT - iterates over list contents
loop: "{{ users }}"

# CORRECT - combining lists (with_items did this automatically)
loop: "{{ users + [server_name] }}"
```

**Always test loop conversions** - verify the task creates expected files.

### Path Variables

Never include trailing slashes - causes double-slash bugs:

```yaml
# WRONG - creates paths like /etc/ipsec.d//private
ipsec_path: "configs/{{ server }}/ipsec/"

# CORRECT
ipsec_path: "configs/{{ server }}/ipsec"
```

### ignore_errors vs failed_when

```yaml
# WRONG - ansible-lint failure
- name: Clear history
  command: some_command
  ignore_errors: true

# CORRECT - explicit about expected failures
- name: Clear history
  command: some_command
  failed_when: false
```

### changed_when on Read-Only Tasks

Handlers and check commands that don't modify state need `changed_when: false`:

```yaml
- name: Check service status
  command: systemctl status foo
  changed_when: false
```

### Jinja2 Native Mode (Ansible 12+)

Ansible 12 enables `jinja2_native` by default, changing how values are evaluated:

**Boolean conditionals require actual booleans:**
```yaml
# WRONG - string "true" is not boolean
ipv6_support: "{% if ipv6 %}true{% else %}false{% endif %}"

# CORRECT - return actual boolean
ipv6_support: "{{ ipv6 is defined }}"
```

**No nested templates in lookup():**
```yaml
# WRONG - deprecated double-templating
key: "{{ lookup('file', '{{ SSH_keys.public }}') }}"

# CORRECT - pass variable directly
key: "{{ lookup('file', SSH_keys.public) }}"
```

**JSON files need explicit parsing:**
```yaml
# WRONG - returns string in native mode
creds: "{{ lookup('file', 'credentials.json') }}"

# CORRECT - parse JSON explicitly
creds: "{{ lookup('file', 'credentials.json') | from_json }}"
```

**default() doesn't trigger on empty strings:**
```yaml
# WRONG - empty string '' is not undefined
key: "{{ lookup('env', 'AWS_KEY') | default('fallback') }}"

# CORRECT - add true to handle falsy values
key: "{{ lookup('env', 'AWS_KEY') | default('fallback', true) }}"
```

**Complex Jinja loops break in set_fact:**
```yaml
# WRONG - list comprehension fails in native mode
servers: "[{% for s in configs %}{{ s.name }},{% endfor %}]"

# CORRECT - use Ansible loop
servers: "{{ servers | default([]) + [item.name] }}"
loop: "{{ configs }}"
```

**Use tests (not filters) for boolean checks:**
```yaml
# WRONG - filters return transformed data, not booleans
that: my_ip | ansible.utils.ipv4

# CORRECT - tests return native booleans
that: my_ip is ansible.utils.ipv4_address
```

## DNS Architecture

Algo uses a randomly generated IP in 172.16.0.0/12 on the loopback interface (`local_service_ip`) for DNS. This provides consistency across WireGuard and IPsec but requires understanding systemd socket activation.

### Why This Design

- Consistent DNS IP across both VPN protocols
- Survives interface changes and restarts
- Works identically across all cloud providers
- Trade-off: Requires `route_localnet=1` sysctl

### systemd Socket Activation

Ubuntu's dnscrypt-proxy uses socket activation which **completely ignores** the `listen_addresses` config setting. You must configure the socket, not the service:

```ini
# /etc/systemd/system/dnscrypt-proxy.socket.d/10-algo-override.conf
[Socket]
ListenStream=              # Clear defaults first
ListenDatagram=
ListenStream=172.x.x.x:53  # Then set VPN IP
ListenDatagram=172.x.x.x:53
```

Common mistakes:
- Trying to disable/mask the socket (breaks service dependency)
- Only setting ListenStream (need ListenDatagram for UDP)
- Forgetting to restart socket after config changes

### Debugging DNS

Many "routing" issues are actually DNS issues. Start here:

```bash
ss -lnup | grep :53                      # Should show local_service_ip:53
systemctl status dnscrypt-proxy.socket   # Check for config warnings
sysctl net.ipv4.conf.all.route_localnet  # Must be 1
dig @172.x.x.x google.com                # Test resolution
```

For comprehensive diagnostics, see [docs/troubleshooting.md](docs/troubleshooting.md#diagnostic-commands).

## Common Issues

### iptables Backend (nft vs legacy)

Ubuntu 22.04+ defaults to iptables-nft which reorders rules unpredictably. Algo forces iptables-legacy for consistent behavior. Switching backends can break DNS routing that previously worked.

### Multi-homed Systems (DigitalOcean, etc.)

Servers with both public and private IPs on the same interface need explicit output interface for NAT:

```yaml
-o {{ ansible_default_ipv4['interface'] }}
```

Don't overengineer with SNAT - MASQUERADE with interface specification works fine.

### OpenSSL Version Compatibility

OpenSSL 3.x dropped support for legacy algorithms. Add `-legacy` flag conditionally:

```yaml
{{ (openssl_version is version('3', '>=')) | ternary('-legacy', '') }}
```

### IPv6 Endpoint Formatting

WireGuard configs must bracket IPv6 addresses:

```jinja2
{% if ':' in IP %}[{{ IP }}]:{{ port }}{% else %}{{ IP }}:{{ port }}{% endif %}
```

### Jinja2 Templates

Many templates use Ansible-specific filters. Test with `tests/unit/test_template_rendering.py` and mock Ansible filters when testing.

## Time Wasters to Avoid

Lessons learned - don't spend time on these unless absolutely necessary:

1. **Converting MASQUERADE to SNAT** - MASQUERADE works fine for Algo's use case
2. **Fighting systemd socket activation** - Configure it properly instead of disabling
3. **Debugging NAT before checking DNS** - Most "routing" issues are DNS issues
4. **Complex IPsec policy matching** - Keep NAT rules simple
5. **Testing on existing servers** - Always test on fresh deployments
6. **Interface-specific route_localnet** - WireGuard interface doesn't exist until service starts
7. **DNAT for loopback addresses** - Packets to local IPs don't traverse PREROUTING

## What to Avoid

- **Speculative features** - Don't add "might be useful" functionality. Open an issue instead.
- **New dependencies without justification** - Vanilla Ansible/Python can do most things.
- **Bundling unrelated fixes** - One PR, one purpose. Separate issues get separate PRs.
- **Assuming behavior** - If converting `with_items` to `loop`, test that it still works. If adding a firewall rule, verify packets flow.
- **Configuration options** - Don't add flags unless users actively need them. Each option doubles testing surface.
- **Undocumented workarounds** - When working around broken upstream modules, file an issue and add a comment linking to it. Future maintainers need to know why workarounds exist.

## Writing Effective Tests

When writing tests, **verify your test actually detects the failure case** (mutation testing approach):

1. Write the test for the bug you're preventing
2. Temporarily introduce the bug to verify the test fails
3. Fix the bug and verify the test passes
4. Document what specific issue the test prevents

```python
def test_regression_openssl_inline_comments():
    """Tests that we detect inline comments in Jinja2 expressions."""
    # This pattern SHOULD fail (has inline comments)
    problematic = "{{ ['DNS:' + id,  # comment ] }}"
    assert not validate(problematic), "Should detect inline comments"

    # This pattern SHOULD pass (no inline comments)
    fixed = "{{ ['DNS:' + id] }}"
    assert validate(fixed), "Should pass without comments"
```

## Quick Reference

### Local Development Setup

```bash
uv sync
uv run ansible-galaxy install -r requirements.yml
ansible-playbook main.yml -e "provider=local"
```

### Common Commands

```bash
# Add/update users
ansible-playbook users.yml -e "server=SERVER_NAME"

# Update dependencies
uv lock && pytest tests/unit/ -q

# Debug deployment
ansible-playbook main.yml -vvv
```

### Key Directories

- `configs/` - Generated client configurations
- `roles/*/tasks/` - Main task files
- `roles/*/templates/` - Jinja2 templates
- `library/` - Custom Ansible modules (add to `mock_modules` in `.ansible-lint`)

## Non-Interactive Deployment

All `pause:` prompts in `input.yml` and provider roles skip when their
variable is pre-defined via `-e` or environment variables. This enables
fully headless deployment for CI, agents, and scripted workflows.
See [docs/deploy-from-ansible.md](docs/deploy-from-ansible.md) for
full human-facing documentation.

### Core variables

These bypass the main prompts in `input.yml`:

| Variable | Type | Default | Purpose |
|----------|------|---------|---------|
| `provider` | string | *(prompt)* | Provider alias (e.g., `digitalocean`, `ec2`, `local`) |
| `server_name` | string | `algo` | VPN server name |
| `ondemand_cellular` | bool | `false` | iOS/macOS Connect On Demand for cellular |
| `ondemand_wifi` | bool | `false` | iOS/macOS Connect On Demand for Wi-Fi |
| `ondemand_wifi_exclude` | string | *(none)* | Comma-separated trusted Wi-Fi networks |
| `store_pki` | bool | `false` | Retain PKI keys (needed to add users later) |
| `dns_adblocking` | bool | `false` | Enable DNS ad blocking |
| `ssh_tunneling` | bool | `false` | Per-user SSH tunnel accounts |

### Provider credentials

| Provider | `-e` variables | Env var fallbacks |
|----------|---------------|-------------------|
| `digitalocean` | `do_token`, `region` | `DO_API_TOKEN` |
| `ec2` | `aws_access_key`, `aws_secret_key`, `region` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (also reads `~/.aws/credentials`) |
| `lightsail` | `aws_access_key`, `aws_secret_key`, `region` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
| `azure` | `azure_secret`, `azure_tenant`, `azure_client_id`, `azure_subscription_id`, `region` | `AZURE_SECRET`, `AZURE_TENANT`, `AZURE_CLIENT_ID`, `AZURE_SUBSCRIPTION_ID` |
| `gce` | `gce_credentials_file`, `region` | `GCE_CREDENTIALS_FILE_PATH` |
| `hetzner` | `hcloud_token`, `region` | `HCLOUD_TOKEN` |
| `vultr` | `vultr_config`, `region` | `VULTR_API_CONFIG` |
| `scaleway` | `scaleway_token`, `scaleway_org_id`, `region` | `SCW_TOKEN`, `SCW_DEFAULT_ORGANIZATION_ID` |
| `linode` | `linode_token`, `region` | `LINODE_API_TOKEN` |
| `cloudstack` | `cs_key`, `cs_secret`, `cs_url`, `region` | `CLOUDSTACK_KEY`, `CLOUDSTACK_SECRET`, `CLOUDSTACK_ENDPOINT` |
| `openstack` | `region` | `OS_AUTH_URL` (source your `openrc.sh`) |
| `local` | `server`, `endpoint`, `local_install_confirmed` | *(none)* |

### Minimal examples

```bash
# DigitalOcean — fully headless
ansible-playbook main.yml -e \
  "provider=digitalocean
   server_name=algo
   region=nyc3
   do_token=YOUR_TOKEN
   ondemand_cellular=false
   ondemand_wifi=false
   dns_adblocking=false
   ssh_tunneling=false
   store_pki=false"

# Local — for CI/testing
ansible-playbook main.yml -e \
  "provider=local
   server=localhost
   endpoint=10.0.0.1
   local_install_confirmed=true
   ondemand_cellular=false
   ondemand_wifi=false
   dns_adblocking=false
   ssh_tunneling=false"
```

### Updating users non-interactively

```bash
ansible-playbook users.yml -e "server=YOUR_SERVER ca_password=YOUR_CA_PASS"
```

The `server` variable bypasses the server selection prompt.
`ca_password` is only required when IPsec is enabled.

## Security Considerations

- **Never expose secrets** - No passwords/keys in commits
- **CVE Response** - Update immediately when security issues found
- **Least Privilege** - Minimal permissions, dropped capabilities
- **Secure Defaults** - Strong crypto (secp384r1), no logging, strict firewall

## Platform Support

- **Primary OS**: Ubuntu 22.04/24.04 LTS
- **Secondary**: Debian 11/12
- **Architectures**: x86_64 and ARM64
- **Testing tip**: DigitalOcean droplets have both public and private IPs on eth0, making them good test cases for multi-IP NAT scenarios


================================================
FILE: CODEOWNERS
================================================
* @jackivanov


================================================
FILE: CONTRIBUTING.md
================================================
### Filing New Issues

* We welcome bug reports! Before filing, a quick check of the [FAQ](docs/faq.md) or [troubleshooting](docs/troubleshooting.md) docs might have your answer
* Algo automatically installs dependencies with uv - no manual setup required
* We support modern clients: macOS 12+, iOS 15+, Windows 11+, Ubuntu 22.04+, etc.
* Supported cloud providers: DigitalOcean, AWS, Azure, GCP, Vultr, Hetzner, Linode, OpenStack, CloudStack
* If you need to file a new issue, fill out any relevant fields in the Issue Template

### Pull Requests

* Run the full linter suite: `./scripts/lint.sh`
* Test your changes on multiple platforms when possible
* Use conventional commit messages that clearly describe your changes
* Pin dependency versions rather than using ranges (e.g., `==1.2.3` not `>=1.2.0`)

### Development Setup

* Clone the repository: `git clone https://github.com/trailofbits/algo.git`
* Run Algo: `./algo` (dependencies installed automatically via uv)
* Install git hooks: `prek install` (optional, for contributors)
* For local testing, consider using Docker or a cloud provider test instance

Thanks!


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
FROM python:3.12-alpine

ARG VERSION="git"
# Removed rust/cargo (not needed with uv), simplified package list
ARG PACKAGES="bash openssh-client openssl rsync tini"

LABEL name="algo" \
      version="${VERSION}" \
      description="Set up a personal IPsec VPN in the cloud" \
      maintainer="Trail of Bits <https://github.com/trailofbits/algo>" \
      org.opencontainers.image.source="https://github.com/trailofbits/algo" \
      org.opencontainers.image.description="Algo VPN - Set up a personal IPsec VPN in the cloud" \
      org.opencontainers.image.licenses="AGPL-3.0"

# Install system packages in a single layer
RUN apk --no-cache add ${PACKAGES} && \
    adduser -D -H -u 19857 algo && \
    mkdir -p /algo /algo/configs

WORKDIR /algo

# Copy uv binary from official image (using latest tag for automatic updates)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# Copy dependency files and install in single layer for better optimization
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev

# Copy application code
COPY . .

# Install Ansible Galaxy collections for cloud provider modules
RUN uv run ansible-galaxy collection install -r requirements.yml

# Set executable permissions and prepare runtime
# Note: /algo must remain root-owned for --cap-drop=all compatibility
# (root without CAP_DAC_OVERRIDE cannot write to files owned by others)
RUN chmod 0755 /algo/algo-docker.sh && \
    mkdir -p /data && \
    chown algo:algo /data

# Multi-arch support metadata
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN printf "Built on: %s\nTarget: %s\n" "${BUILDPLATFORM}" "${TARGETPLATFORM}" > /algo/build-info

# Note: Running as root for bind mount compatibility with algo-docker.sh
# The script handles /data volume permissions and needs root access
# This is a Docker limitation with bind-mounted volumes
USER root

# Health check to ensure container is functional
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD /bin/uv --version || exit 1

VOLUME ["/data"]
CMD [ "/algo/algo-docker.sh" ]
ENTRYPOINT [ "/sbin/tini", "--" ]


================================================
FILE: LICENSE
================================================
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.


================================================
FILE: PULL_REQUEST_TEMPLATE.md
================================================
<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code passes all linters (`./scripts/lint.sh`)
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] Dependencies use exact versions (e.g., `==1.2.3` not `>=1.2.0`).


================================================
FILE: README.md
================================================
# Algo VPN

[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://x.com/AlgoVPN)

Algo VPN is a set of Ansible scripts that simplify the setup of a personal WireGuard and IPsec VPN. It uses the most secure defaults available and works with common cloud providers.

See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information.

## Features

* Supports only IKEv2 with strong crypto (AES-GCM, SHA2, and P-256) for iOS, MacOS, and Linux
* Supports [WireGuard](https://www.wireguard.com/) for all of the above, in addition to Android and Windows 11
* Generates .conf files and QR codes for iOS, macOS, Android, and Windows WireGuard clients
* Generates Apple profiles to auto-configure iOS and macOS devices for IPsec - no client software required
* Includes helper scripts to add, remove, and manage users
* Blocks ads with a local DNS resolver (optional)
* Sets up limited SSH users for tunneling traffic (optional)
* Privacy-focused with minimal logging, automatic log rotation, and configurable privacy enhancements
* Based on Ubuntu 22.04 LTS with automatic security updates
* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack, CloudStack, Hetzner Cloud, Linode, or [your own Ubuntu server (for advanced users)](docs/deploy-to-ubuntu.md)

## Anti-features

* Does not support legacy cipher suites or protocols like L2TP, IKEv1, or RSA
* Does not install Tor, OpenVPN, or other risky servers
* Does not depend on the security of [TLS](https://tools.ietf.org/html/rfc7457)
* Does not claim to provide anonymity or censorship avoidance
* Does not claim to protect you from the [FSB](https://en.wikipedia.org/wiki/Federal_Security_Service), [MSS](https://en.wikipedia.org/wiki/Ministry_of_State_Security_(China)), [DGSE](https://en.wikipedia.org/wiki/Directorate-General_for_External_Security), or [FSM](https://en.wikipedia.org/wiki/Flying_Spaghetti_Monster)

## Deploy the Algo Server

The easiest way to get an Algo server running is to run it on your local system or from [Google Cloud Shell](docs/deploy-from-cloudshell.md) and let it set up a _new_ virtual machine in the cloud for you.

1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon Lightsail](https://aws.amazon.com/lightsail/), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/), [DreamCompute](https://www.dreamhost.com/cloud/computing/), [Linode](https://www.linode.com), other OpenStack-based cloud hosting, CloudStack-based cloud hosting, or [Hetzner Cloud](https://www.hetzner.com/).

2. **Get a copy of Algo.** The Algo scripts will be run from your local system. There are two ways to get a copy:

    - Download the [ZIP file](https://github.com/trailofbits/algo/archive/master.zip). Unzip the file to create a directory named `algo-master` containing the Algo scripts.

    - Use `git clone` to create a directory named `algo` containing the Algo scripts:
        ```bash
        git clone https://github.com/trailofbits/algo.git
        ```

3. **Set your configuration options.** Open `config.cfg` in your favorite text editor. Specify the users you want to create in the `users` list. Create a unique user for each device you plan to connect to your VPN. You should also review the other options before deployment, as changing your mind about them later [may require you to deploy a brand new server](https://github.com/trailofbits/algo/blob/master/docs/faq.md#i-deployed-an-algo-server-can-you-update-it-with-new-features).

4. **Start the deployment.** Return to your terminal. In the Algo directory, run the appropriate script for your platform:

    **macOS/Linux:**
    ```bash
    ./algo
    ```

    **Windows:**
    ```powershell
    .\algo.ps1
    ```

    The first time you run the script, it will automatically install the required Python environment (Python 3.11+). On subsequent runs, it starts immediately and works on all platforms (macOS, Linux, Windows via WSL). The Windows PowerShell script automatically uses WSL when needed, since Ansible requires a Unix-like environment. There are several optional features available, none of which are required for a fully functional VPN server. These optional features are described in the [deployment documentation](docs/deploy-from-ansible.md).

That's it! You can now set up clients to connect to your VPN. Proceed to [Configure the VPN Clients](#configure-the-vpn-clients) below.

```
    "#                          Congratulations!                            #"
    "#                     Your Algo server is running.                     #"
    "#    Config files and certificates are in the ./configs/ directory.    #"
    "#              Go to https://whoer.net/ after connecting               #"
    "#        and ensure that all your traffic passes through the VPN.      #"
    "#                     Local DNS resolver 172.16.0.1                    #"
    "#        The p12 and SSH keys password for new users is XXXXXXXX       #"
    "#        The CA key password is XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX       #"
    "#      Shell access: ssh -F configs/<server_ip>/ssh_config <hostname>  #"
```

## Configure the VPN Clients

Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are saved under a subdirectory named with the IP address of your new Algo VPN server.

**Important for IPsec users**: If you want to add or delete users later, you must select `yes` at the `Do you want to retain the keys (PKI)?` prompt during the server deployment. This preserves the certificate authority needed for user management.

### Apple

WireGuard is used to provide VPN services on Apple devices. Algo generates a WireGuard configuration file, `wireguard/<username>.conf`, and a QR code, `wireguard/<username>.png`, for each user defined in `config.cfg`.

On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the iOS App Store. Then, use the WireGuard app to scan the QR code or AirDrop the configuration file to the device.

On macOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1451685025?mt=12) app from the Mac App Store. WireGuard will appear in the menu bar once you run the app. Click on the WireGuard icon, choose **Import tunnel(s) from file...**, then select the appropriate WireGuard configuration file.

On either iOS or macOS, you can enable "Connect on Demand" and/or exclude certain trusted Wi-Fi networks (such as your home or work) by editing the tunnel configuration in the WireGuard app. (Algo can't do this automatically for you.)

If you prefer to use the built-in IPsec VPN on Apple devices, or need "Connect on Demand" or excluded Wi-Fi networks automatically configured, see the [Apple IPsec client setup guide](docs/client-apple-ipsec.md) for detailed configuration instructions.

### Android

WireGuard is used to provide VPN services on Android. Install the [WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). Import the corresponding `wireguard/<name>.conf` file to your device, then set up a new connection with it. See the [Android setup guide](docs/client-android.md) for detailed installation and configuration instructions.

### Windows

WireGuard is used to provide VPN services on Windows. Algo generates a WireGuard configuration file, `wireguard/<username>.conf`, for each user defined in `config.cfg`.

Install the [WireGuard VPN Client](https://www.wireguard.com/install/#windows-7-8-81-10-2012-2016-2019). Import the generated `wireguard/<username>.conf` file to your device, then set up a new connection with it. See the [Windows setup instructions](docs/client-windows.md) for more detailed walkthrough and troubleshooting.

### Linux

Linux clients can use either WireGuard or IPsec:

WireGuard: WireGuard works great with Linux clients. See the [Linux WireGuard setup guide](docs/client-linux-wireguard.md) for step-by-step instructions on configuring WireGuard on Ubuntu and other distributions.

IPsec: For strongSwan IPsec clients (including OpenWrt, Ubuntu Server, and other distributions), see the [Linux IPsec setup guide](docs/client-linux-ipsec.md) for detailed configuration instructions.

### OpenWrt

For OpenWrt routers using WireGuard, see the [OpenWrt WireGuard setup guide](docs/client-openwrt-router-wireguard.md) for router-specific configuration instructions.

### Other Devices

For devices not covered above or manual configuration, you'll need specific certificate and configuration files. The files you need depend on your device platform and VPN protocol (WireGuard or IPsec).

* ipsec/manual/cacert.pem: CA Certificate
* ipsec/manual/<user>.p12: User Certificate and Private Key (in PKCS#12 format)
* ipsec/manual/<user>.conf: strongSwan client configuration
* ipsec/manual/<user>.secrets: strongSwan client configuration
* ipsec/apple/<user>.mobileconfig: Apple Profile
* wireguard/<user>.conf: WireGuard configuration profile
* wireguard/<user>.png: WireGuard configuration QR code

## Setup an SSH Tunnel

If you turned on the optional SSH tunneling role, local user accounts will be created for each user in `config.cfg`, and SSH authorized_key files for them will be in the `configs` directory (user.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and only have limited tunneling options (e.g., `ssh -N` is required). This ensures that SSH users have the least access required to set up a tunnel and can perform no other actions on the Algo server.

Use the example command below to start an SSH tunnel by replacing `<user>` and `<ip>` with your own. Once the tunnel is set up, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server:

```bash
ssh -D 127.0.0.1:1080 -f -q -C -N <user>@algo -i configs/<ip>/ssh-tunnel/<user>.pem -F configs/<ip>/ssh_config
```

## SSH into Algo Server

Your Algo server is configured for key-only SSH access for administrative purposes. Open the Terminal app, `cd` into the `algo-master` directory where you originally downloaded Algo, and then use the command listed on the success message:

```
ssh -F configs/<ip>/ssh_config <hostname>
```

where `<ip>` is the IP address of your Algo server. If you find yourself regularly logging into the server, it will be useful to load your Algo SSH key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently:

```
ssh-add ~/.ssh/algo > /dev/null 2>&1
```

Alternatively, you can choose to include the generated configuration for any Algo servers created into your SSH config. Edit the file `~/.ssh/config` to include this directive at the top:

```
Include <algodirectory>/configs/*/ssh_config
```

where `<algodirectory>` is the directory where you cloned Algo.

## Adding or Removing Users

Algo makes it easy to add or remove users from your VPN server after initial deployment.

For IPsec users: You must have selected `yes` at the `Do you want to retain the keys (PKI)?` prompt during the initial server deployment. This preserves the certificate authority needed for user management. You should also save the p12 and CA key passwords shown during deployment, as they're only displayed once.

To add or remove users, first edit the `users` list in your `config.cfg` file. Add new usernames or remove existing ones as needed. Then navigate to the algo directory in your terminal and run:

**macOS/Linux:**
```bash
./algo update-users
```

**Windows:**
```powershell
.\algo.ps1 update-users
```

After the process completes, new configuration files will be generated in the `configs` directory for any new users. The Algo VPN server will be updated to contain only the users listed in the `config.cfg` file. Removed users will no longer be able to connect, and new users will have fresh certificates and configuration files ready for use.

## Privacy and Logging

Algo takes a pragmatic approach to privacy. By default, we minimize logging while maintaining enough information for security and troubleshooting.

What IS logged by default:
* System security events (failed SSH attempts, firewall blocks, system updates)
* Kernel messages and boot diagnostics (with reduced verbosity)
* WireGuard client state (visible via `sudo wg` - shows last endpoint and handshake time)
* Basic service status (service starts/stops/errors)
* All logs automatically rotate and delete after 7 days

Privacy is controlled by two main settings in `config.cfg`:
* `strongswan_log_level: -1` - Controls StrongSwan connection logging (-1 = disabled, 2 = debug)
* `privacy_enhancements_enabled: true` - Master switch for log rotation, history clearing, log filtering, and cleanup

To enable full debugging when troubleshooting, set both `strongswan_log_level: 2` and `privacy_enhancements_enabled: false`. This will capture detailed connection logs and disable all privacy features. Remember to revert these changes after debugging.

After deployment, verify your privacy settings:
```bash
ssh -F configs/<server_ip>/ssh_config <hostname>
sudo /usr/local/bin/privacy-monitor.sh
```

Perfect privacy is impossible with any VPN solution. Your cloud provider sees and logs network traffic metadata regardless of your server configuration. And of course, your ISP knows you're connecting to a VPN server, even if they can't see what you're doing through it.

For the highest level of privacy, treat your Algo servers as disposable. Spin up a new instance when you need it, use it for your specific purpose, then destroy it completely. The ephemeral nature of cloud infrastructure can be a privacy feature if you use it intentionally.

## Additional Documentation
* [FAQ](docs/faq.md)
* [Troubleshooting](docs/troubleshooting.md)
* How Algo uses [Firewalls](docs/firewalls.md)

### Setup Instructions for Specific Cloud Providers
* Configure [Amazon EC2](docs/cloud-amazon-ec2.md)
* Configure [Azure](docs/cloud-azure.md)
* Configure [DigitalOcean](docs/cloud-do.md)
* Configure [Google Cloud Platform](docs/cloud-gce.md)
* Configure [Vultr](docs/cloud-vultr.md)
* Configure [CloudStack](docs/cloud-cloudstack.md)
* Configure [Hetzner Cloud](docs/cloud-hetzner.md)

### Install and Deploy from Common Platforms
* Deploy from [macOS](docs/deploy-from-macos.md)
* Deploy from [Windows](docs/deploy-from-windows.md)
* Deploy from [Google Cloud Shell](docs/deploy-from-cloudshell.md)
* Deploy from a [Docker container](docs/deploy-from-docker.md)

### Setup VPN Clients to Connect to the Server
* Setup [Windows](docs/client-windows.md) clients
* Setup [Android](docs/client-android.md) clients
* Setup [Linux](docs/client-linux.md) clients with Ansible
* Setup Ubuntu clients to use [WireGuard](docs/client-linux-wireguard.md)
* Setup Linux clients to use [IPsec](docs/client-linux-ipsec.md)
* Setup Apple devices to use [IPsec](docs/client-apple-ipsec.md)
* Setup Macs running macOS 10.13 or older to use [WireGuard](docs/client-macos-wireguard.md)

### Advanced Deployment
* Deploy to your own [Ubuntu](docs/deploy-to-ubuntu.md) server, and road warrior setup
* Deploy from [Ansible](docs/deploy-from-ansible.md) non-interactively
* Deploy onto a [cloud server at time of creation with shell script or cloud-init](docs/deploy-from-script-or-cloud-init-to-localhost.md)
* Deploy to an [unsupported cloud provider](docs/deploy-to-unsupported-cloud.md)

If you've read all the documentation and have further questions, [create a new discussion](https://github.com/trailofbits/algo/discussions).

## Endorsements

> I've been ranting about the sorry state of VPN svcs for so long, probably about
> time to give a proper talk on the subject. TL;DR: use Algo.

-- [Kenn White](https://twitter.com/kennwhite/status/814166603587788800)

> Before picking a VPN provider/app, make sure you do some research
> https://research.csiro.au/ng/wp-content/uploads/sites/106/2016/08/paper-1.pdf ... – or consider Algo

-- [The Register](https://twitter.com/TheRegister/status/825076303657177088)

> Algo is really easy and secure.

-- [the grugq](https://twitter.com/thegrugq/status/786249040228786176)

> I played around with Algo VPN, a set of scripts that let you set up a VPN in the cloud in very little time, even if you don’t know much about development. I’ve got to say that I was quite impressed with Trail of Bits’ approach.

-- [Romain Dillet](https://twitter.com/romaindillet/status/851037243728965632) for [TechCrunch](https://techcrunch.com/2017/04/09/how-i-made-my-own-vpn-server-in-15-minutes/)

> If you’re uncomfortable shelling out the cash to an anonymous, random VPN provider, this is the best solution.

-- [Thorin Klosowski](https://twitter.com/kingthor) for [Lifehacker](http://lifehacker.com/how-to-set-up-your-own-completely-free-vpn-in-the-cloud-1794302432)

## Contributing

See our [Development Guide](docs/DEVELOPMENT.md) for information on:
* Setting up your development environment
* Using prek hooks for code quality
* Running tests and linters
* Contributing code via pull requests

## Support Algo VPN
[![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E)
[![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn)

All donations support continued development. Thanks!

* We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) and [Patreon](https://www.patreon.com/algovpn).
* Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit.
* We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests.

Algo is licensed and distributed under the AGPLv3. If you want to distribute a closed-source modification or service based on Algo, then please consider <a href="mailto:opensource@trailofbits.com">purchasing an exception</a> . As with the methods above, this will help support continued development.


================================================
FILE: SECURITY.md
================================================
# Reporting Security Issues

The Algo team and community take security bugs in Algo seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.

To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/trailofbits/algo/security/) tab.

The Algo team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.

Report security bugs in third-party modules to the person or team maintaining the module.


================================================
FILE: algo
================================================
#!/usr/bin/env bash

set -e

# Track which installation method succeeded
UV_INSTALL_METHOD=""

# Function to install uv via package managers (most secure)
install_uv_via_package_manager() {
    echo "Attempting to install uv via system package manager..."

    if command -v brew &> /dev/null; then
        echo "Using Homebrew..."
        brew install uv && UV_INSTALL_METHOD="Homebrew" && return 0
    elif command -v apt &> /dev/null && apt list uv 2>/dev/null | grep -q uv; then
        echo "Using apt..."
        sudo apt update && sudo apt install -y uv && UV_INSTALL_METHOD="apt" && return 0
    elif command -v dnf &> /dev/null; then
        echo "Using dnf..."
        sudo dnf install -y uv 2>/dev/null && UV_INSTALL_METHOD="dnf" && return 0
    elif command -v pacman &> /dev/null; then
        echo "Using pacman..."
        sudo pacman -S --noconfirm uv 2>/dev/null && UV_INSTALL_METHOD="pacman" && return 0
    elif command -v zypper &> /dev/null; then
        echo "Using zypper..."
        sudo zypper install -y uv 2>/dev/null && UV_INSTALL_METHOD="zypper" && return 0
    elif command -v winget &> /dev/null; then
        echo "Using winget..."
        winget install --id=astral-sh.uv -e && UV_INSTALL_METHOD="winget" && return 0
    elif command -v scoop &> /dev/null; then
        echo "Using scoop..."
        scoop install uv && UV_INSTALL_METHOD="scoop" && return 0
    fi

    return 1
}

# Function to handle Ubuntu-specific installation alternatives
install_uv_ubuntu_alternatives() {
    # Check if we're on Ubuntu
    if ! command -v lsb_release &> /dev/null || [[ "$(lsb_release -si)" != "Ubuntu" ]]; then
        return 1  # Not Ubuntu, skip these options
    fi

    echo ""
    echo "Ubuntu detected. Additional trusted installation options available:"
    echo ""
    echo "1. pipx (official PyPI, installs ~9 packages)"
    echo "   Command: sudo apt install pipx && pipx install uv"
    echo ""
    echo "2. snap (community-maintained by Canonical employee)"
    echo "   Command: sudo snap install astral-uv --classic"
    echo "   Source: https://github.com/lengau/uv-snap"
    echo ""
    echo "3. Continue to official installer script download"
    echo ""

    while true; do
        read -r -p "Choose installation method (1/2/3): " choice
        case $choice in
            1)
                echo "Installing uv via pipx..."
                if sudo apt update && sudo apt install -y pipx; then
                    if pipx install uv; then
                        # Add pipx bin directory to PATH
                        export PATH="$HOME/.local/bin:$PATH"
                        UV_INSTALL_METHOD="pipx"
                        return 0
                    fi
                fi
                echo "pipx installation failed, trying next option..."
                ;;
            2)
                echo "Installing uv via snap..."
                if sudo snap install astral-uv --classic; then
                    # Snap binaries should be automatically in PATH via /snap/bin
                    UV_INSTALL_METHOD="snap"
                    return 0
                fi
                echo "snap installation failed, trying next option..."
                ;;
            3)
                return 1  # Continue to official installer download
                ;;
            *)
                echo "Invalid option. Please choose 1, 2, or 3."
                ;;
        esac
    done
}

# Function to install uv via download (with user consent)
install_uv_via_download() {
    echo ""
    echo "⚠️  SECURITY NOTICE ⚠️"
    echo "uv is not available via system package managers on this system."
    echo "To continue, we need to download and execute an installation script from:"
    echo "  https://astral.sh/uv/install.sh (Linux/macOS)"
    echo "  https://astral.sh/uv/install.ps1 (Windows)"
    echo ""
    echo "For maximum security, you can install uv manually instead:"
    echo "  1. Visit: https://docs.astral.sh/uv/getting-started/installation/"
    echo "  2. Download the binary for your platform from GitHub releases"
    echo "  3. Verify checksums and install manually"
    echo "  4. Then run: ./algo"
    echo ""

    read -p "Continue with script download? (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Installation cancelled. Please install uv manually and retry."
        exit 1
    fi

    echo "Downloading uv installation script..."
    if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "linux-gnu" && -n "${WSL_DISTRO_NAME:-}" ]] || uname -s | grep -q "MINGW\|MSYS"; then
        # Windows (Git Bash/WSL/MINGW) - use versioned installer
        powershell -ExecutionPolicy ByPass -c "irm https://github.com/astral-sh/uv/releases/download/0.8.5/uv-installer.ps1 | iex"
        UV_INSTALL_METHOD="official installer (Windows)"
    else
        # macOS/Linux - use the versioned script for consistency
        curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.8.5/uv-installer.sh | sh
        UV_INSTALL_METHOD="official installer"
    fi
}

# Check if uv is installed, if not, install it securely
if ! command -v uv &> /dev/null; then
    echo "uv (Python package manager) not found. Installing..."

    # Try package managers first (most secure)
    if ! install_uv_via_package_manager; then
        # Try Ubuntu-specific alternatives if available
        if ! install_uv_ubuntu_alternatives; then
            # Fall back to download with user consent
            install_uv_via_download
        fi
    fi

    # Reload PATH to find uv (includes pipx, cargo, and snap paths)
    # Note: This PATH change only affects the current shell session.
    # Users may need to restart their terminal for subsequent runs.
    export PATH="$HOME/.local/bin:$HOME/.cargo/bin:/snap/bin:$PATH"

    # Verify installation worked
    if ! command -v uv &> /dev/null; then
        echo "Error: uv installation failed. Please restart your terminal and try again."
        echo "Or install manually from: https://docs.astral.sh/uv/getting-started/installation/"
        exit 1
    fi

    echo "✓ uv installed successfully via ${UV_INSTALL_METHOD}!"
fi

# Install Ansible Galaxy collections if requirements.yml exists
# This is needed for cloud providers that use collection modules (Linode, DigitalOcean, Azure, etc.)
if [ -f "requirements.yml" ]; then
    uv run ansible-galaxy collection install -r requirements.yml > /dev/null 2>&1 || true
fi

# Run the appropriate playbook
case "$1" in
  help|-h|--help)
    echo "Usage: ./algo [COMMAND] [ANSIBLE_OPTIONS]"
    echo ""
    echo "Set up a personal VPN in the cloud."
    echo ""
    echo "Commands:"
    echo "  (default)        Deploy a new VPN server"
    echo "  update-users     Add or remove users on an existing server"
    echo "  destroy          Destroy a deployed server and clean up configs"
    echo "  list-servers     List deployed servers (JSON output)"
    echo ""
    echo "Configuration:"
    echo "  Edit config.cfg to set users, DNS, and VPN options before deploying."
    echo ""
    echo "Non-interactive deployment:"
    echo "  ./algo -e 'provider=digitalocean server_name=algo region=nyc3 do_token=TOKEN'"
    echo ""
    echo "Common Ansible options (passed through):"
    echo "  -e KEY=VALUE     Set variable (bypass interactive prompts)"
    echo "  -v, -vvv         Increase output verbosity"
    echo "  --skip-tags TAG  Skip specific components"
    echo "  -t, --tags TAG   Run only specific components"
    echo ""
    echo "Docs: https://trailofbits.github.io/algo/"
    exit 0
    ;;
  update-users)
    uv run ansible-playbook users.yml "${@:2}" -t update-users ;;
  destroy)
    if [ -z "${2:-}" ] || [[ "$2" == -* ]]; then
      echo "Usage: ./algo destroy <server-ip> [ANSIBLE_OPTIONS]"
      echo ""
      echo "Destroy a deployed Algo VPN server and remove local configs."
      echo ""
      echo "Arguments:"
      echo "  server-ip        IP address of the server to destroy"
      echo ""
      echo "Examples:"
      echo "  ./algo destroy 188.166.66.185"
      echo "  ./algo destroy 52.1.2.3 -e \"region=us-east-1\""
      echo "  ./algo destroy 188.166.66.185 -e \"confirm_destroy=true\""
      exit 1
    fi
    uv run ansible-playbook destroy.yml -e "server_ip=$2" "${@:3}" ;;
  list-servers)
    uv run python3 scripts/list_servers.py "${@:2}" ;;
  *)
    uv run ansible-playbook main.yml "${@}" ;;
esac


================================================
FILE: algo-docker.sh
================================================
#!/usr/bin/env bash

set -eEo pipefail

ALGO_DIR="/algo"
DATA_DIR="/data"

umask 0077

usage() {
    retcode="${1:-0}"
    echo "To run algo from Docker:"
    echo ""
    echo "docker run --cap-drop=all -it -v <path to configurations>:${DATA_DIR} ghcr.io/trailofbits/algo:latest"
    echo ""
    exit "${retcode}"
}

if [ ! -f "${DATA_DIR}"/config.cfg ] ; then
  echo "Looks like you're not bind-mounting your config.cfg into this container."
  echo "algo needs a configuration file to run."
  echo ""
  usage -1
fi

if [ ! -e /dev/console ] ; then
  echo "Looks like you're trying to run this container without a TTY."
  echo "If you don't pass -t, you can't interact with the algo script."
  echo ""
  usage -1
fi

# To work around problems with bind-mounting Windows volumes, we need to
# copy files out of ${DATA_DIR}, ensure appropriate line endings and permissions,
# then copy the algo-generated files into ${DATA_DIR}.

tr -d '\r' < "${DATA_DIR}"/config.cfg > "${ALGO_DIR}"/config.cfg
test -d "${DATA_DIR}"/configs && rsync -qLktr --delete "${DATA_DIR}"/configs "${ALGO_DIR}"/

"${ALGO_DIR}"/algo "${ALGO_ARGS[@]}"
retcode=${?}

rsync -qLktr --delete "${ALGO_DIR}"/configs "${DATA_DIR}"/
exit "${retcode}"


================================================
FILE: algo-showenv.sh
================================================
#!/usr/bin/env bash
#
# Print information about Algo's invocation environment to aid in debugging.
# This is normally called from Ansible right before a deployment gets underway.

# Skip printing this header if we're just testing with no arguments.
if [[ $# -gt 0 ]]; then
    echo ""
    echo "--> Please include the following block of text when reporting issues:"
    echo ""
fi

if [[ ! -f ./algo ]]; then
    echo "This should be run from the top level Algo directory"
fi

# Determine the operating system.
case "$(uname -s)" in
    Linux)
        OS="Linux ($(uname -r) $(uname -v))"
        if [[ -f /etc/os-release ]]; then
            # shellcheck disable=SC1091
            # I hope this isn't dangerous.
            . /etc/os-release
            if [[ ${PRETTY_NAME} ]]; then
                OS="${PRETTY_NAME}"
            elif [[ ${NAME} ]]; then
                OS="${NAME} ${VERSION}"
            fi
        fi
        STAT="stat -c %y"
        ;;
    Darwin)
        OS="$(sw_vers -productName) $(sw_vers -productVersion)"
        STAT="stat -f %Sm"
        ;;
    *)
        OS="Unknown"
        ;;
esac

# Determine if virtualization is being used with Linux.
VIRTUALIZED=""
if [[ -x $(command -v systemd-detect-virt) ]]; then
    DETECT_VIRT="$(systemd-detect-virt)"
    if [[ ${DETECT_VIRT} != "none" ]]; then
        VIRTUALIZED=" (Virtualized: ${DETECT_VIRT})"
    fi
elif [[ -f /.dockerenv ]]; then
    VIRTUALIZED=" (Virtualized: docker)"
fi

echo "Algo running on: ${OS}${VIRTUALIZED}"

# Determine the currentness of the Algo software.
if [[ -d .git && -x $(command -v git) ]]; then
    ORIGIN="$(git remote get-url origin)"
    COMMIT="$(git log --max-count=1 --oneline --no-decorate --no-color)"
    if [[ ${ORIGIN} == "https://github.com/trailofbits/algo.git" ]]; then
        SOURCE="clone"
    else
        SOURCE="fork"
    fi
    echo "Created from git ${SOURCE}. Last commit: ${COMMIT}"
elif [[ -f LICENSE && ${STAT} ]]; then
    CREATED="$(${STAT} LICENSE)"
    echo "ZIP file created: ${CREATED}"
fi

# The Python version might be useful to know.
if [[ -x $(command -v uv) ]]; then
    echo "uv Python environment:"
    uv run python --version 2>&1
    uv --version 2>&1
elif [[ -f ./algo ]]; then
    echo "uv not found: try running './algo' to install dependencies"
fi

# Just print out all command line arguments, which are expected
# to be Ansible variables.
if [[ $# -gt 0 ]]; then
    echo "Runtime variables:"
    for VALUE in "$@"; do
        echo "    ${VALUE}"
    done
fi

exit 0


================================================
FILE: algo.ps1
================================================
# PowerShell script for Windows users to run Algo VPN
param(
    [Parameter(ValueFromRemainingArguments)]
    [string[]]$Arguments
)

# Check if we're actually running inside WSL (not just if WSL is available)
function Test-RunningInWSL {
    # These environment variables are only set when running inside WSL
    return $env:WSL_DISTRO_NAME -or $env:WSLENV
}

# Function to run Algo in WSL
function Invoke-AlgoInWSL {
    param($Arguments)

    Write-Host "NOTICE: Ansible requires a Unix-like environment and cannot run natively on Windows."
    Write-Host "Attempting to run Algo via Windows Subsystem for Linux (WSL)..."
    Write-Host ""

    if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
        Write-Host "ERROR: WSL (Windows Subsystem for Linux) is not installed." -ForegroundColor Red
        Write-Host ""
        Write-Host "Algo requires WSL to run Ansible on Windows. To install WSL:" -ForegroundColor Yellow
        Write-Host ""
        Write-Host "  Step 1: Open PowerShell as Administrator and run:"
        Write-Host "          wsl --install -d Ubuntu-22.04" -ForegroundColor Cyan
        Write-Host "          (Note: 22.04 LTS recommended for WSL stability)" -ForegroundColor Gray
        Write-Host ""
        Write-Host "  Step 2: Restart your computer when prompted"
        Write-Host ""
        Write-Host "  Step 3: After restart, open Ubuntu from the Start menu"
        Write-Host "          and complete the initial setup (create username/password)"
        Write-Host ""
        Write-Host "  Step 4: Run this script again: .\algo.ps1"
        Write-Host ""
        Write-Host "For detailed instructions, see:" -ForegroundColor Yellow
        Write-Host "https://github.com/trailofbits/algo/blob/master/docs/deploy-from-windows.md"
        exit 1
    }

    # Check if any WSL distributions are installed and running
    Write-Host "Checking for WSL Linux distributions..."
    $wslList = wsl -l -v 2>$null
    if ($LASTEXITCODE -ne 0) {
        Write-Host "ERROR: WSL is installed but no Linux distributions are available." -ForegroundColor Red
        Write-Host ""
        Write-Host "You need to install Ubuntu. Run this command as Administrator:" -ForegroundColor Yellow
        Write-Host "  wsl --install -d Ubuntu-22.04" -ForegroundColor Cyan
        Write-Host "          (Note: 22.04 LTS recommended for WSL stability)" -ForegroundColor Gray
        Write-Host ""
        Write-Host "Then restart your computer and try again."
        exit 1
    }

    Write-Host "Successfully found WSL. Launching Algo..." -ForegroundColor Green
    Write-Host ""

    # Get current directory name for WSL path mapping
    $currentDir = Split-Path -Leaf (Get-Location)

    try {
        if ($Arguments.Count -gt 0 -and $Arguments[0] -eq "update-users") {
            $remainingArgs = $Arguments[1..($Arguments.Count-1)] -join " "
            wsl bash -c "cd /mnt/c/$currentDir 2>/dev/null || (echo 'Error: Cannot access directory in WSL. Make sure you are running from a Windows drive (C:, D:, etc.)' && exit 1) && ./algo update-users $remainingArgs"
        } else {
            $allArgs = $Arguments -join " "
            wsl bash -c "cd /mnt/c/$currentDir 2>/dev/null || (echo 'Error: Cannot access directory in WSL. Make sure you are running from a Windows drive (C:, D:, etc.)' && exit 1) && ./algo $allArgs"
        }

        if ($LASTEXITCODE -ne 0) {
            Write-Host ""
            Write-Host "Algo finished with exit code: $LASTEXITCODE" -ForegroundColor Yellow
            if ($LASTEXITCODE -eq 1) {
                Write-Host "This may indicate a configuration issue or user cancellation."
            }
        }
    } catch {
        Write-Host ""
        Write-Host "ERROR: Failed to run Algo in WSL." -ForegroundColor Red
        Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host ""
        Write-Host "Troubleshooting:" -ForegroundColor Yellow
        Write-Host "1. Make sure you're running from a Windows drive (C:, D:, etc.)"
        Write-Host "2. Try opening Ubuntu directly and running: cd /mnt/c/$currentDir && ./algo"
        Write-Host "3. See: https://github.com/trailofbits/algo/blob/master/docs/deploy-from-windows.md"
        exit 1
    }
}

# Main execution
try {
    # Check if we're actually running inside WSL
    if (Test-RunningInWSL) {
        Write-Host "Detected WSL environment. Running Algo using standard Unix approach..."

        # Verify bash is available (should be in WSL)
        if (-not (Get-Command bash -ErrorAction SilentlyContinue)) {
            Write-Host "ERROR: Running in WSL but bash is not available." -ForegroundColor Red
            Write-Host "Your WSL installation may be incomplete. Try running:" -ForegroundColor Yellow
            Write-Host "  wsl --shutdown" -ForegroundColor Cyan
            Write-Host "  wsl" -ForegroundColor Cyan
            exit 1
        }

        # Run the standard Unix algo script
        & bash -c "./algo $($Arguments -join ' ')"
        exit $LASTEXITCODE
    }

    # We're on native Windows - need to use WSL
    Invoke-AlgoInWSL $Arguments

} catch {
    Write-Host ""
    Write-Host "UNEXPECTED ERROR:" -ForegroundColor Red
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host ""
    Write-Host "If you continue to have issues:" -ForegroundColor Yellow
    Write-Host "1. Ensure WSL is properly installed and Ubuntu is set up"
    Write-Host "2. See troubleshooting guide: https://github.com/trailofbits/algo/blob/master/docs/deploy-from-windows.md"
    Write-Host "3. Or use WSL directly: open Ubuntu and run './algo'"
    exit 1
}


================================================
FILE: ansible.cfg
================================================
[defaults]
inventory = inventory
pipelining = True
retry_files_enabled = False
host_key_checking = False
timeout = 60
stdout_callback = default
display_skipped_hosts = no
force_valid_group_names = ignore
remote_tmp = /tmp/.ansible/tmp

[paramiko_connection]
record_host_keys = False

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 -o IdentitiesOnly=yes
scp_if_ssh = True
retries = 30


================================================
FILE: cloud.yml
================================================
---
- name: Provision the server
  hosts: localhost
  tags: always
  become: false
  vars_files:
    - config.cfg

  tasks:
    - block:
        - name: Local pre-tasks
          import_tasks: playbooks/cloud-pre.yml

        - name: Include a provisioning role
          include_role:
            name: "{{ 'local' if algo_provider == 'local' else 'cloud-' + algo_provider }}"

        - name: Local post-tasks
          import_tasks: playbooks/cloud-post.yml
      rescue:
        - include_tasks: playbooks/rescue.yml


================================================
FILE: config.cfg
================================================
---

# ============================================
# TROUBLESHOOTING DEPLOYMENT ISSUES
# ============================================
# If your deployment fails with hidden/censored output, temporarily set
# algo_no_log to 'false' below. This will show detailed error messages
# including API responses.
# IMPORTANT: Set back to 'true' before sharing logs or screenshots!
# ============================================
algo_no_log: true  # Set to 'false' for debugging (shows sensitive data in output)

# This is the list of users to generate.
# Every device must have a unique user.
# You can add up to 65,534 new users over the lifetime of an AlgoVPN.
# User names with leading 0's or containing only numbers should be escaped in double quotes, e.g. "000dan" or "123".
# Email addresses are not allowed.
users:
  - phone
  - laptop
  - desktop

### Review these options BEFORE you run Algo, as they are very difficult/impossible to change after the server is deployed.

# SSH port for cloud deployments (doesn't apply to existing Ubuntu servers)
ssh_port: 4160

# VPN protocols to deploy
ipsec_enabled: true
wireguard_enabled: true
wireguard_port: 51820  # Change if blocked by your network (avoid 53/UDP)

# Use different IP for outbound traffic (DigitalOcean only)
alternative_ingress_ip: false

# Reduce MTU if connections hang (0 = auto-detect)
# See: docs/troubleshooting.md#various-websites-appear-to-be-offline-through-the-vpn
reduce_mtu: 0

# Ad blocking lists (modify /usr/local/sbin/adblock.sh after deployment to add more)
adblock_lists:
  - "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"

# DNS encryption (required if using ad blocking)
dns_encryption: true

# Client isolation (set false for "road warrior" setup where clients can reach each other)
BetweenClients_DROP: true
block_smb: true          # Block SMB/CIFS traffic
block_netbios: true      # Block NETBIOS traffic

# Automatic reboot for security updates (time in server's timezone, default UTC)
unattended_reboot:
  enabled: false
  time: 06:00

### Privacy Settings ###
# StrongSwan connection logging (-1 = disabled, 2 = debug)
strongswan_log_level: -1

# Master switch for privacy enhancements (log rotation, history clearing, etc.)
# Set to false for debugging. For advanced privacy options, see roles/privacy/defaults/main.yml
privacy_enhancements_enabled: true

### Advanced users only below this line ###

# DNSCrypt providers (see https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v2/public-resolvers.md)
dnscrypt_servers:
  ipv4:
    - cloudflare
#   - google
#   - YourCustomServer  # For NextDNS etc., add stamp below
  ipv6:
    - cloudflare-ipv6

custom_server_stamps:
# YourCustomServer: 'sdns://...'

# DNS servers when encryption is disabled
dns_servers:
  ipv4:
    - 1.1.1.1
    - 1.0.0.1
  ipv6:
    - 2606:4700:4700::1111
    - 2606:4700:4700::1001

# Store PKI in RAM disk when not retaining (MacOS/Linux only)
pki_in_tmpfs: true

# Regenerate ALL user credentials on update-users (not just new users)
# When false: existing WireGuard keys and IPsec certs are preserved, new users added
# When true: all credentials deleted and regenerated - ALL CLIENTS MUST RECONFIGURE
# Use true after: suspected key compromise, removing untrusted users, or security audit
keys_clean_all: false

### VPN Network Configuration ###
strongswan_network: 10.48.0.0/16
strongswan_network_ipv6: '2001:db8:4160::/48'

wireguard_network_ipv4: 10.49.0.0/16
wireguard_network_ipv6: 2001:db8:a160::/48

# Keep NAT connections alive (0 = disabled)
wireguard_PersistentKeepalive: 0

### Experimental Performance Options ###
# These are experimental and may cause issues. Enable at your own risk.
# performance_skip_optional_reboots: false  # Skip non-kernel reboots
# performance_parallel_crypto: false        # Parallel key generation
# performance_parallel_packages: false      # Batch package installation
# performance_preinstall_packages: false    # Pre-install via cloud-init
# performance_parallel_services: false      # Configure VPN services in parallel

# Randomly generated IP address for the local dns resolver
local_service_ip: "{{ '172.16.0.1' | ansible.utils.ipmath(1048573 | random(seed=algo_server_name + ansible_fqdn)) }}"
local_service_ipv6: "{{ 'fd00::1' | ansible.utils.ipmath(1048573 | random(seed=algo_server_name + ansible_fqdn)) }}"


congrats:
  common: |
    "#                          Congratulations!                            #"
    "#                     Your Algo server is running.                     #"
    "#    Config files and certificates are in the ./configs/ directory.    #"
    "#              Go to https://whoer.net/ after connecting               #"
    "#        and ensure that all your traffic passes through the VPN.      #"
    "#                     Local DNS resolver {{ local_service_ip }}{{ ', ' + local_service_ipv6 if ipv6_support else '' }}                   #"
  p12_pass: |
    "#        The p12 and SSH keys password for new users is {{ p12_export_password }}       #"
  ca_key_pass: |
    "#        The CA key password is {{ CA_password|default(omit) }}       #"
  ssh_access: |
    "#      Shell access: ssh -F configs/{{ ansible_ssh_host|default(omit) }}/ssh_config {{ algo_server_name }}        #"

SSH_keys:
  comment: algo@ssh
  private: configs/algo.pem
  private_tmp: /tmp/algo-ssh.pem
  public: configs/algo.pem.pub

cloud_providers:
  azure:
    size: Standard_B1S
    osDisk:
      # The storage account type to use for the OS disk. Possible values:
      # 'Standard_LRS', 'Premium_LRS', 'StandardSSD_LRS', 'UltraSSD_LRS',
      # 'Premium_ZRS', 'StandardSSD_ZRS', 'PremiumV2_LRS'.
      type: Standard_LRS
    image:
      publisher: Canonical
      offer: 0001-com-ubuntu-minimal-jammy-daily
      sku: minimal-22_04-daily-lts
      version: latest
  digitalocean:
    # See docs for extended droplet options, pricing, and availability.
    # Possible values: 's-1vcpu-512mb-10gb', 's-1vcpu-1gb', ...
    size: s-1vcpu-1gb
    image: "ubuntu-22-04-x64"
  ec2:
    # Change the encrypted flag to "false" to disable AWS volume encryption.
    encrypted: true
    # Set use_existing_eip to "true" if you want to use a pre-allocated Elastic IP
    # Additional prompt will be raised to determine which IP to use
    use_existing_eip: false
    size: t3.micro
    image:
      name: "ubuntu-jammy-22.04"
      arch: x86_64
      owner: "099720109477"
    # Change instance_market_type from "on-demand" to "spot" to launch a spot
    # instance. See deploy-from-ansible.md for spot's additional IAM permission
    instance_market_type: on-demand
  gce:
    size: e2-micro
    image: ubuntu-2204-lts
    external_static_ip: false
  lightsail:
    size: nano_2_0
    image: ubuntu_22_04
  scaleway:
    size: DEV1-S
    image: Ubuntu 22.04 Jammy Jellyfish
    arch: x86_64
  hetzner:
    server_type: cpx22
    image: ubuntu-22.04
  openstack:
    flavor_ram: ">=512"
    image: Ubuntu-22.04
  cloudstack:
    size: Micro
    image: Linux Ubuntu 22.04 LTS 64-bit
    disk: 10
  vultr:
    os: Ubuntu 22.04 LTS x64
    size: vc2-1c-1gb
  linode:
    type: g6-nanode-1
    image: linode/ubuntu22.04
  local:

fail_hint:
  - Sorry, but something went wrong!
  - Check troubleshooting for common fixes, or file an issue if you found a bug.
  - https://trailofbits.github.io/algo/troubleshooting.html
  - https://github.com/trailofbits/algo/issues/new

booleans_map:
  Y: true
  y: true


================================================
FILE: deploy_client.yml
================================================
---
- name: Configure the client
  hosts: localhost
  become: false
  vars_files:
    - config.cfg

  tasks:
    - name: Add the droplet to an inventory group
      add_host:
        name: "{{ client_ip }}"
        groups: client-host
        ansible_ssh_user: "{{ 'root' if client_ip == 'localhost' else ssh_user }}"
        vpn_user: "{{ vpn_user }}"
        IP_subject_alt_name: "{{ server_ip }}"
        ansible_python_interpreter: "{% if client_ip == 'localhost' %}{{ ansible_playbook_python }}{% else %}/usr/bin/python3{% endif %}"

- name: Configure the client and install required software
  hosts: client-host
  gather_facts: false
  become: true
  vars_files:
    - config.cfg
    - roles/strongswan/defaults/main.yml
  roles:
    - role: client


================================================
FILE: destroy.yml
================================================
---
- name: Destroy an Algo VPN server
  hosts: localhost
  gather_facts: false
  become: false
  vars_files:
    - config.cfg

  tasks:
    - block:
        - name: Validate server_ip is provided
          assert:
            that: server_ip is defined and server_ip | length > 0
            fail_msg: |
              server_ip is required. Usage:
                ./algo destroy <server-ip>
                ansible-playbook destroy.yml -e "server_ip=YOUR_SERVER_IP"

        - name: Check that server config exists
          stat:
            path: "configs/{{ server_ip }}/.config.yml"
          register: _server_config

        - name: Fail if server config not found
          fail:
            msg: |
              No config found at configs/{{ server_ip }}/.config.yml

              This server may not have been deployed by Algo, or
              its configs were already removed.

              Known servers:
                ls configs/*/
          when: not _server_config.stat.exists

        - name: Load server configuration
          include_vars:
            file: "configs/{{ server_ip }}/.config.yml"
            name: _server_cfg

        - name: Set provider and server name from config
          set_fact:
            algo_provider: "{{ _server_cfg.algo_provider }}"
            algo_server_name: "{{ _server_cfg.algo_server_name }}"

        - name: Validate required config values
          assert:
            that:
              - algo_provider is defined and algo_provider | length > 0
              - algo_server_name is defined and algo_server_name | length > 0
            fail_msg: |
              Server config is missing algo_provider or algo_server_name.
              Check configs/{{ server_ip }}/.config.yml

        - name: Install cloud provider dependencies
          shell: "uv pip install '.[{{ _provider_extras[algo_provider] | default(algo_provider) }}]'"
          vars:
            _provider_extras:
              ec2: aws
              lightsail: aws
              azure: azure
              gce: gcp
              hetzner: hetzner
              linode: linode
              openstack: openstack
              cloudstack: cloudstack
          when: algo_provider != "local"
          changed_when: false

        - name: Set region from stored config
          set_fact:
            region: "{{ _server_cfg.algo_region }}"
          when:
            - region is not defined
            - _server_cfg.algo_region is defined
            - _server_cfg.algo_region | length > 0

        - name: Validate region for providers that require it
          fail:
            msg: |
              Region is required to destroy {{ algo_provider }} servers.
              Pass it with: -e "region=YOUR_REGION"

              Example:
                ./algo destroy {{ server_ip }} -e "region=us-east-1"
          when:
            - algo_provider in ['ec2', 'lightsail', 'gce', 'scaleway', 'vultr']
            - region is not defined

        - name: Set dummy region for providers that do not need it
          set_fact:
            region: "unused"
          when:
            - region is not defined
            - algo_provider not in ['ec2', 'lightsail', 'gce', 'scaleway', 'vultr']

        - name: Gather provider credentials
          include_tasks: "roles/cloud-{{ algo_provider }}/tasks/prompts.yml"
          when: algo_provider != "local"

        - name: Display destroy plan
          debug:
            msg:
              - "Server IP:  {{ server_ip }}"
              - "Server name: {{ algo_server_name }}"
              - "Provider:    {{ algo_provider }}"

        - name: Confirm destruction
          pause:
            prompt: |
              This will permanently destroy the server and remove local configs.
              Type 'yes' to confirm
          register: _confirm_destroy
          when: confirm_destroy is not defined or not confirm_destroy | bool

        - name: Abort if not confirmed
          fail:
            msg: "Destroy aborted by user."
          when:
            - confirm_destroy is not defined or not confirm_destroy | bool
            - _confirm_destroy.user_input | default('') | lower != 'yes'

        - name: Destroy cloud resources
          include_tasks: "roles/cloud-{{ algo_provider }}/tasks/destroy.yml"
          when: algo_provider != "local"

        - name: Remove local config directory
          file:
            path: "configs/{{ server_ip }}"
            state: absent

        - name: Remove localhost symlink
          file:
            path: configs/localhost
            state: absent
          when: server_ip == "localhost"

        - name: Destroy complete
          debug:
            msg:
              - "Server {{ algo_server_name }} ({{ server_ip }}) destroyed."
              - "Local configs removed from configs/{{ server_ip }}/"
      rescue:
        - include_tasks: playbooks/rescue.yml


================================================
FILE: docs/aws-credentials.md
================================================
# AWS Credential Configuration

Algo supports multiple methods for providing AWS credentials, following standard AWS practices:

## Methods (in order of precedence)

1. **Command-line variables** (highest priority)
   ```bash
   ./algo -e "aws_access_key=YOUR_KEY aws_secret_key=YOUR_SECRET"
   ```

2. **Environment variables**
   ```bash
   export AWS_ACCESS_KEY_ID=YOUR_KEY
   export AWS_SECRET_ACCESS_KEY=YOUR_SECRET
   export AWS_SESSION_TOKEN=YOUR_TOKEN  # Optional, for temporary credentials
   ./algo
   ```

3. **AWS credentials file** (lowest priority)
   - Default location: `~/.aws/credentials`
   - Custom location: Set `AWS_SHARED_CREDENTIALS_FILE` environment variable
   - Profile selection: Set `AWS_PROFILE` environment variable (defaults to "default")

## Using AWS Credentials File

After running `aws configure` or manually creating `~/.aws/credentials`:

```ini
[default]
aws_access_key_id = YOUR_KEY_ID
aws_secret_access_key = YOUR_SECRET_KEY

[work]
aws_access_key_id = WORK_KEY_ID
aws_secret_access_key = WORK_SECRET_KEY
aws_session_token = TEMPORARY_TOKEN  # Optional
```

To use a specific profile:
```bash
AWS_PROFILE=work ./algo
```

## Security Considerations

- Credentials files should have restricted permissions (600)
- Consider using AWS IAM roles or temporary credentials when possible
- Tools like [aws-vault](https://github.com/99designs/aws-vault) can provide additional security by storing credentials encrypted

## Troubleshooting

If Algo isn't finding your credentials:

1. Check file permissions: `ls -la ~/.aws/credentials`
2. Verify the profile name matches: `AWS_PROFILE=your-profile`
3. Test with AWS CLI: `aws sts get-caller-identity`

If credentials are found but authentication fails:
- Ensure your IAM user has the required permissions (see [EC2 deployment guide](deploy-from-ansible.md))
- Check if you need session tokens for temporary credentials


================================================
FILE: docs/client-android.md
================================================
# Android client setup

## Installation via profiles

1. [Install the WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android).
2. Open QR code `configs/<ip_address>/wireguard/<username>.png` and scan it in the WireGuard app


================================================
FILE: docs/client-apple-ipsec.md
================================================
# Using the built-in IPSEC VPN on Apple Devices

## Configure IPsec

Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted.

## Enable the VPN

On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard, you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Settings** -> **Network** (or **VPN** on macOS Sequoia 15.0+), finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar.

## Managing "Connect On Demand"

If you enable "Connect On Demand", the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand".

On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Settings** -> **Network** (or **VPN** on macOS Sequoia 15.0+), finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply.


================================================
FILE: docs/client-linux-ipsec.md
================================================
# Linux strongSwan IPsec Clients (e.g., OpenWRT, Ubuntu Server, etc.)

Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind.

## Ubuntu Server example

1. `sudo apt install strongswan libstrongswan-standard-plugins`: install strongSwan
2. `/etc/ipsec.d/certs`: copy `<name>.crt` from `algo-master/configs/<server_ip>/ipsec/.pki/certs/<name>.crt`
3. `/etc/ipsec.d/private`: copy `<name>.key` from `algo-master/configs/<server_ip>/ipsec/.pki/private/<name>.key`
4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs/<server_ip>/ipsec/manual/cacert.pem`
5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `<server_ip> : ECDSA <name>.key`
6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and ensure `leftcert` matches the `<name>.crt` filename
7. `sudo ipsec restart`: pick up config changes
8. `sudo ipsec up <conn-name>`: start the ipsec tunnel
9. `sudo ipsec down <conn-name>`: shutdown the ipsec tunnel

One common use case is to let your server access your local LAN without going through the VPN. Set up a passthrough connection by adding the following to `/etc/ipsec.conf`:

    conn lan-passthrough
    leftsubnet=192.168.1.1/24 # Replace with your LAN subnet
    rightsubnet=192.168.1.1/24 # Replace with your LAN subnet
    authby=never # No authentication necessary
    type=pass # passthrough
    auto=route # no need to ipsec up lan-passthrough

To configure the connection to come up at boot time replace `auto=add` with `auto=start`.

## Notes on SELinux

If you use a system with SELinux enabled, you might need to set appropriate file contexts:

````
semanage fcontext -a -t ipsec_key_file_t "$(pwd)(/.*)?"
restorecon -R -v $(pwd)
````

See [this comment](https://github.com/trailofbits/algo/issues/263#issuecomment-328053950).


================================================
FILE: docs/client-linux-wireguard.md
================================================
# Using Ubuntu as a Client with WireGuard

## Install WireGuard

To connect to your AlgoVPN using [WireGuard](https://www.wireguard.com) from Ubuntu, make sure your system is up-to-date then install WireGuard:

```shell
# Update your system:
sudo apt update && sudo apt upgrade

# If the file /var/run/reboot-required exists then reboot:
[ -e /var/run/reboot-required ] && sudo reboot

# Install WireGuard:
sudo apt install wireguard
# Note: openresolv is no longer needed on Ubuntu 22.04 LTS+
```

For installation on other Linux distributions, see the [Installation](https://www.wireguard.com/install/) page on the WireGuard site.

## Locate the Config File

The Algo-generated config files for WireGuard are named `configs/<ip_address>/wireguard/<username>.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg`. Each WireGuard client you connect to your AlgoVPN must use a different config file. Choose one of these files and copy it to your Linux client.

## Configure WireGuard

Finally, install the config file on your client as `/etc/wireguard/wg0.conf` and start WireGuard:

```shell
# Install the config file to the WireGuard configuration directory on your
# Linux client:
sudo install -o root -g root -m 600 <username>.conf /etc/wireguard/wg0.conf

# Start the WireGuard VPN:
sudo systemctl start wg-quick@wg0

# Check that it started properly:
sudo systemctl status wg-quick@wg0

# Verify the connection to the AlgoVPN:
sudo wg

# See that your client is using the IP address of your AlgoVPN:
curl ipv4.icanhazip.com

# Optionally configure the connection to come up at boot time:
sudo systemctl enable wg-quick@wg0
```

If your Linux distribution does not use `systemd` you can bring up WireGuard with `sudo wg-quick up wg0`.

## Using a DNS Search Domain

As of the `v1.0.20200510` release of `wireguard-tools` WireGuard supports setting a DNS search domain. In your `wg0.conf` file a non-numeric entry on the `DNS` line will be used as a search domain. For example, this:
```
DNS =  172.27.153.31, fd00::b:991f, mydomain.com
```
will cause your `/etc/resolv.conf` to contain:
```
search mydomain.com
nameserver 172.27.153.31
nameserver fd00::b:991f
```


================================================
FILE: docs/client-linux.md
================================================
# Linux client setup

## Provision client config

After you deploy a server, you can use an included Ansible script to provision Linux clients too! Debian, Ubuntu, CentOS, and Fedora are supported. The playbook is `deploy_client.yml`.

### Required variables

* `client_ip` - The IP address of your client machine (You can use `localhost` in order to deploy locally)
* `vpn_user` - The username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory)
* `ssh_user` - The username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally)
* `server_ip` - The vpn server ip address

### Example

```shell
ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com ssh_user=root'
```

### Additional options

If the user requires sudo password use the following argument: `--ask-become-pass`.

## OS Specific instructions

Some Linux clients may require more specific and details instructions to configure a connection to the deployed Algo VPN, these are documented here.

### Fedora Workstation

#### (Gnome) Network Manager install

First, install the required plugins.

````
dnf install NetworkManager-strongswan NetworkManager-strongswan-gnome
````

#### (Gnome) Network Manager configuration

In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the user we created is `user-name`.

* Go to *Settings* > *Network*
* Add a new Network (`+` bottom left of the window)
* Select *IPsec/IKEv2 (strongswan)*
* Fill out the options:
  * Name: your choice, e.g.: *ikev2-1.2.3.4*
  * Gateway:
    * Address: IP of the Algo VPN server, e.g: `1.2.3.4`
    * Certificate: `cacert.pem` found at `/path/to/algo/configs/1.2.3.4/ipsec/.pki/cacert.pem`
  * Client:
    * Authentication: *Certificate/Private key*
    * Certificate: `user-name.crt` found at `/path/to/algo/configs/1.2.3.4/ipsec/.pki/certs/user-name.crt`
    * Private key: `user-name.key` found at `/path/to/algo/configs/1.2.3.4/ipsec/.pki/private/user-name.key`
  * Options:
    * Check *Request an inner IP address*, connection will fail without this option
    * Optionally check *Enforce UDP encapsulation*
    * Optionally check *Use IP compression*
    * For the later 2 options, hover to option in the settings to see a description
  * Cipher proposal:
    * Check *Enable custom proposals*
    * IKE: `aes256gcm16-prfsha512-ecp384`
    * ESP: `aes256gcm16-ecp384`
* Apply and turn the connection on, you should now be connected


================================================
FILE: docs/client-macos-wireguard.md
================================================
# MacOS WireGuard Client Setup

The WireGuard macOS app is unavailable for older operating systems. Please update your operating system if you can. If you are on a macOS High Sierra (10.13) or earlier, then you can still use WireGuard via their userspace drivers via the process detailed below.

## Install WireGuard

Install the wireguard-go userspace driver:

```
brew install wireguard-tools
```

## Locate the Config File

Algo generates a WireGuard configuration file, `wireguard/<username>.conf`, and a QR code, `wireguard/<username>.png`, for each user defined in `config.cfg`. Find the configuration file and copy it to your device if you don't already have it.

Note that each client you use to connect to Algo VPN must have a unique WireGuard config.

## Configure WireGuard

You'll need to copy the appropriate WireGuard configuration file into a location where the userspace driver can find it. After it is in the right place, start the VPN, and verify connectivity.

```
# Copy the config file to the WireGuard configuration directory on your macOS device
mkdir /usr/local/etc/wireguard/
cp <username>.conf /usr/local/etc/wireguard/wg0.conf

# Start the WireGuard VPN
sudo wg-quick up wg0

# Verify the connection to the Algo VPN
wg

# See that your client is using the IP address of your Algo VPN:
curl ipv4.icanhazip.com
```


================================================
FILE: docs/client-openwrt-router-wireguard.md
================================================
# OpenWrt Router as WireGuard Client

This guide explains how to configure an OpenWrt router as a WireGuard VPN client, allowing all devices connected to your network to route traffic through your Algo VPN automatically. This setup is ideal for devices that don't support VPN natively (smart TVs, IoT devices, game consoles) or when you want seamless VPN access for all network clients.

## Use Cases

- Connect devices without native VPN support (smart TVs, gaming consoles, IoT devices)
- Automatically route all connected devices through the VPN
- Create a secure connection when traveling with multiple devices
- Configure VPN once at the router level instead of per-device

## Prerequisites

You'll need an OpenWrt-compatible router with sufficient RAM (minimum 64MB recommended) and OpenWrt 23.05 or later installed. Your Algo VPN server must be deployed and running, and you'll need the WireGuard configuration file from your Algo deployment.

Ensure your router's LAN subnet doesn't conflict with upstream networks. The default OpenWrt IP is `192.168.1.1` - change to `192.168.2.1` if conflicts exist.

This configuration has been verified on TP-Link TL-WR1043ND and TP-Link Archer C20i AC750 with OpenWrt 23.05+. For compatibility with other devices, check the [OpenWrt Table of Hardware](https://openwrt.org/toh/start).

## Install Required Packages

### Web Interface Method

1. Access your router's web interface (typically `http://192.168.1.1`)
2. Login with your credentials (default: username `root`, no password)
3. Navigate to System → Software
4. Click "Update lists" to refresh the package database
5. Search for and install these packages:
   - `wireguard-tools`
   - `kmod-wireguard`
   - `luci-app-wireguard`
   - `wireguard`
   - `kmod-crypto-sha256`
   - `kmod-crypto-sha1`
   - `kmod-crypto-md5`
6. Restart the router after installation completes

### SSH Method

1. SSH into your router: `ssh root@192.168.1.1`
2. Update the package list:
   ```bash
   opkg update
   ```
3. Install required packages:
   ```bash
   opkg install wireguard-tools kmod-wireguard luci-app-wireguard wireguard kmod-crypto-sha256 kmod-crypto-sha1 kmod-crypto-md5
   ```
4. Reboot the router:
   ```bash
   reboot
   ```

## Locate Your WireGuard Configuration

Before proceeding, locate your WireGuard configuration file from your Algo deployment. This file is typically located at:
```
configs/<server_ip>/wireguard/<username>.conf
```

Your configuration file should look similar to:
```ini
[Interface]
PrivateKey = <your_private_key>
Address = 10.49.0.2/16
DNS = 172.16.0.1

[Peer]
PublicKey = <server_public_key>
PresharedKey = <preshared_key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <server_ip>:51820
PersistentKeepalive = 25
```

## Configure WireGuard Interface

1. In the OpenWrt web interface, navigate to Network → Interfaces
2. Click "Add new interface..."
3. Set the name to `AlgoVPN` (or your preferred name) and select "WireGuard VPN" as the protocol
4. Click "Create interface"

In the General Settings tab:
- Check "Bring up on boot"
- Enter your private key from the Algo config file
- Add your IP address from the Algo config file (e.g., `10.49.0.2/16`)

Switch to the Peers tab and click "Add peer":
- Description: `Algo Server`
- Public Key: Copy from the `[Peer]` section of your config
- Preshared Key: Copy from the `[Peer]` section of your config
- Allowed IPs: `0.0.0.0/0, ::/0` (routes all traffic through VPN)
- Route Allowed IPs: Check this box
- Endpoint Host: Extract the IP address from the `Endpoint` line
- Endpoint Port: Extract the port from the `Endpoint` line (typically `51820`)
- Persistent Keep Alive: `25`

Click "Save & Apply".

## Configure Firewall Rules

1. Navigate to Network → Firewall
2. Click "Add" to create a new zone
3. Configure the firewall zone:
   - Name: `vpn`
   - Input: `Reject`
   - Output: `Accept`
   - Forward: `Reject`
   - Masquerading: Check this box
   - MSS clamping: Check this box
   - Covered networks: Select your WireGuard interface (`AlgoVPN`)

4. In the Inter-Zone Forwarding section:
   - Allow forward from source zones: Select `lan`
   - Allow forward to destination zones: Leave unspecified

5. Click "Save & Apply"
6. Reboot your router to ensure all changes take effect

## Verification and Testing

Navigate to Network → Interfaces and verify your WireGuard interface shows as "Connected" with a green status. Check that it has received the correct IP address.

From a device connected to your router, visit https://whatismyipaddress.com/. Your public IP should match your Algo VPN server's IP address. Test DNS resolution to ensure it's working through the VPN.

For command line verification, SSH into your router and check:
```bash
# Check interface status
wg show

# Check routing table
ip route

# Test connectivity
ping 8.8.8.8
```

## Configuration File Reference

Your OpenWrt network configuration (`/etc/config/network`) should include sections similar to:

```uci
config interface 'AlgoVPN'
    option proto 'wireguard'
    list addresses '10.49.0.2/16'
    option private_key '<your_private_key>'

config wireguard_AlgoVPN
    option public_key '<server_public_key>'
    option preshared_key '<preshared_key>'
    option route_allowed_ips '1'
    list allowed_ips '0.0.0.0/0'
    list allowed_ips '::/0'
    option endpoint_host '<server_ip>'
    option endpoint_port '51820'
    option persistent_keepalive '25'
```

## Troubleshooting

If the interface won't connect, verify all keys are correctly copied with no extra spaces or line breaks. Check that your Algo server is running and accessible, and confirm the endpoint IP and port are correct.

If you have no internet access after connecting, verify firewall rules allow forwarding from LAN to VPN zone. Check that masquerading is enabled on the VPN zone and ensure MSS clamping is enabled.

If some websites don't work, try disabling MSS clamping temporarily to test. Verify DNS is working by testing `nslookup google.com` and check that IPv6 is properly configured if used.

For DNS resolution issues, configure custom DNS servers in Network → DHCP and DNS. Consider using your Algo server's DNS (typically `172.16.0.1`).

Check system logs for WireGuard-related errors:
```bash
# View system logs
logread | grep -i wireguard

# Check kernel messages
dmesg | grep -i wireguard
```

## Advanced Configuration

For split tunneling (routing only specific traffic through the VPN), change "Allowed IPs" in the peer configuration to specific subnets and add custom routing rules for desired traffic.

If your Algo server supports IPv6, add the IPv6 address to your interface configuration and include `::/0` in "Allowed IPs" for the peer.

For optimal privacy, configure your router to use your Algo server's DNS by navigating to Network → DHCP and DNS and adding your Algo DNS server IP (typically `172.16.0.1`) to the DNS forwardings.

## Security Notes

Store your private keys securely and never share them. Keep OpenWrt and packages updated for security patches. Regularly check VPN connectivity to ensure ongoing protection, and save your configuration before making changes.

This configuration routes ALL traffic from your router through the VPN. If you need selective routing or have specific requirements, consider consulting the [OpenWrt WireGuard documentation](https://openwrt.org/docs/guide-user/services/vpn/wireguard/start) for advanced configurations.


================================================
FILE: docs/client-windows.md
================================================
# Windows Client Setup

This guide will help you set up your Windows device to connect to your Algo VPN server.

## Supported Versions

- Windows 10 (all editions)
- Windows 11 (all editions)
- Windows Server 2016 and later

## WireGuard Setup (Recommended)

WireGuard is the recommended VPN protocol for Windows clients due to its simplicity and performance.

### Installation

1. Download and install the official [WireGuard client for Windows](https://www.wireguard.com/install/)
2. Locate your configuration file: `configs/<server-ip>/wireguard/<username>.conf`
3. In the WireGuard application, click "Import tunnel(s) from file"
4. Select your `.conf` file and import it
5. Click "Activate" to connect to your VPN

### Alternative Import Methods

- **QR Code**: If you have access to the QR code (`wireguard/<username>.png`), you can scan it using a mobile device first, then export the configuration
- **Manual Entry**: You can create a new empty tunnel and paste the contents of your `.conf` file

## IPsec/IKEv2 Setup (Legacy)

While Algo supports IPsec/IKEv2, it requires PowerShell scripts for Windows setup. WireGuard is strongly recommended instead.

If you must use IPsec:
1. Locate the PowerShell setup script in your configs directory
2. Run PowerShell as Administrator
3. Execute the setup script
4. The VPN connection will appear in Settings → Network & Internet → VPN

## Troubleshooting

### "The parameter is incorrect" Error

This is a common error that occurs when trying to connect. See the [troubleshooting guide](troubleshooting.md#windows-the-parameter-is-incorrect-error-when-connecting) for the solution.

### Connection Issues

1. **Check Windows Firewall**: Ensure Windows Firewall isn't blocking the VPN connection
2. **Verify Server Address**: Make sure the server IP/domain in your configuration is correct
3. **Check Date/Time**: Ensure your system date and time are correct
4. **Disable Other VPNs**: Disconnect from any other VPN services before connecting

### WireGuard Specific Issues

- **DNS Not Working**: Check if "Block untunneled traffic (kill-switch)" is enabled in tunnel settings
- **Slow Performance**: Try reducing the MTU in the tunnel configuration (default is 1420)
- **Can't Import Config**: Ensure the configuration file has a `.conf` extension

### Performance Optimization

1. **Use WireGuard**: It's significantly faster than IPsec on Windows
2. **Close Unnecessary Apps**: Some antivirus or firewall software can slow down VPN connections
3. **Check Network Adapter**: Update your network adapter drivers to the latest version

## Advanced Configuration

### Split Tunneling

To exclude certain traffic from the VPN:
1. Edit your WireGuard configuration file
2. Modify the `AllowedIPs` line to exclude specific networks
3. For example, to exclude local network: Remove `0.0.0.0/0` and add specific routes

### Automatic Connection

To connect automatically:
1. Open WireGuard
2. Select your tunnel
3. Edit → Uncheck "On-demand activation"
4. Windows will maintain the connection automatically

### Multiple Servers

You can import multiple `.conf` files for different Algo servers. Give each a descriptive name to distinguish them.

## Security Notes

- Keep your configuration files secure - they contain your private keys
- Don't share your configuration with others
- Each user should have their own unique configuration
- Regularly update your WireGuard client for security patches

## Need More Help?

- Check the main [troubleshooting guide](troubleshooting.md)
- Review [WireGuard documentation](https://www.wireguard.com/quickstart/)
- [Create a discussion](https://github.com/trailofbits/algo/discussions) for help


================================================
FILE: docs/cloud-alternative-ingress-ip.md
================================================
# Alternative Ingress IP

This feature allows you to configure the Algo server to send outbound traffic through a different external IP address than the one you are establishing the VPN connection with.

![cloud-alternative-ingress-ip](/docs/images/cloud-alternative-ingress-ip.png)

Additional info might be found in [this issue](https://github.com/trailofbits/algo/issues/1047)




#### Caveats

##### Extra charges

- DigitalOcean: Floating IPs are free when assigned to a Droplet, but after manually deleting a Droplet, you need to also delete the Floating IP or you'll get charged for it.

##### IPv6

Some cloud providers provision a VM with an `/128` address block size. This is the only IPv6 address provided and for outbound and incoming traffic.

If the provided address block size is bigger, e.g., `/64`, Algo takes a separate address than the one is assigned to the server to send outbound IPv6 traffic.


================================================
FILE: docs/cloud-amazon-ec2.md
================================================
# Amazon EC2 Cloud Setup

This guide walks you through setting up Algo VPN on Amazon EC2, including account creation, permissions configuration, and deployment process.

## AWS Account Creation

Creating an Amazon AWS account requires providing a phone number that can receive automated calls with PIN verification. The phone verification system occasionally fails, but you can request a new PIN and try again until it succeeds.

## Choose Your EC2 Plan

### AWS Free Tier

The most cost-effective option for new AWS customers is the [AWS Free Tier](https://aws.amazon.com/free/), which provides:

- 750 hours of Amazon EC2 Linux t2.micro or t3.micro instance usage per month
- 100 GB of outbound data transfer per month
- 30 GB of cloud storage

The Free Tier is available for 12 months from account creation. Some regions like Middle East (Bahrain), EU (Stockholm), and Israel (il-central-1) don't offer t2.micro instances, but t3.micro is available as an alternative.

Note that your Algo instance will continue working if you exceed bandwidth limits - you'll just start accruing standard charges on your AWS account.

### Cost-Effective Alternatives

If you're not eligible for the Free Tier or prefer more predictable costs, consider AWS Graviton instances. To use Graviton instances, modify your `config.cfg` file:

```yaml
ec2:
  size: t4g.nano
  arch: arm64
```

The t4g.nano instance is currently the least expensive option without promotional requirements. AWS is also running a promotion offering free t4g.small instances until December 31, 2025 - see the [AWS documentation](https://aws.amazon.com/ec2/faqs/#t4g-instances) for details.

For additional EC2 configuration options, see the [deploy from ansible guide](https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md#amazon-ec2).

## Set Up IAM Permissions

### Create IAM Policy

1. In the AWS console, navigate to Services → IAM → Policies
2. Click "Create Policy"
3. Switch to the JSON tab
4. Replace the default content with the [minimum required AWS policy for Algo deployment](https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md#minimum-required-iam-permissions-for-deployment)
5. Name the policy `AlgoVPN_Provisioning`

![Creating a new permissions policy in the AWS console.](/docs/images/aws-ec2-new-policy.png)

### Create IAM User

1. Navigate to Services → IAM → Users
2. Enable multi-factor authentication (MFA) on your root account using Google Authenticator or a hardware token
3. Click "Add User" and create a username (e.g., `algovpn`)
4. Select "Programmatic access"
5. Click "Next: Permissions"

![The new user screen in the AWS console.](/docs/images/aws-ec2-new-user.png)

6. Choose "Attach existing policies directly"
7. Search for "Algo" and select the `AlgoVPN_Provisioning` policy you created
8. Click "Next: Tags" (optional), then "Next: Review"

![Attaching a policy to an IAM user in the AWS console.](/docs/images/aws-ec2-attach-policy.png)

9. Review your settings and click "Create user"
10. Download the CSV file containing your access credentials - you'll need these for Algo deployment

![Downloading the credentials for an AWS IAM user.](/docs/images/aws-ec2-new-user-csv.png)

Keep the CSV file secure as it contains sensitive credentials that grant access to your AWS account.

## Deploy with Algo

Once you've installed Algo and its dependencies, you can deploy your VPN server to EC2.

### Provider Selection

Run `./algo` and select Amazon EC2 when prompted:

```
$ ./algo

  What provider would you like to use?
    1. DigitalOcean
    2. Amazon Lightsail
    3. Amazon EC2
    4. Microsoft Azure
    5. Google Compute Engine
    6. Hetzner Cloud
    7. Vultr
    8. Scaleway
    9. OpenStack (DreamCompute optimised)
    10. CloudStack
    11. Linode
    12. Install to existing Ubuntu server (for more advanced users)

Enter the number of your desired provider
: 3
```

### AWS Credentials

Algo will automatically detect AWS credentials in this order:

1. Command-line variables
2. Environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`)
3. AWS credentials file (`~/.aws/credentials`)

If no credentials are found, you'll be prompted to enter them manually:

```
Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)
Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md).
[pasted values will not be displayed]
[AKIA...]:

Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)
[pasted values will not be displayed]
[ABCD...]:
```

For detailed credential configuration options, see the [AWS Credentials guide](aws-credentials.md).

### Server Configuration

You'll be prompted to name your server (default is "algo"):

```
Name the vpn server:
[algo]: algovpn
```

Next, select your preferred AWS region:

```
What region should the server be located in?
(https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region)
    1. ap-northeast-1
    2. ap-northeast-2
    3. ap-south-1
    4. ap-southeast-1
    5. ap-southeast-2
    6. ca-central-1
    7. eu-central-1
    8. eu-north-1
    9. eu-west-1
    10. eu-west-2
    11. eu-west-3
    12. sa-east-1
    13. us-east-1
    14. us-east-2
    15. us-west-1
    16. us-west-2

Enter the number of your desired region
[13]
:
```

Choose a region close to your location for optimal performance, keeping in mind that some regions may have different pricing or instance availability.

After region selection, Algo will continue with the standard setup questions for user configuration and VPN options.

## Resource Cleanup

If you deploy Algo to EC2 multiple times, unused resources (instances, VPCs, subnets) may accumulate and potentially cause future deployment issues.

The cleanest way to remove an Algo deployment is through CloudFormation:

1. Go to the AWS console and navigate to CloudFormation
2. Find the stack associated with your Algo server
3. Delete the entire stack

Warning: Deleting a CloudFormation stack will permanently delete your EC2 instance and all associated resources unless you've enabled termination protection. Make sure you're deleting the correct stack and have backed up any important data.

This approach ensures all related AWS resources are properly cleaned up, preventing resource conflicts in future deployments.


================================================
FILE: docs/cloud-azure.md
================================================
# Azure cloud setup

The easiest way to get started with the Azure CLI is by running it in an Azure Cloud Shell environment through your browser.

Here you can find some information from [the official doc](https://docs.microsoft.com/en-us/cli/azure/get-started-with-azure-cli?view=azure-cli-latest). We put the essential commands together for simplest usage.

## Install azure-cli

- macOS ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-macos?view=azure-cli-latest)):
  ```bash
  $ brew update && brew install azure-cli
  ```

- Linux (deb-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest)):
  ```bash
  $ sudo apt-get update && sudo apt-get install \
      apt-transport-https \
      lsb-release \
      software-properties-common \
      dirmngr -y
  $ AZ_REPO=$(lsb_release -cs)
  $ echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \
      sudo tee /etc/apt/sources.list.d/azure-cli.list
  $ sudo apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv \
      --keyserver packages.microsoft.com \
      --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF
  $ sudo apt-get update
  $ sudo apt-get install azure-cli
  ```

- Linux (rpm-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-yum?view=azure-cli-latest)):
  ```bash
  $ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
  $ sudo sh -c 'echo -e "[azure-cli]\nname=Azure CLI\nbaseurl=https://packages.microsoft.com/yumrepos/azure-cli\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/azure-cli.repo'
  $ sudo yum install azure-cli
  ```

- Windows ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest)):
  For Windows the Azure CLI is installed via an MSI, which gives you access to the CLI through the Windows Command Prompt (CMD) or PowerShell. When installing for Windows Subsystem for Linux (WSL), packages are available for your Linux distribution. [Download the MSI installer](https://aka.ms/installazurecliwindows)

If your OS is missing or to get more information, see [the official doc](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)


## Sign in

1. Run the `login` command:
```bash
az login
```

  If the CLI can open your default browser, it will do so and load a sign-in page.

  Otherwise, you need to open a browser page and follow the instructions on the command line to enter an authorization code after navigating to https://aka.ms/devicelogin in your browser.

2. Sign in with your account credentials in the browser.

There are ways to sign in non-interactively, which are covered in detail in [Sign in with Azure CLI](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest).


**Now you are able to deploy an AlgoVPN instance without hassle**


================================================
FILE: docs/cloud-cloudstack.md
================================================
### Configuration file

> **⚠️ Important Note:** Exoscale is no longer supported as they deprecated their CloudStack API on May 1, 2024. Please use alternative providers like Hetzner, DigitalOcean, Vultr, or Scaleway.

Algo scripts will ask you for the API details. You need to fetch the API credentials and the endpoint from your CloudStack provider's control panel.

For CloudStack providers, you'll need to set:

```bash
export CLOUDSTACK_KEY="<your api key>"
export CLOUDSTACK_SECRET="<your secret>"
export CLOUDSTACK_ENDPOINT="<your provider's API endpoint>"
```

Make sure your provider supports the CloudStack API. Contact your provider for the correct API endpoint URL.


================================================
FILE: docs/cloud-do.md
================================================
# DigitalOcean cloud setup

## API Token creation

First, login into your DigitalOcean account.

Select **API** from the titlebar. This will take you to the "Applications & API" page.

![The Applications & API page](/docs/images/do-api.png)

On the **Tokens/Keys** tab, select **Generate New Token**. A dialog will pop up. In that dialog, give your new token a name, and make sure **Write** is checked off. Click the **Generate Token** button when you are ready.

![The new token dialog, showing a form requesting a name and confirmation on the scope for the new token.](/docs/images/do-new-token.png)

You will be returned to the **Tokens/Keys** tab, and your new key will be shown under the **Personal Access Tokens** header.

![The new token in the listing.](/docs/images/do-view-token.png)

Copy or note down the hash that shows below the name you entered, as this will be necessary for the steps below. This value will disappear if you leave this page, and you'll need to regenerate it if you forget it.

## Select a Droplet (optional)

The default option is the `s-1vcpu-1gb` because it is available in all regions. However, you may want to switch to a cheaper droplet such as `s-1vcpu-512mb-10gb` even though it is not available in all regions. This can be edited in the [Configuration File](config.cfg) under `cloud_providers > digitalocean > size`. See this brief comparison between the two droplets below:

| Droplet Type | Monthly Cost | Bandwidth | Availability |
|:--|:-:|:-:|:--|
| `s-1vcpu-512mb-10gb` | $4/month | 0.5 TB | Limited |
| `s-1vcpu-1gb`        | $6/month | 1.0 TB | All regions |
| ... | ... | ... | ... |

*Note: Exceeding bandwidth limits costs $0.01/GiB at time of writing ([docs](https://docs.digitalocean.com/products/billing/bandwidth/#droplets)). See the live list of droplets [here](https://slugs.do-api.dev/).*

## Using DigitalOcean with Algo (interactive)

These steps are for those who run Algo using Docker or using the `./algo` command.

Choose DigitalOcean as your provider:

```
What provider would you like to use?
    1. DigitalOcean
    2. Amazon Lightsail
    3. Amazon EC2
    4. Vultr
    5. Microsoft Azure
    6. Google Compute Engine
    7. Scaleway
    8. OpenStack (DreamCompute optimised)
    9. Install to existing Ubuntu server (Advanced)

Enter the number of your desired provider
:
1
```

Enter a name for your server. Leave this as the default if you are not certain how this will affect your setup:

```
Name the vpn server:
[algo]:
```

After several prompts related to Algo features you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (you won't see any output as the key is not echoed by Algo):

```
Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens):
 (output is hidden):
```

Finally, you will be asked the region in which you wish to setup your new Algo server. This list is dynamic and can change based on availability of resources. Enter the number next to name of the region:

```
What region should the server be located in?
    1. ams3     Amsterdam 3
    2. blr1     Bangalore 1
    3. fra1     Frankfurt 1
    4. lon1     London 1
    5. nyc1     New York 1
    6. nyc3     New York 3
    7. sfo2     San Francisco 2
    8. sgp1     Singapore 1
    9. tor1     Toronto 1

Enter the number of your desired region
[6]
:
9
```

## Using DigitalOcean with Algo (scripted)

If you are using Ansible directly to run Algo you will need to pass the API Token as `do_token`. For example:

```shell
ansible-playbook main.yml -e "provider=digitalocean
                                server_name=algo
                                ondemand_cellular=true
                                ondemand_wifi=true
          
Download .txt
gitextract_xk3afjz7/

├── .ansible-lint
├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── actions/
│   │   ├── setup-algo/
│   │   │   └── action.yml
│   │   └── setup-uv/
│   │       └── action.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker-image.yaml
│       ├── integration-tests.yml
│       ├── lint.yml
│       ├── main.yml
│       ├── security.yml
│       ├── smart-tests.yml
│       └── test-effectiveness.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint
├── CLAUDE.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── SECURITY.md
├── algo
├── algo-docker.sh
├── algo-showenv.sh
├── algo.ps1
├── ansible.cfg
├── cloud.yml
├── config.cfg
├── deploy_client.yml
├── destroy.yml
├── docs/
│   ├── aws-credentials.md
│   ├── client-android.md
│   ├── client-apple-ipsec.md
│   ├── client-linux-ipsec.md
│   ├── client-linux-wireguard.md
│   ├── client-linux.md
│   ├── client-macos-wireguard.md
│   ├── client-openwrt-router-wireguard.md
│   ├── client-windows.md
│   ├── cloud-alternative-ingress-ip.md
│   ├── cloud-amazon-ec2.md
│   ├── cloud-azure.md
│   ├── cloud-cloudstack.md
│   ├── cloud-do.md
│   ├── cloud-gce.md
│   ├── cloud-hetzner.md
│   ├── cloud-linode.md
│   ├── cloud-scaleway.md
│   ├── cloud-vultr.md
│   ├── deploy-from-ansible.md
│   ├── deploy-from-cloudshell.md
│   ├── deploy-from-docker.md
│   ├── deploy-from-macos.md
│   ├── deploy-from-script-or-cloud-init-to-localhost.md
│   ├── deploy-from-windows.md
│   ├── deploy-to-ubuntu.md
│   ├── deploy-to-unsupported-cloud.md
│   ├── faq.md
│   ├── firewalls.md
│   ├── index.md
│   └── troubleshooting.md
├── files/
│   └── cloud-init/
│       ├── README.md
│       ├── base.sh
│       ├── base.yml
│       └── sshd_config
├── input.yml
├── install.sh
├── inventory
├── library/
│   ├── gcp_compute_location_info.py
│   ├── lightsail_region_facts.py
│   ├── scaleway_compute.py
│   └── x25519_pubkey.py
├── main.yml
├── playbooks/
│   ├── cloud-post.yml
│   ├── cloud-pre.yml
│   ├── rescue.yml
│   └── tmpfs/
│       ├── linux.yml
│       ├── macos.yml
│       ├── main.yml
│       └── umount.yml
├── pyproject.toml
├── pytest.ini
├── requirements.yml
├── roles/
│   ├── client/
│   │   ├── files/
│   │   │   └── libstrongswan-relax-constraints.conf
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── main.yml
│   │       └── systems/
│   │           ├── CentOS.yml
│   │           ├── Debian.yml
│   │           ├── Fedora.yml
│   │           ├── Ubuntu.yml
│   │           └── main.yml
│   ├── cloud-azure/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   └── deployment.json
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-cloudstack/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-digitalocean/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-ec2/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   └── stack.yaml
│   │   └── tasks/
│   │       ├── cloudformation.yml
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-gce/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-hetzner/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-lightsail/
│   │   ├── files/
│   │   │   └── stack.yaml
│   │   └── tasks/
│   │       ├── cloudformation.yml
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-linode/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-openstack/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       └── main.yml
│   ├── cloud-scaleway/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── cloud-vultr/
│   │   └── tasks/
│   │       ├── destroy.yml
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── common/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── aip/
│   │   │   │   ├── digitalocean.yml
│   │   │   │   ├── main.yml
│   │   │   │   └── placeholder.yml
│   │   │   ├── facts.yml
│   │   │   ├── iptables.yml
│   │   │   ├── main.yml
│   │   │   ├── packages.yml
│   │   │   ├── ubuntu.yml
│   │   │   └── unattended-upgrades.yml
│   │   └── templates/
│   │       ├── 10-algo-lo100.network.j2
│   │       ├── 10periodic.j2
│   │       ├── 50unattended-upgrades.j2
│   │       ├── 99-algo-ipv6-egress.yaml.j2
│   │       ├── rules.v4.j2
│   │       └── rules.v6.j2
│   ├── dns/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── files/
│   │   │   ├── 50-dnscrypt-proxy-unattended-upgrades
│   │   │   └── apparmor.profile.dnscrypt-proxy
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── dns_adblocking.yml
│   │   │   ├── main.yml
│   │   │   └── ubuntu.yml
│   │   └── templates/
│   │       ├── adblock.sh.j2
│   │       ├── dnscrypt-proxy/
│   │       │   ├── cache.toml.j2
│   │       │   ├── filters.toml.j2
│   │       │   ├── global.toml.j2
│   │       │   └── sources.toml.j2
│   │       ├── dnscrypt-proxy.toml.j2
│   │       └── ip-blacklist.txt.j2
│   ├── local/
│   │   └── tasks/
│   │       ├── main.yml
│   │       └── prompts.yml
│   ├── privacy/
│   │   ├── README.md
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── advanced_privacy.yml
│   │   │   ├── auto_cleanup.yml
│   │   │   ├── clear_history.yml
│   │   │   ├── log_filtering.yml
│   │   │   ├── log_rotation.yml
│   │   │   └── main.yml
│   │   └── templates/
│   │       ├── 46-privacy-ssh-filter.conf.j2
│   │       ├── 47-privacy-auth-filter.conf.j2
│   │       ├── 48-privacy-kernel-filter.conf.j2
│   │       ├── 49-privacy-vpn-filter.conf.j2
│   │       ├── auth-logrotate.j2
│   │       ├── clear-history-on-logout.sh.j2
│   │       ├── kern-logrotate.j2
│   │       ├── privacy-auto-cleanup.sh.j2
│   │       ├── privacy-log-cleanup.sh.j2
│   │       ├── privacy-logrotate.j2
│   │       ├── privacy-monitor.sh.j2
│   │       ├── privacy-rsyslog.conf.j2
│   │       └── privacy-shutdown-cleanup.service.j2
│   ├── ssh_tunneling/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── templates/
│   │       └── ssh_config.j2
│   ├── strongswan/
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── meta/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   ├── client_configs.yml
│   │   │   ├── distribute_keys.yml
│   │   │   ├── ipsec_configuration.yml
│   │   │   ├── main.yml
│   │   │   ├── openssl.yml
│   │   │   └── ubuntu.yml
│   │   └── templates/
│   │       ├── 100-CustomLimitations.conf.j2
│   │       ├── charon.conf.j2
│   │       ├── client_ipsec.conf.j2
│   │       ├── client_ipsec.secrets.j2
│   │       ├── ipsec.conf.j2
│   │       ├── ipsec.secrets.j2
│   │       ├── mobileconfig.j2
│   │       └── strongswan.conf.j2
│   └── wireguard/
│       ├── defaults/
│       │   └── main.yml
│       ├── files/
│       │   └── wireguard.sh
│       ├── handlers/
│       │   └── main.yml
│       ├── tasks/
│       │   ├── keys.yml
│       │   ├── main.yml
│       │   ├── mobileconfig.yml
│       │   └── ubuntu.yml
│       └── templates/
│           ├── client.conf.j2
│           ├── mobileconfig.j2
│           ├── server.conf.j2
│           └── vpn-dict.j2
├── scripts/
│   ├── annotate-test-failure.sh
│   ├── lint.sh
│   ├── list_servers.py
│   ├── test-templates.sh
│   └── track-test-effectiveness.py
├── server.yml
├── tests/
│   ├── README.md
│   ├── conftest.py
│   ├── e2e/
│   │   ├── README.md
│   │   └── test-vpn-connectivity.sh
│   ├── fixtures/
│   │   ├── __init__.py
│   │   └── test_variables.yml
│   ├── integration/
│   │   ├── ansible-service-wrapper.py
│   │   ├── ansible.cfg
│   │   ├── mock-apparmor_status.sh
│   │   ├── mock_modules/
│   │   │   ├── apt.py
│   │   │   ├── command.py
│   │   │   └── shell.py
│   │   ├── test-configs/
│   │   │   ├── .provisioned
│   │   │   └── 10.99.0.10/
│   │   │       ├── .config.yml
│   │   │       ├── ipsec/
│   │   │       │   ├── .pki/
│   │   │       │   │   ├── .rnd
│   │   │       │   │   ├── 10.99.0.10_ca_generated
│   │   │       │   │   ├── cacert.pem
│   │   │       │   │   ├── certs/
│   │   │       │   │   │   ├── 01.pem
│   │   │       │   │   │   ├── 02.pem
│   │   │       │   │   │   ├── 03.pem
│   │   │       │   │   │   ├── 10.99.0.10.crt
│   │   │       │   │   │   ├── 10.99.0.10_crt_generated
│   │   │       │   │   │   ├── testuser1.crt
│   │   │       │   │   │   ├── testuser1_crt_generated
│   │   │       │   │   │   ├── testuser2.crt
│   │   │       │   │   │   └── testuser2_crt_generated
│   │   │       │   │   ├── ecparams/
│   │   │       │   │   │   └── secp384r1.pem
│   │   │       │   │   ├── index.txt
│   │   │       │   │   ├── index.txt.attr
│   │   │       │   │   ├── index.txt.attr.old
│   │   │       │   │   ├── index.txt.old
│   │   │       │   │   ├── openssl.cnf
│   │   │       │   │   ├── private/
│   │   │       │   │   │   ├── .rnd
│   │   │       │   │   │   ├── 10.99.0.10.key
│   │   │       │   │   │   ├── cakey.pem
│   │   │       │   │   │   ├── testuser1.key
│   │   │       │   │   │   ├── testuser1.p12
│   │   │       │   │   │   ├── testuser1_ca.p12
│   │   │       │   │   │   ├── testuser2.key
│   │   │       │   │   │   ├── testuser2.p12
│   │   │       │   │   │   └── testuser2_ca.p12
│   │   │       │   │   ├── public/
│   │   │       │   │   │   ├── testuser1.pub
│   │   │       │   │   │   └── testuser2.pub
│   │   │       │   │   ├── reqs/
│   │   │       │   │   │   ├── 10.99.0.10.req
│   │   │       │   │   │   ├── testuser1.req
│   │   │       │   │   │   └── testuser2.req
│   │   │       │   │   ├── serial
│   │   │       │   │   ├── serial.old
│   │   │       │   │   └── serial_generated
│   │   │       │   ├── apple/
│   │   │       │   │   ├── testuser1.mobileconfig
│   │   │       │   │   └── testuser2.mobileconfig
│   │   │       │   └── manual/
│   │   │       │       ├── cacert.pem
│   │   │       │       ├── testuser1.conf
│   │   │       │       ├── testuser1.p12
│   │   │       │       ├── testuser1.secrets
│   │   │       │       ├── testuser2.conf
│   │   │       │       ├── testuser2.p12
│   │   │       │       └── testuser2.secrets
│   │   │       └── wireguard/
│   │   │           ├── .pki/
│   │   │           │   ├── index.txt
│   │   │           │   ├── preshared/
│   │   │           │   │   ├── 10.99.0.10
│   │   │           │   │   ├── testuser1
│   │   │           │   │   └── testuser2
│   │   │           │   ├── private/
│   │   │           │   │   ├── 10.99.0.10
│   │   │           │   │   ├── testuser1
│   │   │           │   │   └── testuser2
│   │   │           │   └── public/
│   │   │           │       ├── 10.99.0.10
│   │   │           │       ├── testuser1
│   │   │           │       └── testuser2
│   │   │           ├── apple/
│   │   │           │   ├── ios/
│   │   │           │   │   ├── testuser1.mobileconfig
│   │   │           │   │   └── testuser2.mobileconfig
│   │   │           │   └── macos/
│   │   │           │       ├── testuser1.mobileconfig
│   │   │           │       └── testuser2.mobileconfig
│   │   │           ├── testuser1.conf
│   │   │           └── testuser2.conf
│   │   └── test-run.log
│   ├── test-aws-credentials.yml
│   ├── test-local-config.sh
│   ├── test-wireguard-async.yml
│   ├── test-wireguard-fix.yml
│   ├── test-wireguard-real-async.yml
│   ├── test_cloud_init_template.py
│   ├── test_package_preinstall.py
│   ├── unit/
│   │   ├── test_ansible_12_boolean_fix.py
│   │   ├── test_basic_sanity.py
│   │   ├── test_boolean_variables.py
│   │   ├── test_cloud_provider_configs.py
│   │   ├── test_comprehensive_boolean_scan.py
│   │   ├── test_config_validation.py
│   │   ├── test_destroy.py
│   │   ├── test_docker_localhost_deployment.py
│   │   ├── test_double_templating.py
│   │   ├── test_generated_configs.py
│   │   ├── test_iptables_rules.py
│   │   ├── test_lightsail_boto3_fix.py
│   │   ├── test_list_servers.py
│   │   ├── test_openssl_compatibility.py
│   │   ├── test_scaleway_fix.py
│   │   ├── test_strongswan_templates.py
│   │   ├── test_template_rendering.py
│   │   ├── test_user_management.py
│   │   ├── test_wireguard_key_generation.py
│   │   └── test_yaml_jinja2_expressions.py
│   └── validate_jinja2_templates.py
└── users.yml
Download .txt
SYMBOL INDEX (247 symbols across 34 files)

FILE: library/gcp_compute_location_info.py
  function main (line 19) | def main():
  function collection (line 41) | def collection(module):
  function fetch_list (line 45) | def fetch_list(module, link, query):
  function query_options (line 51) | def query_options(filters):
  function return_if_object (line 69) | def return_if_object(module, response):

FILE: library/lightsail_region_facts.py
  function main (line 72) | def main():

FILE: library/scaleway_compute.py
  function check_image_id (line 170) | def check_image_id(compute_api, image_id):
  function fetch_state (line 183) | def fetch_state(compute_api, server):
  function wait_to_complete_state_transition (line 201) | def wait_to_complete_state_transition(compute_api, server):
  function public_ip_payload (line 221) | def public_ip_payload(compute_api, public_ip):
  function create_server (line 247) | def create_server(compute_api, server):
  function restart_server (line 282) | def restart_server(compute_api, server):
  function stop_server (line 286) | def stop_server(compute_api, server):
  function start_server (line 290) | def start_server(compute_api, server):
  function perform_action (line 294) | def perform_action(compute_api, server, action):
  function remove_server (line 305) | def remove_server(compute_api, server):
  function present_strategy (line 317) | def present_strategy(compute_api, wished_server):
  function absent_strategy (line 346) | def absent_strategy(compute_api, wished_server):
  function running_strategy (line 382) | def running_strategy(compute_api, wished_server):
  function stop_strategy (line 424) | def stop_strategy(compute_api, wished_server):
  function restart_strategy (line 477) | def restart_strategy(compute_api, wished_server):
  function find (line 537) | def find(compute_api, wished_server, per_page=1):
  function server_attributes_should_be_changed (line 560) | def server_attributes_should_be_changed(compute_api, target_server, wish...
  function server_change_attributes (line 589) | def server_change_attributes(compute_api, target_server, wished_server):
  function core (line 620) | def core(module):
  function main (line 648) | def main():

FILE: library/x25519_pubkey.py
  function run_module (line 32) | def run_module():
  function main (line 126) | def main():

FILE: scripts/list_servers.py
  function list_servers (line 11) | def list_servers(configs_dir: Path) -> list[dict]:
  function main (line 22) | def main() -> None:

FILE: scripts/track-test-effectiveness.py
  function get_github_api_data (line 14) | def get_github_api_data(endpoint):
  function analyze_workflow_runs (line 24) | def analyze_workflow_runs(repo_owner, repo_name, days_back=30):
  function extract_failed_test (line 65) | def extract_failed_test(job_name, run_id):
  function extract_pr_number (line 78) | def extract_pr_number(run):
  function correlate_with_issues (line 85) | def correlate_with_issues(repo_owner, repo_name, test_failures):
  function generate_effectiveness_report (line 111) | def generate_effectiveness_report(test_failures, correlations):
  function save_metrics (line 161) | def save_metrics(test_failures, correlations):

FILE: tests/conftest.py
  function test_variables (line 17) | def test_variables():
  function test_config (line 25) | def test_config(test_variables):
  function temp_directory (line 31) | def temp_directory():
  function wireguard_private_key (line 38) | def wireguard_private_key():
  function wireguard_key_pair (line 45) | def wireguard_key_pair(temp_directory):
  class MockAnsibleModule (line 60) | class MockAnsibleModule:
    method __init__ (line 63) | def __init__(self, params):
    method fail_json (line 70) | def fail_json(self, **kwargs):
    method exit_json (line 76) | def exit_json(self, **kwargs):
  function mock_ansible_module (line 82) | def mock_ansible_module():
  function mock_to_uuid (line 88) | def mock_to_uuid(value):
  function mock_bool (line 93) | def mock_bool(value):
  function mock_lookup (line 98) | def mock_lookup(lookup_type, path):
  function jinja2_env (line 111) | def jinja2_env():
  function project_root (line 126) | def project_root():
  function roles_dir (line 132) | def roles_dir(project_root):
  function pytest_configure (line 138) | def pytest_configure(config):
  function skip_wireguard_tests (line 145) | def skip_wireguard_tests(request):

FILE: tests/fixtures/__init__.py
  function load_test_variables (line 8) | def load_test_variables():
  function get_test_config (line 15) | def get_test_config(overrides=None):

FILE: tests/integration/mock_modules/apt.py
  function main (line 9) | def main():

FILE: tests/integration/mock_modules/command.py
  function main (line 9) | def main():

FILE: tests/integration/mock_modules/shell.py
  function main (line 9) | def main():

FILE: tests/test_cloud_init_template.py
  function create_expected_cloud_init (line 28) | def create_expected_cloud_init():
  class TestCloudInitTemplate (line 79) | class TestCloudInitTemplate:
    method test_yaml_validity (line 82) | def test_yaml_validity(self):
    method test_required_sections (line 97) | def test_required_sections(self):
    method test_ssh_configuration (line 110) | def test_ssh_configuration(self):
    method test_user_creation (line 143) | def test_user_creation(self):
    method test_runcmd_section (line 172) | def test_runcmd_section(self):
    method test_indentation_consistency (line 192) | def test_indentation_consistency(self):
  function run_tests (line 229) | def run_tests():

FILE: tests/test_package_preinstall.py
  class TestPackagePreinstall (line 8) | class TestPackagePreinstall(unittest.TestCase):
    method setUp (line 9) | def setUp(self):
    method test_preinstall_disabled_by_default (line 27) | def test_preinstall_disabled_by_default(self):
    method test_preinstall_enabled (line 38) | def test_preinstall_enabled(self):
    method test_preinstall_disabled_explicitly (line 53) | def test_preinstall_disabled_explicitly(self):
    method test_package_count (line 64) | def test_package_count(self):

FILE: tests/unit/test_ansible_12_boolean_fix.py
  class TestAnsible12BooleanFix (line 11) | class TestAnsible12BooleanFix:
    method test_ipv6_support_not_string_boolean (line 14) | def test_ipv6_support_not_string_boolean(self):
    method test_input_yml_algo_variables_not_string_boolean (line 31) | def test_input_yml_algo_variables_not_string_boolean(self):
    method test_no_bare_true_false_in_templates (line 68) | def test_no_bare_true_false_in_templates(self):
    method test_conditional_uses_of_variables (line 94) | def test_conditional_uses_of_variables(self):

FILE: tests/unit/test_basic_sanity.py
  function test_python_version (line 13) | def test_python_version():
  function test_pyproject_file_exists (line 19) | def test_pyproject_file_exists():
  function test_config_file_valid (line 33) | def test_config_file_valid():
  function test_ansible_syntax (line 46) | def test_ansible_syntax():
  function test_shellcheck (line 54) | def test_shellcheck():
  function test_dockerfile_exists (line 65) | def test_dockerfile_exists():
  function test_cloud_init_header_format (line 77) | def test_cloud_init_header_format():

FILE: tests/unit/test_boolean_variables.py
  function render_template (line 10) | def render_template(template_str, variables=None):
  class TestBooleanVariables (line 17) | class TestBooleanVariables:
    method test_ipv6_support_produces_boolean (line 20) | def test_ipv6_support_produces_boolean(self):
    method test_algo_variables_boolean_fallbacks (line 41) | def test_algo_variables_boolean_fallbacks(self):
    method test_boolean_filter_on_strings (line 57) | def test_boolean_filter_on_strings(self):
    method test_ansible_12_conditional_compatibility (line 75) | def test_ansible_12_conditional_compatibility(self):
    method test_regression_no_string_booleans (line 98) | def test_regression_no_string_booleans(self):

FILE: tests/unit/test_cloud_provider_configs.py
  function load_config (line 42) | def load_config():
  function test_no_deprecated_instance_types (line 48) | def test_no_deprecated_instance_types():
  function test_required_fields_present (line 60) | def test_required_fields_present():
  function test_no_malformed_values (line 75) | def test_no_malformed_values():

FILE: tests/unit/test_comprehensive_boolean_scan.py
  class TestComprehensiveBooleanScan (line 42) | class TestComprehensiveBooleanScan:
    method get_yaml_files (line 45) | def get_yaml_files(self):
    method test_no_string_true_false_in_set_fact (line 98) | def test_no_string_true_false_in_set_fact(self):
    method test_no_bare_false_in_jinja_else (line 113) | def test_no_bare_false_in_jinja_else(self):
    method test_when_conditions_use_booleans (line 129) | def test_when_conditions_use_booleans(self):
    method test_template_files_boolean_usage (line 163) | def test_template_files_boolean_usage(self):
    method test_all_when_conditions_would_work (line 189) | def test_all_when_conditions_would_work(self):
    method test_no_other_problematic_patterns (line 218) | def test_no_other_problematic_patterns(self):
    method test_verify_our_fixes_are_correct (line 267) | def test_verify_our_fixes_are_correct(self):
    method test_templates_handle_booleans_correctly (line 289) | def test_templates_handle_booleans_correctly(self):

FILE: tests/unit/test_config_validation.py
  function test_wireguard_config_format (line 14) | def test_wireguard_config_format():
  function test_base64_key_format (line 44) | def test_base64_key_format():
  function test_ip_address_format (line 60) | def test_ip_address_format():
  function test_mobile_config_xml (line 75) | def test_mobile_config_xml():
  function test_port_ranges (line 113) | def test_port_ranges():

FILE: tests/unit/test_destroy.py
  function test_destroy_playbook_exists (line 25) | def test_destroy_playbook_exists():
  function test_destroy_playbook_valid_yaml (line 30) | def test_destroy_playbook_valid_yaml():
  function test_destroy_playbook_syntax (line 41) | def test_destroy_playbook_syntax():
  function test_destroy_playbook_has_rescue (line 51) | def test_destroy_playbook_has_rescue():
  function test_destroy_playbook_has_confirmation (line 59) | def test_destroy_playbook_has_confirmation():
  function test_all_provider_destroy_files_exist (line 66) | def test_all_provider_destroy_files_exist():
  function test_all_provider_destroy_files_valid_yaml (line 73) | def test_all_provider_destroy_files_valid_yaml():
  function test_provider_destroy_uses_absent_state (line 82) | def test_provider_destroy_uses_absent_state():
  function test_ec2_destroy_uses_cloudformation (line 91) | def test_ec2_destroy_uses_cloudformation():
  function test_lightsail_destroy_uses_cloudformation (line 99) | def test_lightsail_destroy_uses_cloudformation():
  function test_gce_destroy_cleans_subsidiary_resources (line 107) | def test_gce_destroy_cleans_subsidiary_resources():
  function test_vultr_destroy_cleans_firewall_group (line 116) | def test_vultr_destroy_cleans_firewall_group():
  function test_openstack_destroy_cleans_security_group (line 123) | def test_openstack_destroy_cleans_security_group():
  function test_cloudstack_destroy_cleans_security_group (line 130) | def test_cloudstack_destroy_cleans_security_group():
  function test_subsidiary_cleanup_is_best_effort (line 137) | def test_subsidiary_cleanup_is_best_effort():
  function test_linode_uses_label_not_name (line 152) | def test_linode_uses_label_not_name():
  function test_azure_deletes_resource_group (line 159) | def test_azure_deletes_resource_group():
  function test_algo_script_has_destroy_command (line 167) | def test_algo_script_has_destroy_command():
  function test_algo_script_destroy_requires_ip (line 175) | def test_algo_script_destroy_requires_ip():
  function test_server_yml_stores_algo_region (line 182) | def test_server_yml_stores_algo_region():
  function test_destroy_playbook_validates_region_for_required_providers (line 189) | def test_destroy_playbook_validates_region_for_required_providers():
  function test_destroy_playbook_loads_server_config (line 197) | def test_destroy_playbook_loads_server_config():

FILE: tests/unit/test_docker_localhost_deployment.py
  function check_docker_available (line 12) | def check_docker_available():
  function test_wireguard_config_validation (line 21) | def test_wireguard_config_validation():
  function test_strongswan_config_validation (line 52) | def test_strongswan_config_validation():
  function test_docker_algo_image (line 88) | def test_docker_algo_image():
  function test_localhost_deployment_requirements (line 119) | def test_localhost_deployment_requirements():

FILE: tests/unit/test_double_templating.py
  function find_yaml_files (line 17) | def find_yaml_files() -> list[Path]:
  function detect_double_templating (line 33) | def detect_double_templating(content: str) -> list[tuple[int, str]]:
  function test_no_double_templating (line 65) | def test_no_double_templating():
  function test_specific_known_issues (line 99) | def test_specific_known_issues():
  function test_valid_patterns_not_flagged (line 117) | def test_valid_patterns_not_flagged():

FILE: tests/unit/test_generated_configs.py
  function check_command_available (line 12) | def check_command_available(cmd):
  function test_wireguard_config_syntax (line 21) | def test_wireguard_config_syntax():
  function test_strongswan_ipsec_conf (line 96) | def test_strongswan_ipsec_conf():
  function test_ssh_config_syntax (line 172) | def test_ssh_config_syntax():
  function test_iptables_rules_syntax (line 230) | def test_iptables_rules_syntax():
  function test_dns_config_syntax (line 298) | def test_dns_config_syntax():

FILE: tests/unit/test_iptables_rules.py
  function _ansible_bool (line 15) | def _ansible_bool(value):
  function load_template (line 24) | def load_template(template_name):
  function test_wireguard_nat_rules_ipv4 (line 32) | def test_wireguard_nat_rules_ipv4():
  function test_ipsec_nat_rules_ipv4 (line 60) | def test_ipsec_nat_rules_ipv4():
  function test_both_vpns_nat_rules_ipv4 (line 86) | def test_both_vpns_nat_rules_ipv4():
  function test_alternative_ingress_snat (line 118) | def test_alternative_ingress_snat():
  function test_ipsec_forward_rule_has_policy_match (line 148) | def test_ipsec_forward_rule_has_policy_match():
  function test_wireguard_forward_rule_no_policy_match (line 171) | def test_wireguard_forward_rule_no_policy_match():
  function test_output_interface_in_nat_rules (line 197) | def test_output_interface_in_nat_rules():
  function test_dns_firewall_restricted_to_vpn (line 225) | def test_dns_firewall_restricted_to_vpn():

FILE: tests/unit/test_lightsail_boto3_fix.py
  class TestLightsailBoto3Fix (line 18) | class TestLightsailBoto3Fix(unittest.TestCase):
    method setUp (line 21) | def setUp(self):
    method tearDown (line 37) | def tearDown(self):
    method test_lightsail_region_facts_imports (line 42) | def test_lightsail_region_facts_imports(self):
    method test_get_aws_connection_info_called_without_boto3 (line 64) | def test_get_aws_connection_info_called_without_boto3(self):
    method test_no_boto3_parameter_in_source (line 116) | def test_no_boto3_parameter_in_source(self):
    method test_regression_issue_14822 (line 135) | def test_regression_issue_14822(self):

FILE: tests/unit/test_list_servers.py
  function configs_dir (line 20) | def configs_dir(tmp_path):
  function test_empty_directory (line 32) | def test_empty_directory(tmp_path):
  function test_missing_directory (line 37) | def test_missing_directory(tmp_path):
  function test_lists_servers (line 42) | def test_lists_servers(configs_dir):
  function test_sorted_output (line 50) | def test_sorted_output(configs_dir):
  function test_skips_empty_yaml (line 57) | def test_skips_empty_yaml(tmp_path):
  function test_cli_output (line 66) | def test_cli_output(tmp_path):
  function test_cli_missing_dir (line 83) | def test_cli_missing_dir():

FILE: tests/unit/test_openssl_compatibility.py
  function find_generated_certificates (line 20) | def find_generated_certificates():
  function test_openssl_version_detection (line 43) | def test_openssl_version_detection():
  function validate_ca_certificate_real (line 60) | def validate_ca_certificate_real(cert_files):
  function validate_ca_certificate_config (line 114) | def validate_ca_certificate_config():
  function test_ca_certificate (line 168) | def test_ca_certificate():
  function validate_server_certificates_real (line 177) | def validate_server_certificates_real(cert_files):
  function validate_server_certificates_config (line 221) | def validate_server_certificates_config():
  function test_server_certificates (line 267) | def test_server_certificates():
  function validate_client_certificates_real (line 276) | def validate_client_certificates_real(cert_files):
  function validate_client_certificates_config (line 324) | def validate_client_certificates_config():
  function test_client_certificates (line 374) | def test_client_certificates():
  function validate_pkcs12_files_real (line 383) | def validate_pkcs12_files_real(cert_files):
  function validate_pkcs12_files_config (line 422) | def validate_pkcs12_files_config():
  function test_pkcs12_files (line 449) | def test_pkcs12_files():
  function validate_certificate_chain_real (line 458) | def validate_certificate_chain_real(cert_files):
  function validate_certificate_chain_config (line 492) | def validate_certificate_chain_config():
  function test_certificate_chain (line 524) | def test_certificate_chain():
  function find_ansible_file (line 533) | def find_ansible_file(relative_path):

FILE: tests/unit/test_scaleway_fix.py
  function load_yaml_file (line 17) | def load_yaml_file(file_path):
  function test_scaleway_main_uses_project_parameter (line 23) | def test_scaleway_main_uses_project_parameter():
  function test_scaleway_prompts_collect_org_id (line 52) | def test_scaleway_prompts_collect_org_id():
  function test_scaleway_config_has_valid_settings (line 77) | def test_scaleway_config_has_valid_settings():
  function test_scaleway_marketplace_api_usage (line 94) | def test_scaleway_marketplace_api_usage():

FILE: tests/unit/test_strongswan_templates.py
  function mock_to_uuid (line 18) | def mock_to_uuid(value):
  function mock_bool (line 23) | def mock_bool(value):
  function mock_version (line 28) | def mock_version(version_string, comparison):
  function mock_b64encode (line 34) | def mock_b64encode(value):
  function mock_b64decode (line 43) | def mock_b64decode(value):
  function get_strongswan_test_variables (line 50) | def get_strongswan_test_variables(scenario="default"):
  function test_strongswan_templates (line 102) | def test_strongswan_templates():
  function test_openssl_template_constraints (line 178) | def test_openssl_template_constraints():
  function test_mobileconfig_template (line 223) | def test_mobileconfig_template():

FILE: tests/unit/test_template_rendering.py
  function mock_to_uuid (line 19) | def mock_to_uuid(value):
  function mock_bool (line 24) | def mock_bool(value):
  function mock_lookup (line 29) | def mock_lookup(type, path):
  function get_test_variables (line 42) | def get_test_variables():
  function find_templates (line 48) | def find_templates():
  function test_template_syntax (line 56) | def test_template_syntax():
  function test_critical_templates (line 101) | def test_critical_templates():
  function test_variable_consistency (line 158) | def test_variable_consistency():
  function test_wireguard_ipv6_endpoints (line 187) | def test_wireguard_ipv6_endpoints():
  function test_template_conditionals (line 247) | def test_template_conditionals():

FILE: tests/unit/test_user_management.py
  function test_user_list_parsing (line 15) | def test_user_list_parsing():
  function test_server_selection_format (line 42) | def test_server_selection_format():
  function test_ssh_key_preservation (line 71) | def test_ssh_key_preservation():
  function test_ca_password_handling (line 98) | def test_ca_password_handling():
  function test_user_config_generation (line 127) | def test_user_config_generation():
  function test_duplicate_user_handling (line 149) | def test_duplicate_user_handling():

FILE: tests/unit/test_wireguard_key_generation.py
  function test_wireguard_tools_available (line 17) | def test_wireguard_tools_available():
  function test_x25519_module_import (line 29) | def test_x25519_module_import():
  function generate_test_private_key (line 40) | def generate_test_private_key():
  function test_x25519_pubkey_from_raw_file (line 70) | def test_x25519_pubkey_from_raw_file():
  function test_x25519_pubkey_from_b64_string (line 141) | def test_x25519_pubkey_from_b64_string():
  function test_wireguard_validation (line 194) | def test_wireguard_validation():
  function test_key_consistency (line 272) | def test_key_consistency():

FILE: tests/unit/test_yaml_jinja2_expressions.py
  function find_yaml_files_with_jinja2 (line 15) | def find_yaml_files_with_jinja2():
  function extract_jinja2_expressions (line 32) | def extract_jinja2_expressions(content):
  function find_line_number (line 63) | def find_line_number(content, position):
  function validate_jinja2_expression (line 68) | def validate_jinja2_expression(expression, context_vars=None):
  function get_test_variables (line 161) | def get_test_variables():
  function validate_yaml_file (line 209) | def validate_yaml_file(yaml_path, check_inline_comments_only=False):
  function test_regression_openssl_inline_comments (line 258) | def test_regression_openssl_inline_comments():
  function test_edge_cases_inline_comments (line 301) | def test_edge_cases_inline_comments():
  function test_yaml_files_no_inline_comments (line 364) | def test_yaml_files_no_inline_comments():
  function test_openssl_file_specifically (line 387) | def test_openssl_file_specifically():

FILE: tests/validate_jinja2_templates.py
  function find_jinja2_templates (line 17) | def find_jinja2_templates(root_dir: str = ".") -> list[Path]:
  function check_inline_comments_in_expressions (line 34) | def check_inline_comments_in_expressions(template_content: str, template...
  function check_undefined_variables (line 69) | def check_undefined_variables(template_path: Path) -> list[str]:
  function validate_template_syntax (line 120) | def validate_template_syntax(template_path: Path) -> tuple[bool, list[st...
  function check_common_antipatterns (line 180) | def check_common_antipatterns(template_path: Path) -> list[str]:
  function main (line 210) | def main():
Condensed preview — 335 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,032K chars).
[
  {
    "path": ".ansible-lint",
    "chars": 1972,
    "preview": "# Ansible-lint configuration\nexclude_paths:\n  - .cache/\n  - .github/\n  - tests/\n  - files/cloud-init/  # Cloud-init file"
  },
  {
    "path": ".dockerignore",
    "chars": 506,
    "preview": "# Version control and CI\n.git/\n.github/\n.gitignore\n\n# Development environment\n.env\n.venv/\n.ruff_cache/\n__pycache__/\n*.py"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 657,
    "preview": "---\n# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 208,
    "preview": "---\nname: Bug report\nabout: Report a problem with Algo\n---\n\n**What happened?**\n\n\n**Environment** (cloud provider, OS, Wi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 202,
    "preview": "---\nblank_issues_enabled: true\ncontact_links:\n  - name: Troubleshooting Guide\n    url: https://trailofbits.github.io/alg"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 560,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? "
  },
  {
    "path": ".github/actions/setup-algo/action.yml",
    "chars": 1092,
    "preview": "---\nname: 'Setup Algo Environment'\ndescription: 'Setup Python, uv, and dependencies for Algo VPN CI'\ninputs:\n  python-ve"
  },
  {
    "path": ".github/actions/setup-uv/action.yml",
    "chars": 487,
    "preview": "---\nname: 'Setup uv Environment'\ndescription: 'Install uv and sync dependencies for Algo VPN project'\noutputs:\n  uv-vers"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 830,
    "preview": "---\nversion: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directo"
  },
  {
    "path": ".github/workflows/docker-image.yaml",
    "chars": 1682,
    "preview": "---\nname: Create and publish a Docker image\n\n'on':\n  push:\n    branches: ['master']\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NA"
  },
  {
    "path": ".github/workflows/integration-tests.yml",
    "chars": 10750,
    "preview": "---\nname: Integration Tests\n\n'on':\n  pull_request:\n    types: [opened, synchronize, reopened]\n    paths:\n      - 'main.y"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 6306,
    "preview": "---\nname: Lint\n\n'on':\n  push:\n    branches: [main, master]\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  ansib"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 5013,
    "preview": "---\nname: Main\n\n'on':\n  push:\n    branches:\n      - master\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: r"
  },
  {
    "path": ".github/workflows/security.yml",
    "chars": 1007,
    "preview": "---\nname: Security\n\n'on':\n  push:\n    branches: [main, master]\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  s"
  },
  {
    "path": ".github/workflows/smart-tests.yml",
    "chars": 10073,
    "preview": "---\nname: Smart Test Selection\n\n'on':\n  pull_request:\n    types: [opened, synchronize, reopened]\n\npermissions:\n  content"
  },
  {
    "path": ".github/workflows/test-effectiveness.yml",
    "chars": 2199,
    "preview": "---\nname: Test Effectiveness Tracking\n\n'on':\n  schedule:\n    - cron: '0 0 * * 0'  # Weekly on Sunday\n  workflow_dispatch"
  },
  {
    "path": ".gitignore",
    "chars": 128,
    "preview": "*.retry\n.idea/\nconfigs/*\ninventory_users\n*.kate-swp\n.env/\n.venv/\n.DS_Store\n.vagrant\n.ansible/\n__pycache__/\n*.pyc\nalgo.eg"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 3366,
    "preview": "# See https://prek.j178.dev for more information\n---\n# Apply to all files without committing:\n#   prek run --all-files\n#"
  },
  {
    "path": ".yamllint",
    "chars": 571,
    "preview": "---\nextends: default\n\n# Cloud-init files must be excluded from normal YAML rules\n# The #cloud-config header cannot have "
  },
  {
    "path": "CLAUDE.md",
    "chars": 17531,
    "preview": "# CLAUDE.md - LLM Guidance for Algo VPN\n\nThis document provides essential context and guidance for LLMs working on the A"
  },
  {
    "path": "CODEOWNERS",
    "chars": 14,
    "preview": "* @jackivanov\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1126,
    "preview": "### Filing New Issues\n\n* We welcome bug reports! Before filing, a quick check of the [FAQ](docs/faq.md) or [troubleshoot"
  },
  {
    "path": "Dockerfile",
    "chars": 2107,
    "preview": "# syntax=docker/dockerfile:1\nFROM python:3.12-alpine\n\nARG VERSION=\"git\"\n# Removed rust/cargo (not needed with uv), simpl"
  },
  {
    "path": "LICENSE",
    "chars": 34523,
    "preview": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C)"
  },
  {
    "path": "PULL_REQUEST_TEMPLATE.md",
    "chars": 1471,
    "preview": "<!--- Provide a general summary of your changes in the Title above -->\n\n## Description\n<!--- Describe your changes in de"
  },
  {
    "path": "README.md",
    "chars": 18543,
    "preview": "# Algo VPN\n\n[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%"
  },
  {
    "path": "SECURITY.md",
    "chars": 735,
    "preview": "# Reporting Security Issues\n\nThe Algo team and community take security bugs in Algo seriously. We appreciate your effort"
  },
  {
    "path": "algo",
    "chars": 8452,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\n# Track which installation method succeeded\nUV_INSTALL_METHOD=\"\"\n\n# Function to install uv "
  },
  {
    "path": "algo-docker.sh",
    "chars": 1214,
    "preview": "#!/usr/bin/env bash\n\nset -eEo pipefail\n\nALGO_DIR=\"/algo\"\nDATA_DIR=\"/data\"\n\numask 0077\n\nusage() {\n    retcode=\"${1:-0}\"\n "
  },
  {
    "path": "algo-showenv.sh",
    "chars": 2526,
    "preview": "#!/usr/bin/env bash\n#\n# Print information about Algo's invocation environment to aid in debugging.\n# This is normally ca"
  },
  {
    "path": "algo.ps1",
    "chars": 5646,
    "preview": "# PowerShell script for Windows users to run Algo VPN\nparam(\n    [Parameter(ValueFromRemainingArguments)]\n    [string[]]"
  },
  {
    "path": "ansible.cfg",
    "chars": 486,
    "preview": "[defaults]\ninventory = inventory\npipelining = True\nretry_files_enabled = False\nhost_key_checking = False\ntimeout = 60\nst"
  },
  {
    "path": "cloud.yml",
    "chars": 521,
    "preview": "---\n- name: Provision the server\n  hosts: localhost\n  tags: always\n  become: false\n  vars_files:\n    - config.cfg\n\n  tas"
  },
  {
    "path": "config.cfg",
    "chars": 7451,
    "preview": "---\n\n# ============================================\n# TROUBLESHOOTING DEPLOYMENT ISSUES\n# =============================="
  },
  {
    "path": "deploy_client.yml",
    "chars": 756,
    "preview": "---\n- name: Configure the client\n  hosts: localhost\n  become: false\n  vars_files:\n    - config.cfg\n\n  tasks:\n    - name:"
  },
  {
    "path": "destroy.yml",
    "chars": 4908,
    "preview": "---\n- name: Destroy an Algo VPN server\n  hosts: localhost\n  gather_facts: false\n  become: false\n  vars_files:\n    - conf"
  },
  {
    "path": "docs/aws-credentials.md",
    "chars": 1902,
    "preview": "# AWS Credential Configuration\n\nAlgo supports multiple methods for providing AWS credentials, following standard AWS pra"
  },
  {
    "path": "docs/client-android.md",
    "chars": 259,
    "preview": "# Android client setup\n\n## Installation via profiles\n\n1. [Install the WireGuard VPN Client](https://play.google.com/stor"
  },
  {
    "path": "docs/client-apple-ipsec.md",
    "chars": 1679,
    "preview": "# Using the built-in IPSEC VPN on Apple Devices\n\n## Configure IPsec\n\nFind the corresponding `mobileconfig` (Apple Profil"
  },
  {
    "path": "docs/client-linux-ipsec.md",
    "chars": 2046,
    "preview": "# Linux strongSwan IPsec Clients (e.g., OpenWRT, Ubuntu Server, etc.)\n\nInstall strongSwan, then copy the included ipsec_"
  },
  {
    "path": "docs/client-linux-wireguard.md",
    "chars": 2229,
    "preview": "# Using Ubuntu as a Client with WireGuard\n\n## Install WireGuard\n\nTo connect to your AlgoVPN using [WireGuard](https://ww"
  },
  {
    "path": "docs/client-linux.md",
    "chars": 2537,
    "preview": "# Linux client setup\n\n## Provision client config\n\nAfter you deploy a server, you can use an included Ansible script to p"
  },
  {
    "path": "docs/client-macos-wireguard.md",
    "chars": 1340,
    "preview": "# MacOS WireGuard Client Setup\n\nThe WireGuard macOS app is unavailable for older operating systems. Please update your o"
  },
  {
    "path": "docs/client-openwrt-router-wireguard.md",
    "chars": 7422,
    "preview": "# OpenWrt Router as WireGuard Client\n\nThis guide explains how to configure an OpenWrt router as a WireGuard VPN client, "
  },
  {
    "path": "docs/client-windows.md",
    "chars": 3683,
    "preview": "# Windows Client Setup\n\nThis guide will help you set up your Windows device to connect to your Algo VPN server.\n\n## Supp"
  },
  {
    "path": "docs/cloud-alternative-ingress-ip.md",
    "chars": 916,
    "preview": "# Alternative Ingress IP\n\nThis feature allows you to configure the Algo server to send outbound traffic through a differ"
  },
  {
    "path": "docs/cloud-amazon-ec2.md",
    "chars": 6468,
    "preview": "# Amazon EC2 Cloud Setup\n\nThis guide walks you through setting up Algo VPN on Amazon EC2, including account creation, pe"
  },
  {
    "path": "docs/cloud-azure.md",
    "chars": 2953,
    "preview": "# Azure cloud setup\n\nThe easiest way to get started with the Azure CLI is by running it in an Azure Cloud Shell environm"
  },
  {
    "path": "docs/cloud-cloudstack.md",
    "chars": 678,
    "preview": "### Configuration file\n\n> **⚠️ Important Note:** Exoscale is no longer supported as they deprecated their CloudStack API"
  },
  {
    "path": "docs/cloud-do.md",
    "chars": 4900,
    "preview": "# DigitalOcean cloud setup\n\n## API Token creation\n\nFirst, login into your DigitalOcean account.\n\nSelect **API** from the"
  },
  {
    "path": "docs/cloud-gce.md",
    "chars": 1724,
    "preview": "# Google Cloud Platform setup\n\n* Follow the [`gcloud` installation instructions](https://cloud.google.com/sdk/)\n\n* Log i"
  },
  {
    "path": "docs/cloud-hetzner.md",
    "chars": 385,
    "preview": "## API Token\n\nSign in into the [Hetzner Cloud Console](https://console.hetzner.cloud/) choose a project, go to `Security"
  },
  {
    "path": "docs/cloud-linode.md",
    "chars": 369,
    "preview": "## API Token\n\nSign in to the Linode Manager and go to the\n[tokens management page](https://cloud.linode.com/profile/toke"
  },
  {
    "path": "docs/cloud-scaleway.md",
    "chars": 794,
    "preview": "### Configuration file\n\nAlgo requires an API key from your Scaleway account to create a server.\nThe API key is generated"
  },
  {
    "path": "docs/cloud-vultr.md",
    "chars": 1088,
    "preview": "### Configuration file\n\nAlgo requires an API key from your Vultr account in order to create a server. The API key is gen"
  },
  {
    "path": "docs/deploy-from-ansible.md",
    "chars": 12897,
    "preview": "# Deployment from Ansible\n\nBefore you begin, make sure you have installed all the dependencies necessary for your operat"
  },
  {
    "path": "docs/deploy-from-cloudshell.md",
    "chars": 1114,
    "preview": "# Deploy from Google Cloud Shell\n\nIf you want to try Algo but don't wish to install anything on your own system, you can"
  },
  {
    "path": "docs/deploy-from-docker.md",
    "chars": 5560,
    "preview": "# Docker Support\n\nWhile it is not possible to run your Algo server from within a Docker container, it is possible to use"
  },
  {
    "path": "docs/deploy-from-macos.md",
    "chars": 893,
    "preview": "# Deploy from macOS\n\nYou can install the Algo scripts on a macOS system and use them to deploy your AlgoVPN to a cloud p"
  },
  {
    "path": "docs/deploy-from-script-or-cloud-init-to-localhost.md",
    "chars": 4324,
    "preview": "# Deploy from script or cloud-init\n\nYou can use `install.sh` to prepare the environment and deploy AlgoVPN on the local "
  },
  {
    "path": "docs/deploy-from-windows.md",
    "chars": 3134,
    "preview": "# Deploy from Windows\n\nYou have three options to run Algo on Windows:\n\n1. **PowerShell Script** (Recommended) - Automate"
  },
  {
    "path": "docs/deploy-to-ubuntu.md",
    "chars": 2320,
    "preview": "# Local Installation\n\n**IMPORTANT**: Algo is designed to create a dedicated VPN server. There is no uninstallation optio"
  },
  {
    "path": "docs/deploy-to-unsupported-cloud.md",
    "chars": 4417,
    "preview": "# Deploying to Unsupported Cloud Providers\n\nAlgo officially supports the [cloud providers listed in the README](https://"
  },
  {
    "path": "docs/faq.md",
    "chars": 14235,
    "preview": "# FAQ\n\n* [Has Algo been audited?](#has-algo-been-audited)\n* [What's the current status of WireGuard?](#whats-the-current"
  },
  {
    "path": "docs/firewalls.md",
    "chars": 2636,
    "preview": "# AlgoVPN and Firewalls\n\nYour AlgoVPN requires properly configured firewalls. The key points to know are:\n\n* If you depl"
  },
  {
    "path": "docs/index.md",
    "chars": 1558,
    "preview": "# Algo VPN documentation\n\n* Deployment instructions\n  - Deploy from [RedHat/CentOS 6.x](deploy-from-redhat-centos6.md)\n "
  },
  {
    "path": "docs/troubleshooting.md",
    "chars": 34363,
    "preview": "# Troubleshooting\n\nFirst of all, check [this](https://github.com/trailofbits/algo#features) and ensure that you are depl"
  },
  {
    "path": "files/cloud-init/README.md",
    "chars": 2116,
    "preview": "# Cloud-Init Files - Critical Format Requirements\n\n## ⚠️ CRITICAL WARNING ⚠️\n\nThe files in this directory have **STRICT "
  },
  {
    "path": "files/cloud-init/base.sh",
    "chars": 797,
    "preview": "#!/bin/sh\nset -eux\n\n# shellcheck disable=SC2230\nwhich sudo || until \\\n  apt-get update -y && \\\n  apt-get install sudo -y"
  },
  {
    "path": "files/cloud-init/base.yml",
    "chars": 1202,
    "preview": "#cloud-config\n# CRITICAL: The above line MUST be exactly \"#cloud-config\" (no space after #)\n# This is required by cloud-"
  },
  {
    "path": "files/cloud-init/sshd_config",
    "chars": 223,
    "preview": "Port {{ ssh_port }}\nAllowGroups algo\nPermitRootLogin no\nPasswordAuthentication no\nChallengeResponseAuthentication no\nUse"
  },
  {
    "path": "input.yml",
    "chars": 6384,
    "preview": "---\n- name: Ask user for the input\n  hosts: localhost\n  tags: always\n  vars:\n    defaults:\n      server_name: algo\n     "
  },
  {
    "path": "install.sh",
    "chars": 4389,
    "preview": "#!/usr/bin/env sh\n\nset -ex\n\nMETHOD=\"${1:-${METHOD:-cloud}}\"\nONDEMAND_CELLULAR=\"${2:-${ONDEMAND_CELLULAR:-false}}\"\nONDEMA"
  },
  {
    "path": "inventory",
    "chars": 78,
    "preview": "[local]\nlocalhost ansible_connection=local ansible_python_interpreter=python3\n"
  },
  {
    "path": "library/gcp_compute_location_info.py",
    "chars": 2682,
    "preview": "#!/usr/bin/python\n\n\nimport json\n\nfrom ansible.module_utils.gcp_utils import GcpModule, GcpSession, navigate_hash\n\n######"
  },
  {
    "path": "library/lightsail_region_facts.py",
    "chars": 2700,
    "preview": "#!/usr/bin/python\n# Copyright: Ansible Project\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/li"
  },
  {
    "path": "library/scaleway_compute.py",
    "chars": 23737,
    "preview": "#!/usr/bin/python\n#\n# Scaleway Compute management module\n#\n# Copyright (C) 2018 Online SAS.\n# https://www.scaleway.com\n#"
  },
  {
    "path": "library/x25519_pubkey.py",
    "chars": 4748,
    "preview": "#!/usr/bin/python\n\n# x25519_pubkey.py - Ansible module to derive a base64-encoded WireGuard-compatible public key\n# from"
  },
  {
    "path": "main.yml",
    "chars": 3512,
    "preview": "---\n- name: Algo VPN Setup\n  hosts: localhost\n  become: false\n  tasks:\n    - name: Playbook dir stat\n      stat:\n       "
  },
  {
    "path": "playbooks/cloud-post.yml",
    "chars": 2442,
    "preview": "---\n- name: Set subjectAltName as a fact\n  set_fact:\n    IP_subject_alt_name: \"{{ (IP_subject_alt_name if algo_provider "
  },
  {
    "path": "playbooks/cloud-pre.yml",
    "chars": 2284,
    "preview": "---\n- block:\n    - name: Display the invocation environment\n      shell: >\n            ./algo-showenv.sh \\\n             "
  },
  {
    "path": "playbooks/rescue.yml",
    "chars": 71,
    "preview": "---\n- debug:\n    var: fail_hint\n\n- name: Fail the installation\n  fail:\n"
  },
  {
    "path": "playbooks/tmpfs/linux.yml",
    "chars": 143,
    "preview": "---\n- name: Linux | set OS specific facts\n  set_fact:\n    tmpfs_volume_name: AlgoVPN-{{ IP_subject_alt_name }}\n    tmpfs"
  },
  {
    "path": "playbooks/tmpfs/macos.yml",
    "chars": 446,
    "preview": "---\n- name: MacOS | set OS specific facts\n  set_fact:\n    tmpfs_volume_name: AlgoVPN-{{ IP_subject_alt_name }}\n    tmpfs"
  },
  {
    "path": "playbooks/tmpfs/main.yml",
    "chars": 486,
    "preview": "---\n- name: Include tasks for MacOS\n  import_tasks: macos.yml\n  when: ansible_system == \"Darwin\"\n\n- name: Include tasks "
  },
  {
    "path": "playbooks/tmpfs/umount.yml",
    "chars": 822,
    "preview": "---\n- name: Linux | Delete the PKI directory\n  file:\n    path: /{{ facts.tmpfs_volume_path }}/{{ facts.tmpfs_volume_name"
  },
  {
    "path": "pyproject.toml",
    "chars": 4267,
    "preview": "[build-system]\nrequires = [\"setuptools>=68.0.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"algo\"\ndescri"
  },
  {
    "path": "pytest.ini",
    "chars": 216,
    "preview": "[pytest]\ntestpaths = tests/unit\npython_files = test_*.py\npython_classes = Test*\npython_functions = test_*\naddopts = -v -"
  },
  {
    "path": "requirements.yml",
    "chars": 418,
    "preview": "---\ncollections:\n  - name: ansible.posix\n    version: \"==2.1.0\"\n  - name: ansible.utils\n    version: \">=4.0.0\"\n  - name:"
  },
  {
    "path": "roles/client/files/libstrongswan-relax-constraints.conf",
    "chars": 57,
    "preview": "libstrongswan {\n  x509 {\n    enforce_critical = no\n  }\n}\n"
  },
  {
    "path": "roles/client/handlers/main.yml",
    "chars": 88,
    "preview": "---\n- name: restart strongswan\n  service: name={{ strongswan_service }} state=restarted\n"
  },
  {
    "path": "roles/client/tasks/main.yml",
    "chars": 2250,
    "preview": "---\n- name: Gather Facts\n  setup:\n- name: Include system based facts and tasks\n  import_tasks: systems/main.yml\n\n- name:"
  },
  {
    "path": "roles/client/tasks/systems/CentOS.yml",
    "chars": 122,
    "preview": "---\n- name: Set OS specific facts\n  set_fact:\n    prerequisites:\n      - epel-release\n    configs_prefix: /etc/strongswa"
  },
  {
    "path": "roles/client/tasks/systems/Debian.yml",
    "chars": 129,
    "preview": "---\n- name: Set OS specific facts\n  set_fact:\n    prerequisites:\n      - libstrongswan-standard-plugins\n    configs_pref"
  },
  {
    "path": "roles/client/tasks/systems/Fedora.yml",
    "chars": 127,
    "preview": "---\n- name: Set OS specific facts\n  set_fact:\n    prerequisites:\n      - libselinux-python\n    configs_prefix: /etc/stro"
  },
  {
    "path": "roles/client/tasks/systems/Ubuntu.yml",
    "chars": 129,
    "preview": "---\n- name: Set OS specific facts\n  set_fact:\n    prerequisites:\n      - libstrongswan-standard-plugins\n    configs_pref"
  },
  {
    "path": "roles/client/tasks/systems/main.yml",
    "chars": 283,
    "preview": "---\n- include_tasks: Debian.yml\n  when: ansible_distribution == 'Debian'\n\n- include_tasks: Ubuntu.yml\n  when: ansible_di"
  },
  {
    "path": "roles/cloud-azure/defaults/main.yml",
    "chars": 8667,
    "preview": "---\n# az account list-locations --query 'sort_by([].{name:name,displayName:displayName,regionalDisplayName:regionalDispl"
  },
  {
    "path": "roles/cloud-azure/files/deployment.json",
    "chars": 6816,
    "preview": "{\n  \"$schema\": \"http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json\",\n  \"contentVersio"
  },
  {
    "path": "roles/cloud-azure/tasks/destroy.yml",
    "chars": 303,
    "preview": "---\n- name: Destroy Azure resource group\n  azure.azcollection.azure_rm_resourcegroup:\n    name: \"{{ algo_server_name }}\""
  },
  {
    "path": "roles/cloud-azure/tasks/main.yml",
    "chars": 1761,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- set_fact:\n    algo_region: >-\n      {%- if region is defined "
  },
  {
    "path": "roles/cloud-azure/tasks/prompts.yml",
    "chars": 940,
    "preview": "---\n- set_fact:\n    secret: \"{{ azure_secret | default(lookup('env', 'AZURE_SECRET'), true) }}\"\n    tenant: \"{{ azure_te"
  },
  {
    "path": "roles/cloud-cloudstack/tasks/destroy.yml",
    "chars": 455,
    "preview": "---\n- environment:\n    CLOUDSTACK_KEY: \"{{ algo_cs_key }}\"\n    CLOUDSTACK_SECRET: \"{{ algo_cs_token }}\"\n    CLOUDSTACK_E"
  },
  {
    "path": "roles/cloud-cloudstack/tasks/main.yml",
    "chars": 2172,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- environment:\n    CLOUDSTACK_KEY: \"{{ algo_cs_key }}\"\n    CLOU"
  },
  {
    "path": "roles/cloud-cloudstack/tasks/prompts.yml",
    "chars": 3000,
    "preview": "---\n- block:\n    - pause:\n        prompt: |\n          Enter the API key (https://trailofbits.github.io/algo/cloud-clouds"
  },
  {
    "path": "roles/cloud-digitalocean/tasks/destroy.yml",
    "chars": 180,
    "preview": "---\n- name: Destroy DigitalOcean droplet\n  digital_ocean_droplet:\n    state: absent\n    name: \"{{ algo_server_name }}\"\n "
  },
  {
    "path": "roles/cloud-digitalocean/tasks/main.yml",
    "chars": 1583,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Upload the SSH key\n  digital_ocean_sshkey:\n    oauth_to"
  },
  {
    "path": "roles/cloud-digitalocean/tasks/prompts.yml",
    "chars": 3740,
    "preview": "---\n- pause:\n    prompt: |\n      Enter your API token. The token must have read and write permissions (https://cloud.dig"
  },
  {
    "path": "roles/cloud-ec2/defaults/main.yml",
    "chars": 143,
    "preview": "---\nencrypted: \"{{ cloud_providers.ec2.encrypted }}\"\nec2_vpc_nets:\n  cidr_block: 172.16.0.0/16\n  subnet_cidr: 172.16.254"
  },
  {
    "path": "roles/cloud-ec2/files/stack.yaml",
    "chars": 5079,
    "preview": "---\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: 'Algo VPN stack'\nParameters:\n  InstanceTypeParameter:\n    Type: "
  },
  {
    "path": "roles/cloud-ec2/tasks/cloudformation.yml",
    "chars": 1038,
    "preview": "---\n# Note: Using template_body instead of deprecated 'template' parameter.\n# The 'template' parameter is deprecated and"
  },
  {
    "path": "roles/cloud-ec2/tasks/destroy.yml",
    "chars": 406,
    "preview": "---\n- name: Set stack name\n  set_fact:\n    stack_name: \"{{ algo_server_name | replace('.', '-') }}\"\n\n- name: Destroy Clo"
  },
  {
    "path": "roles/cloud-ec2/tasks/main.yml",
    "chars": 831,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Locate official AMI for region\n  ec2_ami_info:\n    aws_"
  },
  {
    "path": "roles/cloud-ec2/tasks/prompts.yml",
    "chars": 5535,
    "preview": "---\n# Discover AWS credentials from standard locations\n- name: Set AWS credentials file path\n  set_fact:\n    aws_credent"
  },
  {
    "path": "roles/cloud-gce/tasks/destroy.yml",
    "chars": 1439,
    "preview": "---\n- name: Get zones\n  gcp_compute_location_info:\n    auth_kind: serviceaccount\n    service_account_file: \"{{ credentia"
  },
  {
    "path": "roles/cloud-gce/tasks/main.yml",
    "chars": 2425,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Network configured\n  gcp_compute_network:\n    auth_kind"
  },
  {
    "path": "roles/cloud-gce/tasks/prompts.yml",
    "chars": 2792,
    "preview": "---\n- pause:\n    prompt: |\n      Enter the local path to your credentials JSON file\n      (https://support.google.com/cl"
  },
  {
    "path": "roles/cloud-hetzner/tasks/destroy.yml",
    "chars": 154,
    "preview": "---\n- name: Destroy Hetzner server\n  hetzner.hcloud.server:\n    name: \"{{ algo_server_name }}\"\n    state: absent\n    api"
  },
  {
    "path": "roles/cloud-hetzner/tasks/main.yml",
    "chars": 987,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Create an ssh key\n  hetzner.hcloud.ssh_key:\n    name: a"
  },
  {
    "path": "roles/cloud-hetzner/tasks/prompts.yml",
    "chars": 1583,
    "preview": "---\n- pause:\n    prompt: |\n      Enter your API token (https://trailofbits.github.io/algo/cloud-hetzner.html#api-token):"
  },
  {
    "path": "roles/cloud-lightsail/files/stack.yaml",
    "chars": 1872,
    "preview": "---\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: 'Algo VPN stack (LightSail)'\nParameters:\n  InstanceTypeParameter"
  },
  {
    "path": "roles/cloud-lightsail/tasks/cloudformation.yml",
    "chars": 842,
    "preview": "---\n# Note: Using template_body instead of deprecated 'template' parameter.\n# The 'template' parameter is deprecated and"
  },
  {
    "path": "roles/cloud-lightsail/tasks/destroy.yml",
    "chars": 334,
    "preview": "---\n- name: Set stack name\n  set_fact:\n    stack_name: \"{{ algo_server_name | replace('.', '-') }}\"\n\n- name: Destroy Clo"
  },
  {
    "path": "roles/cloud-lightsail/tasks/main.yml",
    "chars": 277,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Deploy the stack\n  import_tasks: cloudformation.yml\n\n- "
  },
  {
    "path": "roles/cloud-lightsail/tasks/prompts.yml",
    "chars": 2489,
    "preview": "---\n- pause:\n    prompt: |\n      Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-ac"
  },
  {
    "path": "roles/cloud-linode/defaults/main.yml",
    "chars": 60,
    "preview": "---\nlinode_venv: \"{{ playbook_dir }}/configs/.venvs/linode\"\n"
  },
  {
    "path": "roles/cloud-linode/tasks/destroy.yml",
    "chars": 156,
    "preview": "---\n- name: Destroy Linode instance\n  linode.cloud.instance:\n    api_token: \"{{ algo_linode_token }}\"\n    label: \"{{ alg"
  },
  {
    "path": "roles/cloud-linode/tasks/main.yml",
    "chars": 1599,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- name: Set facts\n  set_fact:\n    stackscript: |\n      {{ looku"
  },
  {
    "path": "roles/cloud-linode/tasks/prompts.yml",
    "chars": 1596,
    "preview": "---\n- pause:\n    prompt: |\n      Enter your ACCESS token. (https://developers.linode.com/api/v4/#access-and-authenticati"
  },
  {
    "path": "roles/cloud-openstack/tasks/destroy.yml",
    "chars": 270,
    "preview": "---\n- name: Destroy OpenStack server\n  openstack.cloud.server:\n    state: absent\n    name: \"{{ algo_server_name }}\"\n\n- n"
  },
  {
    "path": "roles/cloud-openstack/tasks/main.yml",
    "chars": 2749,
    "preview": "---\n- fail:\n    msg: >-\n      OpenStack credentials are not set. Download it from the OpenStack dashboard->Compute->API "
  },
  {
    "path": "roles/cloud-scaleway/defaults/main.yml",
    "chars": 54,
    "preview": "---\nscaleway_regions:\n  - alias: par1\n  - alias: ams1\n"
  },
  {
    "path": "roles/cloud-scaleway/tasks/destroy.yml",
    "chars": 228,
    "preview": "---\n- environment:\n    SCW_TOKEN: \"{{ algo_scaleway_token }}\"\n  block:\n    - name: Destroy Scaleway server\n      scalewa"
  },
  {
    "path": "roles/cloud-scaleway/tasks/main.yml",
    "chars": 2514,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- environment:\n    SCW_TOKEN: \"{{ algo_scaleway_token }}\"\n  blo"
  },
  {
    "path": "roles/cloud-scaleway/tasks/prompts.yml",
    "chars": 1724,
    "preview": "---\n- pause:\n    prompt: |\n      Enter your auth token (https://trailofbits.github.io/algo/cloud-scaleway.html)\n    echo"
  },
  {
    "path": "roles/cloud-vultr/tasks/destroy.yml",
    "chars": 440,
    "preview": "---\n- environment:\n    VULTR_API_KEY: \"{{ lookup('ini', 'key', section='default', file=algo_vultr_config) }}\"\n  block:\n "
  },
  {
    "path": "roles/cloud-vultr/tasks/main.yml",
    "chars": 2196,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n\n- environment:\n    VULTR_API_KEY: \"{{ lookup('ini', 'key', sect"
  },
  {
    "path": "roles/cloud-vultr/tasks/prompts.yml",
    "chars": 2021,
    "preview": "---\n- pause:\n    prompt: |\n      Enter the local path to your configuration INI file\n      (https://trailofbits.github.i"
  },
  {
    "path": "roles/common/defaults/main.yml",
    "chars": 432,
    "preview": "---\ninstall_headers: false\naip_supported_providers:\n  - digitalocean\nsnat_aipv4: false\nipv6_default: \"{{ ansible_default"
  },
  {
    "path": "roles/common/handlers/main.yml",
    "chars": 527,
    "preview": "---\n- name: restart rsyslog\n  service: name=rsyslog state=restarted\n\n- name: flush routing cache\n  shell: echo 1 > /proc"
  },
  {
    "path": "roles/common/tasks/aip/digitalocean.yml",
    "chars": 557,
    "preview": "---\n- name: Get the anchor IP\n  uri:\n    url: http://169.254.169.254/metadata/v1/interfaces/public/0/anchor_ipv4/address"
  },
  {
    "path": "roles/common/tasks/aip/main.yml",
    "chars": 580,
    "preview": "---\n- name: Verify the provider\n  assert:\n    that: algo_provider in aip_supported_providers\n    msg: Algo does not supp"
  },
  {
    "path": "roles/common/tasks/aip/placeholder.yml",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "roles/common/tasks/facts.yml",
    "chars": 1098,
    "preview": "---\n- name: Set OS platform facts\n  set_fact:\n    is_debian_based: \"{{ ansible_distribution in ['Debian', 'Ubuntu'] }}\"\n"
  },
  {
    "path": "roles/common/tasks/iptables.yml",
    "chars": 515,
    "preview": "---\n- name: Iptables configured\n  template:\n    src: \"{{ item.src }}\"\n    dest: \"{{ item.dest }}\"\n    owner: root\n    gr"
  },
  {
    "path": "roles/common/tasks/main.yml",
    "chars": 608,
    "preview": "---\n- name: Check the system\n  raw: uname -a\n  register: OS\n  changed_when: false\n  tags:\n    - update-users\n\n- fail:\n  "
  },
  {
    "path": "roles/common/tasks/packages.yml",
    "chars": 2261,
    "preview": "---\n- name: Initialize package lists\n  set_fact:\n    algo_packages: \"{{ tools | default([]) if not performance_preinstal"
  },
  {
    "path": "roles/common/tasks/ubuntu.yml",
    "chars": 5277,
    "preview": "---\n- name: Gather facts\n  setup:\n- name: Cloud only tasks\n  when: algo_provider != \"local\"\n  block:\n    - name: Install"
  },
  {
    "path": "roles/common/tasks/unattended-upgrades.yml",
    "chars": 445,
    "preview": "---\n- name: Install unattended-upgrades\n  apt:\n    name: unattended-upgrades\n    state: present\n\n- name: Configure unatt"
  },
  {
    "path": "roles/common/templates/10-algo-lo100.network.j2",
    "chars": 117,
    "preview": "[Match]\nName=lo\n\n[Network]\nDescription=lo:100\nAddress={{ local_service_ip }}/32\nAddress={{ local_service_ipv6 }}/128\n"
  },
  {
    "path": "roles/common/templates/10periodic.j2",
    "chars": 168,
    "preview": "APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInter"
  },
  {
    "path": "roles/common/templates/50unattended-upgrades.j2",
    "chars": 3854,
    "preview": "// Automatically upgrade packages from these (origin:archive) pairs\n//\n// Note that in Ubuntu security updates may pull "
  },
  {
    "path": "roles/common/templates/99-algo-ipv6-egress.yaml.j2",
    "chars": 145,
    "preview": "network:\n    version: 2\n    ethernets:\n        {{ ansible_default_ipv6.interface }}:\n            addresses:\n            "
  },
  {
    "path": "roles/common/templates/rules.v4.j2",
    "chars": 5120,
    "preview": "{% set subnets = ([strongswan_network] if ipsec_enabled | bool else []) + ([wireguard_network_ipv4] if wireguard_enabled"
  },
  {
    "path": "roles/common/templates/rules.v6.j2",
    "chars": 6138,
    "preview": "{% set subnets = ([strongswan_network_ipv6] if ipsec_enabled | bool else []) + ([wireguard_network_ipv6] if wireguard_en"
  },
  {
    "path": "roles/dns/defaults/main.yml",
    "chars": 168,
    "preview": "---\nalgo_dns_adblocking: false\napparmor_enabled: true\ndns_encryption: true\nipv6_support: false\ndnscrypt_servers:\n  ipv4:"
  },
  {
    "path": "roles/dns/files/50-dnscrypt-proxy-unattended-upgrades",
    "chars": 166,
    "preview": "// Automatically upgrade packages from these (origin:archive) pairs\nUnattended-Upgrade::Allowed-Origins {\n    \"LP-PPA-sh"
  },
  {
    "path": "roles/dns/files/apparmor.profile.dnscrypt-proxy",
    "chars": 737,
    "preview": "#include <tunables/global>\n\n/usr/{s,}bin/dnscrypt-proxy flags=(attach_disconnected) {\n  #include <abstractions/base>\n  #"
  },
  {
    "path": "roles/dns/handlers/main.yml",
    "chars": 371,
    "preview": "---\n- name: daemon-reload\n  systemd:\n    daemon_reload: true\n\n- name: restart dnscrypt-proxy.socket\n  systemd:\n    name:"
  },
  {
    "path": "roles/dns/tasks/dns_adblocking.yml",
    "chars": 488,
    "preview": "---\n- name: Adblock script created\n  template:\n    src: adblock.sh.j2\n    dest: /usr/local/sbin/adblock.sh\n    owner: ro"
  },
  {
    "path": "roles/dns/tasks/main.yml",
    "chars": 999,
    "preview": "---\n- name: Include tasks for Debian/Ubuntu\n  include_tasks: ubuntu.yml\n  when: is_debian_based | bool\n\n- name: dnscrypt"
  },
  {
    "path": "roles/dns/tasks/ubuntu.yml",
    "chars": 4355,
    "preview": "---\n- when: ansible_facts['distribution_version'] is version('20.04', '<')\n  block:\n    - name: Add the repository\n     "
  },
  {
    "path": "roles/dns/templates/adblock.sh.j2",
    "chars": 1373,
    "preview": "#!/bin/sh\n# Block ads, malware, etc..\n\nTEMP=\"$(mktemp)\"\nTEMP_SORTED=\"$(mktemp)\"\nWHITELIST=\"/etc/dnscrypt-proxy/white.lis"
  },
  {
    "path": "roles/dns/templates/dnscrypt-proxy/cache.toml.j2",
    "chars": 444,
    "preview": "###########################\n#        DNS cache        #\n###########################\n\n## Enable a DNS cache to reduce lat"
  },
  {
    "path": "roles/dns/templates/dnscrypt-proxy/filters.toml.j2",
    "chars": 1495,
    "preview": "#########################\n#        Filters        #\n#########################\n\n## Immediately respond to IPv6-related qu"
  },
  {
    "path": "roles/dns/templates/dnscrypt-proxy/global.toml.j2",
    "chars": 2687,
    "preview": "##################################\n#         Global settings        #\n##################################\n\n## List of ser"
  },
  {
    "path": "roles/dns/templates/dnscrypt-proxy/sources.toml.j2",
    "chars": 785,
    "preview": "#########################\n#        Servers        #\n#########################\n\n## Remote lists of available servers\n[sou"
  },
  {
    "path": "roles/dns/templates/dnscrypt-proxy.toml.j2",
    "chars": 480,
    "preview": "##############################################\n#                                            #\n#        dnscrypt-proxy co"
  },
  {
    "path": "roles/dns/templates/ip-blacklist.txt.j2",
    "chars": 529,
    "preview": "0.0.0.0\n10.*\n127.*\n169.254.*\n172.16.*\n172.17.*\n172.18.*\n172.19.*\n172.20.*\n172.21.*\n172.22.*\n172.23.*\n172.24.*\n172.25.*\n1"
  },
  {
    "path": "roles/local/tasks/main.yml",
    "chars": 56,
    "preview": "---\n- name: Include prompts\n  import_tasks: prompts.yml\n"
  },
  {
    "path": "roles/local/tasks/prompts.yml",
    "chars": 2766,
    "preview": "---\n- name: Display local installation warning\n  pause:\n    prompt: |\n\n      ==========================================="
  },
  {
    "path": "roles/privacy/README.md",
    "chars": 6543,
    "preview": "# Privacy Enhancements Role\n\nThis Ansible role implements additional privacy enhancements for Algo VPN to minimize serve"
  },
  {
    "path": "roles/privacy/defaults/main.yml",
    "chars": 1659,
    "preview": "---\n# Privacy enhancement configuration defaults\n# These settings can be overridden in config.cfg\n\n# Enable privacy enha"
  },
  {
    "path": "roles/privacy/handlers/main.yml",
    "chars": 555,
    "preview": "---\n# Privacy role handlers\n# These handlers are triggered by privacy configuration changes\n\n- name: restart rsyslog\n  s"
  },
  {
    "path": "roles/privacy/tasks/advanced_privacy.yml",
    "chars": 2540,
    "preview": "---\n# Advanced privacy settings for enhanced anonymity\n\n- name: Reduce kernel log verbosity for privacy\n  sysctl:\n    na"
  },
  {
    "path": "roles/privacy/tasks/auto_cleanup.yml",
    "chars": 2064,
    "preview": "---\n# Automatic cleanup tasks for enhanced privacy\n\n- name: Create privacy cleanup script\n  template:\n    src: privacy-a"
  },
  {
    "path": "roles/privacy/tasks/clear_history.yml",
    "chars": 1597,
    "preview": "---\n# Clear command history and disable persistent history for privacy\n\n- name: Clear bash history for all users\n  shell"
  },
  {
    "path": "roles/privacy/tasks/log_filtering.yml",
    "chars": 1672,
    "preview": "---\n# Configure rsyslog to filter out VPN-related logs for privacy\n\n- name: Create rsyslog privacy configuration directo"
  },
  {
    "path": "roles/privacy/tasks/log_rotation.yml",
    "chars": 1668,
    "preview": "---\n# Aggressive log rotation configuration for privacy\n# Reduces log retention time and implements more frequent rotati"
  },
  {
    "path": "roles/privacy/tasks/main.yml",
    "chars": 1089,
    "preview": "---\n# Privacy enhancements for Algo VPN\n# This role implements additional privacy measures to reduce log retention\n# and"
  },
  {
    "path": "roles/privacy/templates/46-privacy-ssh-filter.conf.j2",
    "chars": 558,
    "preview": "# Privacy-enhanced SSH log filtering\n# Filters successful SSH connections while keeping failures for security\n# Generate"
  },
  {
    "path": "roles/privacy/templates/47-privacy-auth-filter.conf.j2",
    "chars": 643,
    "preview": "# Privacy-enhanced authentication log filtering\n# WARNING: Use with caution - this reduces security logging\n# Only enabl"
  },
  {
    "path": "roles/privacy/templates/48-privacy-kernel-filter.conf.j2",
    "chars": 644,
    "preview": "# Privacy-enhanced kernel log filtering\n# Filters kernel messages that may reveal VPN usage patterns\n# Generated by Algo"
  },
  {
    "path": "roles/privacy/templates/49-privacy-vpn-filter.conf.j2",
    "chars": 997,
    "preview": "# Privacy-enhanced rsyslog configuration\n# Filters VPN-related log entries for enhanced privacy\n# Generated by Algo VPN "
  },
  {
    "path": "roles/privacy/templates/auth-logrotate.j2",
    "chars": 568,
    "preview": "# Privacy-enhanced auth log rotation\n# Reduces retention time for authentication logs\n# Generated by Algo VPN privacy ro"
  },
  {
    "path": "roles/privacy/templates/clear-history-on-logout.sh.j2",
    "chars": 778,
    "preview": "#!/bin/bash\n# Privacy-enhanced history clearing on logout\n# This script clears command history when users log out\n# Gene"
  },
  {
    "path": "roles/privacy/templates/kern-logrotate.j2",
    "chars": 1037,
    "preview": "# Privacy-enhanced kernel log rotation\n# Reduces retention time for kernel logs that may contain VPN traces\n# Generated "
  },
  {
    "path": "roles/privacy/templates/privacy-auto-cleanup.sh.j2",
    "chars": 2570,
    "preview": "#!/bin/bash\n# Privacy auto-cleanup script\n# Automatically cleans up logs and temporary files for enhanced privacy\n# Gene"
  },
  {
    "path": "roles/privacy/templates/privacy-log-cleanup.sh.j2",
    "chars": 932,
    "preview": "#!/bin/bash\n# Privacy log cleanup script\n# Immediately cleans up existing logs and applies privacy settings\n# Generated "
  },
  {
    "path": "roles/privacy/templates/privacy-logrotate.j2",
    "chars": 1308,
    "preview": "# Privacy-enhanced logrotate configuration\n# This configuration enforces aggressive log rotation for privacy\n# Generated"
  },
  {
    "path": "roles/privacy/templates/privacy-monitor.sh.j2",
    "chars": 2790,
    "preview": "#!/bin/bash\n# Privacy monitoring script\n# Monitors and reports on privacy settings status\n# Generated by Algo VPN privac"
  },
  {
    "path": "roles/privacy/templates/privacy-rsyslog.conf.j2",
    "chars": 1039,
    "preview": "# Privacy-enhanced rsyslog configuration\n# Minimal logging configuration for enhanced privacy\n# Generated by Algo VPN pr"
  },
  {
    "path": "roles/privacy/templates/privacy-shutdown-cleanup.service.j2",
    "chars": 1159,
    "preview": "# Privacy shutdown cleanup systemd service\n# Clears logs and sensitive data on system shutdown\n# Generated by Algo VPN p"
  },
  {
    "path": "roles/ssh_tunneling/defaults/main.yml",
    "chars": 75,
    "preview": "---\nssh_tunnels_config_path: configs/{{ IP_subject_alt_name }}/ssh-tunnel/\n"
  },
  {
    "path": "roles/ssh_tunneling/handlers/main.yml",
    "chars": 98,
    "preview": "---\n- name: restart ssh\n  service: name=\"{{ ssh_service_name | default('ssh') }}\" state=restarted\n"
  },
  {
    "path": "roles/ssh_tunneling/tasks/main.yml",
    "chars": 3357,
    "preview": "---\n- name: Ensure that the sshd_config file has desired options\n  blockinfile:\n    dest: /etc/ssh/sshd_config\n    marke"
  },
  {
    "path": "roles/ssh_tunneling/templates/ssh_config.j2",
    "chars": 187,
    "preview": "Host algo\n  DynamicForward 127.0.0.1:1080\n  LogLevel quiet\n  Compression yes\n  IdentitiesOnly yes\n  IdentityFile {{ item"
  },
  {
    "path": "roles/strongswan/defaults/main.yml",
    "chars": 2991,
    "preview": "---\nipsec_config_path: configs/{{ IP_subject_alt_name }}/ipsec\nipsec_pki_path: \"{{ ipsec_config_path }}/.pki\"\nstrongswan"
  },
  {
    "path": "roles/strongswan/handlers/main.yml",
    "chars": 1081,
    "preview": "---\n- name: restart strongswan\n  service: name={{ strongswan_service }} state=restarted\n\n- name: daemon-reload\n  systemd"
  },
  {
    "path": "roles/strongswan/meta/main.yml",
    "chars": 4,
    "preview": "---\n"
  }
]

// ... and 135 more files (download for full content)

About this extraction

This page contains the full source code of the trailofbits/algo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 335 files (946.0 KB), approximately 263.6k tokens, and a symbol index with 247 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!