diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 83c2fca0..837e01ec 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,6 @@ # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - - package-ecosystem: "gitsubmodule" - directory: "/" - schedule: - interval: "weekly" - package-ecosystem: "gomod" directory: "/" schedule: diff --git a/.github/workflows/docs-gen.yml b/.github/workflows/docs-gen.yml new file mode 100644 index 00000000..7a5cb635 --- /dev/null +++ b/.github/workflows/docs-gen.yml @@ -0,0 +1,33 @@ +name: Docs reference drift + +on: + # Narrowed to master: an unfiltered `push` also fires on every topic branch + # and every tag, duplicating the run each pull request already performs. + push: + branches: [ master ] + pull_request: + +jobs: + docs-gen: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: 1.26 + - name: Regenerate the reference + run: make docs-gen + - name: Fail if the generated reference is stale + # The output is staged before diffing because `git diff --exit-code` + # ignores untracked paths: a brand-new command makes the generator emit + # a new cli/.md, and an unstaged diff would pass while that page + # went uncommitted. Diffing the index catches additions alongside + # modifications and deletions. + run: | + git add -A docs/content/en/docs/reference/ + if ! git diff --cached --exit-code docs/content/en/docs/reference/; then + echo "::error::The generated CLI reference is out of date. Run 'make docs-gen' and commit the result." + exit 1 + fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 00203b2d..d8ff2f50 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,6 +2,11 @@ name: Github Pages on: push: branches: [ master ] + # Pull requests build the docs so breakage is caught before merge. They must + # never deploy: the Deploy step below is guarded on a push to master. + pull_request: + paths: + - 'docs/**' jobs: build: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 7d0d0d43..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/themes/docsy"] - path = docs/themes/docsy - url = https://github.com/google/docsy.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fba0ce4e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing to EdgeVPN + +Thanks for wanting to help. This page is the short version; the full +documentation lives at . + +## Build + +EdgeVPN ships as a single binary, but the web interface is a React application +compiled into it, so you need Go 1.26 (the version in `go.mod`) **and** Node.js +20.19 or newer: + +```bash +make build # compiles the web interface, then the Go binary +make react-ui-force # force a clean rebuild of the interface +``` + +`go build` on its own works only when `api/react-ui/dist` already exists — the +interface is embedded with `//go:embed`, so a missing directory is a compile +error. Working on the Go side only? Stub it: + +```bash +mkdir -p api/react-ui/dist && touch api/react-ui/dist/index.html +``` + +Building the **documentation site** additionally needs Hugo and Node/npm — +`docs/scripts/build.sh` downloads the pinned Hugo (see `docs/Makefile`) and +installs `postcss-cli` and `autoprefixer` for you: + +```bash +cd docs && make build # one-off build into docs/public +cd docs && make serve # live preview on http://localhost:1313 +``` + +## Test + +```bash +make test # go test ./... +``` + +The end-to-end suites that CI runs (VPN connectivity, services, file transfer) +are the scripts under `.github/`; see `.github/workflows/test.yml` for how they +are invoked. + +## Adding or changing a CLI flag + +The CLI and environment-variable reference under +`docs/content/en/docs/reference/` is **generated** from the real `cli.App` — do +not hand-edit those pages. After touching any flag or command, run: + +```bash +make docs-gen +``` + +and commit the result. The `Docs reference drift` workflow regenerates the +reference on every push and pull request and fails if the committed output +differs, so a forgotten `make docs-gen` will turn CI red. + +## Issues and pull requests + +- Open issues and feature requests at + . +- Questions and general discussion belong in + [GitHub Discussions](https://github.com/mudler/edgevpn/discussions) or the + [Matrix room](https://matrix.to/#/#edgevpn:matrix.org). +- Pull requests go against `master`. Please make sure `make test` and + `make docs-gen` are clean before asking for review, and mark work in progress + with a draft PR or a `WIP` prefix. + +Docs-only changes have their own walkthrough (including the *Edit this page* +shortcut) at +. + +## License + +EdgeVPN is Apache 2.0 licensed. By contributing you agree that your +contributions are licensed under the same terms. diff --git a/Makefile b/Makefile index c3abdd6a..98e16f6c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build react-ui react-ui-force test clean +.PHONY: all build react-ui react-ui-force test clean docs-gen all: build @@ -25,5 +25,11 @@ build: api/react-ui/dist test: api/react-ui/dist go test ./... +# docs-gen regenerates the CLI and environment-variable reference from the real +# cli.App. The output is committed; CI re-runs this and fails on a diff, so the +# docs cannot drift from the binary. +docs-gen: + go run ./internal/docsgen + clean: rm -rf api/react-ui/dist edgevpn diff --git a/README.md b/README.md index 62818915..9d541ed0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ It can: - **Create a VPN** : Secure VPN between p2p peers - Automatically assign IPs to nodes - Embedded tiny DNS server to resolve internal/external IPs - - Create trusted zones to prevent network access if token is leaked + - Create trusted zones to restrict which peers may join (experimental: it does not currently stop a token holder from entering the zone — see the [security model](https://mudler.github.io/edgevpn/docs/explanation/security-model/)) - For example, the [Kairos](https://github.com/kairos-io/kairos) CNCF project uses it as a layer for creating decentralized clusters with Kubernetes - **Act as a reverse Proxy** : Share a tcp service like you would do with `ngrok`. EdgeVPN let expose TCP services to the p2p network nodes without establishing a VPN connection: creates reverse proxy and tunnels traffic into the p2p network. @@ -75,6 +75,14 @@ Check out [Kairos](https://github.com/kairos-io/kairos) for seeing EdgeVPN in ac Download the precompiled static release in the [releases page](https://github.com/mudler/edgevpn/releases). You can either install it in your system or just run it. +Or install the latest release with the one-liner: + +```bash +curl -sfL https://raw.githubusercontent.com/mudler/edgevpn/master/install.sh | sh +``` + +Every installation route — install script, release archives, Homebrew, the container image and building from source — is covered in [Install EdgeVPN](https://mudler.github.io/edgevpn/docs/tutorials/install/). + # :hammer: Building from source The web UI is a React application compiled into the binary, so a Node @@ -149,18 +157,7 @@ $ EDGEVPNTOKEN=.. edgevpn --address 10.1.0.13/24 # :question: Is it for me? -EdgeVPN makes VPN decentralization a first strong requirement. - -Its main use is for edge and low-end devices and especially for development. - -The decentralized approach has few cons: - -- The underlying network is chatty. It uses a Gossip protocol for synchronizing the routing table and p2p. Every blockchain message is broadcasted to all peers, while the traffic is to the host only. -- Might be not suited for low latency workload. - -Keep that in mind before using it for your prod networks! - -But it has a strong pro: it just works everywhere libp2p works! +The decentralized approach is chatty and might not suit low-latency workloads, and this software has not been security audited. Read [when not to use EdgeVPN](https://mudler.github.io/edgevpn/docs/explanation/when-not-to-use-edgevpn/) before you rely on it. # :question: Why? @@ -168,71 +165,12 @@ First of all it's my first experiment with libp2p. Second, I always wanted a mor # :warning: Warning! -I'm not a security expert, and this software didn't went through a full security audit, so don't use and rely on it for sensible traffic and not even for production environment! I did this mostly for fun while I was experimenting with libp2p. - -## Example use case: network-decentralized [k3s](https://github.com/k3s-io/k3s) test cluster - -Let's see a practical example, you are developing something for kubernetes and you want to try a multi-node setup, but you have machines available that are only behind NAT (pity!) and you would really like to leverage HW. - -If you are not really interested in network performance (again, that's for development purposes only!) then you could use `edgevpn` + [k3s](https://github.com/k3s-io/k3s) in this way: - -1) Generate edgevpn config: `edgevpn -g > vpn.yaml` -2) Start the vpn: - - on node A: `sudo IFACE=edgevpn0 ADDRESS=10.1.0.3/24 EDGEVPNCONFIG=vpn.yml edgevpn` - - on node B: `sudo IFACE=edgevpn0 ADDRESS=10.1.0.4/24 EDGEVPNCONFIG=vpm.yml edgevpn` -3) Start k3s: - - on node A: `k3s server --flannel-iface=edgevpn0` - - on node B: `K3S_URL=https://10.1.0.3:6443 K3S_TOKEN=xx k3s agent --flannel-iface=edgevpn0 --node-ip 10.1.0.4` - -We have used flannel here, but other CNI should work as well. - - -# :notebook: As a library - -EdgeVPN can be used as a library. It is very portable and offers a functional interface. - -To join a node in a network from a token, without starting the vpn: - -```golang +This software has not been through a full security audit — don't rely on it for sensitive traffic or production environments. The full caveats are in [when not to use EdgeVPN](https://mudler.github.io/edgevpn/docs/explanation/when-not-to-use-edgevpn/). -import ( - node "github.com/mudler/edgevpn/pkg/node" -) +# :books: Examples -e := node.New( - node.Logger(l), - node.LogLevel(log.LevelInfo), - node.MaxMessageSize(2 << 20), - node.FromBase64( mDNSEnabled, DHTEnabled, token ), - // .... - ) - -e.Start(ctx) - -``` - -or to start a VPN: - -```golang - -import ( - vpn "github.com/mudler/edgevpn/pkg/vpn" - node "github.com/mudler/edgevpn/pkg/node" -) - -opts, err := vpn.Register(vpnOpts...) -if err != nil { - return err -} - -e := edgevpn.New(append(o, opts...)...) - -e.Start(ctx) -``` +- [A network-decentralized k3s test cluster](https://mudler.github.io/edgevpn/docs/tutorials/decentralized-k3s-cluster/) — a multi-node Kubernetes development cluster across machines behind NAT. +- [Use EdgeVPN as a library](https://mudler.github.io/edgevpn/docs/how-to/use-as-a-library/) — embed a node in your own Go program. # 🧑‍💻 Projects using EdgeVPN @@ -259,23 +197,7 @@ and any other way if not mentioned here. # :notebook: Troubleshooting -If during bootstrap you see messages like: - -``` -edgevpn[3679]: * [/ip4/104.131.131.82/tcp/4001] failed to negotiate stream multiplexer: context deadline exceeded -``` - -or - -``` -edgevpn[9971]: 2021/12/16 20:56:34 failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size for details. -``` - -or generally experiencing poor network performance, it is recommended to increase the maximum buffer size by running: - -``` -sysctl -w net.core.rmem_max=2500000 -``` +Bootstrap failures, receive-buffer warnings and poor network performance are covered in [Troubleshooting](https://mudler.github.io/edgevpn/docs/troubleshooting/). # :notebook: TODO diff --git a/docs/config.toml b/docs/config.toml index d3d9a0d9..756845e4 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -1,4 +1,7 @@ -baseURL = "https://mudler.github.io/edgevpn/docs/" +# Must match the -b value passed by scripts/build.sh, which is what production +# is actually built with. The site root is https://mudler.github.io/edgevpn/; +# /docs/ is a content section inside it, not the base. +baseURL = "https://mudler.github.io/edgevpn/" title = "EdgeVPN" enableRobotsTXT = true @@ -42,10 +45,9 @@ resampleFilter = "CatmullRom" quality = 75 anchor = "smart" -[services] -[services.googleAnalytics] -# Comment out the next line to disable GA tracking. Also disables the feature described in [params.ui.feedback]. -id = "UA-00000000-0" +# No analytics property is configured. The Docsy template ships a placeholder +# "UA-00000000-0" here; leaving it in place sent page views and the +# [params.ui.feedback] events to a property that does not exist. # Language configuration @@ -118,8 +120,9 @@ offlineSearch = true [params.ui] # Enable to show the side bar menu in its compact state. sidebar_menu_compact = false -# Set to true to disable breadcrumb navigation. -breadcrumb_disable = true +# Set to true to disable breadcrumb navigation. The docs tree is three levels +# deep (docs / section / page), so breadcrumbs earn their keep. +breadcrumb_disable = false # Set to true to hide the sidebar search box (the top nav search box will still be displayed if search is enabled) sidebar_search_disable = false # Set to false if you don't want to display a logo (/assets/icons/logo.svg) in the top nav bar @@ -131,8 +134,11 @@ footer_about_disable = false # This feature depends on [services.googleAnalytics] and will be disabled if "services.googleAnalytics.id" is not set. # If you want this feature, but occasionally need to remove the "Feedback" section from a single page, # add "hide_feedback: true" to the page's front matter. +# Disabled along with the analytics property above: the responses are delivered +# as Google Analytics events, so with no property configured the widget would +# only collect clicks and drop them. Re-enable once a GA4 property exists. [params.ui.feedback] -enable = true +enable = false # The responses that the user sees after clicking "yes" (the page was helpful) or "no" (the page was not helpful). yes = 'Glad to hear it! Please tell us how we can improve.' no = 'Sorry to hear that. Please tell us how we can improve.' diff --git a/docs/content/en/_index.html b/docs/content/en/_index.html index b5208e27..db157815 100644 --- a/docs/content/en/_index.html +++ b/docs/content/en/_index.html @@ -71,10 +71,10 @@

- }}/getting-started/api/"> + }}"> WebUI - }}/getting-started/gui/">GUI + }}">GUI
Keep an eye on your network with the Web UI.
Connect easily from your workstation with the frontend GUI app. diff --git a/docs/content/en/community/_index.md b/docs/content/en/community/_index.md deleted file mode 100644 index cdade163..00000000 --- a/docs/content/en/community/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Community -menu: - main: - weight: 40 ---- - - diff --git a/docs/content/en/docs/Concepts/Architecture/_index.md b/docs/content/en/docs/Concepts/Architecture/_index.md deleted file mode 100644 index ad583d91..00000000 --- a/docs/content/en/docs/Concepts/Architecture/_index.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "Architecture" -linkTitle: "Architecture" -weight: 2 -description: > - EdgeVPN internal architecture -resources: -- src: "**edgevpn_*.png" ---- - -## Introduction - -EdgeVPN uses [libp2p](https://github.com/libp2p/go-libp2p) to establish a decentralized, asymmetrically encrypted gossip network which propagate a (symmetrically encrypted) blockchain states between nodes. - -The blockchain is lightweight as: -- There is no PoW mechanism -- It is in memory only, no DAG, CARv2, or GraphSync protocol - the usage is restricted to hold metadata, and not real addressable content - -EdgeVPN uses the blockchain to store Services UUID, Files UUID, VPN and other metadata (such as DNS records, IP, etc.) and co-ordinate events between the nodes of the network. Besides, it is used as a mechanism of protection: if nodes are not part of the blockchain, they can't talk to each other. - -The blockchain is ephemeral and on-memory, optionally can be stored on disk. - -Each node keeps broadcasting it's state until it is reconciled in the blockchain. If the blockchain would get start from scratch, the hosts would re-announce and try to fill the blockchain with their data. - - -- Simple (KISS) interface to display network data from the blockchain -- asymmetric p2p encryption between peers with libp2p -- randezvous points dynamically generated from OTP keys -- extra AES symmetric encryption on top. In case rendezvous point is compromised -- blockchain is used as a sealed encrypted store for the routing table -- connections are created host to host and encrypted asymmetrically - -### Connection bootstrap - -Network is bootstrapped with libp2p and is composed of 3 phases: - -{{< imgproc edevpn_bootstrap.png Fit "1200x550" >}} -{{< /imgproc >}} - -In the first phase, nodes do discover each others via DHT and a rendezvous secret which is automatically generated via OTP. - -Once peers know about each other a gossip network is established, where the nodes exchange a blockchain over an p2p e2e encrypted channel. The blockchain is sealed with a symmetric key which is rotated via OTP that is shared between the nodes. - -At that point a blockchain and an API is established between the nodes, and optionally start the VPN binding on the tun/tap device. diff --git a/docs/content/en/docs/Concepts/_index.md b/docs/content/en/docs/Concepts/_index.md deleted file mode 100644 index 8874472e..00000000 --- a/docs/content/en/docs/Concepts/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: "Concepts" -linkTitle: "Concepts" -weight: 20 -description: > - Expore EdgeVPN functionalities by looking at practical use-cases ---- \ No newline at end of file diff --git a/docs/content/en/docs/Getting started/_index.md b/docs/content/en/docs/Getting started/_index.md deleted file mode 100644 index b937dc22..00000000 --- a/docs/content/en/docs/Getting started/_index.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "Getting Started" -linkTitle: "Getting Started" -weight: 1 -description: > - First steps with EdgeVPN ---- - -## Get EdgeVPN - -Prerequisites: No dependencies. EdgeVPN releases are statically compiled. - -### From release - -Just grab a release from [the release page on GitHub](https://github.com/mudler/edgevpn/releases). The binaries are statically compiled. - -### Via Homebrew on Macos - -If you're using homebrew in MacOS, you can use the [edgevpn formula](https://formulae.brew.sh/formula/edgevpn) - -``` -brew install edgevpn -``` - - -### Building EdgeVPN from source - -Requirements: - -- [Golang](https://golang.org/) installed in your system. -- [Node.js](https://nodejs.org/) 20.19 or newer. The web interface is a React application which is compiled and embedded into the binary. -- make - -```bash -$> git clone https://github.com/mudler/edgevpn -$> cd edgevpn -$> make build -``` - -`make build` compiles the web interface first, and the Go binary afterwards. Running `go build` on its own works only if `api/react-ui/dist` already exists: the web interface is embedded with `//go:embed`, so a missing directory is a compile error. If you are working on the Go side only and don't need the web interface, you can stub it out: - -```bash -$> mkdir -p api/react-ui/dist && touch api/react-ui/dist/index.html -``` - -### Using Docker Compose - -Using docker is still experimental as setups can vary wildly. -An example [docker-compose.yml](https://github.com/mudler/edgevpn/blob/master/docker-compose.yml) file is provided for convenience but you'll likely need to edit it. - -```bash -$> git clone https://github.com/mudler/edgevpn -$> cd edgevpn -$> sudo docker compose up --detach -``` - -## Creating Your First VPN - -Let's create our first vpn now and start it: - -```bash -$> EDGEVPNTOKEN=$(edgevpn -b -g) -$> edgevpn --dhcp --api -``` - -That's it! - -You can now access the web interface on [http://localhost:8080](http://localhost:8080). - -To join new nodes in the network, simply copy the `EDGEVPNTOKEN` and use it to start edgevpn in other nodes: - -```bash -$> EDGEVPNTOKEN= edgevpn --dhcp -``` diff --git a/docs/content/en/docs/Getting started/api.md b/docs/content/en/docs/Getting started/api.md deleted file mode 100644 index 0d7d2f94..00000000 --- a/docs/content/en/docs/Getting started/api.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: "WebUI and API" -linkTitle: "WebUI and API" -weight: 1 -description: > - Query the network status and operate the ledger with the built-in API ---- - -The API has a simple webUI embedded to display network informations. - - -To access the web interface, run in the console: - -```bash -$ edgevpn api -``` - -with either a `EDGEVPNCONFIG` or `EDGEVPNTOKEN`. - -Dashboard (Dark mode) | Dashboard (Light mode) -:-------------------------:|:-------------------------: -![Screenshot 2021-10-31 at 00-12-16 EdgeVPN - Machines index](https://user-images.githubusercontent.com/2420543/163020448-8e9238c1-3b6d-435d-9b25-7729d8779ebd.png) | ![Screenshot 2021-10-31 at 23-03-26 EdgeVPN - Machines index](https://user-images.githubusercontent.com/2420543/163020460-e18c07d7-8426-4992-aab3-0b2fd90279ae.png) - -DNS | Machine index -:-------------------------:|:-------------------------: -![Screenshot 2021-10-31 at 23-03-44 EdgeVPN - Services index](https://user-images.githubusercontent.com/2420543/163020465-3d481da4-4912-445e-afc0-2614966dcadf.png) | ![Screenshot 2021-10-31 at 23-03-59 EdgeVPN - Files index](https://user-images.githubusercontent.com/2420543/163020462-7821a622-8c13-4971-8abe-9c5b6b491ae8.png) - -Services | Blockchain index -:-------------------------:|:-------------------------: -![Screenshot 2021-10-31 at 23-04-12 EdgeVPN - Users connected](https://user-images.githubusercontent.com/2420543/163021285-3c5a980d-2562-4c10-b266-7e99f19d8a87.png) | ![Screenshot 2021-10-31 at 23-04-20 EdgeVPN - Blockchain index](https://user-images.githubusercontent.com/2420543/163020457-77ef6e50-40a6-4e3b-83c4-a81db729bd7d.png) - - -In API mode, EdgeVPN will connect to the network without routing any packet, and without setting up a VPN interface. - -By default edgevpn will listen on the `8080` port. See `edgevpn api --help` for the available options - -API can also be started together with the vpn with `--api`. - -## API endpoints - -### GET - -#### `/api/users` - -Returns the users connected to services in the blockchain - -#### `/api/services` - -Returns the services running in the blockchain - -#### `/api/dns` - -Returns the domains registered in the blockchain - -#### `/api/machines` - -Returns the machines connected to the VPN - -#### `/api/blockchain` - -Returns the latest available blockchain - -#### `/api/ledger` - -Returns the current data in the ledger - -#### `/api/ledger/:bucket` - -Returns the current data in the ledger inside the `:bucket` - -#### `/api/ledger/:bucket/:key` - -Returns the current data in the ledger inside the `:bucket` at given `:key` - -#### `/api/peergate` - -Returns peergater status - -### PUT - -#### `/api/ledger/:bucket/:key/:value` - -Puts `:value` in the ledger inside the `:bucket` at given `:key` - -#### `/api/peergate/:state` - -Enables/disables peergating: - -```bash -# enable -$ curl -X PUT 'http://localhost:8080/api/peergate/enable' -# disable -$ curl -X PUT 'http://localhost:8080/api/peergate/disable' -``` - -### POST - -#### `/api/dns` - -The endpoint accept a JSON payload of the following form: - -```json -{ "Regex": "", - "Records": { - "A": "2.2.2.2", - "AAAA": "...", - }, -} -``` - -Takes a regex and a set of records and registers them to the blockchain. - -The DNS table in the ledger will be used by the embedded DNS server to handle requests locally. - -To create a new entry, for example: - -```bash -$ curl -X POST http://localhost:8080/api/dns --header "Content-Type: application/json" -d '{ "Regex": "foo.bar", "Records": { "A": "2.2.2.2" } }' -``` - -### DELETE - -#### `/api/ledger/:bucket/:key` - -Deletes the `:key` into `:bucket` inside the ledger - -#### `/api/ledger/:bucket` - -Deletes the `:bucket` from the ledger - -## Binding to a socket - -The API can also be bound to a socket, for instance: - -```bash -$ edgevpn api --listen "unix://" -``` - -or as well while running the vpn: - -```bash -$ edgevpn api --api-listen "unix://" -``` diff --git a/docs/content/en/docs/_index.md b/docs/content/en/docs/_index.md index 1760b92b..992aa77a 100755 --- a/docs/content/en/docs/_index.md +++ b/docs/content/en/docs/_index.md @@ -1,4 +1,3 @@ - --- title: "Documentation" linkTitle: "Documentation" @@ -8,28 +7,36 @@ menu: weight: 20 --- - -EdgeVPN uses libp2p to build private decentralized networks that can be accessed via shared secrets. - -It can: - -- **Create a VPN** : - - Secure VPN between p2p peers - - Automatically assign IPs to nodes - - Embedded tiny DNS server to resolve internal/external IPs - -- **Act as a reverse Proxy** - - Share a tcp service like you would do with `ngrok` to the p2p network nodes without establishing a VPN connection - -- **Send files via p2p** - - Send files over p2p between nodes without establishing a VPN connection. - -- **Be used as a library** - - Plug a distributed p2p ledger easily in your golang code! - -Check out the docs below for further example and reference, have a look at our [getting started guide]({{< relref "/docs">}}/getting-started), the [cli interface]({{< relref "/docs">}}/getting-started/cli), [gui desktop app]({{< relref "/docs">}}/getting-started/gui), and the embedded [WebUI/API]({{< relref "/docs">}}/getting-started/api/). - - -| [WebUI]({{< relref "/docs">}}/getting-started/api) | [Desktop](https://github.com/mudler/edgevpn-gui) | -| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| ![img](https://user-images.githubusercontent.com/2420543/163020448-8e9238c1-3b6d-435d-9b25-7729d8779ebd.png) | ![](https://user-images.githubusercontent.com/2420543/147854909-a223a7c1-5caa-4e90-b0ac-0ae04dc0949d.png) | +EdgeVPN uses libp2p to build private, decentralized networks that are accessed +with a shared secret (a *token*). There is no VPN server and no central +coordinator: every peer that holds the token joins the same network and +discovers the others over p2p. + +A single statically compiled binary can: + +- **Create a VPN** between peers. Each node takes its virtual address from + `--address`; an experimental `--dhcp` mode lets peers negotiate addresses + among themselves instead. +- **Serve DNS** for the network, if you enable it with `--dns`. It answers from + the records peers announce on the shared ledger, and forwards anything it + does not have to an upstream resolver. +- **Act as a reverse proxy**, exposing a TCP service to the network the way + `ngrok` would, without bringing up a VPN interface. +- **Send files** directly between peers, again without a VPN interface. +- **Be used as a Go library**, so you can embed the same distributed ledger and + p2p connectivity in your own program. + +## Where to go next + +- **[Tutorials](tutorials/)** — start here if you are new. End-to-end + walkthroughs that get you from nothing to a working network. +- **[How-to guides](how-to/)** — task-oriented recipes: expose a service, proxy + traffic through another peer, lock a network down. +- **[Reference](reference/)** — every command, flag, environment variable and + API endpoint. +- **[Explanation](explanation/)** — how EdgeVPN works and why, including the + security model you should read before deploying it. + +There is also a web UI and HTTP API for inspecting a running network +([WebUI/API](reference/api/)), and an alpha +[desktop GUI](tools/desktop-gui/) for Linux. diff --git a/docs/content/en/docs/contribution-guidelines.md b/docs/content/en/docs/contributing.md similarity index 95% rename from docs/content/en/docs/contribution-guidelines.md rename to docs/content/en/docs/contributing.md index 8222ce29..b4451e0e 100644 --- a/docs/content/en/docs/contribution-guidelines.md +++ b/docs/content/en/docs/contributing.md @@ -1,12 +1,14 @@ - --- title: "Contributing" -linkTitle: "Contribution guidelines" -weight: 159 +linkTitle: "Contributing" +weight: 60 +aliases: + - /docs/contribution-guidelines/ description: > - See how to contribute to EdgeVPN + See how to contribute to EdgeVPN and to these docs. --- + ## Contributing to EdgeVPN Contribution guidelines for the EdgeVPN project are on the [Github repository](https://github.com/mudler/edgevpn/blob/master/CONTRIBUTING.md). Here you can find some heads up for contributing to the documentation website. diff --git a/docs/content/en/docs/explanation/_index.md b/docs/content/en/docs/explanation/_index.md new file mode 100644 index 00000000..df45f54c --- /dev/null +++ b/docs/content/en/docs/explanation/_index.md @@ -0,0 +1,12 @@ +--- +title: "Explanation" +linkTitle: "Explanation" +weight: 40 +aliases: + - /docs/concepts/ +description: > + How EdgeVPN works and why it is built this way — architecture, the ledger, and the security model. +--- + +Background reading. Nothing here is required to use EdgeVPN, but the +[security model](security-model/) is worth reading before you deploy it. diff --git a/docs/content/en/docs/Concepts/Architecture/edevpn_bootstrap.png b/docs/content/en/docs/explanation/architecture/edevpn_bootstrap.png similarity index 100% rename from docs/content/en/docs/Concepts/Architecture/edevpn_bootstrap.png rename to docs/content/en/docs/explanation/architecture/edevpn_bootstrap.png diff --git a/docs/content/en/docs/explanation/architecture/index.md b/docs/content/en/docs/explanation/architecture/index.md new file mode 100644 index 00000000..c51a18c3 --- /dev/null +++ b/docs/content/en/docs/explanation/architecture/index.md @@ -0,0 +1,126 @@ +--- +title: "Architecture" +linkTitle: "Architecture" +weight: 10 +aliases: + - /docs/concepts/architecture/ +description: > + How EdgeVPN's p2p network, encryption and ledger fit together. +resources: +- src: "**edgevpn_*.png" +--- + + +## Introduction + +EdgeVPN uses [libp2p](https://github.com/libp2p/go-libp2p) to establish a +decentralized, asymmetrically encrypted gossip network which propagates a +(symmetrically encrypted) ledger state between nodes. + +The ledger is a hash-linked chain of blocks, and it is deliberately minimal: + +- There is no proof of work and no consensus protocol. A block is just + `Index`, `Timestamp`, `Storage`, `Hash` and `PrevHash`, where `Hash` is a + SHA256 over the other four — `Index`, `Timestamp`, `Storage` and `PrevHash`. +- There is no DAG, CARv2 or GraphSync. The chain holds metadata only — + service and file names, machine records, DNS entries, IP allocations, + heartbeats — never addressable content. + +Because there is no consensus round, "blockchain" here means *hash-chained +gossiped state*, not a distributed ledger with agreement guarantees. Nodes +converge because every node keeps re-announcing its own entries and merges what +it receives; a node that has just joined, or one that has restarted with an +empty chain, is refilled by those re-announcements. + +EdgeVPN uses the ledger to store Services UUID, Files UUID, VPN and other +metadata (such as DNS records, IP, etc.) and to co-ordinate events between the +nodes of the network. + +## Where the state lives + +By default the chain is kept in memory only, and a node that restarts starts +from an empty chain and refills it from the network. Starting a node with +`--ledger-state ` swaps the in-memory store for a disk-backed one, so the +chain survives restarts. Persisting it is recommended when running +[trusted networks](../../how-to/trusted-networks/), so that authorization keys +do not have to be re-seeded on every restart. + +## Ownership of ledger entries + +Entries are not anonymous. Every write a node makes is signed with that node's +libp2p private key, and carries the author's peer ID, a monotonic version, an +`UpdatedAt` timestamp and a signature over a canonical encoding of all of them. +Verification needs no key distribution: the public key is recovered from the +owner's peer ID, which embeds it. + +When merging an incoming block, a node applies a per-key policy rather than +replacing the whole chain: + +- Buckets such as machines, services, files, users, DNS, heartbeats and egress + are *owned*. A peer cannot overwrite a live entry that belongs to another + peer, replay an older version over a newer one, or forge an entry whose value + claims a different peer as its author. +- Buckets that are not registered — including anything you create yourself + through the API — are open: the highest version wins and anyone may write + them. +- Owned entries have a lease. Most of them are alive only while their owner's + heartbeat is fresh (an eight minute window by default, `--ownership-ttl`); the + heartbeat itself expires on an absolute TTL. The elected leader periodically + reaps expired entries by writing signed tombstones, and prunes old tombstones + once every node has had time to see them. + +This is controlled by `--ownership`, which defaults to `enforce`. `observe` +runs the same merge but accepts and logs violations instead of dropping them, +and `off` restores the legacy behaviour: unsigned entries and a whole-block +replace where the higher block index wins. All nodes of a network must agree on +the mode, because the wire format differs. + +For the details, see +[the authenticated ledger](../authenticated-ledger/). + +## What this does and does not protect + +The ledger authenticates *authorship*, not *membership*. Anything that holds the +network token can join the gossip network, announce itself, and write to any +open bucket. Ownership stops a member from impersonating or evicting another +member; it does not stop a token holder from being a member in the first place. + +The VPN data plane does check the ledger: an inbound VPN stream is reset unless +the remote peer appears in the machines bucket (or in a configured static peer +table), and packets are only routed to a destination IP that resolves to a live +machine entry. So a node that is not in the ledger cannot exchange VPN traffic — +but since a peer announces its own machine entry, this is a routing table, not +an access control list. + +Restricting *which* peers may join is the job of PeerGuardian and peergating, +which are opt-in (`--peerguard`, `--peergate`); see +[trusted networks](../../how-to/trusted-networks/) and the +[security model](../security-model/). + +## Layers + +- Simple (KISS) interface to display network data from the ledger +- asymmetric p2p encryption between peers with libp2p +- rendezvous points dynamically generated from OTP keys +- extra AES symmetric encryption on top, in case the rendezvous point is + compromised +- the ledger acts as a sealed encrypted store for the routing table +- connections are created host to host and encrypted asymmetrically + +### Connection bootstrap + +Network is bootstrapped with libp2p and is composed of 3 phases: + +{{< imgproc edevpn_bootstrap.png Fit "1200x550" >}} +{{< /imgproc >}} + +In the first phase, nodes do discover each others via DHT and a rendezvous +secret which is automatically generated via OTP. + +Once peers know about each other a gossip network is established, where the +nodes exchange ledger blocks over a p2p e2e encrypted channel. The messages are +sealed with a symmetric AES key which is rotated via OTP and shared between the +nodes. + +At that point a ledger and an API is established between the nodes, and +optionally start the VPN binding on the tun/tap device. diff --git a/docs/design/authenticated-ledger.md b/docs/content/en/docs/explanation/authenticated-ledger.md similarity index 87% rename from docs/design/authenticated-ledger.md rename to docs/content/en/docs/explanation/authenticated-ledger.md index 270dc65f..aa0d2000 100644 --- a/docs/design/authenticated-ledger.md +++ b/docs/content/en/docs/explanation/authenticated-ledger.md @@ -1,6 +1,19 @@ -# Authenticated, per-owner ledger entries - -Status: **proposed** · Target: single PR · The `edgevpn` binary defaults to `--ownership=enforce`; operators opt out with `--ownership=off` (`EDGEVPNOWNERSHIP`). The library default (`node.New` without the option) stays off so embedders opt in deliberately. +--- +title: "The authenticated ledger" +linkTitle: "Authenticated ledger" +weight: 30 +description: > + How ledger entries are signed, owned, versioned and reaped. +--- + +{{% alert title="This is the design document" color="info" %}} +This is the design note for ledger ownership: why entries are signed, and how +the merge, the policy registry and the reaper work. For the operator-facing side +— which mode to run, and how to move a live network between modes without +splitting it — see [Ledger ownership](../../how-to/ledger-ownership/). +{{% /alert %}} + +Status: **implemented**. The `edgevpn` binary defaults to `--ownership=enforce`; operators opt out with `--ownership=off` (`EDGEVPNOWNERSHIP`). The library default (`node.New` without the option) stays off so embedders opt in deliberately. ## 1. Problem @@ -29,7 +42,9 @@ not tied to liveness. 2. An entry cannot be **rolled back or replayed** to an older value. 3. Entries owned by an **inactive** node expire and are **reaped**, bounding ledger size. 4. **Extensible**: adding a new bucket with ownership + TTL is a one-line registry entry. -5. **Backwards compatible by default**: ships behind a flag; existing networks keep working. +5. **Backwards compatible behind a flag**: `--ownership=off` keeps existing networks working. + (As shipped, the library default is `off` but the binary defaults to `enforce`, so this + holds on opt-out rather than out of the box.) Non-goals: changing the libp2p transport, the seal-key/OTP scheme, or per-peer message encryption. This work is purely about *authorising writes to the ledger* and *bounding @@ -120,15 +135,28 @@ type Registry map[string]BucketPolicy Default registry: -| Bucket | Owned | OwnerOf | Expiry | Reclaimable | -|-----------------|-------|--------------------|-----------------|-------------| -| `machines` | yes | `PeerID` field | Liveness | yes | -| `services` | yes | `PeerID` field | Liveness | yes | -| `files` | yes | `PeerID` field | Liveness | yes | -| `users` | yes | key == peer.ID | Liveness | no | -| `healthcheck` | yes | key == peer.ID | Absolute(maxTime) | no | -| `dns` | yes | self-owned (nil OwnerOf) | Liveness | yes (first-claim) | -| *(unregistered)*| no | — | NoExpiry | — | +| Bucket | Owned | OwnerOf | Expiry | +|-----------------|-------|--------------------|-----------------| +| `machines` | yes | `PeerID` field | Liveness | +| `services` | yes | `PeerID` field | Liveness | +| `files` | yes | `PeerID` field | Liveness | +| `users` | yes | key == peer.ID | Liveness | +| `egress` | yes | key == peer.ID | Liveness | +| `healthcheck` | yes | key == peer.ID | Absolute(`--ownership-ttl`) | +| `dns` | yes | self-owned (nil OwnerOf) | Liveness | +| *(unregistered)*| no | — | NoExpiry | + +`healthcheck` is the one bucket on an `Absolute` TTL, and that TTL is the +ownership liveness window, *not* the alive service's separate offline threshold +(`--aliveness-healthcheck-max-interval`). `egress` is signed so that a peer +cannot advertise *another* peer as an HTTP egress and intercept its proxied +traffic. + +> Implementation note: `Reclaimable` is declared on `BucketPolicy` but the merge +> does not currently consult it — expiry alone decides whether a slot may change +> hands, so every owned bucket behaves as reclaimable once its owner's lease has +> lapsed. The column is omitted above rather than describing a distinction the +> code does not yet make. A `nil` `OwnerOf` marks a **self-owned** bucket: the value carries no owner field, so the first signer to claim a key owns it (first-claim), and the normal @@ -234,7 +262,7 @@ before any network service runs. The host private key is read from | Flag / field | Default | Meaning | |--------------------------|-----------|-------------------------------------------------------| | `--ownership` (`EDGEVPNOWNERSHIP`) | `enforce` | `enforce` = sign + reject; `observe` = sign + log-only; `off` = legacy/opt-out | -| `--ownership-ttl` | node default (2m) | liveness window after which an inactive owner's entries may be reclaimed/reaped | +| `--ownership-ttl` | `0` = derive (8m on defaults) | liveness window after which an inactive owner's entries may be reclaimed/reaped. `0` derives it as 4× the alive service's heartbeat interval, which clears the worst-case jittered gap with margin rather than landing on it | Reaping cadence reuses the existing alive `scrub-interval`, and tombstones are retained for `3×maxtime` before pruning (see `pkg/services/alive.go`). @@ -284,10 +312,10 @@ IPs / service names / DNS names (ownership stops hijacking *existing* entries, n ## 12. Backwards compatibility -The signed wire format differs from the legacy bare-value one, so observe/enforce nodes do not -interoperate with pre-authentication nodes on the owned buckets. Because the binary now -defaults to `enforce`, **a network must be upgraded together** (all nodes on the same version -and mode). Notes: +The signed wire format differs from the legacy bare-value one, so `enforce` nodes do not +interoperate with pre-authentication nodes on the owned buckets: an unsigned entry fails +verification and is dropped. Because the binary now defaults to `enforce`, **a network must be +upgraded together** (all nodes on the same version and mode). Notes: - The library default (`node.New` without `WithOwnership`) stays `off`, emitting the exact legacy bare-value encoding, so embedders (e.g. LocalAI, Kairos) are unaffected until they @@ -362,8 +390,9 @@ A review of the first cut found and fixed several issues: sites is a small further addition. - **Wire format.** Observe/enforce modes change the on-wire entry encoding to the signed object form; a network running observe/enforce must have all nodes on this version. - The default (`off`) keeps the exact legacy bare-value encoding, so mixed old/new nodes - interoperate only while enforcement is off. + `off` keeps the exact legacy bare-value encoding, so mixed old/new nodes interoperate + only while enforcement is off — which is not the out-of-the-box case, since the binary + defaults to `enforce` (the library default is still `off`). - **`--enforce-ownership` UX.** Exposed as `--ownership=off|observe|enforce` with `--ownership-ttl`; the recommended rollout is `observe` (logs violations) before `enforce`. diff --git a/docs/content/en/docs/explanation/discovery-and-nat.md b/docs/content/en/docs/explanation/discovery-and-nat.md new file mode 100644 index 00000000..f33b99f8 --- /dev/null +++ b/docs/content/en/docs/explanation/discovery-and-nat.md @@ -0,0 +1,44 @@ +--- +title: "Discovery and NAT traversal" +linkTitle: "Discovery and NAT" +weight: 35 +description: > + Placeholder — how peers find each other and get a connection through NAT. Not written yet. +--- + +{{% pageinfo color="warning" %}} +**This page has not been written.** [Architecture](../architecture/) sketches +the three bootstrap phases in a few paragraphs, and +[relays and hop nodes](../../how-to/relays-and-hop-nodes/) covers the relay case +from the operator's side. Nothing explains the mechanism as a whole, or what to +expect when a given piece of it fails. + +What is missing, and where the source is: + +- **The OTP rendezvous.** `pkg/discovery/dht.go` derives the DHT rendezvous + string from a TOTP over the token's OTP key (`Rendezvous()`: TOTP-SHA256, then + MD5), so the point peers meet at rotates on the token's `otp.dht.interval` + (see [network config](../../reference/network-config/)). A two-entry ring + (`rendezvousHistory`, `pkg/discovery/ring.go`) keeps the previous rendezvous + announced across a rotation so nodes do not lose each other at the boundary. + The consequences — clock skew between peers, and what a node sees when it + drifts — are undocumented. +- **DHT versus mDNS.** `--dht` and `--mdns` are both on by default + (`cmd/util.go`), and they solve different problems: `pkg/discovery/mdns.go` + finds peers on the same LAN and dials them directly, while the DHT + (`pkg/discovery/dht.go`) is the internet-wide path and needs bootstrap peers. + What happens with only one of them enabled is not written down. +- **Hole punching and reachability.** `--holepunch`, `--natservice` and + `--natmap` (all default on) map onto libp2p's DCUtR, AutoNAT and UPnP + respectively — see the wiring in `pkg/config/config.go`. Hole punching needs a + third party both peers can already reach, which is why it interacts with the + relay settings. +- **Relay fallback.** `--autorelay`, `--autorelay-static-peer`, + `--autorelay-static-only`, `--autorelay-discovery-interval` and + `--relay-service`, and the order in which a node tries direct, hole-punched + and relayed connections. +- **Diagnosing it.** Which of the above a stuck "0 peers" state actually points + at. See [troubleshooting](../../troubleshooting/) for what exists today. + +Contributions welcome — see [contributing](../../contributing/). +{{% /pageinfo %}} diff --git a/docs/content/en/docs/explanation/security-model.md b/docs/content/en/docs/explanation/security-model.md new file mode 100644 index 00000000..494352cb --- /dev/null +++ b/docs/content/en/docs/explanation/security-model.md @@ -0,0 +1,407 @@ +--- +title: "The security model" +linkTitle: "Security model" +weight: 15 +description: > + What the network token protects, what it does not, and what every mechanism layered on top actually buys you. +--- + +{{% pageinfo color="warning" %}} +**EdgeVPN's security model is perimeter-only.** Anyone holding the network +token is a fully trusted member of the network. There is no per-peer +authorization on the data plane, and no audit trail of which peer did what. +The token *is* the security boundary. +{{% /pageinfo %}} + +Everything else on this page — ledger ownership, trust zones, relay ACLs, +socket permissions — narrows *specific* abuses by someone who is already +inside, or protects the machine you are running on. None of it changes the +sentence above. Read this before you decide who gets a copy of your token, +because that decision is the security design. + +EdgeVPN has also **not been through a security audit**. See +[when not to use EdgeVPN](../when-not-to-use-edgevpn/). + +## The token is the boundary + +A token is a base64-encoded YAML config — nothing more. It carries a gossip +room name, a DHT rendezvous string, an mDNS service tag, and two OTP secrets +(`otp.dht.key` and `otp.crypto.key`). You can decode one with `base64 -d` and +read it. The [network config reference](../../reference/network-config/) +describes every field. + +There is no per-node credential, no enrollment step, no revocation list. A node +is a member because it has the token. + +### What a leaked token grants + +Someone who obtains your token can, with no further access: + +- **Join the network.** Derive the rendezvous point, find peers on the public + DHT, and join the gossip topic. +- **Read the entire ledger.** Every machine's IP and peer ID, every announced + service, every DNS record, every file announcement. +- **Write to the ledger.** Claim any unclaimed DNS name, announce services and + files, and — on a network running `--ownership off` — overwrite anyone's + entries. +- **Join the VPN.** Announce a machine entry, get an IP over + [DHCP](../../how-to/addressing-and-dhcp/), and exchange packets with every + other node. +- **Use any egress node.** If a node on the network runs + [HTTP egress](../../how-to/http-egress-and-proxy/), the intruder can proxy + traffic through it. There is no way to allow one member and deny another. +- **Reach any tunnelled service.** Anything published with + [`service-add`](../../how-to/tunnel-tcp-services/) is reachable by any member + that can run `service-connect`. + +Treat a token exactly as you would a private key with no passphrase. Store it +in a secrets manager, pass it via `EDGEVPNTOKEN` or a config file readable only +by root, and keep it out of shell history, CI logs and process listings. + +## What the token *does* protect + +The token is weak as an authorization mechanism and strong as a confidentiality +mechanism. Traffic between members is protected on three independent layers: + +1. **libp2p transport encryption.** Every peer-to-peer connection is + authenticated to the remote peer ID and encrypted by libp2p itself. VPN + packets ride these streams; an on-path observer sees libp2p noise, not your + frames. +2. **A rotating DHT rendezvous.** The rendezvous point announced to the public + DHT is not the token's `rendezvous` string but `MD5(TOTP(otp.dht.key))`, + recomputed on every interval (`pkg/discovery/dht.go`). Without the token you + cannot compute the current rendezvous, so you cannot enumerate the network's + members through the DHT. Setting `otp.dht.key` to an empty value falls back + to the static `rendezvous` string, which is permanent and therefore + greppable forever once observed. +3. **AES sealing of gossip messages.** Every message published to the gossip + room is sealed with AES-GCM under `MD5(TOTP(otp.crypto.key))` + (`pkg/node/connection.go`, `pkg/crypto/aes.go`). This is a second layer on + top of libp2p's own encryption, so the ledger stays unreadable even to + someone who joined the topic without the crypto key. + +{{% alert title="A note on the sealing key" color="info" %}} +The seal key is the hex encoding of an MD5 digest — 32 ASCII characters, used +directly as a 32-byte AES-256 key. The cipher is AES-256, but the key material +behind it carries at most the 128 bits of an MD5 output. This is adequate +against an eavesdropper who does not have the token, which is what it is there +for; it is not a reason to relax how you handle the token. +{{% /alert %}} + +## Rotation does not revoke + +The OTP mechanism rotates *derived* values — the rendezvous point and the seal +key — on a fixed interval. It does not rotate the secrets they are derived +from. Those live in the token, unchanged, forever. + +So if a token leaks, waiting for the next OTP tick achieves nothing: the holder +recomputes the new rendezvous and the new seal key just as every legitimate +node does. OTP rotation defends against someone who observed *one* rendezvous +point or *one* sealed message, not against someone who has the token. + +`--key-otp-interval` (default `360` seconds) only sets the interval baked into +a token at the moment you generate it: + +```bash +edgevpn --key-otp-interval 120 -g -b > token.txt +``` + +It has no effect on an existing network — the interval is read from the token, +so all nodes must use the same one. + +**The only way to revoke a leaked token is to generate a new one and restart +every node with it.** There is no partial revocation, and a network with the +old token keeps working for anyone still holding it. Plan for that: if you +cannot reach every node to re-key it, you cannot recover from a leak. + +Two mechanisms let you keep the token and still deny the leaker. If your +membership is a fixed set of hosts, [static peer tables](#static-peer-tables) +are the non-experimental option: each node accepts only the peer IDs you list. +If membership changes at runtime, a +[trust zone](#trust-zones-peerguardian-and-peergating) can withdraw a peer's +authorization while the network runs — at the cost of an experimental feature. +Neither is a substitute for re-keying: the leaked token still admits its holder +anywhere you have not applied one of them. + +## Trust zones: PeerGuardian and peergating + +{{% alert title="Experimental" color="warning" %}} +`--peerguard`, `--peergate`, `--peergate-autoclean` and `--peergate-relaxed` +are all marked *(Experimental)* in their own usage strings — that is, every +flag that switches the feature on or changes how it gates. (`--peergate-auth` +and `--peergate-interval` carry no such marker, but they only supply the key +material and the sync cadence for the same experimental machinery.) Do not +build a security posture on it. It is the right shape for admission control, +but the implementation has the gaps described below. +{{% /alert %}} + +[Trust zones](../../how-to/trusted-networks/) add an admission-control layer +*inside* the token perimeter. The shape is: + +- A node started with `--peergate-auth` holding an ECDSA P-521 private key + signs a challenge and publishes it to the gossip room + (`pkg/trustzone/authprovider/ecdsa/provider.go`). +- Nodes running `--peerguard` verify that signature against the public keys + stored in the `trustzoneAuth` ledger bucket. On success, they write the + sender's peer ID into the `trustzone` bucket + (`pkg/trustzone/peerguardian.go`). +- Nodes running `--peergate` drop gossip messages from any peer that is not in + `trustzone` (`pkg/trustzone/peergater.go`, applied in + `pkg/node/connection.go`). + +**What this adds:** a token holder without an authorized ECDSA key has its +ledger messages dropped by gated nodes. Because the VPN data plane only accepts +streams from peers present in the `machines` bucket, a peer whose announcements +never land cannot exchange VPN traffic with a gated node either. That is real, +and it is the only mechanism that can exclude a token holder *dynamically* — +authorization is added and withdrawn in the ledger at runtime. + +It is not, however, the only mechanism that can exclude a token holder. If your +membership is a fixed set of hosts, [static peer tables](#static-peer-tables) +do it without any of the caveats below. Weigh both before reaching for an +experimental feature. + +**What this does not add:** + +- **It gates who may join, not what a member may do.** Once a peer is in the + trust zone it is an ordinary full member with every capability listed under + [what a leaked token grants](#what-a-leaked-token-grants). +- **The buckets it runs on are outside ledger ownership.** Both `trustzone` + (the admitted peers) and `trustzoneAuth` (the public keys they are admitted + against) are absent from the policy registry in `pkg/blockchain/policy.go`, so + they take the zero policy — no owner, never expiring, and writable by + anything whose ledger writes a node already accepts, including the + [unauthenticated API](#the-api-is-unauthenticated) on *any* node. (A signing + node does still sign what it writes there; what the zero policy skips is the + *verification*, so a signature says who wrote an entry and nothing about + whether they were entitled to.) Adding a + trusted key is therefore an ordinary ledger write, which means passing a + challenge is not the only way into the trust zone: a token holder can instead + supply the key the challenge is checked against. Admission rests on a store + with weaker integrity than the `machines` and `dns` entries it exists to + protect. See [ledger buckets](../../reference/ledger-buckets/). +- **The challenge is a constant, and the signature is not bound to the sender.** + The provider signs the fixed string `"challenge"`, and PeerGuardian admits + `m.SenderID` whenever the attached signature verifies against a trusted public + key. Nothing ties that signature to the peer that sent it, and the signed + challenge travels over the same gossip room every token holder can already + read. A token holder that has never held an authorized ECDSA key can + therefore still end up inside the trust zone. Treat peergating as raising the + cost of an attack, not as a boundary. +- **`--peergate-relaxed` gates nothing while `trustzone` is empty**, by design, + so a network can bootstrap. During that window every token holder is + admitted. Prefer a persistent ledger (`--ledger-state `) so authorized + keys survive restarts and you can stop using relaxed mode. +- **`--peergate-autoclean` removes peers from the trust zone when they leave + the gossip topic**, which means a transient disconnect costs a peer its + admission until it re-authenticates. +- **Gating is per-node local policy.** A peer that has not enabled `--peergate` + keeps accepting everything, and it is still on the same network. + +## Ledger ownership: write integrity, not admission + +`--ownership` (default `enforce`) makes every entry in a **registered** bucket +carry its author's peer ID, a version, a timestamp and an Ed25519 signature, +verified against the public key embedded in the peer ID. A live peer's entries +in those buckets cannot be overwritten or replayed by anyone else. + +**"Registered" is the load-bearing word.** The registry in +`pkg/blockchain/policy.go` covers `machines`, `services`, `files`, `users`, +`egress`, `healthcheck` and `dns`. Every other bucket — `trustzone`, +`trustzoneAuth`, `dhcp`, and any bucket you invent through the API — takes the +zero policy: no owner, never expiring, and overwritable by any writer that +supplies a strictly higher version. Those entries are still signed; it is the +verification the zero policy skips, not the signing. +Enforcement says nothing about those, which matters most for the two the +[trust zone](#trust-zones-peerguardian-and-peergating) is built on. See +[ledger buckets](../../reference/ledger-buckets/) for the full list. + +This constrains a malicious member; it does not keep one out. Identities are +free, so a token holder can still mint peer IDs and claim any *unclaimed* name +or address. See [ledger ownership](../../how-to/ledger-ownership/) for +operating it and [the authenticated ledger](../authenticated-ledger/) for the +design. + +{{% alert title="Check your ownership mode on older releases" color="warning" %}} +On **every released version at the time of writing, up to and including +v0.35.3**, an unrecognised `--ownership` value — say `enabled` instead of +`enforce`, or any typo — falls through to `off`. The node starts normally, logs +nothing, and accepts unsigned writes from any token holder. + +A fix that rejects an invalid mode at startup, before the node joins a network, +has landed but is not yet in a tagged release. Until you are running a build +that contains it, verify the exact spelling on every node. +{{% /alert %}} + +## The API is unauthenticated + +The [HTTP API](../../reference/api/) has no authentication, authorization or +CSRF protection on any route. Anything that can reach the listener can read the +whole ledger and write to it: + +```bash +curl -X PUT 'http://localhost:8080/api/ledger///' +``` + +Writes are gossiped to the rest of the network. An exposed API port is +therefore equivalent to handing out the token, with the extra property that the +writes are signed as *your* node. + +- The API is **off by default** when running the VPN; `--api` turns it on. +- The default listener is `127.0.0.1:8080`. **Never bind it to a routable + address.** +- Prefer a unix socket on any shared host: + + ```bash + edgevpn --api --api-listen "unix:///run/edgevpn/api.sock" + ``` + + The socket is created mode `0660` (owner and group only), overridable with + `APILISTENUNIXMODE`. An unparseable value falls back to `0660` rather than + widening permissions. systemd socket activation is honoured, in which case + the unit's own ownership and permissions apply. + +Filesystem permissions are the only access control the API has. Anyone who can +open that socket controls the node. + +## Relay ACLs protect your bandwidth + +`--relay-service-network-only` (default **on**) restricts incoming circuit-v2 +relay *reservations* to peers seen in the local ledger's alive bucket, so +strangers who found you through the public DHT cannot use you as a relay +(`pkg/config/relay_acl.go`). Two caveats worth knowing: + +- Only the reservation step is gated. Once a peer holds a reservation, connects + through it are permitted. +- The ACL stays fully open during a bootstrap window, and stays open + indefinitely if the alive bucket is empty — for example if the aliveness + service is disabled. A debug line is logged on each refresh in that case. + +This is a resource-abuse control, not a network access control: everyone it +admits is already a token holder. + +Note that `--whitelist` is **not** an access control despite the name. It +passes multiaddrs to the libp2p resource manager's allowlist, exempting them +from connection limits. It does not restrict who may connect. + +## Static peer tables + +`--static-peertable` is the one **non-experimental** way to exclude a peer that +holds a valid token. It takes `ip:peerid` pairs, and a node configured with it +accepts VPN streams and gossip messages only from the peer IDs on the list +(`pkg/vpn/vpn.go`, `pkg/node/connection.go`): + +```bash +edgevpn --static-peertable 10.1.0.1:12D3KooW... \ + --static-peertable 10.1.0.2:12D3KooW... +``` + +When it is set it *replaces* the ledger lookup rather than adding to it. Inbound +streams are matched against the table instead of the `machines` bucket, and +outbound packets are routed only to addresses in the table. A peer that is not +on the list cannot reach that node's data plane whatever it announces to the +ledger — which is exactly the property you want against a leaked token. + +The trade-offs are why it is not the general answer: + +- **It is local policy.** Each node carries its own table, and a node without + one still accepts every token holder. +- **It is static.** Adding or removing a peer means editing configuration and + restarting nodes. There is no runtime revocation. +- **It replaces automatic addressing** for the peers it covers: you are + maintaining the IP-to-peer-ID mapping by hand instead of letting + [DHCP](../../how-to/addressing-and-dhcp/) assign it. + +For a handful of fixed hosts this is a genuine boundary with no experimental +caveats. For membership that changes, trust zones are the only dynamic option, +with the gap described above. + +## What EdgeVPN does not protect against + +### A malicious member + +This is the big one. A member with a valid token — or a compromised node on +your own network — can: + +- Read every machine, service and DNS entry in the ledger. +- Claim unclaimed DNS names and route your internal name lookups wherever it + likes, if you use [the DNS service](../../how-to/enable-dns/). +- Announce a service or file and have any member connect to it. +- Send traffic to every VPN IP on the network. EdgeVPN routes packets between + members; it does not filter them. **Host firewalls on the `edgevpn0` + interface are your only per-service access control**, and you should treat + the VPN as a flat, hostile LAN. +- Run an egress node and proxy — or observe — other members' HTTP traffic. + +Ledger ownership, trust zones and firewalls each shave a piece off this list. +Nothing removes it. + +### Traffic analysis by an egress node + +An [HTTP egress](../../how-to/http-egress-and-proxy/) node is a fully trusted +intermediary. Its operator sees every URL and header, and because only +unencrypted HTTP is proxied at all (there is no `CONNECT` handling, so HTTPS +does not work through it), it can read and modify request and response bodies +at will. Any token holder can use any egress, and egress selection is random +per request, so you cannot even predict which node saw a given request. + +### A compromised or hostile bootstrap peer + +With no `--discovery-bootstrap-peers` set, EdgeVPN bootstraps against the +**public IPFS DHT bootstrap nodes**. Those nodes learn that your peer ID exists +and see the addresses you dial from; anyone able to observe DHT queries for +your current rendezvous can enumerate the peer IDs and IP addresses of your +network's members. They cannot read gossip (sealed) or VPN traffic +(libp2p-encrypted), and they cannot join without the token — but membership is +metadata, and metadata leaks. If that matters, point +`--discovery-bootstrap-peers` at infrastructure you control, or disable the DHT +and rely on mDNS on a trusted LAN. + +### After the fact: there is no audit trail + +Nothing in EdgeVPN records who did what. Under ownership an entry names its +current author and last update time, but the default in-memory store keeps only +the current block (`pkg/blockchain/store_memory.go`) — previous values are +gone. Reads are never recorded at all: there is no log of who queried the +ledger, who resolved a DNS name, or whose traffic went through an egress. If +you need accountability, it has to come from somewhere else in your stack. + +### Host compromise + +The token, the persisted ledger state (`--ledger-state`) and any cached private +key (`--privkey-cache-dir`, default `~/.edgevpn`) sit on disk. Root on any node +means the token, which means the network. The default cache directory is shared +per user, so co-located EdgeVPN processes would load the same identity — which +is exactly why `--privkey-cache` is opt-in and wants a distinct directory per +process. + +## A short checklist + +- Treat the token as a root credential. Rotating it means restarting every + node, so plan the distribution path before you need it. +- Leave `--ownership enforce` on, and confirm the exact spelling on every node + until you are running a build that validates it — no released version does + yet. +- Keep the API on loopback or, better, a unix socket. Never on a routable + address. +- Firewall the `edgevpn0` interface per host. The VPN is a flat network. +- Leave `--relay-service-network-only` on unless you deliberately want to relay + for strangers. +- Run an egress node only on a network whose members you trust with your plain + HTTP traffic. +- If you need to exclude a token holder without re-keying: on a fixed set of + hosts use [static peer tables](#static-peer-tables), which are not + experimental. Only if membership changes at runtime reach for trust zones, + and weigh the admission gap described above first. + +## See also + +- [When not to use EdgeVPN](../when-not-to-use-edgevpn/) — the honest list of + what the design costs you. +- [The ledger](../the-ledger/) and + [the authenticated ledger](../authenticated-ledger/). +- [Ledger ownership](../../how-to/ledger-ownership/) — operating `--ownership`. +- [Trusted networks](../../how-to/trusted-networks/) — configuring PeerGuardian + and peergating. +- [WebUI and API](../../reference/api/) — every route, and how to bind the + listener safely. diff --git a/docs/content/en/docs/Concepts/Overview/_index.md b/docs/content/en/docs/explanation/the-ledger.md similarity index 50% rename from docs/content/en/docs/Concepts/Overview/_index.md rename to docs/content/en/docs/explanation/the-ledger.md index 39e18fec..c43275fc 100644 --- a/docs/content/en/docs/Concepts/Overview/_index.md +++ b/docs/content/en/docs/explanation/the-ledger.md @@ -1,11 +1,14 @@ --- -title: "Overview" -linkTitle: "Overview" -weight: 1 +title: "The ledger" +linkTitle: "The ledger" +weight: 20 +aliases: + - /docs/concepts/overview/ description: > - EdgeVPN overview + What the shared ledger stores, and what it deliberately does not. --- + EdgeVPN have a simplified model of a blockchain embedded. The model is actually simplified on purpose as the blockchain is used to store merely network and services metadata and not transaction, or content addressable network. The only data stored in the blockchain is: @@ -14,3 +17,9 @@ The only data stored in the blockchain is: - Healthchecks, DNS records and IP allocation However, the ledger is freely accessible via API, allowing for external coordination to use the blockchain mechanism as a shared memory access (which can be optionally persisted on disk). + +Writes to the ledger are authenticated by default: each entry is signed by the +peer that wrote it, and a peer cannot overwrite an entry owned by another live +peer. See [ledger ownership](../../how-to/ledger-ownership/) for how to operate +it — in particular before changing `--ownership` on a running network — and +[the authenticated ledger](../authenticated-ledger/) for how it works. diff --git a/docs/content/en/docs/explanation/when-not-to-use-edgevpn.md b/docs/content/en/docs/explanation/when-not-to-use-edgevpn.md new file mode 100644 index 00000000..37cc4664 --- /dev/null +++ b/docs/content/en/docs/explanation/when-not-to-use-edgevpn.md @@ -0,0 +1,33 @@ +--- +title: "When not to use EdgeVPN" +linkTitle: "When not to use it" +weight: 40 +description: > + What the decentralized design costs you, and the cases it is not a good fit for. +--- + +## Is it for me? + +EdgeVPN makes VPN decentralization a first strong requirement. + +Its main use is for edge and low-end devices and especially for development. + +The decentralized approach has few cons: + +- The underlying network is chatty. It uses a Gossip protocol for synchronizing + the routing table and p2p. Every blockchain message is broadcasted to all + peers, while the traffic is to the host only. +- Might be not suited for low latency workload. + +Keep that in mind before using it for your prod networks! + +But it has a strong pro: it just works everywhere libp2p works! + +## Warning + +{{% pageinfo color="warning" %}} +I'm not a security expert, and this software didn't went through a full +security audit, so don't use and rely on it for sensible traffic and not even +for production environment! I did this mostly for fun while I was experimenting +with libp2p. +{{% /pageinfo %}} diff --git a/docs/content/en/docs/how-to/_index.md b/docs/content/en/docs/how-to/_index.md new file mode 100644 index 00000000..d70bbb99 --- /dev/null +++ b/docs/content/en/docs/how-to/_index.md @@ -0,0 +1,10 @@ +--- +title: "How-to guides" +linkTitle: "How-to" +weight: 20 +description: > + Task-oriented recipes for a specific job — expose a service, proxy HTTP through an egress node, lock a network down. +--- + +Each guide assumes you already have a working network. If you don't, start with +[your first network](../tutorials/your-first-network/). diff --git a/docs/content/en/docs/how-to/addressing-and-dhcp.md b/docs/content/en/docs/how-to/addressing-and-dhcp.md new file mode 100644 index 00000000..850f128e --- /dev/null +++ b/docs/content/en/docs/how-to/addressing-and-dhcp.md @@ -0,0 +1,98 @@ +--- +title: "Addressing and DHCP" +linkTitle: "Addressing and DHCP" +weight: 20 +description: > + Assign virtual addresses by hand, let peers negotiate them, or pin a static peer table. +--- + +Every VPN node needs a virtual address on the `edgevpn0` interface. There are +three ways to get one, and two extra flags that change how packets are routed +once you have it. + +## Static addresses with `--address` + +`--address` takes a CIDR and defaults to `10.1.0.1/24`. It is the address the +node is reachable at from inside the VPN, and it must be unique across the +network — nothing checks this for you. + +```bash +# on Node A +$ EDGEVPNTOKEN=.. edgevpn --address 10.1.0.11/24 +# on Node B +$ EDGEVPNTOKEN=.. edgevpn --address 10.1.0.12/24 +``` + +The interface name defaults to `edgevpn0` and can be changed with +`--interface` (or `IFACE`). + +## Automatic addresses with `--dhcp` + +{{% pageinfo color="warning"%}} +Experimental feature! +{{% /pageinfo %}} + +`--dhcp` lets peers negotiate addresses among themselves over the ledger, so +you do not have to keep a list of who has which IP. There is no DHCP server: +the allocation is agreed on through the shared ledger like every other piece of +network metadata. + +```bash +$ EDGEVPNTOKEN=.. edgevpn --dhcp +``` + +With `--dhcp` enabled, `--address` can be omitted. If an address *is* given, it +is the base the allocator counts up from when picking the next free IP, not a +reservation for this node. The allocated subnet is `/24`. + +Once a node has an address it writes a lease under `--lease-dir` (default +`$HOME/.edgevpn/leases`) and reuses it on the next start. Allocation needs at +least two nodes visible on the ledger, so a single node started with `--dhcp` +waits until a peer shows up. + +## Sending everything to one node with `--router` + +`--router` takes the virtual address of another node in the network: + +```bash +$ EDGEVPNTOKEN=.. edgevpn --address 10.1.0.11/24 --router 10.1.0.1 +``` + +When a packet originates on this node and its destination is *not* a machine +announced on the ledger, the packet is sent to the router node instead of being +dropped. Packets addressed to nodes that are on the ledger still go directly to +that peer — `--router` is a fallback for unknown destinations, not a blanket +redirect. + +The router node is a normal EdgeVPN node. What it does with the traffic it +receives — forwarding it to a LAN, to the internet, or nowhere — is up to that +host's own kernel routing and NAT configuration; EdgeVPN itself only delivers +the packet to it. + +## Pinning the routing table with `--static-peertable` + +`--static-peertable` takes one or more `ip:peerid` pairs and can be repeated: + +```bash +$ edgevpn --address 10.1.0.11/24 \ + --static-peertable 10.1.0.12:12D3KooW... \ + --static-peertable 10.1.0.13:12D3KooW... +``` + +The value must contain exactly one `:` separating the virtual IP from the +libp2p peer ID; anything else is a startup error. + +Setting it changes the node's behaviour in two ways: + +- **Outbound**, the ledger's machine table is not consulted at all. A + destination that is not in the static table is unroutable, even if the peer + is announcing itself on the ledger. +- **Inbound**, only peers whose IDs appear in the static table may open a VPN + stream to this node. Streams from anyone else are reset. + +That makes it a way to run the VPN with a fixed, hand-maintained topology +instead of a discovered one. It is per-node configuration: a node with a static +peer table and a node without can coexist in the same network, and each applies +its own rule. + +The equivalent environment variable is `EDGEVPNSTATICPEERTABLE`. diff --git a/docs/content/en/docs/Concepts/Overview/dns.md b/docs/content/en/docs/how-to/enable-dns.md similarity index 90% rename from docs/content/en/docs/Concepts/Overview/dns.md rename to docs/content/en/docs/how-to/enable-dns.md index 00a038c0..e4b7fb9a 100644 --- a/docs/content/en/docs/Concepts/Overview/dns.md +++ b/docs/content/en/docs/how-to/enable-dns.md @@ -1,12 +1,14 @@ --- -title: "DNS" -linkTitle: "DNS" -weight: 20 -date: 2017-01-05 +title: "Enable the DNS server" +linkTitle: "Enable DNS" +weight: 40 +aliases: + - /docs/concepts/overview/dns/ description: > - Embedded DNS server documentation + Resolve names from the shared ledger with the embedded DNS server. --- + {{% pageinfo color="warning"%}} Experimental feature! {{% /pageinfo %}} diff --git a/docs/content/en/docs/how-to/http-egress-and-proxy.md b/docs/content/en/docs/how-to/http-egress-and-proxy.md new file mode 100644 index 00000000..ff8b63c6 --- /dev/null +++ b/docs/content/en/docs/how-to/http-egress-and-proxy.md @@ -0,0 +1,207 @@ +--- +title: "HTTP egress and the proxy" +linkTitle: "HTTP egress and proxy" +weight: 70 +description: > + Let one node make HTTP requests on behalf of the network, and reach it through a local HTTP proxy. +--- + +{{% pageinfo color="warning"%}} +Only plain HTTP is proxied. HTTPS does not work — see +[HTTPS does not work](#https-does-not-work) below. +{{% /pageinfo %}} + +EdgeVPN can designate one or more nodes as **HTTP egress** nodes. Another peer +runs `edgevpn proxy`, which exposes an ordinary local HTTP proxy. Requests sent +to that proxy travel over libp2p to one of the egress nodes, which performs the +request from its own network and streams the response back. + +## What this is, and what it is not + +This proxies **HTTP requests**, not arbitrary IP traffic. It is not a VPN exit +node: nothing is rerouted at the IP layer, your default route is untouched, and +only the clients you explicitly point at the local proxy are affected. If you +want a real network interface between peers, that is +[run as a VPN](../run-as-a-vpn/) instead. + +The distinguishing property is on the client side: the machine running +`edgevpn proxy` needs no VPN interface and no privileges — it only joins the +network and listens on a local TCP port. + +The egress side is different. `--egress` is a flag of the top-level `edgevpn` +command, which always brings up a TUN interface, so an egress node does need +privileges and a VPN address even though the egress feature itself never uses +the interface. Started unprivileged it fails with: + +``` +error while starting network service: 'ioctl: operation not permitted' +``` + +## Running an egress node + +```bash +sudo edgevpn --egress --token "$TOKEN" +``` + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--egress` | off | `EGRESS` | Announce this node as an HTTP egress | +| `--egress-announce-time` | `200` | `EGRESSANNOUNCE` | Egress announce time, in seconds | + +The node then advertises itself in the `egress` bucket of the +[ledger](../../explanation/the-ledger/) and serves the `/edgevpn/egress/0.1` +protocol. With the API enabled you can see the announcement on the node itself: + +```bash +sudo edgevpn --egress --api --token "$TOKEN" # API defaults to 127.0.0.1:8080 +curl -s http://127.0.0.1:8080/api/ledger/egress +``` + +```json +{"12D3KooWMrvbf8SX1B6nj64mA2yJYiR1HBRcvX54KxocQ75g7knK":"\"ok\""} +``` + +Because the egress node runs the top-level command it also runs the aliveness +service automatically, which is what lets proxies tell whether it is still up. +Egress nodes get a VPN address like any other node, so give each one its own — +see [addressing and DHCP](../addressing-and-dhcp/). + +## Using an egress node + +On any other peer in the same network: + +```bash +edgevpn proxy --listen :8080 --token "$TOKEN" +``` + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--listen` | `":8080"` | `PROXYLISTEN` | Address the local HTTP proxy listens on | +| `--interval` | `120` | `PROXYINTERVAL` | How often the proxy announces itself, in seconds | +| `--dead-interval` | `600` | `PROXYDEADINTERVAL` | Age, in seconds, after which an egress node is treated as offline | +| `--api` | `false` | `API` | Also start the API daemon and web UI | +| `--api-listen` | `"127.0.0.1:8081"` | `APILISTEN` | Address for the API, used only with `--api` | +| `--debug` | `false` | — | Start the API with `pprof` attached | + +Then point a client at it like any other HTTP proxy: + +```bash +http_proxy=http://localhost:8080 curl http://example.com/ +``` + +or + +```bash +curl -x http://localhost:8080 http://example.com/ +``` + +Browsers should work the same way — set the HTTP proxy to `localhost:8080` in +the network settings — though only the two `curl` forms above have been tested, +and browsers default to HTTPS for most destinations, which +[does not work](#https-does-not-work). + +The proxy also announces itself into the ledger's `users` bucket, and egress +nodes reject streams from peers that are not listed there. A freshly started +proxy therefore needs one announce round before its first request succeeds. + +### Inspecting the proxy + +Like the root command, `edgevpn proxy` only starts the API daemon and web UI if +you ask for it, and the API needs an address of its own — it cannot share +`--listen` with the proxy: + +```bash +edgevpn proxy --listen :8080 --api --api-listen 127.0.0.1:8081 --token "$TOKEN" +``` + +Passing an `--api-listen` that would compete with `--listen` is refused at +startup rather than leaving one of the two servers silently dead. The two +addresses are resolved before being compared, so naming the same socket a +different way is caught too: + +```console +$ edgevpn proxy --api --listen :8080 --api-listen :8080 +--listen (":8080") and --api-listen (":8080") would bind the same address: give the API a different one + +$ edgevpn proxy --api --listen localhost:8080 --api-listen 127.0.0.1:8080 +--listen ("localhost:8080") and --api-listen ("127.0.0.1:8080") would bind the same address: give the API a different one +``` + +{{% alert title="Note" color="warning" %}} +`APILISTEN` is shared with the root command, where it defaults to +`127.0.0.1:8080`. If you export it globally, `edgevpn proxy --api` will refuse +to start, because that address collides with the proxy's own `--listen` default +of `:8080`. Pass `--api-listen` explicitly on the proxy, or unset the variable. +{{% /alert %}} + +The API is a good way to check that an egress node has been seen: + +```bash +curl -s http://127.0.0.1:8081/api/ledger/egress +``` + +## HTTPS does not work + +There is no `CONNECT` handling anywhere in EdgeVPN. When a client asks the proxy +for an HTTPS URL it sends `CONNECT example.com:443`, and instead of opening a +tunnel the egress node forwards that request verbatim to the origin server, +which rejects it: + +```console +$ curl -x http://localhost:8080 https://example.com/ +curl: (56) CONNECT tunnel failed, response 400 +``` + +The `400` comes from the destination server, not from EdgeVPN — the request does +reach it, it is simply not a request any web server will answer. Only plain HTTP +works today. + +## Security + +An HTTP egress node is a fully trusted intermediary, and this is the part to +think hardest about before enabling it. + +- **The egress operator sees everything.** Requests leave the network from the + egress node's own IP address, and that node's operator can observe every URL + requested and every header sent. Because only unencrypted HTTP is proxied at + all, they can also read and modify request and response bodies at will. +- **Destinations see the egress node, not you.** Whoever runs an egress is + accepting responsibility for the traffic it emits — abuse reports, rate + limits, and blocklists land on them. +- **Any token holder can use any egress.** EdgeVPN's trust model is + perimeter-only: holding the network token makes a peer a full member, and + there is no per-peer authorization for egress. You cannot allow one peer to + proxy through an egress and deny another. + +One thing the proxy does *not* do is expose the +[API](../../reference/api/) unless you ask: `--api` is off by default, because +the API writes to the ledger as the node running it and is unauthenticated. +Bind it to loopback if you enable it. + +Read the [security model](../../explanation/security-model/) before running an +egress node on a network whose token is shared widely, and see +[trusted networks](../trusted-networks/) for narrowing who can join at all. + +## How an egress is chosen + +For every single request the proxy builds the list of nodes that appear in both +the `egress` bucket and the aliveness bucket with a healthcheck newer than +`--dead-interval`, then picks one of them uniformly at random. + +Two consequences follow: + +- **Requests are not pinned.** Consecutive requests from the same client may + leave through different egress nodes, so anything that depends on a stable + source address — session cookies tied to an IP, login flows, rate limits — + can break when more than one egress node is running. +- **With no egress available the proxy answers `503`.** If no egress node has + announced itself, or all of them went quiet longer ago than `--dead-interval`, + the request fails immediately: + + ```console + $ curl -x http://localhost:8080 http://example.com/ + no egress nodes available + ``` + +Raising `--dead-interval` keeps egress nodes in the pool longer across brief +outages, at the cost of sending requests to nodes that have already gone away. diff --git a/docs/content/en/docs/how-to/ipv6.md b/docs/content/en/docs/how-to/ipv6.md new file mode 100644 index 00000000..7c5e278a --- /dev/null +++ b/docs/content/en/docs/how-to/ipv6.md @@ -0,0 +1,30 @@ +--- +title: "IPv6" +linkTitle: "IPv6" +weight: 30 +description: > + Run the VPN over IPv6 with static addresses. Experimental, single-stack only. +--- + +{{% pageinfo color="warning"%}} +Experimental feature. IPv6 support is provisional and has known gaps — see +below before relying on it. +{{% /pageinfo %}} + +IPv6 works with static addresses only. One address per interface; dual stack is +not supported, so a node is either IPv4 or IPv6, not both. + +```bash +$ EDGEVPNTOKEN=.. edgevpn --address fd:ed4e::11/64 --mtu 1500 +``` + +Two things to get right: + +- **The address must be static.** `--dhcp` allocates IPv4 addresses only, so it + cannot be combined with an IPv6 `--address`. +- **`--mtu` must be above 1280**, the IPv6 minimum link MTU. EdgeVPN's default + is `1200`, which is below it, so you have to set `--mtu` explicitly. + +Tracking issue [#15](https://github.com/mudler/edgevpn/issues/15) is still open +at the time of writing; it is the place to check for the current state of IPv6 +support. diff --git a/docs/content/en/docs/how-to/ledger-ownership.md b/docs/content/en/docs/how-to/ledger-ownership.md new file mode 100644 index 00000000..c014e843 --- /dev/null +++ b/docs/content/en/docs/how-to/ledger-ownership.md @@ -0,0 +1,363 @@ +--- +title: "Ledger ownership" +linkTitle: "Ledger ownership" +weight: 80 +description: > + Sign and authorise ledger writes — and change the mode on a live network without splitting it. +--- + +{{% pageinfo color="warning"%}} +`--ownership` is the one EdgeVPN setting that **every node on a network must +agree on**. Mixing `off` with `enforce` produces a network that looks up but +silently drops half its ledger. If you are upgrading an existing network, read +[Changing the mode on a live network](#changing-the-mode-on-a-live-network) +first. +{{% /pageinfo %}} + +Every peer that holds the network token can write to the +[ledger](../../explanation/the-ledger/). Without ownership, it can write to +*anyone's* entries: overwrite the `machines` record that maps an IP to a peer, +claim someone else's DNS name, or replace a service announcement. Ownership +closes that gap by binding each entry to the libp2p identity that wrote it. + +Under ownership each ledger entry carries the author's peer ID, a monotonic +version, a timestamp and an Ed25519 signature. Peers verify the signature using +the public key embedded in the author's peer ID — there is no PKI and no key to +distribute. A write to a key owned by a live peer is only accepted if it is +signed by that peer. + +This is **write integrity, not admission control**. The token is still the only +thing keeping outsiders out, and identities are free, so a token holder can +still create many identities and claim *unclaimed* names or addresses. +Ownership stops it from taking over entries that already belong to someone +else. See [the security model](../../explanation/security-model/) for the whole +picture, and [the authenticated ledger](../../explanation/authenticated-ledger/) +for the design. + +## The three modes + +```bash +sudo edgevpn --ownership enforce --token "$TOKEN" # the default +``` + +| Mode | Signs its own writes | Incoming unauthorised write | Use it for | +|---|---|---|---| +| `enforce` | yes | **rejected** and logged | normal operation (the default) | +| `observe` | yes | accepted, logged | migrating a live network | +| `off` | no | accepted (legacy whole-block replace) | staying compatible with pre-ownership nodes | + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--ownership` | `enforce` | `EDGEVPNOWNERSHIP` | `enforce`, `observe` or `off` | +| `--ownership-ttl` | `0` (derived: 8 minutes) | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds, after which an inactive owner's entries may be reclaimed or reaped. `0` derives it from the heartbeat interval | + +Values are matched case-insensitively, and each mode has aliases: `enforce` +and `on`; `observe`, `log` and `log-only`; `off`. Anything else is rejected at +startup, before the node joins a network: + +``` +invalid ownership mode "enabled": must be one of "off" (legacy, no +authentication), "observe" (sign and log violations; aliases "log", "log-only") +or "enforce" (sign and reject unauthorized writes; alias "on") +``` + +{{% alert title="Older releases disabled ownership on a typo" color="warning" %}} +Before this validation existed, an unrecognised value — `true`, `enabled`, or a +misspelling — fell through to `off` with no error and no warning, silently +turning ledger authentication off on a node the operator believed was enforcing +it. If you are on an older build, confirm the mode in the startup log rather +than trusting the flag. +{{% /alert %}} + +When ownership is active the node logs one line at startup: + +``` +ledger ownership enforcement: mode=enforce ttl=8m0s +``` + +If that line is absent, the node is running unsigned. Usually that means `off`, +but it is also what you see when enforcement was requested and the host private +key turned out to be unavailable — that case logs `ownership enforcement +requested but host private key is unavailable; running unsigned` instead, so +check for it before concluding the flag did not take. + +### Which entries are owned + +Ownership is per-bucket. These buckets are signed and owner-enforced: + +| Bucket | Owner | Expiry | +|---|---|---| +| `machines` | the `PeerID` in the value | while the owner's heartbeat is fresh | +| `services` | the `PeerID` in the value | while the owner's heartbeat is fresh | +| `files` | the `PeerID` in the value | while the owner's heartbeat is fresh | +| `users` | the key (a peer ID) | while the owner's heartbeat is fresh | +| `egress` | the key (a peer ID) | while the owner's heartbeat is fresh | +| `dns` | the first peer to claim the name | while the owner's heartbeat is fresh | +| `healthcheck` | the key (a peer ID) | `--ownership-ttl` after the entry's own timestamp | + +`Reclaimable` appears on the policy struct but the merge does not consult it, so +every bucket above becomes claimable once its owner's lease has lapsed. + +The `dhcp` bucket (IP-lease leader election) is deliberately left open: its +single `leader` key changes owner on every handoff, and readers already +cross-check it against the deterministic leader election. Any bucket not listed +here — including buckets your own application writes through the API — takes the +zero policy: no owner, no expiry, writable by any peer. Its entries are still +signed like every other write, but nothing verifies them; the only constraint +the merge applies is the version check, which accepts an incoming entry when its +version is strictly higher than the stored one. + +Ownership decides liveness from heartbeats, so the **alive service must be +running**. The VPN (`edgevpn`), `api`, `service-add`/`service-connect` and +`file-send`/`file-receive` all start it. Two commands do not: + +- `edgevpn start` only joins the network as a relay. It publishes no heartbeat, + and it claims nothing in an owned bucket either, so the omission costs it + nothing. +- `edgevpn proxy` publishes no heartbeat but *does* claim an owned entry. See + the known issue below. + +Embedders using the library must run the alive service themselves. + +{{% alert title="Known issue: `edgevpn proxy` claims an entry it cannot keep alive" color="warning" %}} +`services.Proxy` bundles the proxy service alone, with no alive service — yet +the proxy announces its own peer ID into `users`, a bucket that is owned with a +liveness expiry. With no heartbeat behind it, every other node reads that entry +as belonging to an inactive owner from the moment it appears, and the elected +leader tombstones it on each scrub. The proxy's next announce re-adds it, so the +entry churns for as long as the node runs. + +Nothing breaks outright — `users` gates *dialing* a service, file or egress, and +the proxy is the side doing the dialing — but the ledger carries an entry that is +permanently expired and permanently rewritten, and the tombstone traffic is real. +Until this is fixed, run `edgevpn proxy` on a host that also runs a command +which starts the alive service if the churn matters to you. This is a behaviour +question rather than a documentation one and is tracked separately. +{{% /alert %}} + +## Changing the mode on a live network + +Modes are not freely mixable. What one node accepts from another depends on +both nodes' modes: + +| Writer → Reader | `off` | `observe` | `enforce` | +|---|---|---|---| +| **`off`** | works | works (accepted, logged) | **silently dropped** | +| **`observe`** | works | works | works | +| **`enforce`** | works | works | works | + +An `off` node writes entries in the legacy bare-value format with no signature. +An `enforce` node rejects every one of them. Nothing errors, nothing exits — the +writes just never land. + +`observe` is compatible with **both** neighbours: it signs its own writes (so +`enforce` nodes accept them) and it accepts unsigned writes (so `off` nodes +still get through). That is what makes it the bridge. + +### The safe sequence + +1. **Move the whole network to `observe`.** Restart every node with + `--ownership observe`. This is safe to do one node at a time: an `observe` + node interoperates with the `off` nodes that have not been restarted yet. +2. **Let it run and watch the logs.** Expect a burst of + `ownership violation (observe, accepting): …` lines while `off` nodes are + still writing unsigned entries. Once every node is on `observe`, those lines + must stop. +3. **Only when the violation lines have stopped, move to `enforce`.** Restart + every node with `--ownership enforce`. Also safe one node at a time, because + `observe` and `enforce` nodes both sign. + +Going the other way (`enforce` → `off`) has the same hazard in reverse and wants +the same route: `enforce` → `observe` everywhere, then `off` everywhere. + +Two things make step 2 a stage to pass through rather than to camp in. + +- While legacy nodes remain, an `observe` node that is elected leader will + tombstone their heartbeats on every scrub. A legacy heartbeat carries no + signed timestamp, so it reads as infinitely old and is always past its + absolute expiry. The legacy node re-announces and reappears, but it flickers + in and out of the live set — and while it is out, its address, services and + DNS names are eligible for reaping and reclaim. +- If you persist the ledger with `--ledger-state`, entries written under `off` + are unsigned and survive the restart into `enforce`. (`observe` is not + affected: it installs a signer and signs every write, exactly as `enforce` + does — the difference is only whether violations are rejected or logged.) + Each unsigned entry is re-signed by the peer it names on that peer's next + announce, and until then the merge resolves the owner from the value, so + nobody else can take it. The exception is `dns`, whose values carry no peer + ID: with no owner to resolve, an unsigned DNS entry reads as unclaimed and + any peer can take the name before its rightful owner re-announces. Nodes on + the default in-memory ledger start clean and skip this entirely. + + {{% alert title="Older releases froze those entries" color="warning" %}} + A persisted unsigned entry used to be unreplaceable even by its own rightful + owner — the stored entry named no owner, the owner was still live, and the + signed update was rejected as `overwrite of a live entry owned by another + peer`, permanently. Deleting one behaved worse still: the delete took effect + on the owner's own node and was rejected everywhere else + (`tombstone by non-owner of a live entry`), so the network diverged with no + error shown on the node that issued it. If you are on an older build, clear + the state directory before restarting a node into `enforce`. + {{% /alert %}} + +### What it looks like when you get it wrong + +Suppose one node is left on `off` while the rest are on `enforce`. + +**On the `enforce` nodes**, every write from the `off` node is dropped and +logged at `warn` — visible at the default `--log-level info`: + +``` +ownership violation (rejected): machines/10.1.0.9 from : invalid signature +ownership violation (rejected): healthcheck/12D3KooWQCErhoGPk64ST3Cs6pz8kS1s7Eyd77SS7mLLaqaf8VYG from : invalid signature +``` + +The empty owner between `from` and `:` is the tell — a legacy entry has no owner +field at all. This is the signature of a mixed-mode network, as opposed to a +genuine ownership violation, which names the offending peer. + +Because the `healthcheck` write is rejected too, the `off` node never joins the +live set on those nodes. It is not merely unroutable, it is invisible. Start an +`enforce` node with `--api` and look for it: + +```bash +curl -s http://127.0.0.1:8080/api/ledger/machines # the off node's IP is absent +curl -s http://127.0.0.1:8080/api/ledger/healthcheck # the off node's peer ID is absent +``` + +Traffic to that address is dropped at the routing lookup, and any service, file +or DNS name it announces is never seen. + +**On the `off` node**, the failure looks completely different — and much more +confusing, because it logs nothing at all. An `off` node still adopts whole +blocks from the `enforce` nodes, so it *can* see them and their entries decode +fine. But adopting a block **replaces its entire local state**, wiping its own +entries; its announce loop then re-adds them, and the next incoming block wipes +them again. The result is a node that shows a plausible peer list, cannot be +reached by anyone, and produces no error. + +If you are diagnosing a network where "some nodes can't see each other", check +the ownership mode on every node before anything else. + +## Identity and restarts + +Ownership binds entries to a node's **libp2p identity**, which is not the +network token. By default that identity is **ephemeral**: it is regenerated from +`crypto/rand` on every start, so a restarted node comes back as a *different +owner*. Its old entries are orphaned, and it can only reclaim its address, +services and DNS names once the previous owner's lease expires — up to +`--ownership-ttl`. + +With ownership on and no persisted key, the node warns once at startup: + +``` +ownership enforcement is on with an ephemeral identity: this node's ledger +entries will be reclaimed after the liveness TTL on each restart. Use +--privkey-cache with a per-node --privkey-cache-dir for a stable identity. +``` + +For a long-lived node, persist the identity: + +```bash +sudo edgevpn --ownership enforce --privkey-cache \ + --privkey-cache-dir /var/lib/edgevpn/node1 --token "$TOKEN" +``` + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--privkey-cache` | off | `EDGEVPNPRIVKEYCACHE` | Persist the libp2p identity to disk and reuse it | +| `--privkey-cache-dir` | `$HOME/.edgevpn` | `EDGEVPNPRIVKEYCACHEDIR` | Where the key is stored (file `privkey`, mode `0600`) | + +{{% alert title="Give every process its own cache directory" color="warning" %}} +The default cache directory is shared per user. Two EdgeVPN processes on the +same machine — say `edgevpn` for the VPN and `edgevpn api` beside it — would +load the *same* key and join with the *same* peer ID, which breaks the network +far more thoroughly than an ephemeral identity does. This is why +`--privkey-cache` is never enabled automatically. Set a distinct +`--privkey-cache-dir` per process. +{{% /alert %}} + +Ephemeral identities are still fine for short-lived clients (`file-send`, +`file-receive`, `service-connect`) — they claim a slot, use it and leave. + +## The liveness window and reaping + +`--ownership-ttl` is the liveness window: a node is considered live for that +long after its last heartbeat. It drives three things. + +- **Routing.** Packets are not routed to an address whose owner's heartbeat has + gone stale. This takes effect immediately, without waiting for any cleanup. +- **Reclaim.** Once an owner's lease has expired, another peer may take over its + keys. This is how an address is recycled after a node leaves for good. +- **Reaping.** The elected leader periodically walks the ledger and writes + signed tombstones over entries whose owner is no longer live, then physically + prunes tombstones once they are old enough for everyone to have seen them. + This is what stops a long-running network from accumulating entries from + nodes that never came back. It replaces the older, blunter behaviour of + wiping the whole `healthcheck` bucket on a timer, and it runs only on the + leader, so there is no tombstone storm. + +A node that goes offline therefore disappears in stages: it stops being routed +within a TTL, its entries are tombstoned on the next leader scrub, and the +tombstones are pruned later. A node that comes back with the *same* identity +re-claims its own keys immediately, even over a tombstone. + +### Choosing a value + +Leave it at `0`. A node is declared dead purely on the age of its last +heartbeat, so the window has to cover the worst case gap between two heartbeats +— and `0` derives exactly that, as four times +`--aliveness-healthcheck-interval` (8 minutes on the stock 120-second +heartbeat). Retune the heartbeat and the window follows. + +Four intervals rather than one because the heartbeat runs on a jittered ticker: +each tick lands anywhere between 0.5× and 1.5× the configured interval, so a +perfectly healthy node can go 180 seconds between heartbeats on a 120-second +setting. Surviving one lost heartbeat therefore means covering 360 seconds of +silence — and the window has to be strictly longer than that rather than equal +to it, because a node whose last heartbeat is exactly one window old is already +expired. Four nominal intervals clears it with a full interval to spare. + +{{% alert title="Releases before this derived the window used 2 minutes" color="warning" %}} +That was exactly the nominal heartbeat interval and therefore *shorter* than the +180 seconds a healthy node can actually take. Live nodes were periodically +treated as inactive: packets to them dropped, their addresses became claimable +by other peers, and a leader scrub landing in the gap tombstoned their entries. +It self-corrected on the next announce, so it showed up as unexplained blips +rather than an outage. If you are on an older build, set `--ownership-ttl 480` +explicitly. +{{% /alert %}} + +Set an explicit value only if you have a reason to, and then set it on **every** +node. Unlike `--ownership`, the TTL is a local judgement about when a peer is +dead rather than a wire format, so nodes that disagree can still read each +other — but they will not agree on what the ledger says. The merge is per-key +and nothing reconciles whole blocks afterwards, so a node with a shorter window +declares an owner dead, stops routing to it and lets its entries be reclaimed or +reaped, while a node with a longer window still routes to that owner. The two +stay split until the owner re-announces. A longer window also means dead nodes +linger; a shorter one risks evicting live ones, and anything at or below three +heartbeat intervals will. + +## Turning it off + +```bash +sudo edgevpn --ownership off --token "$TOKEN" +``` + +You want this only to interoperate with nodes that predate ownership, and only +after moving the *entire* network through `observe` as described above. With +ownership off, any token holder can overwrite any entry. + +Embedders are unaffected by default: `node.New` without `WithOwnership` stays +`off` and emits the exact legacy encoding, so a library user opts in +deliberately. + +## Where next + +- [The authenticated ledger](../../explanation/authenticated-ledger/) — the + design: canonical signing bytes, the merge rules, the policy registry and the + reaper. +- [The ledger](../../explanation/the-ledger/) — what the ledger stores. +- [The security model](../../explanation/security-model/) — what the token + protects and what it does not. diff --git a/docs/content/en/docs/how-to/persist-node-identity.md b/docs/content/en/docs/how-to/persist-node-identity.md new file mode 100644 index 00000000..25acce3e --- /dev/null +++ b/docs/content/en/docs/how-to/persist-node-identity.md @@ -0,0 +1,36 @@ +--- +title: "Persist node identity and state" +linkTitle: "Persist node identity" +weight: 120 +description: > + Placeholder — keeping a node's peer ID and ledger across restarts. Not written yet. +--- + +{{% pageinfo color="warning" %}} +**This page has not been written.** By default an EdgeVPN node generates a fresh +libp2p key on every start, so its peer ID changes on every restart. Under +`--ownership enforce` (the default) that orphans everything the node had +announced. The flags that change this are documented only by their one-line +`--help` text. + +What is missing, and where the source is: + +- **`--privkey-cache`** (env `EDGEVPNPRIVKEYCACHE`, off by default, marked + experimental) and **`--privkey-cache-dir`** (env `EDGEVPNPRIVKEYCACHEDIR`, + defaulting to `$HOME/.edgevpn`). The implementation is in `cmd/util.go`: it + reads or generates `/privkey`, writing the directory `0700` and the file + `0600`. The comment above it explains why it is not enabled automatically — + the default directory is per-user, so two co-located EdgeVPN processes sharing + it would boot with the *same* peer ID. Each node needs its own directory. +- **The interaction with ownership.** `cmd/util.go` emits a warning when + ownership enforcement is on and the identity is ephemeral, because the node's + entries are reclaimed after the liveness TTL on every restart. See + [ledger ownership](../ledger-ownership/). +- **`--ledger-state`** (env `EDGEVPNLEDGERSTATE`) is a different thing that gets + confused with the above: it points the ledger at a `DiskStore` + (`pkg/blockchain/store_disk.go`) instead of the default in-memory store. It + persists the block chain, not the identity. +- **Backup, rotation and revocation** of a cached key: not addressed anywhere. + +Contributions welcome — see [contributing](../../contributing/). +{{% /pageinfo %}} diff --git a/docs/content/en/docs/how-to/relays-and-hop-nodes.md b/docs/content/en/docs/how-to/relays-and-hop-nodes.md new file mode 100644 index 00000000..deef7b29 --- /dev/null +++ b/docs/content/en/docs/how-to/relays-and-hop-nodes.md @@ -0,0 +1,238 @@ +--- +title: "Relays and hop nodes" +linkTitle: "Relays and hop nodes" +weight: 75 +description: > + Run a node that carries no VPN traffic of its own but helps everyone else connect. +--- + +Two nodes behind NAT cannot always dial each other. EdgeVPN tries hole punching +first (`--holepunch`, on by default), but hole punching needs a third party that +both peers can already reach, and it does not work through every NAT. A **relay** +is that third party: a node on a reachable address that other peers connect +through. + +A relay does not need a VPN interface, an IP on the virtual network, or root. +`edgevpn start` is the command for it: + +```bash +edgevpn start --token "$TOKEN" +``` + +That joins the p2p network — gossip, ledger, discovery, relay service — and +stops there. No TUN device is created, so unlike `edgevpn` it runs unprivileged. + +## What a relay actually carries + +This is worth being precise about, because "relay" suggests more than EdgeVPN +currently does with it. + +- **Gossip and the ledger travel over relays.** The pubsub layer explicitly opts + into relayed (*limited*) connections, so a peer that can only be reached + through a relay still sees the ledger, still announces itself, and is still + discovered by everyone else. +- **Relays are the rendezvous for hole punching.** Once two peers are connected + through a circuit, libp2p's DCUtR runs over it and tries to upgrade to a direct + connection. When that succeeds, the relay drops out of the path and the VPN + works over the direct link. +- **VPN frames do not travel over relays.** The data plane opens its streams from + a context that does not permit limited connections (`pkg/vpn/vpn.go`), so a + packet destined for a peer reachable *only* through a circuit fails with: + + ``` + could not open stream to 12D3KooW…: limited connection to peer + ``` + + If you see that line, the relay is doing its job and hole punching is not. + `--transient-conn` on the root command does not change it: it marks the node's + own start context, which the frame path does not use. + +So a relay fixes discovery and gives hole punching a chance. It is not a fallback +data path for the VPN. + +## Running one + +Put the relay somewhere with an address other peers can reach — a public IP, or +a port-forwarded host — and pin the port so the address is stable: + +```bash +edgevpn start \ + --token "$TOKEN" \ + --listen-maddrs /ip4/0.0.0.0/tcp/4501 \ + --privkey-cache --privkey-cache-dir /var/lib/edgevpn/relay +``` + +`--privkey-cache` matters more here than on an ordinary node: peers that pin this +relay do so **by peer ID**, and without a persisted key the node comes back with a +different identity after every restart, invalidating every `--autorelay-static-peer` +entry pointing at it. Give each process its own directory — the default is shared +per user, and two processes sharing a key join with the same peer ID. + +The two lines you need are printed at startup: + +``` +Node ID: 12D3KooWRCbBhhGGowBmt1kMEoSqTzEE8o5mMPNPo8WbwCuqsqJz +Node Addresses: [/ip4/10.9.0.23/tcp/4501 /ip4/127.0.0.1/tcp/4501 …] +``` + +Combine the reachable address with the ID to get the multiaddr other nodes will +use: `/ip4/198.51.100.7/tcp/4501/p2p/12D3KooWRCbB…`. + +If the relay is behind a static port-forward, the addresses it discovers locally +are private ones. `--dht-announce-maddrs` replaces what it publishes on the DHT: + +```bash +edgevpn start --token "$TOKEN" \ + --listen-maddrs /ip4/0.0.0.0/tcp/4501 \ + --dht-announce-maddrs /ip4/198.51.100.7/tcp/4501 +``` + +{{% alert title="A relay must run the network's ownership mode" color="warning" %}} +A relay holds and gossips the ledger like any other node, so `--ownership` has to +match the rest of the network. A relay left on the wrong mode drops or is dropped +by everyone else's writes. See [ledger ownership](../ledger-ownership/) and the +[compatibility matrix](../../reference/compatibility/). +{{% /alert %}} + +`edgevpn start` takes the common flags only — there is no `--api` on it, so a +relay cannot expose the inspection API. To watch a network's state, run +`edgevpn api` on a node that has it. + +## Choosing relays from the client side + +Every node is an AutoRelay client by default (`--autorelay`, default on). It +finds candidate relays by asking the DHT for the peers closest to itself and +offering those to libp2p, which reserves a slot on the ones that accept. + +To pin specific relays instead, list them as multiaddrs: + +```bash +sudo edgevpn --token "$TOKEN" \ + --autorelay-static-peer /ip4/198.51.100.7/tcp/4501/p2p/12D3KooWRCbB… \ + --autorelay-static-peer /ip4/198.51.100.8/tcp/4501/p2p/12D3KooWabcd… +``` + +Static peers are *added* to the DHT-discovered ones. `--autorelay-static-only` +drops the DHT lookup and offers only the peers you listed — the setting to use +when you want traffic to leave through hosts you control. + +{{% alert title="Two things to know about the autorelay flags" color="warning" %}} +- **`--autorelay-discovery-interval` currently does nothing.** It is parsed and + stored, but the value is never handed to libp2p — the autorelay option that + once took it no longer exists in the version EdgeVPN builds against. Setting it + has no effect in either direction. +- **Do not combine `--dht=false` with dynamic autorelay.** Relay candidate + discovery queries the DHT, and with the DHT disabled that lookup dereferences a + nil pointer and takes the process down when libp2p first asks for candidates. + If you turn the DHT off, either pass `--autorelay-static-only` together with + `--autorelay-static-peer`, or turn autorelay off with `--autorelay=false`. +{{% /alert %}} + +## Serving as a relay: the resource limits + +`--relay-service` (default on) is what makes a node accept reservations and carry +other peers' traffic. Turning it off does **not** stop the node from *using* other +relays — that is `--autorelay` — so a resource-constrained edge device can keep +its own NAT traversal while refusing to carry anyone else's traffic: + +```bash +sudo edgevpn --token "$TOKEN" --relay-service=false +``` + +EdgeVPN deliberately runs wider limits than libp2p's stock circuit-v2 defaults, +because a cluster peer relaying for another cluster peer is a different threat +model from a public relay serving the open internet. The *direction* is the part +worth knowing; for the values, read the +[`start` reference](../../reference/cli/start/) or +[environment variables](../../reference/environment-variables/), which are +generated from the binary and cannot drift from it. + +| Knob | What it bounds | libp2p stock | EdgeVPN | Why | +|---|---|---|---|---| +| `--relay-service-max-data` | bytes per direction on one circuit | 128 KiB | raised | 128 KiB resets a circuit almost immediately; cluster transfers (container images, model files) need room | +| `--relay-service-max-duration` | lifetime of one circuit | 2 minutes | raised | long-running relayed sessions instead of forced resets | +| `--relay-service-max-circuits` | concurrent circuits **per peer** | 16 | raised | one peer may hold more simultaneous circuits through this node — it does not change how many peers may use it | +| `--relay-service-buffer-size` | buffer held per circuit | 2 KiB | raised | throughput on large relayed transfers | +| `--relay-service-reservation-ttl` | how long a reservation lasts | 1 hour | unchanged | reservation churn is already tolerable at an hour | + +Every one of these costs memory per circuit, so a node that raises them and then +carries many circuits pays for all of them at once. Lower them on small nodes; +`--relay-service=false` is the blunt version. + +**How many peers** a relay serves is a different limit, and it is not tunable. +The reservation caps are not exposed as flags and stay at libp2p's defaults: 128 +reservations in total, one per peer, 8 per IP and 32 per ASN. A single relay +therefore serves at most 128 peers no matter how the flags above are set — +raising `--relay-service-max-circuits` buys each of those peers more concurrent +circuits, not more peers. + +## Restricting who may reserve + +`--relay-service-network-only` (default **on**) only lets peers already seen in +the local ledger's alive bucket reserve a slot. Strangers who found the node +through the public DHT are refused, so the relay's bandwidth is spent on the +cluster rather than on the internet at large. + +Membership is re-snapshotted from the ledger every +`--relay-service-acl-refresh` (30s by default; keep it at or below the aliveness +announce interval so churn is picked up within a tick or two). + +Two behaviours follow from how that set is built, and both matter in practice: + +- **The ACL is open until the set is non-empty.** A fresh relay that has not yet + seen anyone's heartbeat admits everyone; that bootstrap window is deliberate, + since a node cannot prove membership before it has joined. If the alive bucket + never fills — nobody running the aliveness service — the ACL stays open forever + and logs a line per refresh at debug level. +- **An `edgevpn start` node never appears in that set.** `start` runs no + aliveness service and publishes no heartbeat, so it is invisible to every + network-only ACL on the network. That is harmless for a relay, which *accepts* + reservations rather than making them, but it means a `start` node behind NAT + cannot reserve a slot on someone else's network-only relay. Relays belong on + reachable hosts anyway. + +To open a relay to peers outside the cluster, pass +`--relay-service-network-only=false` — and read +[the security model](../../explanation/security-model/#relay-acls-protect-your-bandwidth) +first. Note that this is a bandwidth-abuse control, not access control: everyone +it admits already holds the token. + +## Checking it works + +On the relay, at the default `--log-level info`, startup prints the node ID, the +listen addresses, and the ownership mode. Two more lines are worth recognising +because they look like problems and are not: + +``` +connmanager disabled + go-libp2p resource manager protection disabled +``` + +Both are the defaults: connection watermarks are off until you set +`--connection-low-water` *and* `--connection-high-water` (both, to non-zero), and +the libp2p resource manager is off until `--limit-enable` — and stays off on +macOS regardless. On a relay carrying other peers' traffic, both are worth +turning on. + +Reservation activity is logged by libp2p itself, not by EdgeVPN, so raise +`--libp2p-log-level` (default `fatal`) to see it: + +```bash +edgevpn start --token "$TOKEN" --libp2p-log-level info +``` + +On a client, the sign that relaying is in use is a peer address containing +`/p2p-circuit`. The sign that it is *stuck* there — that hole punching never +completed — is the `limited connection to peer` line above. + +## Where next + +- [`edgevpn start` reference](../../reference/cli/start/) — the generated flag + list, always in sync with the binary. +- [Version and wire-format compatibility](../../reference/compatibility/) — before + you upgrade a relay separately from the rest of the network. +- [Ledger ownership](../ledger-ownership/) — the one setting every node, + relays included, must agree on. +- [The security model](../../explanation/security-model/) — what the relay ACL + does and does not protect. +- [Troubleshooting](../../troubleshooting/) — nodes that never see each other. diff --git a/docs/content/en/docs/Getting started/cli.md b/docs/content/en/docs/how-to/run-as-a-vpn.md similarity index 75% rename from docs/content/en/docs/Getting started/cli.md rename to docs/content/en/docs/how-to/run-as-a-vpn.md index 6bd6b8cc..ea6134ba 100644 --- a/docs/content/en/docs/Getting started/cli.md +++ b/docs/content/en/docs/how-to/run-as-a-vpn.md @@ -1,12 +1,13 @@ --- -title: "CLI" -linkTitle: "CLI" -weight: 1 +title: "Run as a VPN" +linkTitle: "Run as a VPN" +weight: 10 +aliases: + - /docs/getting-started/cli/ description: > - Command line interface + Join a network as a VPN peer and route traffic between nodes. --- - To start the VPN, simply run `edgevpn` without any argument. An example of running edgevpn on multiple hosts: @@ -25,8 +26,10 @@ $ EDGEVPNTOKEN=.. edgevpn --address 10.1.0.13/24 *Note*: It might take up time to build the connection between nodes. Wait at least 5 mins, it depends on the network behind the hosts. -The VPN takes several options, below you will find a reference for the most important features: - +For how addresses are handed out, how to let peers negotiate them among +themselves, and how to pin a static routing table, see +[Addressing and DHCP](../addressing-and-dhcp/). For IPv6, see +[IPv6](../ipv6/). ## Generate a network token @@ -41,6 +44,9 @@ $ edgevpn -g -b b3RwOgogIGRodDoKICAgIGludGVydmFsOiA5MDAwCiAgICBrZXk6IDRPNk5aUUMyTzVRNzdKRlJJT1BCWDVWRUkzRUlKSFdECiAgICBsZW5ndGg6IDMyCiAgY3J5cHRvOgogICAgaW50ZXJ2YWw6IDkwMDAKICAgIGtleTogN1hTUUNZN0NaT0haVkxQR0VWTVFRTFZTWE5ORzNOUUgKICAgIGxlbmd0aDogMzIKcm9vbTogWUhmWXlkSUpJRlBieGZDbklLVlNmcGxFa3BhVFFzUk0KcmVuZGV6dm91czoga1hxc2VEcnNqbmFEbFJsclJCU2R0UHZGV0RPZGpXd0cKbWRuczogZ0NzelJqZk5XZEFPdHhubm1mZ3RlSWx6Zk1BRHRiZGEKbWF4X21lc3NhZ2Vfc2l6ZTogMjA5NzE1MjAK ``` +The fields of that configuration are documented in +[Network configuration and tokens](../../reference/network-config/). + A network token needs to be specified for all later interactions with edgevpn, in order to connect and establish a network connection between peers. For example, to start `edgevpn` in API mode: @@ -51,7 +57,6 @@ $ edgevpn api --token # or alternatively using $EDGEVPNTOKEN This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. - INFO Version: v0.8.4 commit: INFO Starting EdgeVPN network INFO Node ID: 12D3KooWRW4RXSMAh7CTRsTjX7iEjU6DEU8QKJZvFjSosv7zCCeZ INFO Node Addresses: [/ip6/::1/tcp/38637 /ip4/192.168.1.234/tcp/41607 /ip4/127.0.0.1/tcp/41607] @@ -69,21 +74,5 @@ $ EDGEVPNTOKEN=$(edgevpn -g | tee config.yaml | base64 -w0) ## API -While starting in VPN mode, it is possible _also_ to start in API mode by specifying `--api`. - -## DHCP - -Note: Experimental feature! - -Automatic IP negotiation is available since version `0.8.1`. - -DHCP can be enabled with `--dhcp` and `--address` can be omitted. If an IP is specfied with `--address` it will be the default IP. - -## IPv6 (experimental) - -Node: Very experimental feature! Highly unstable! - -Very provisional support for IPv6 is available using static addresses only. Currently only one address is supported per interface, dual stack is not available. -For more information, checkout [issue #15](https://github.com/mudler/edgevpn/issues/15) - -IPv6 can be enabled with `--address fd:ed4e::/64` and `--mtu >1280`. +While starting in VPN mode, it is possible _also_ to start in API mode by +specifying `--api`. See [WebUI and API](../../reference/api/). diff --git a/docs/content/en/docs/how-to/run-with-docker.md b/docs/content/en/docs/how-to/run-with-docker.md new file mode 100644 index 00000000..6a36ff3f --- /dev/null +++ b/docs/content/en/docs/how-to/run-with-docker.md @@ -0,0 +1,210 @@ +--- +title: "Run with Docker" +linkTitle: "Run with Docker" +weight: 110 +description: > + Run a VPN node in a container — why it needs host networking, NET_ADMIN and /dev/net/tun, and what the repository's compose file actually does. +--- + +The repository ships a +[`docker-compose.yml`](https://github.com/mudler/edgevpn/blob/master/docker-compose.yml) +that brings up a single VPN node. It is short, and every line in it is there for +a reason. This page explains those reasons, so you can adapt it instead of +copying it. + +For the published image and its tags — in particular why `:latest` is a +development build and not the newest release — see +[install](../../tutorials/install/#container-image). Everything below assumes +you have picked a tag. + +{{% pageinfo color="warning" %}} +Running the VPN in a container gives it the same reach as running it on the +host: with `network_mode: host` and `NET_ADMIN` it creates a systemwide network +interface and can reconfigure host networking. The container boundary is not +buying you isolation here. +{{% /pageinfo %}} + +## The compose file, line by line + +```yaml +services: + edgevpn: + image: quay.io/mudler/edgevpn:latest + pull_policy: always + container_name: edgevpn + restart: unless-stopped + volumes: + - /home/CHANGEME/.edgevpn:/root/.edgevpn + environment: + - EDGEVPNTOKEN=CHANGEME + network_mode: host + devices: + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + healthcheck: + test: ["CMD", "sh", "-c", "ifconfig | grep -q edgevpn0"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +Two values are marked `CHANGEME` and the file does not work until you replace +both: the token, and the host path for the volume. + +### `network_mode: host` + +Required, and it is the setting people most often try to remove. EdgeVPN creates +a TUN interface (`edgevpn0` by default) inside whatever network namespace it is +running in. In a normal bridged container that namespace belongs to the +container alone: the interface comes up, the node joins the network, and nothing +on the host — or in any other container — can send a packet through it. Sharing +the host namespace is what makes `edgevpn0` a systemwide interface. + +It has a second effect: mDNS peer discovery (`--mdns`, on by default) is +announced on the host's LAN interfaces rather than on a private bridge, so +local peers can find each other without going through the DHT. + +The trade-off is that `--api-listen` and any port EdgeVPN binds land directly on +the host's ports. The API default (`127.0.0.1:8080`, only with `--api`) stays on +host loopback, which is what you want — see +[the API has no authentication](../../reference/api/#the-api-has-no-authentication). + +### `devices: /dev/net/tun` + +The TUN/TAP character device. Without it there is nothing to open, so the +interface cannot be created and bringing up the VPN fails. Passing the device is +not enough on its own — opening it and configuring the resulting interface also +needs the capability below. + +### `cap_add: NET_ADMIN` + +`CAP_NET_ADMIN` is what allows creating the TUN interface, assigning it an +address and bringing it up. Docker drops it from the default capability set, so +it has to be added back explicitly. It is the *only* extra capability needed — +if you find yourself reaching for `privileged: true`, you have a different +problem. + +### `volumes: .../.edgevpn:/root/.edgevpn` + +`/root/.edgevpn` is where EdgeVPN's state lives inside the container: the image +has no `USER`, so the process runs as root, and `--privkey-cache-dir` defaults +to `$HOME/.edgevpn`. + +{{% pageinfo color="warning" %}} +**The volume alone does not persist anything.** `--privkey-cache` is off by +default, and the compose file leaves the line that enables it commented out. As +written, the container generates a fresh libp2p identity — a new peer ID — on +every restart, and the mounted directory stays empty. +{{% /pageinfo %}} + +That matters because `--ownership` defaults to `enforce`, where a node's ledger +entries are tied to its identity. A node that changes peer ID on every restart +orphans its `machines`, `services` and `dns` entries each time; they are only +reclaimed once the previous identity's liveness window expires. EdgeVPN logs a +warning saying exactly this at startup. + +To actually get a stable identity, uncomment the `entrypoint` line — or, more +simply, append the flags with `command:`, which keeps the image's own entrypoint: + +```yaml + command: + - --address=10.1.0.11/24 + - --privkey-cache + - --privkey-cache-dir=/root/.edgevpn +``` + +The image's `ENTRYPOINT` is `/usr/bin/edgevpn`, so `command:` entries are +appended to it as arguments. Give each node its own cache directory — two +processes sharing one would boot with the same peer ID. + +### `environment: EDGEVPNTOKEN` + +Every EdgeVPN flag has a matching environment variable, which is what makes the +container usable without an entrypoint override at all. `EDGEVPNTOKEN` is +`--token`; `ADDRESS` is `--address`; `EDGEVPNLOWPROFILE` is `--low-profile`, and +so on. The full mapping is the +[environment variables reference](../../reference/environment-variables/). +Flags win over environment variables when both are set. + +Keep the token out of the compose file itself — put it in a `.env` file next to +it, or use a secrets mechanism. Anyone holding it is a full member of the +network. + +### `healthcheck` + +```yaml +test: ["CMD", "sh", "-c", "ifconfig | grep -q edgevpn0"] +``` + +`ifconfig` comes from busybox in the `alpine` base image, so the check needs no +extra tooling. It asks one question: does an interface whose name contains +`edgevpn0` exist? With `start_period: 40s` the container is given 40 seconds to +come up before failures count, then a failing check three times at 30-second +intervals marks it unhealthy. + +Be clear about what this does and does not tell you: + +- It does **not** check that the node has peers, that the ledger is syncing, or + that traffic is flowing. An isolated node with a live interface is healthy by + this definition. +- Because the container shares the host's network namespace, the check passes if + *anything* on the host has created an `edgevpn0` interface — including a + different EdgeVPN process, or a stale interface left behind by a previous run. +- `restart: unless-stopped` restarts the container when the process exits. Docker + does not restart containers for being unhealthy, so an unhealthy-but-running + node stays up until something acts on the status. + +If you have `--api` enabled, `/api/summary` is a much better liveness signal — +it reports peer and machine counts, not just the presence of an interface. + +### `pull_policy: always` + +Combined with the pinned `:latest`, this pulls a new image on every `up`. Since +`:latest` tracks `master`, that means an unattended restart can move you onto a +different development build. Pin a release tag if you do not want that. + +## Running it + +```bash +docker compose up --detach +docker compose logs -f +``` + +Creating the interface needs root on the host (or membership of the `docker` +group, which is equivalent). Check the result from the host, not from inside the +container — with host networking they are the same namespace anyway: + +```bash +ip addr show edgevpn0 +``` + +## Running more than one node on a host + +`network_mode: host` puts every container in the same network namespace, so a +second node using the defaults collides with the first on the `edgevpn0` +interface name. Give each one its own: + +- `--interface` (env `IFACE`, default `edgevpn0`) +- `--address` (env `ADDRESS`, default `10.1.0.1/24`) — a distinct VPN address +- `--privkey-cache-dir`, if you enable `--privkey-cache`; two processes sharing + one directory load the same key and appear on the network as one peer ID +- `--api-listen` (env `APILISTEN`, default `127.0.0.1:8080`), if you enable + `--api` on more than one + +The [systemd template unit](../../tutorials/install/#running-it-as-a-service) +that `install.sh` writes is built for exactly this shape, and is usually less +work. + +## Sub-commands that need none of this + +`edgevpn file-send`, `edgevpn file-receive`, `edgevpn service-add`, +`edgevpn service-connect` and `edgevpn proxy` never create a network interface. +They need no capability, no device and no host networking — just the token and, +for the ones that listen locally, a published port: + +```bash +docker run --rm -e EDGEVPNTOKEN= -p 127.0.0.1:9090:9090 \ + quay.io/mudler/edgevpn:v0.35.3 service-connect --name mysvc --address :9090 +``` diff --git a/docs/content/en/docs/how-to/run-with-systemd.md b/docs/content/en/docs/how-to/run-with-systemd.md new file mode 100644 index 00000000..616e3f15 --- /dev/null +++ b/docs/content/en/docs/how-to/run-with-systemd.md @@ -0,0 +1,34 @@ +--- +title: "Run with systemd" +linkTitle: "Run with systemd" +weight: 130 +description: > + Placeholder — running EdgeVPN as a systemd service, including API socket activation. Not written yet. +--- + +{{% pageinfo color="warning" %}} +**This page has not been written.** There is no systemd guide on this site +beyond the short +[template-unit section of the install page](../../tutorials/install/#running-it-as-a-service), +which covers only the `edgevpn@.service` unit that `install.sh` drops in. + +What is missing, and where the source is: + +- **API socket activation.** `api/api.go` (`systemdSocketListener`) reads + `LISTEN_PID` and `LISTEN_FDS` and, when `LISTEN_PID` matches the process and + `LISTEN_FDS` is exactly `1`, adopts the already-bound listener on FD 3 instead + of binding one itself. The socket's path, owner, group and mode are then + entirely whatever the `.socket` unit declares — EdgeVPN deliberately does not + chmod or unlink it. No example `.socket`/`.service` pair is documented + anywhere. +- **`APILISTENUNIXMODE`.** Read by `unixSocketMode` in `api/api.go`, it sets the + mode only on the path where EdgeVPN creates the socket itself + (`--api-listen unix:///run/edgevpn.sock`). It defaults to `0660` and silently + falls back to that default if the value is not valid octal. It has no effect + under socket activation. +- **Hardening a unit.** `NET_ADMIN`, `/dev/net/tun`, `DynamicUser`, and which + of the [environment variables](../../reference/environment-variables/) belong + in an `EnvironmentFile` rather than the unit. + +Contributions welcome — see [contributing](../../contributing/). +{{% /pageinfo %}} diff --git a/docs/content/en/docs/Concepts/Overview/files.md b/docs/content/en/docs/how-to/send-and-receive-files.md similarity index 67% rename from docs/content/en/docs/Concepts/Overview/files.md rename to docs/content/en/docs/how-to/send-and-receive-files.md index 364f9148..c9cf60c7 100644 --- a/docs/content/en/docs/Concepts/Overview/files.md +++ b/docs/content/en/docs/how-to/send-and-receive-files.md @@ -1,12 +1,14 @@ --- -title: "Sending and receiving files" -linkTitle: "File transfer" -weight: 20 -date: 2017-01-05 +title: "Send and receive files" +linkTitle: "Send and receive files" +weight: 50 +aliases: + - /docs/concepts/overview/files/ description: > - Send and receive files between p2p nodes + Transfer files directly between peers, without bringing up a VPN interface. --- + ## Sending and receiving files EdgeVPN can be used to send and receive files between hosts via p2p with the `file-send` and `file-receive` subcommand. diff --git a/docs/content/en/docs/Concepts/Overview/peerguardian.md b/docs/content/en/docs/how-to/trusted-networks.md similarity index 74% rename from docs/content/en/docs/Concepts/Overview/peerguardian.md rename to docs/content/en/docs/how-to/trusted-networks.md index 87816962..5f45ee82 100644 --- a/docs/content/en/docs/Concepts/Overview/peerguardian.md +++ b/docs/content/en/docs/how-to/trusted-networks.md @@ -1,24 +1,38 @@ --- -title: "Peerguardian" -linkTitle: "Peerguardian" -weight: 25 -date: 2022-01-05 +title: "Trusted networks" +linkTitle: "Trusted networks" +weight: 90 +aliases: + - /docs/concepts/overview/peerguardian/ description: > - Prevent unauthorized access to the network if tokens are leaked + Restrict a network to authorized peers with PeerGuardian and peergating. --- + {{% pageinfo color="warning"%}} Experimental feature! {{% /pageinfo %}} +Trust zones are the only mechanism in EdgeVPN that can exclude a peer holding a +valid network token *at runtime* — everything else, the token itself, ledger +ownership and relay ACLs, assumes every token holder is a full member. If your +membership is a fixed set of hosts, `--static-peertable` excludes token holders +too, without being experimental; see +[static peer tables](../../explanation/security-model/#static-peer-tables). + +Read [the security model](../../explanation/security-model/) first either way. +It covers what peergating adds, what it deliberately does not add, and the +admission gap in the current implementation that you should weigh before +relying on it. + ## Peerguardian PeerGuardian is a mechanism to prevent unauthorized access to the network if tokens are leaked or either revoke network access. -In order to enable it, start edgevpn nodes adding the `--peerguardian` flag. +In order to enable it, start edgevpn nodes adding the `--peerguard` flag. ```bash -edgevpn --peerguardian +edgevpn --peerguard ``` To turn on peer gating, specify also `--peergate`. @@ -60,12 +74,15 @@ Now the private key can be used while starting new nodes: ```bash PEERGATE_AUTH="{ 'ecdsa' : { 'private_key': 'LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQkhUZnRSTVZSRmlvaWZrdllhZEE2NXVRQXlSZTJSZHM0MW1UTGZlNlRIT3FBTTdkZW9sak0KZXVPbTk2V0hacEpzNlJiVU1tL3BCWnZZcElSZ0UwZDJjdUdnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdVWStMNQptUzcvVWVoSjg0b3JieGo3ZmZUMHBYZ09MSzNZWEZLMWVrSTlEWnR6YnZWOUdwMHl6OTB3aVZxajdpMDFVRnhVCnRKbU1lWURIRzBTQkNuVWpDZ0FGT3ByUURpTXBFR2xYTmZ4LzIvdEVySDIzZDNwSytraFdJbUIza01QL2tRNEIKZzJmYnk2cXJpY1dHd3B4TXBXNWxKZVZXUGlkeWJmMSs0cVhPTWdQbmRnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=' } }" -$ edgevpn --peerguardian --peergate +$ edgevpn --peerguard --peergate ``` ## Enabling/Disabling peergating in runtime -Peergating can be disabled in runtime by leveraging the api: +Peergating can be disabled in runtime by leveraging the api. + +These routes are registered only on nodes started with `--peerguard`: on a node +without it they return `404`, because there is no peer gater to query. ### Query status @@ -88,10 +105,10 @@ $ curl -X PUT 'http://localhost:8080/api/peergate/disable' To init a new Trusted network, start nodes with `--peergate-relaxed` and add the neccessary auth keys: ```bash -$ edgevpn --peerguardian --peergate --peergate-relaxed +$ edgevpn --peerguard --peergate --peergate-relaxed $ curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/keytype_1/XXX' ``` {{% alert title="Note" %}} -It is strongly suggested to use a local store for the blockchain with PeerGuardian. In this way nodes persist locally auth keys and you can avoid starting nodes with `--peergate-relaxed' +It is strongly suggested to use a local store for the ledger with PeerGuardian (`--ledger-state `). In this way nodes persist auth keys locally and you can avoid starting nodes with `--peergate-relaxed`. {{% /alert %}} diff --git a/docs/content/en/docs/how-to/tune-for-low-end-devices.md b/docs/content/en/docs/how-to/tune-for-low-end-devices.md new file mode 100644 index 00000000..11c6a60b --- /dev/null +++ b/docs/content/en/docs/how-to/tune-for-low-end-devices.md @@ -0,0 +1,42 @@ +--- +title: "Tune for low-end devices" +linkTitle: "Tune for low-end devices" +weight: 140 +description: > + Placeholder — cutting memory, connection and file-descriptor usage on small hardware. Not written yet. +--- + +{{% pageinfo color="warning" %}} +**This page has not been written.** EdgeVPN has a full set of resource-limiting +flags and none of them are explained beyond their `--help` line. There is no +guidance on which to reach for on a Raspberry Pi, a router or a container with a +tight memory cgroup. + +What is missing, and where the source is: + +- **`--low-profile`** (env `EDGEVPNLOWPROFILE`) is **on by default**, which is + itself undocumented, and its name promises more than it does: in + `pkg/config/config.go` the flag's only effect is `dht.BucketSize(20)`. A + second, unrelated `vpn.LowProfile` library option in `pkg/vpn/config.go` + swaps in a bounded stream manager (`pkg/vpn/vpn.go`) and is *not* wired to the + CLI flag. The difference needs writing up. +- **The ten `limit-*` flags** in `cmd/util.go`, which configure the libp2p + resource manager: `--limit-enable` (off by default — the others do nothing + until it is on), `--limit-file`, `--limit-scope`, `--limit-config-streams`, + `--limit-config-streams-inbound`, `--limit-config-streams-outbound`, + `--limit-config-conn`, `--limit-config-conn-inbound`, + `--limit-config-conn-outbound` and `--limit-config-fd`. Their defaults + (200/30/30, 200/30/30, 30) are not documented and their relationship to + `--limit-scope` is not explained. +- **Connection water marks.** `--connection-high-water` and + `--connection-low-water` (env `EDGEVPN_CONNECTION_HIGH_WATER` / + `EDGEVPN_CONNECTION_LOW_WATER`) both default to `0`, and what `0` means is not + stated anywhere. `--max-connections` (env `EDGEVPNMAXCONNS`) is a third knob + in the same area. +- **What to turn off.** `--dht`, `--mdns`, `--natservice`, `--natmap`, + `--autorelay` and `--relay-service` are all on by default and all cost + something; `--relay-service=false` in particular stops the node carrying other + peers' traffic. See [relays and hop nodes](../relays-and-hop-nodes/). + +Contributions welcome — see [contributing](../../contributing/). +{{% /pageinfo %}} diff --git a/docs/content/en/docs/Concepts/Overview/services.md b/docs/content/en/docs/how-to/tunnel-tcp-services.md similarity index 73% rename from docs/content/en/docs/Concepts/Overview/services.md rename to docs/content/en/docs/how-to/tunnel-tcp-services.md index 44ffe170..9b27d41b 100644 --- a/docs/content/en/docs/Concepts/Overview/services.md +++ b/docs/content/en/docs/how-to/tunnel-tcp-services.md @@ -1,11 +1,16 @@ --- -title: "Tunnel connections" -linkTitle: "Tunnelling" -weight: 1 +title: "Tunnel TCP services" +linkTitle: "Tunnel TCP services" +weight: 60 +aliases: + - /docs/concepts/overview/services/ description: > - EdgeVPN network services for tunnelling TCP services + Expose a local or remote TCP service to the network and connect to it from another peer. --- +If you have not done this before, the step-by-step version is +[Share a service between two hosts](../../tutorials/share-a-service/). + ## Forwarding a local connection EdgeVPN can also be used to expose local(or remote) services without establishing a VPN and allocating a local tun/tap device, similarly to `ngrok`. diff --git a/docs/content/en/docs/how-to/use-as-a-library.md b/docs/content/en/docs/how-to/use-as-a-library.md new file mode 100644 index 00000000..f11752ad --- /dev/null +++ b/docs/content/en/docs/how-to/use-as-a-library.md @@ -0,0 +1,74 @@ +--- +title: "Use EdgeVPN as a library" +linkTitle: "Use as a library" +weight: 100 +description: > + Embed a node in your own Go program — join a network from a token, or bring up the VPN. +--- + +EdgeVPN can be used as a library. It is very portable and offers a functional +interface. + +To join a node in a network from a token, without starting the vpn: + +```golang +import ( + "github.com/ipfs/go-log" + "github.com/mudler/edgevpn/pkg/discovery" + "github.com/mudler/edgevpn/pkg/logger" + node "github.com/mudler/edgevpn/pkg/node" +) + +d := discovery.NewDHT() +m := &discovery.MDNS{} + +e, err := node.New( + node.Logger(logger.New(log.LevelInfo)), + node.MaxMessageSize(2 << 20), + node.FromBase64(mDNSEnabled, DHTEnabled, token, d, m), + // .... +) +if err != nil { + return err +} + +if err := e.Start(ctx); err != nil { + return err +} +``` + +`node.FromBase64` decodes the same base64 token the CLI uses (`edgevpn -g -b`) +and wires the discovery services into the node, which is why it takes the +`*discovery.DHT` and `*discovery.MDNS` values you built above. The two booleans +enable mDNS and DHT discovery respectively. + +or to start a VPN: + +```golang +import ( + node "github.com/mudler/edgevpn/pkg/node" + vpn "github.com/mudler/edgevpn/pkg/vpn" +) + +opts, err := vpn.Register(vpnOpts...) +if err != nil { + return err +} + +e, err := node.New(append(o, opts...)...) +if err != nil { + return err +} + +if err := e.Start(ctx); err != nil { + return err +} +``` + +`vpn.Register` turns a set of `vpn.Option` values into node options — it +registers the VPN as a network service on the node, so there is no separate VPN +start call. `o` is your own `[]node.Option` slice, built the same way as in the +first example. + +Bringing up the TUN interface needs `CAP_NET_ADMIN` and access to +`/dev/net/tun`, exactly as the `edgevpn` binary does. diff --git a/docs/content/en/docs/reference/_index.md b/docs/content/en/docs/reference/_index.md new file mode 100644 index 00000000..24f810d7 --- /dev/null +++ b/docs/content/en/docs/reference/_index.md @@ -0,0 +1,11 @@ +--- +title: "Reference" +linkTitle: "Reference" +weight: 30 +description: > + Every command, flag, environment variable and API endpoint. +--- + +The [CLI reference](cli/) and [environment variables](environment-variables/) +pages are generated directly from the source, so they cannot drift from the +binary you are running. diff --git a/docs/content/en/docs/reference/api.md b/docs/content/en/docs/reference/api.md new file mode 100644 index 00000000..3798d447 --- /dev/null +++ b/docs/content/en/docs/reference/api.md @@ -0,0 +1,281 @@ +--- +title: "WebUI and API" +linkTitle: "WebUI and API" +weight: 40 +aliases: + - /docs/getting-started/api/ +description: > + Query the network status and operate the ledger with the built-in HTTP API. +--- + + +EdgeVPN embeds an HTTP API with a small web UI on top of it. The API exposes the +shared ledger, the peers a node knows about and libp2p bandwidth counters. + +To start a node in API-only mode, run: + +```bash +$ edgevpn api +``` + +with either a `EDGEVPNCONFIG` or `EDGEVPNTOKEN` set (or `--config `). + +In API mode, EdgeVPN will connect to the network without routing any packet, and +without setting up a VPN interface. + +By default the API listens on `127.0.0.1:8080`. Use `--listen` to change it, and +see `edgevpn api --help` for the other options. + +The API can also be started together with the VPN with `--api`; in that case the +address is set with `--api-listen` instead (see +[Binding to a socket](#binding-to-a-socket) below). + +## The API has no authentication + +{{% alert title="The API is a full control plane, and it is unauthenticated" color="warning" %}} +There is no authentication, authorization or CSRF protection on any route. +Anything that can open a TCP connection to the API port can read the whole +ledger and write to it, for example with: + +```bash +$ curl -X PUT 'http://localhost:8080/api/ledger///' +``` + +Writes are announced to the rest of the network, so an unprotected API port is +enough to inject DNS records, service and file announcements into a network +you are not otherwise a member of. + +Keep the listener on loopback (the default) or on a unix socket, and treat +exposing it on a routable address as equivalent to handing out the network +token. +{{% /alert %}} + +Ledger writes authored by a node are signed with that node's libp2p identity and +merged under the per-bucket ownership rules, so a peer cannot silently overwrite +entries owned by another live peer. That constrains *what* an API caller can +forge on other nodes, but it does not authenticate the caller: whatever the API +writes is signed as the node running it. See the +[security model](../../explanation/security-model/) and +[the ledger](../../explanation/the-ledger/). + +## Response format + +Responses are JSON encoded straight from the Go types, which carry no `json` +struct tags. Field names are therefore **PascalCase** exactly as declared in the +source: `PeerID`, `RateIn`, `BlockChain`, `NodeID`, and so on. + +Ledger reads (`/api/ledger...`) return only the *values* of the entries. The +signature envelope (`Owner`, `Version`, `UpdatedAt`, `Deleted`, `Sig`) is visible +through `/api/blockchain`, which returns the raw last block. + +## API endpoints + +### GET + +#### `/api/summary` + +Counters for the current node: number of files, machines, users and services in +the ledger, the current block index, the number of nodes seen on the gossip +topic, the size of the libp2p peerstore, and this node's peer ID. + +```bash +$ curl -s http://localhost:8080/api/summary +{"Files":0,"Machines":0,"Users":0,"Services":0,"BlockChain":0,"OnChainNodes":0,"Peers":1,"NodeID":"12D3KooW..."} +``` + +#### `/api/users` + +Returns the users connected to services in the blockchain + +#### `/api/services` + +Returns the services running in the blockchain + +#### `/api/files` + +Returns the files announced to the ledger (`PeerID`, `Name`) + +#### `/api/dns` + +Returns the domains registered in the blockchain + +#### `/api/machines` + +Returns the machines connected to the VPN. Each entry is the ledger `Machine` +record plus `Connected` (a live libp2p connection exists), `OnChain` (the peer +is on the gossip topic) and `Online` (the peer announced itself recently). + +#### `/api/nodes` + +Returns the peers currently considered online: the union of the gossip topic +members and the peers whose healthcheck entry in the ledger is younger than 10 +minutes. + +#### `/api/peerstore` + +Returns every peer ID in the local libp2p peerstore — peers this node has +discovered, whether or not they are part of the network. + +The two lists overlap but neither contains the other. `/api/nodes` is read +partly from the ledger, so it can name a peer that announced a healthcheck but +that this node has never met and so is absent from its peerstore; conversely the +peerstore holds peers discovered over the DHT that never announced themselves. + +#### `/api/blockchain` + +Returns the latest available block, including the full signature envelope of +every entry + +#### `/api/ledger` + +Returns the current data in the ledger. For what the buckets are called and what +their keys mean, see [ledger buckets](../ledger-buckets/). + +#### `/api/ledger/:bucket` + +Returns the current data in the ledger inside the `:bucket` + +#### `/api/ledger/:bucket/:key` + +Returns the current data in the ledger inside the `:bucket` at given `:key` + +#### `/api/peergate` + +Returns peergater status. + +Registered only when the node runs with `--peerguard`; otherwise it returns +`404`. See [Trusted networks](../../how-to/trusted-networks/). + +### Metrics + +The metrics endpoints report the libp2p bandwidth counters. They are registered +only when the node was given a bandwidth reporter. Every CLI entry point that +serves the API (`edgevpn api`, `edgevpn --api`, `edgevpn proxy`) attaches one, so +in practice they are always present; a `404` here means the API was started +programmatically without a reporter, not that something is broken. + +All of them return a `metrics.Stats` object: + +```bash +$ curl -s http://localhost:8080/api/metrics +{"TotalIn":0,"TotalOut":0,"RateIn":0,"RateOut":0} +``` + +#### `/api/metrics` + +Aggregate bandwidth totals and rates for the node + +#### `/api/metrics/protocol` + +Bandwidth broken down by libp2p protocol ID + +#### `/api/metrics/protocol/:protocol` + +Bandwidth for a single protocol. The protocol ID contains slashes, so it has to +be URL-encoded: + +```bash +$ curl -s 'http://localhost:8080/api/metrics/protocol/%2Fedgevpn%2F0.1' +``` + +#### `/api/metrics/peer` + +Bandwidth broken down by peer ID + +#### `/api/metrics/peer/:peer` + +Bandwidth for a single peer, by peer ID + +### PUT + +#### `/api/ledger/:bucket/:key/:value` + +Puts `:value` in the ledger inside the `:bucket` at given `:key`. Returns +`{"State":"Announcing"}` — the write is queued for announcement, so it is not +visible to a subsequent `GET` until it has been committed to a block. + +#### `/api/peergate/:state` + +Enables/disables peergating (only present with `--peerguard`): + +```bash +# enable +$ curl -X PUT 'http://localhost:8080/api/peergate/enable' +# disable +$ curl -X PUT 'http://localhost:8080/api/peergate/disable' +``` + +### POST + +#### `/api/dns` + +The endpoint accept a JSON payload of the following form: + +```json +{ "Regex": "", + "Records": { + "A": "2.2.2.2", + "AAAA": "...", + }, +} +``` + +Takes a regex and a set of records and registers them to the blockchain. + +The DNS table in the ledger will be used by the embedded DNS server to handle requests locally. + +To create a new entry, for example: + +```bash +$ curl -X POST http://localhost:8080/api/dns --header "Content-Type: application/json" -d '{ "Regex": "foo.bar", "Records": { "A": "2.2.2.2" } }' +``` + +### DELETE + +#### `/api/ledger/:bucket/:key` + +Deletes the `:key` into `:bucket` inside the ledger + +#### `/api/ledger/:bucket` + +Deletes the `:bucket` from the ledger + +### Debug endpoints + +#### `/debug/pprof/*` + +When the node is started with `--debug`, the Go `net/http/pprof` handlers are +mounted under `/debug/pprof/`. They are not mounted otherwise. + +Like the rest of the API these are unauthenticated, and they expose goroutine +stacks and heap profiles of the process — do not enable `--debug` on a node +whose API port is reachable by anything you do not trust. + +### Web UI + +Every other path is served from the assets embedded in the binary: the single +page UI at `/`, with sections for nodes, DNS, the blockchain, services and +peers. It is a read/write front-end for the endpoints above and inherits the +same lack of authentication. + +## Binding to a socket + +The API can also be bound to a unix socket, for instance: + +```bash +$ edgevpn api --listen "unix://" +``` + +or as well while running the vpn, where the flag is `--api-listen`: + +```bash +$ edgevpn --api --api-listen "unix://" +``` + +The socket is created with mode `0660`, which can be overridden with the +`APILISTENUNIXMODE` environment variable. systemd socket activation is honoured: +if a `.socket` unit passes a listener, EdgeVPN inherits it and leaves the socket +file's ownership and permissions alone. + +A unix socket is the recommended way to run the API on a shared host, because +filesystem permissions are the only access control the API has. diff --git a/docs/content/en/docs/reference/cli/_index.md b/docs/content/en/docs/reference/cli/_index.md new file mode 100644 index 00000000..aa9e7c7d --- /dev/null +++ b/docs/content/en/docs/reference/cli/_index.md @@ -0,0 +1,114 @@ +--- +title: "CLI" +linkTitle: "CLI" +weight: 10 +description: > + Every EdgeVPN command and flag. +--- + + + +Running `edgevpn` with no subcommand starts the VPN. + +## Global flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--key-otp-interval` | `360` | — | Tweaks default otp interval (in seconds) when generating new tokens | +| `-g` | `false` | — | Generates a new configuration and prints it on screen | +| `-b` | `false` | — | Encodes the new config in base64, so it can be used as a token | +| `--debug` | `false` | — | Starts API with pprof attached | +| `--api` | `false` | `API` | Starts also the API daemon locally for inspecting the network status | +| `--api-listen` | `"127.0.0.1:8080"` | `APILISTEN` | API listen address. Accepts a TCP host:port or a unix socket path with the 'unix://' prefix (e.g. unix:///run/edgevpn.sock). Socket mode defaults to 0660 and can be overridden via APILISTENUNIXMODE. | +| `--dhcp` | `false` | `DHCP` | Enables p2p ip negotiation (experimental) | +| `--transient-conn` | `false` | `TRANSIENTCONN` | Allow transient connections | +| `--lease-dir` | `"$HOME/.edgevpn/leases"` | `DHCPLEASEDIR` | DHCP leases directory | +| `--address` | `"10.1.0.1/24"` | `ADDRESS` | VPN virtual address | +| `--dns` | — | `DNSADDRESS` | DNS listening address. Empty to disable dns server | +| `--dns-forwarder` | `true` | `DNSFORWARD` | Enables dns forwarding | +| `--egress` | `false` | `EGRESS` | Enables nodes for egress | +| `--egress-announce-time` | `200` | `EGRESSANNOUNCE` | Egress announce time (s) | +| `--dns-cache-size` | `200` | `DNSCACHESIZE` | DNS LRU cache size | +| `--dns-forward-server` | `"8.8.8.8:53", "1.1.1.1:53"` | `DNSFORWARDSERVER` | List of DNS forward server, e.g. 8.8.8.8:53, 192.168.1.1:53 ... | +| `--router` | — | `ROUTER` | Sends all packets to this node | +| `--interface` | `"edgevpn0"` | `IFACE` | Interface name | +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | + +## Commands + +- [`start`](start/) — Start the network without activating any interface +- [`api`](api/) — Starts an http server to display network informations +- [`service-add`](service-add/) — Expose a service to the network without creating a VPN +- [`service-connect`](service-connect/) — Connects to a service in the network without creating a VPN +- [`file-receive`](file-receive/) — Receive a file which is served from the network +- [`proxy`](proxy/) — Starts a local http proxy server to egress nodes +- [`file-send`](file-send/) — Serve a file to the network +- [`dns`](dns/) — Starts a local dns server +- [`peergater`](peergater/) — peergater ecdsa-genkey diff --git a/docs/content/en/docs/reference/cli/api.md b/docs/content/en/docs/reference/cli/api.md new file mode 100644 index 00000000..d3124b70 --- /dev/null +++ b/docs/content/en/docs/reference/cli/api.md @@ -0,0 +1,92 @@ +--- +title: "api" +linkTitle: "api" +weight: 20 +description: > + Starts an http server to display network informations +--- + + + +Start listening locally, providing an API for the network. +A simple UI interface is available to display network data. + +``` +edgevpn api [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--enable-healthchecks` | `false` | `ENABLE_HEALTHCHECKS` | | +| `--debug` | `false` | — | | +| `--listen` | `"127.0.0.1:8080"` | — | Listening address. To listen to a socket, prefix with unix://, e.g. unix:///socket.path | diff --git a/docs/content/en/docs/reference/cli/dns.md b/docs/content/en/docs/reference/cli/dns.md new file mode 100644 index 00000000..1cdfaed5 --- /dev/null +++ b/docs/content/en/docs/reference/cli/dns.md @@ -0,0 +1,92 @@ +--- +title: "dns" +linkTitle: "dns" +weight: 80 +description: > + Starts a local dns server +--- + + + +Start a local dns server which uses the blockchain to resolve addresses + +``` +edgevpn dns [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--listen` | — | `DNSADDRESS` | DNS listening address. Empty to disable dns server | +| `--dns-forwarder` | `true` | `DNSFORWARD` | Enables dns forwarding | +| `--dns-cache-size` | `200` | `DNSCACHESIZE` | DNS LRU cache size | +| `--dns-forward-server` | `"8.8.8.8:53", "1.1.1.1:53"` | `DNSFORWARDSERVER` | List of DNS forward server, e.g. 8.8.8.8:53, 192.168.1.1:53 ... | diff --git a/docs/content/en/docs/reference/cli/file-receive.md b/docs/content/en/docs/reference/cli/file-receive.md new file mode 100644 index 00000000..1a66bd1e --- /dev/null +++ b/docs/content/en/docs/reference/cli/file-receive.md @@ -0,0 +1,92 @@ +--- +title: "file-receive" +linkTitle: "file-receive" +weight: 50 +description: > + Receive a file which is served from the network +--- + + + +Aliases: `fr` + +Receive a file from the network without connecting over VPN + +``` +edgevpn file-receive [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--name` | — | — | Unique name of the file to be received over the network. | +| `--path` | — | — | Destination where to save the file | diff --git a/docs/content/en/docs/reference/cli/file-send.md b/docs/content/en/docs/reference/cli/file-send.md new file mode 100644 index 00000000..8639505c --- /dev/null +++ b/docs/content/en/docs/reference/cli/file-send.md @@ -0,0 +1,92 @@ +--- +title: "file-send" +linkTitle: "file-send" +weight: 70 +description: > + Serve a file to the network +--- + + + +Aliases: `fs` + +Serve a file to the network without connecting over VPN + +``` +edgevpn file-send [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--name` | — | — | Unique name of the file to be served over the network. This is also the ID used to refer when receiving it. | +| `--path` | — | — | File to serve | diff --git a/docs/content/en/docs/reference/cli/peergater.md b/docs/content/en/docs/reference/cli/peergater.md new file mode 100644 index 00000000..98f90580 --- /dev/null +++ b/docs/content/en/docs/reference/cli/peergater.md @@ -0,0 +1,26 @@ +--- +title: "peergater" +linkTitle: "peergater" +weight: 90 +description: > + peergater ecdsa-genkey +--- + + + +Peergater auth utilities + +``` +edgevpn peergater [options] +``` + +## Flags + +_This command takes no flags of its own._ + +## `peergater ecdsa-genkey` + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--privkey` | `false` | — | | +| `--pubkey` | `false` | — | | diff --git a/docs/content/en/docs/reference/cli/proxy.md b/docs/content/en/docs/reference/cli/proxy.md new file mode 100644 index 00000000..2e838d66 --- /dev/null +++ b/docs/content/en/docs/reference/cli/proxy.md @@ -0,0 +1,94 @@ +--- +title: "proxy" +linkTitle: "proxy" +weight: 60 +description: > + Starts a local http proxy server to egress nodes +--- + + + +Start a proxy locally, providing an ingress point for the network. + +``` +edgevpn proxy [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--listen` | `":8080"` | `PROXYLISTEN` | Listening address | +| `--api` | `false` | `API` | Starts also the API daemon locally for inspecting the network status | +| `--api-listen` | `"127.0.0.1:8081"` | `APILISTEN` | API listen address, used only with --api. Must differ from --listen. Accepts a TCP host:port or a unix socket path with the 'unix://' prefix (e.g. unix:///run/edgevpn.sock). Socket mode defaults to 0660 and can be overridden via APILISTENUNIXMODE. | +| `--debug` | `false` | — | Starts the API with pprof attached | +| `--interval` | `120` | `PROXYINTERVAL` | proxy announce time interval | +| `--dead-interval` | `600` | `PROXYDEADINTERVAL` | interval (in seconds) wether detect egress nodes offline | diff --git a/docs/content/en/docs/reference/cli/service-add.md b/docs/content/en/docs/reference/cli/service-add.md new file mode 100644 index 00000000..0fedcc56 --- /dev/null +++ b/docs/content/en/docs/reference/cli/service-add.md @@ -0,0 +1,93 @@ +--- +title: "service-add" +linkTitle: "service-add" +weight: 30 +description: > + Expose a service to the network without creating a VPN +--- + + + +Aliases: `sa` + +Expose a local or a remote endpoint connection as a service in the VPN. + The host will act as a proxy between the service and the connection + +``` +edgevpn service-add [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--name` | — | — | Unique name of the service to be server over the network. | +| `--address` | — | — | Remote address that the service is running to. That can be a remote webserver, a local SSH server, etc. For example, '192.168.1.1:80', or '127.0.0.1:22'. | diff --git a/docs/content/en/docs/reference/cli/service-connect.md b/docs/content/en/docs/reference/cli/service-connect.md new file mode 100644 index 00000000..fdbcad32 --- /dev/null +++ b/docs/content/en/docs/reference/cli/service-connect.md @@ -0,0 +1,94 @@ +--- +title: "service-connect" +linkTitle: "service-connect" +weight: 40 +description: > + Connects to a service in the network without creating a VPN +--- + + + +Aliases: `sc` + +Bind a local port to connect to a remote service in the network. +Creates a local listener which connects over the service in the network without creating a VPN. + + +``` +edgevpn service-connect [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | +| `--name` | — | — | Unique name of the service in the network. | +| `--address` | — | — | Address where to bind locally. E.g. ':8080'. A proxy will be created to the service over the network | diff --git a/docs/content/en/docs/reference/cli/start.md b/docs/content/en/docs/reference/cli/start.md new file mode 100644 index 00000000..46d1aa43 --- /dev/null +++ b/docs/content/en/docs/reference/cli/start.md @@ -0,0 +1,89 @@ +--- +title: "start" +linkTitle: "start" +weight: 10 +description: > + Start the network without activating any interface +--- + + + +Connect over the p2p network without establishing a VPN. +Useful for setting up relays or hop nodes to improve the network connectivity. + +``` +edgevpn start [options] +``` + +## Flags + +| Flag | Default | Environment | Description | +|---|---|---|---| +| `--config` | — | `EDGEVPNCONFIG` | Specify a path to a edgevpn config file | +| `--listen-maddrs` | — | `EDGEVPNLISTENMADDRS` | Override default 0.0.0.0 listen multiaddresses | +| `--dht-announce-maddrs` | — | `EDGEVPNDHTANNOUNCEMADDRS` | Override listen-maddrs on DHT announce | +| `--timeout` | `"15s"` | `EDGEVPNTIMEOUT` | Specify a default timeout for connection stream | +| `--mtu` | `1200` | `EDGEVPNMTU` | Specify a mtu | +| `--bootstrap-iface` | `true` | `EDGEVPNBOOTSTRAPIFACE` | Setup interface on startup (need privileges) | +| `--packet-mtu` | `1420` | `EDGEVPNPACKETMTU` | Specify a mtu | +| `--channel-buffer-size` | `0` | `EDGEVPNCHANNELBUFFERSIZE` | Specify a channel buffer size | +| `--discovery-interval` | `720` | `EDGEVPNDHTINTERVAL` | DHT discovery interval time | +| `--ledger-announce-interval` | `10` | `EDGEVPNLEDGERINTERVAL` | Ledger announce interval time | +| `--autorelay-discovery-interval` | `"5m"` | `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | Autorelay discovery interval | +| `--autorelay-static-only` | `false` | `EDGEVPNAUTORELAYSTATICONLY` | Use only defined static relays | +| `--ledger-synchronization-interval` | `10` | `EDGEVPNLEDGERSYNCINTERVAL` | Ledger synchronization interval time | +| `--nat-ratelimit-global` | `10` | `EDGEVPNNATRATELIMITGLOBAL` | Rate limit global requests | +| `--nat-ratelimit-peer` | `10` | `EDGEVPNNATRATELIMITPEER` | Rate limit perr requests | +| `--nat-ratelimit-interval` | `60` | `EDGEVPNNATRATELIMITINTERVAL` | Rate limit interval | +| `--nat-ratelimit` | `true` | `EDGEVPNNATRATELIMIT` | Changes the default rate limiting configured in helping other peers determine their reachability status | +| `--max-connections` | `0` | `EDGEVPNMAXCONNS` | Max connections | +| `--ledger-state` | — | `EDGEVPNLEDGERSTATE` | Specify a ledger state directory | +| `--mdns` | `true` | `EDGEVPNMDNS` | Enable mDNS for peer discovery | +| `--autorelay` | `true` | `EDGEVPNAUTORELAY` | Automatically act as a relay if the node can accept inbound connections | +| `--concurrency` | `20` | — | Number of concurrent requests to serve | +| `--holepunch` | `true` | `EDGEVPNHOLEPUNCH` | Automatically try holepunching when possible | +| `--natservice` | `true` | `EDGEVPNNATSERVICE` | Tries to determine reachability status of nodes | +| `--natmap` | `true` | `EDGEVPNNATMAP` | Tries to open a port in the firewall via upnp | +| `--dht` | `true` | `EDGEVPNDHT` | Enable DHT for peer discovery | +| `--low-profile` | `true` | `EDGEVPNLOWPROFILE` | Enable low profile. Lowers connections usage | +| `--aliveness-healthcheck-interval` | `120` | `HEALTHCHECKINTERVAL` | Healthcheck interval | +| `--aliveness-healthcheck-scrub-interval` | `600` | `HEALTHCHECKSCRUBINTERVAL` | Healthcheck scrub interval | +| `--aliveness-healthcheck-max-interval` | `900` | `HEALTHCHECKMAXINTERVAL` | Healthcheck max interval. Threshold after a node is determined offline | +| `--log-level` | `"info"` | `EDGEVPNLOGLEVEL` | Specify loglevel | +| `--libp2p-log-level` | `"fatal"` | `EDGEVPNLIBP2PLOGLEVEL` | Specify libp2p loglevel | +| `--discovery-bootstrap-peers` | — | `EDGEVPNBOOTSTRAPPEERS` | List of discovery peers to use | +| `--connection-high-water` | `0` | `EDGEVPN_CONNECTION_HIGH_WATER` | max number of connection allowed | +| `--connection-low-water` | `0` | `EDGEVPN_CONNECTION_LOW_WATER` | low number of connection allowed | +| `--autorelay-static-peer` | — | `EDGEVPNAUTORELAYPEERS` | List of autorelay static peers to use | +| `--relay-service` | `true` | `EDGEVPN_RELAY_SERVICE` | Offer the circuit-v2 relay service to cluster peers (i.e. let other peers reserve a slot on this node and route relayed traffic through us). Disabling does NOT prevent this node from USING other relays as a client via AutoRelay — set this to false on resource-constrained nodes or nodes that should not act as relays. | +| `--relay-service-network-only` | `true` | `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | Restrict incoming relay reservations to peers observed in the local ledger's alive bucket (cluster members). Strangers that found us via the public DHT or another relay discovery path are rejected. Requires the alive service to be running. During a short bootstrap window — before the alive bucket is first observed — every reservation is allowed so the node itself can finish joining the cluster. Default ON: secure by default; pass --relay-service-network-only=false to open the relay to all peers. | +| `--relay-service-acl-refresh` | `"30s"` | `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | Cadence at which the NetworkOnly relay-service ACL re-snapshots the alive bucket (Go duration). Should be <= the alive-service announce interval so peer churn is reflected within a couple of ticks. | +| `--relay-service-max-data` | `1073741824` | `EDGEVPN_RELAY_MAX_DATA` | Bytes (per direction) a relayed connection may carry before reset. Higher values let cluster peers carry larger relayed transfers (e.g. model files for distributed inference) at the cost of a larger memory footprint per relay client. Set lower for resource-constrained deployments. | +| `--relay-service-max-duration` | `"30m0s"` | `EDGEVPN_RELAY_MAX_DURATION` | Maximum lifetime of a single relayed connection (Go duration). Higher values let cluster peers carry longer-running relayed transfers at the cost of holding circuits open. Set lower for resource-constrained deployments. | +| `--relay-service-max-circuits` | `64` | `EDGEVPN_RELAY_MAX_CIRCUITS` | Maximum number of concurrent relay circuits per peer. Higher values let a single peer hold more simultaneous circuits through this node at the cost of a larger memory footprint; the number of peers that may relay through us is bounded separately by the reservation limits. Set lower for resource-constrained deployments. | +| `--relay-service-reservation-ttl` | `"1h0m0s"` | `EDGEVPN_RELAY_RESERVATION_TTL` | Time-to-live of a relay reservation (Go duration). Higher values reduce reservation churn for stable cluster peers; lower values free relay slots faster. | +| `--relay-service-buffer-size` | `65536` | `EDGEVPN_RELAY_BUFFER_SIZE` | Per-circuit relayed connection buffer size in bytes. Higher values improve throughput of large relayed transfers at the cost of memory per relay client. Set lower for resource-constrained deployments. | +| `--blacklist` | — | `EDGEVPNBLACKLIST` | List of peers/cidr to gate | +| `--token` | — | `EDGEVPNTOKEN` | Specify an edgevpn token in place of a config file | +| `--limit-enable` | `false` | `LIMITENABLE` | Enable resource management | +| `--limit-file` | — | `LIMITFILE` | Specify a resource limit config (json) | +| `--limit-scope` | `"system"` | `LIMITSCOPE` | Specify a limit scope | +| `--limit-config-streams` | `200` | `LIMITCONFIGSTREAMS` | Streams resource limit configuration | +| `--limit-config-streams-inbound` | `30` | `LIMITCONFIGSTREAMSINBOUND` | Inbound streams resource limit configuration | +| `--limit-config-streams-outbound` | `30` | `LIMITCONFIGSTREAMSOUTBOUND` | Outbound streams resource limit configuration | +| `--limit-config-conn` | `200` | `LIMITCONFIGCONNS` | Connections resource limit configuration | +| `--limit-config-conn-inbound` | `30` | `LIMITCONFIGCONNSINBOUND` | Inbound connections resource limit configuration | +| `--limit-config-conn-outbound` | `30` | `LIMITCONFIGCONNSOUTBOUND` | Outbound connections resource limit configuration | +| `--limit-config-fd` | `30` | `LIMITCONFIGFD` | Max fd resource limit configuration | +| `--peerguard` | `false` | `PEERGUARD` | Enable peerguard. (Experimental) | +| `--ownership` | `"enforce"` | `EDGEVPNOWNERSHIP` | Ledger ownership enforcement: enforce (sign + reject unauthorized writes, default), observe (sign + log violations) or off (legacy, opt-out). All nodes on a network must run the same mode/wire format, so flip the whole network together. | +| `--ownership-ttl` | `0` | `EDGEVPNOWNERSHIPTTL` | Liveness window in seconds after which an inactive owner's ledger entries may be reclaimed/reaped. 0 derives it from --aliveness-healthcheck-interval (4x, so 8 minutes on defaults), which keeps healthy nodes from expiring when the heartbeat is retuned. | +| `--privkey-cache` | `false` | `EDGEVPNPRIVKEYCACHE` | Enable privkey caching. (Experimental) | +| `--privkey-cache-dir` | `"$HOME/.edgevpn"` | `EDGEVPNPRIVKEYCACHEDIR` | Specify a directory used to store the generated privkey | +| `--static-peertable` | — | `EDGEVPNSTATICPEERTABLE` | List of static peers to use (in `ip:peerid` format) | +| `--whitelist` | — | `EDGEVPNWHITELIST` | List of peers in the whitelist | +| `--peergate` | `false` | `PEERGATE` | Enable peergating. (Experimental) | +| `--peergate-autoclean` | `false` | `PEERGATE_AUTOCLEAN` | Enable peergating autoclean. (Experimental) | +| `--peergate-relaxed` | `false` | `PEERGATE_RELAXED` | Enable peergating relaxation. (Experimental) | +| `--peergate-auth` | — | `PEERGATE_AUTH` | Peergate auth | +| `--peergate-interval` | `120` | `EDGEVPNPEERGATEINTERVAL` | Peergater interval time | diff --git a/docs/content/en/docs/reference/compatibility.md b/docs/content/en/docs/reference/compatibility.md new file mode 100644 index 00000000..1dc20d5b --- /dev/null +++ b/docs/content/en/docs/reference/compatibility.md @@ -0,0 +1,197 @@ +--- +title: "Version and wire-format compatibility" +linkTitle: "Compatibility" +weight: 50 +description: > + Which EdgeVPN versions and which --ownership modes can share a network, and how to move between them. +--- + +Two nodes holding the same token are on the same network only if they also agree +on the format of what they gossip. Almost every EdgeVPN setting is a local +choice; a small number are not. This page is the list of the ones that are not. + +## How to tell what a node is running + +Release binaries carry their tag: + +```bash +edgevpn --version +``` + +``` +edgevpn version v0.35.3 +``` + +Every command also logs it at startup, alongside the ownership mode: + +``` +Version: v0.35.3 commit: a9f4e17af58d565a0a2caee26be8ea0311d0a7e9 +ledger ownership enforcement: mode=2 ttl=2m0s +``` + +Released binaries print the mode as an **integer**: `1` is `observe`, `2` is +`enforce`. There is no `mode=0` — the line is only logged when enforcement is +on, so `off` prints nothing at all (and so does a node whose host private key +was unavailable, which logs `ownership enforcement requested but host private +key is unavailable` instead and runs unsigned). The window is `2m0s` on every +released version. Builds newer than v0.35.3 print the name (`mode=enforce`) and +derive the window from the heartbeat, so they show `ttl=8m0s` on stock +settings — a quick way to tell a released binary from a build off `master`. + +A binary built from source without the release `-ldflags` reports an empty +version (`Version: commit:`) and has no `--version` flag at all. If you build +your own, record the commit yourself — a network of self-built binaries has no +other way to answer "what are these nodes running". + +## The only wire format that changes: `--ownership` + +`--ownership` selects how ledger entries are encoded, so nodes that disagree do +not merely behave differently — they cannot read each other. + +- **Unsigned (legacy) entries** are bare JSON values. Every EdgeVPN release up to + and including **v0.34.0** speaks only this, and so does any newer node running + `--ownership off`. +- **Signed entries** are JSON objects carrying the author's peer ID, a version, a + timestamp and a signature. `observe` and `enforce` emit these. + +The decoder used by `observe` and `enforce` accepts *both* shapes. The decoder in +releases up to v0.34.0 accepts only the first, and a single signed entry +anywhere in an incoming block makes the **whole block** undecodable to it — not +just that entry. + +See [ledger ownership](../../how-to/ledger-ownership/) for what the modes do and +[the authenticated ledger](../../explanation/authenticated-ledger/) for the +design. + +### Which release introduced it + +Ownership landed in **v0.35.0**, in commit `1c969bd` — which is the commit +v0.35.0 tags, so there is no intermediate release that has part of it. It has +been present in every release since (v0.35.0, v0.35.1, v0.35.2, v0.35.3), and it +has defaulted to `enforce` from the first of them: **a v0.35.x node started with +no ownership flags will not interoperate with a v0.34.0 node.** + +That is the upgrade trap. Nothing about the upgrade announces it. + +## The matrix + +Rows write, columns read. "≤ v0.34.0" means any release before ownership existed; +those binaries have no `--ownership` flag. + +| Writer → Reader | ≤ v0.34.0 | v0.35+ `off` | v0.35+ `observe` | v0.35+ `enforce` | +|---|---|---|---|---| +| **≤ v0.34.0** | works | works | works (accepted, logged) | **dropped** (silent to the writer) | +| **v0.35+ `off`** | works (see below) | works | works (accepted, logged) | **dropped** (silent to the writer) | +| **v0.35+ `observe`** | **whole blocks dropped** | works | works | works | +| **v0.35+ `enforce`** | **whole blocks dropped** | works | works | works | + +The `off` → `≤ v0.34.0` cell has an exception. An `off` node adopts incoming +blocks wholesale, so in a mixed network it stores and re-broadcasts signed +entries authored by others; its own blocks then contain signed entries too, which +a pre-v0.35 node cannot decode either. `off` is reliably legacy-compatible only +in a network that contains no signing nodes at all. + +Reading the rest of it: + +- **`off` ↔ `enforce` fails in one direction and says nothing on the other.** The + `enforce` node logs each dropped write; the `off` node logs nothing, because + from its point of view everything it receives decodes and everything it sends + is sent. +- **`observe` is compatible with both `off` and `enforce`.** It signs (so + `enforce` accepts it) and it accepts unsigned writes (so `off` reaches it). + That is the whole reason for the `off → observe → enforce` sequence. +- **`observe` is *not* compatible with pre-v0.35 binaries.** It signs, and old + binaries cannot decode signed entries. Against a v0.34 node, `observe` is as + incompatible as `enforce`. Only `off` is. + +### What each failure looks like + +| Where | Log line | Level | +|---|---|---| +| `enforce` node receiving an unsigned entry | `ownership violation (rejected): machines/10.1.0.9 from : invalid signature` | warn | +| `observe` node receiving an unsigned entry | `ownership violation (observe, accepting): machines/10.1.0.9 from : invalid signature` | warn | +| pre-v0.35 node receiving a signed block | `handler error: failed unmarshalling blockchain data: json: cannot unmarshal object into Go struct field Block.Storage of type blockchain.Data` | warn | +| `off` node in a mixed network | *nothing* | — | + +The empty owner between `from` and `:` marks a legacy write, as opposed to a +genuine ownership violation, which names the offending peer. + +## Upgrading a network across the boundary + +The binary upgrade and the mode change are two separate migrations, and they have +to happen in that order. + +1. **Upgrade the binaries, pinning the old wire format.** Roll v0.35.x out one + node at a time with `--ownership off` (or `EDGEVPNOWNERSHIP=off`). A v0.35 + node in `off` mode is wire-identical to a v0.34 node in both directions, so + the network keeps working throughout with any mixture of old and new binaries. + + Do **not** skip this by letting the new nodes take their `enforce` default: + every node you upgrade would then vanish from the ones you have not. + +2. **Once every node is on v0.35.x, change the mode.** Follow + [changing the mode on a live network](../../how-to/ledger-ownership/#changing-the-mode-on-a-live-network): + `off` → `observe` everywhere, wait for the `ownership violation (observe, + accepting)` lines to stop, then `observe` → `enforce` everywhere. Each step is + safe one node at a time. + +Downgrading reverses it: `enforce` → `observe` → `off` on every node first, and +only then replace binaries with a pre-v0.35 release. + +## What is *not* a compatibility concern + +These differ freely between nodes on one network: + +- **All relay settings.** `--relay-service*` and `--autorelay*` are per-node + resource and policy choices. See + [relays and hop nodes](../../how-to/relays-and-hop-nodes/). +- **Resource limits, connection watermarks, log levels, API settings.** +- **`--privkey-cache`.** Identity persistence is per-node. + +The ledger protocol identifiers (`/edgevpn/0.1` and the service, file and egress +protocols) have not changed across the history of those files, and neither has +the block structure apart from the entry encoding described above. + +## `--ownership-ttl` is not a wire format, but it still has to match + +The TTL is a local judgement about when a peer counts as dead, so nodes that +disagree can still read each other. That is the whole of the good news. Under +`observe`/`enforce` the merge is per-key and nothing reconciles whole blocks +afterwards, so two nodes running different windows durably disagree on ledger +*state*: the node with the shorter window declares an owner dead, stops routing +to it, and lets its addresses, services and DNS names be reclaimed — or reaps +them outright if it is the leader — while the node with the longer window still +holds the original entry and still routes to the original owner. Nothing +resolves the split until the owner re-announces. + +Set the same value on every node. Leaving it at the default `0` does that for +you: the window is derived from `--aliveness-healthcheck-interval` (4×, so 8 +minutes on the stock 120-second heartbeat), so nodes stay in agreement as long +as the heartbeat interval matches too. + +## What this page does not establish + +Stated plainly, because guessing here is worse than a gap: + +- **Interoperability *among* pre-v0.35 releases was not audited.** The claim + above is only that they share one entry encoding with `--ownership off`. +- **Transport-level compatibility across large libp2p version gaps was not + tested.** EdgeVPN v0.35.x builds against go-libp2p v0.48.0. Multistream + negotiation is designed to be backward compatible, but very old EdgeVPN + releases have not been run against current ones here. +- **Token and network-configuration format** compatibility across releases was + not tested beyond noting that the fields have not changed. + +## Fixes that are not in any release yet + +Three ownership defects are fixed in the source tree but appear in **no released +version**. If you are running v0.35.0 through v0.35.3, they are all present: + +| Defect | Symptom on a released binary | Workaround until a release ships | +|---|---|---| +| An unrecognised `--ownership` value fell through to `off` | A typo (`--ownership enabled`, `--ownership true`) silently disabled ledger authentication, with no error and no warning | Confirm the mode in the startup log (`ledger ownership enforcement: mode=…`, where `1` is `observe` and `2` is `enforce`) rather than trusting the flag. The line's absence means the node is unsigned — either `off`, or enforcement was requested but the host private key was unavailable, which logs a warning of its own | +| The liveness window was a fixed 2 minutes | Shorter than the 180 s a healthy node can take between jittered heartbeats, so live nodes were intermittently treated as inactive: packets dropped, addresses reclaimable, entries tombstoned by a leader scrub | Set `--ownership-ttl 480` explicitly on every node | +| A persisted unsigned entry could not be replaced or deleted by its own owner | With `--ledger-state`, entries written under `off`/`observe` were frozen after a restart into `enforce`; deleting one succeeded locally and was rejected everywhere else, diverging the network with nothing logged on the node that issued it | Clear the state directory before restarting a node into `enforce` | + +All three are described in more detail on +[ledger ownership](../../how-to/ledger-ownership/). diff --git a/docs/content/en/docs/reference/environment-variables.md b/docs/content/en/docs/reference/environment-variables.md new file mode 100644 index 00000000..1451f0ad --- /dev/null +++ b/docs/content/en/docs/reference/environment-variables.md @@ -0,0 +1,641 @@ +--- +title: "Environment variables" +linkTitle: "Environment variables" +weight: 20 +description: > + Every environment variable EdgeVPN reads, and the flag it corresponds to. +--- + + + +Environment variables are read when the corresponding flag is not passed. + +| Variable | Flag | Command | Default | +|---|---|---|---| +| `ADDRESS` | `--address` | global | `"10.1.0.1/24"` | +| `API` | `--api` | global | `false` | +| `API` | `--api` | proxy | `false` | +| `APILISTEN` | `--api-listen` | global | `"127.0.0.1:8080"` | +| `APILISTEN` | `--api-listen` | proxy | `"127.0.0.1:8081"` | +| `DHCP` | `--dhcp` | global | `false` | +| `DHCPLEASEDIR` | `--lease-dir` | global | `"$HOME/.edgevpn/leases"` | +| `DNSADDRESS` | `--dns` | global | — | +| `DNSADDRESS` | `--listen` | dns | — | +| `DNSCACHESIZE` | `--dns-cache-size` | global | `200` | +| `DNSCACHESIZE` | `--dns-cache-size` | dns | `200` | +| `DNSFORWARD` | `--dns-forwarder` | global | `true` | +| `DNSFORWARD` | `--dns-forwarder` | dns | `true` | +| `DNSFORWARDSERVER` | `--dns-forward-server` | global | `"8.8.8.8:53", "1.1.1.1:53"` | +| `DNSFORWARDSERVER` | `--dns-forward-server` | dns | `"8.8.8.8:53", "1.1.1.1:53"` | +| `EDGEVPNAUTORELAY` | `--autorelay` | global | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | start | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | api | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | service-add | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | service-connect | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | file-receive | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | proxy | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | file-send | `true` | +| `EDGEVPNAUTORELAY` | `--autorelay` | dns | `true` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | global | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | start | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | api | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | service-add | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | service-connect | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | file-receive | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | proxy | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | file-send | `"5m"` | +| `EDGEVPNAUTORELAYDISCOVERYINTERVAL` | `--autorelay-discovery-interval` | dns | `"5m"` | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | global | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | start | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | api | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | service-add | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | service-connect | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | file-receive | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | proxy | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | file-send | — | +| `EDGEVPNAUTORELAYPEERS` | `--autorelay-static-peer` | dns | — | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | global | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | start | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | api | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | service-add | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | service-connect | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | file-receive | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | proxy | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | file-send | `false` | +| `EDGEVPNAUTORELAYSTATICONLY` | `--autorelay-static-only` | dns | `false` | +| `EDGEVPNBLACKLIST` | `--blacklist` | global | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | start | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | api | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | service-add | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | service-connect | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | file-receive | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | proxy | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | file-send | — | +| `EDGEVPNBLACKLIST` | `--blacklist` | dns | — | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | global | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | start | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | api | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | service-add | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | service-connect | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | file-receive | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | proxy | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | file-send | `true` | +| `EDGEVPNBOOTSTRAPIFACE` | `--bootstrap-iface` | dns | `true` | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | global | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | start | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | api | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | service-add | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | service-connect | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | file-receive | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | proxy | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | file-send | — | +| `EDGEVPNBOOTSTRAPPEERS` | `--discovery-bootstrap-peers` | dns | — | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | global | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | start | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | api | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | service-add | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | service-connect | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | file-receive | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | proxy | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | file-send | `0` | +| `EDGEVPNCHANNELBUFFERSIZE` | `--channel-buffer-size` | dns | `0` | +| `EDGEVPNCONFIG` | `--config` | global | — | +| `EDGEVPNCONFIG` | `--config` | start | — | +| `EDGEVPNCONFIG` | `--config` | api | — | +| `EDGEVPNCONFIG` | `--config` | service-add | — | +| `EDGEVPNCONFIG` | `--config` | service-connect | — | +| `EDGEVPNCONFIG` | `--config` | file-receive | — | +| `EDGEVPNCONFIG` | `--config` | proxy | — | +| `EDGEVPNCONFIG` | `--config` | file-send | — | +| `EDGEVPNCONFIG` | `--config` | dns | — | +| `EDGEVPNDHT` | `--dht` | global | `true` | +| `EDGEVPNDHT` | `--dht` | start | `true` | +| `EDGEVPNDHT` | `--dht` | api | `true` | +| `EDGEVPNDHT` | `--dht` | service-add | `true` | +| `EDGEVPNDHT` | `--dht` | service-connect | `true` | +| `EDGEVPNDHT` | `--dht` | file-receive | `true` | +| `EDGEVPNDHT` | `--dht` | proxy | `true` | +| `EDGEVPNDHT` | `--dht` | file-send | `true` | +| `EDGEVPNDHT` | `--dht` | dns | `true` | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | global | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | start | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | api | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | service-add | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | service-connect | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | file-receive | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | proxy | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | file-send | — | +| `EDGEVPNDHTANNOUNCEMADDRS` | `--dht-announce-maddrs` | dns | — | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | global | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | start | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | api | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | service-add | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | service-connect | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | file-receive | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | proxy | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | file-send | `720` | +| `EDGEVPNDHTINTERVAL` | `--discovery-interval` | dns | `720` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | global | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | start | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | api | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | service-add | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | service-connect | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | file-receive | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | proxy | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | file-send | `true` | +| `EDGEVPNHOLEPUNCH` | `--holepunch` | dns | `true` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | global | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | start | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | api | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | service-add | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | service-connect | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | file-receive | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | proxy | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | file-send | `10` | +| `EDGEVPNLEDGERINTERVAL` | `--ledger-announce-interval` | dns | `10` | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | global | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | start | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | api | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | service-add | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | service-connect | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | file-receive | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | proxy | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | file-send | — | +| `EDGEVPNLEDGERSTATE` | `--ledger-state` | dns | — | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | global | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | start | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | api | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | service-add | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | service-connect | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | file-receive | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | proxy | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | file-send | `10` | +| `EDGEVPNLEDGERSYNCINTERVAL` | `--ledger-synchronization-interval` | dns | `10` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | global | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | start | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | api | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | service-add | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | service-connect | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | file-receive | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | proxy | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | file-send | `"fatal"` | +| `EDGEVPNLIBP2PLOGLEVEL` | `--libp2p-log-level` | dns | `"fatal"` | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | global | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | start | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | api | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | service-add | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | service-connect | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | file-receive | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | proxy | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | file-send | — | +| `EDGEVPNLISTENMADDRS` | `--listen-maddrs` | dns | — | +| `EDGEVPNLOGLEVEL` | `--log-level` | global | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | start | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | api | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | service-add | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | service-connect | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | file-receive | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | proxy | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | file-send | `"info"` | +| `EDGEVPNLOGLEVEL` | `--log-level` | dns | `"info"` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | global | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | start | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | api | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | service-add | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | service-connect | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | file-receive | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | proxy | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | file-send | `true` | +| `EDGEVPNLOWPROFILE` | `--low-profile` | dns | `true` | +| `EDGEVPNMAXCONNS` | `--max-connections` | global | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | start | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | api | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | service-add | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | service-connect | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | file-receive | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | proxy | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | file-send | `0` | +| `EDGEVPNMAXCONNS` | `--max-connections` | dns | `0` | +| `EDGEVPNMDNS` | `--mdns` | global | `true` | +| `EDGEVPNMDNS` | `--mdns` | start | `true` | +| `EDGEVPNMDNS` | `--mdns` | api | `true` | +| `EDGEVPNMDNS` | `--mdns` | service-add | `true` | +| `EDGEVPNMDNS` | `--mdns` | service-connect | `true` | +| `EDGEVPNMDNS` | `--mdns` | file-receive | `true` | +| `EDGEVPNMDNS` | `--mdns` | proxy | `true` | +| `EDGEVPNMDNS` | `--mdns` | file-send | `true` | +| `EDGEVPNMDNS` | `--mdns` | dns | `true` | +| `EDGEVPNMTU` | `--mtu` | global | `1200` | +| `EDGEVPNMTU` | `--mtu` | start | `1200` | +| `EDGEVPNMTU` | `--mtu` | api | `1200` | +| `EDGEVPNMTU` | `--mtu` | service-add | `1200` | +| `EDGEVPNMTU` | `--mtu` | service-connect | `1200` | +| `EDGEVPNMTU` | `--mtu` | file-receive | `1200` | +| `EDGEVPNMTU` | `--mtu` | proxy | `1200` | +| `EDGEVPNMTU` | `--mtu` | file-send | `1200` | +| `EDGEVPNMTU` | `--mtu` | dns | `1200` | +| `EDGEVPNNATMAP` | `--natmap` | global | `true` | +| `EDGEVPNNATMAP` | `--natmap` | start | `true` | +| `EDGEVPNNATMAP` | `--natmap` | api | `true` | +| `EDGEVPNNATMAP` | `--natmap` | service-add | `true` | +| `EDGEVPNNATMAP` | `--natmap` | service-connect | `true` | +| `EDGEVPNNATMAP` | `--natmap` | file-receive | `true` | +| `EDGEVPNNATMAP` | `--natmap` | proxy | `true` | +| `EDGEVPNNATMAP` | `--natmap` | file-send | `true` | +| `EDGEVPNNATMAP` | `--natmap` | dns | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | global | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | start | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | api | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | service-add | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | service-connect | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | file-receive | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | proxy | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | file-send | `true` | +| `EDGEVPNNATRATELIMIT` | `--nat-ratelimit` | dns | `true` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | global | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | start | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | api | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | service-add | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | service-connect | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | file-receive | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | proxy | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | file-send | `10` | +| `EDGEVPNNATRATELIMITGLOBAL` | `--nat-ratelimit-global` | dns | `10` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | global | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | start | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | api | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | service-add | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | service-connect | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | file-receive | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | proxy | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | file-send | `60` | +| `EDGEVPNNATRATELIMITINTERVAL` | `--nat-ratelimit-interval` | dns | `60` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | global | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | start | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | api | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | service-add | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | service-connect | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | file-receive | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | proxy | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | file-send | `10` | +| `EDGEVPNNATRATELIMITPEER` | `--nat-ratelimit-peer` | dns | `10` | +| `EDGEVPNNATSERVICE` | `--natservice` | global | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | start | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | api | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | service-add | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | service-connect | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | file-receive | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | proxy | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | file-send | `true` | +| `EDGEVPNNATSERVICE` | `--natservice` | dns | `true` | +| `EDGEVPNOWNERSHIP` | `--ownership` | global | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | start | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | api | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | service-add | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | service-connect | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | file-receive | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | proxy | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | file-send | `"enforce"` | +| `EDGEVPNOWNERSHIP` | `--ownership` | dns | `"enforce"` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | global | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | start | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | api | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | service-add | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | service-connect | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | file-receive | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | proxy | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | file-send | `0` | +| `EDGEVPNOWNERSHIPTTL` | `--ownership-ttl` | dns | `0` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | global | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | start | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | api | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | service-add | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | service-connect | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | file-receive | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | proxy | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | file-send | `1420` | +| `EDGEVPNPACKETMTU` | `--packet-mtu` | dns | `1420` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | global | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | start | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | api | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | service-add | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | service-connect | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | file-receive | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | proxy | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | file-send | `120` | +| `EDGEVPNPEERGATEINTERVAL` | `--peergate-interval` | dns | `120` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | global | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | start | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | api | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | service-add | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | service-connect | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | file-receive | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | proxy | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | file-send | `false` | +| `EDGEVPNPRIVKEYCACHE` | `--privkey-cache` | dns | `false` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | global | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | start | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | api | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | service-add | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | service-connect | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | file-receive | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | proxy | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | file-send | `"$HOME/.edgevpn"` | +| `EDGEVPNPRIVKEYCACHEDIR` | `--privkey-cache-dir` | dns | `"$HOME/.edgevpn"` | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | global | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | start | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | api | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | service-add | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | service-connect | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | file-receive | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | proxy | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | file-send | — | +| `EDGEVPNSTATICPEERTABLE` | `--static-peertable` | dns | — | +| `EDGEVPNTIMEOUT` | `--timeout` | global | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | start | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | api | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | service-add | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | service-connect | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | file-receive | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | proxy | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | file-send | `"15s"` | +| `EDGEVPNTIMEOUT` | `--timeout` | dns | `"15s"` | +| `EDGEVPNTOKEN` | `--token` | global | — | +| `EDGEVPNTOKEN` | `--token` | start | — | +| `EDGEVPNTOKEN` | `--token` | api | — | +| `EDGEVPNTOKEN` | `--token` | service-add | — | +| `EDGEVPNTOKEN` | `--token` | service-connect | — | +| `EDGEVPNTOKEN` | `--token` | file-receive | — | +| `EDGEVPNTOKEN` | `--token` | proxy | — | +| `EDGEVPNTOKEN` | `--token` | file-send | — | +| `EDGEVPNTOKEN` | `--token` | dns | — | +| `EDGEVPNWHITELIST` | `--whitelist` | global | — | +| `EDGEVPNWHITELIST` | `--whitelist` | start | — | +| `EDGEVPNWHITELIST` | `--whitelist` | api | — | +| `EDGEVPNWHITELIST` | `--whitelist` | service-add | — | +| `EDGEVPNWHITELIST` | `--whitelist` | service-connect | — | +| `EDGEVPNWHITELIST` | `--whitelist` | file-receive | — | +| `EDGEVPNWHITELIST` | `--whitelist` | proxy | — | +| `EDGEVPNWHITELIST` | `--whitelist` | file-send | — | +| `EDGEVPNWHITELIST` | `--whitelist` | dns | — | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | global | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | start | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | api | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | service-add | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | service-connect | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | file-receive | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | proxy | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | file-send | `0` | +| `EDGEVPN_CONNECTION_HIGH_WATER` | `--connection-high-water` | dns | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | global | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | start | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | api | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | service-add | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | service-connect | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | file-receive | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | proxy | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | file-send | `0` | +| `EDGEVPN_CONNECTION_LOW_WATER` | `--connection-low-water` | dns | `0` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | global | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | start | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | api | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | service-add | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | service-connect | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | file-receive | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | proxy | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | file-send | `65536` | +| `EDGEVPN_RELAY_BUFFER_SIZE` | `--relay-service-buffer-size` | dns | `65536` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | global | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | start | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | api | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | service-add | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | service-connect | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | file-receive | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | proxy | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | file-send | `64` | +| `EDGEVPN_RELAY_MAX_CIRCUITS` | `--relay-service-max-circuits` | dns | `64` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | global | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | start | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | api | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | service-add | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | service-connect | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | file-receive | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | proxy | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | file-send | `1073741824` | +| `EDGEVPN_RELAY_MAX_DATA` | `--relay-service-max-data` | dns | `1073741824` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | global | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | start | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | api | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | service-add | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | service-connect | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | file-receive | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | proxy | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | file-send | `"30m0s"` | +| `EDGEVPN_RELAY_MAX_DURATION` | `--relay-service-max-duration` | dns | `"30m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | global | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | start | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | api | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | service-add | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | service-connect | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | file-receive | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | proxy | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | file-send | `"1h0m0s"` | +| `EDGEVPN_RELAY_RESERVATION_TTL` | `--relay-service-reservation-ttl` | dns | `"1h0m0s"` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | global | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | start | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | api | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | service-add | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | service-connect | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | file-receive | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | proxy | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | file-send | `true` | +| `EDGEVPN_RELAY_SERVICE` | `--relay-service` | dns | `true` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | global | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | start | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | api | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | service-add | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | service-connect | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | file-receive | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | proxy | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | file-send | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_ACL_REFRESH` | `--relay-service-acl-refresh` | dns | `"30s"` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | global | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | start | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | api | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | service-add | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | service-connect | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | file-receive | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | proxy | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | file-send | `true` | +| `EDGEVPN_RELAY_SERVICE_NETWORK_ONLY` | `--relay-service-network-only` | dns | `true` | +| `EGRESS` | `--egress` | global | `false` | +| `EGRESSANNOUNCE` | `--egress-announce-time` | global | `200` | +| `ENABLE_HEALTHCHECKS` | `--enable-healthchecks` | api | `false` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | global | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | start | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | api | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | service-add | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | service-connect | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | file-receive | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | proxy | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | file-send | `120` | +| `HEALTHCHECKINTERVAL` | `--aliveness-healthcheck-interval` | dns | `120` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | global | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | start | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | api | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | service-add | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | service-connect | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | file-receive | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | proxy | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | file-send | `900` | +| `HEALTHCHECKMAXINTERVAL` | `--aliveness-healthcheck-max-interval` | dns | `900` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | global | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | start | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | api | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | service-add | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | service-connect | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | file-receive | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | proxy | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | file-send | `600` | +| `HEALTHCHECKSCRUBINTERVAL` | `--aliveness-healthcheck-scrub-interval` | dns | `600` | +| `IFACE` | `--interface` | global | `"edgevpn0"` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | global | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | start | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | api | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | service-add | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | service-connect | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | file-receive | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | proxy | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | file-send | `200` | +| `LIMITCONFIGCONNS` | `--limit-config-conn` | dns | `200` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | global | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | start | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | api | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | service-add | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | service-connect | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | file-receive | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | proxy | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | file-send | `30` | +| `LIMITCONFIGCONNSINBOUND` | `--limit-config-conn-inbound` | dns | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | global | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | start | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | api | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | service-add | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | service-connect | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | file-receive | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | proxy | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | file-send | `30` | +| `LIMITCONFIGCONNSOUTBOUND` | `--limit-config-conn-outbound` | dns | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | global | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | start | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | api | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | service-add | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | service-connect | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | file-receive | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | proxy | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | file-send | `30` | +| `LIMITCONFIGFD` | `--limit-config-fd` | dns | `30` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | global | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | start | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | api | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | service-add | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | service-connect | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | file-receive | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | proxy | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | file-send | `200` | +| `LIMITCONFIGSTREAMS` | `--limit-config-streams` | dns | `200` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | global | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | start | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | api | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | service-add | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | service-connect | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | file-receive | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | proxy | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | file-send | `30` | +| `LIMITCONFIGSTREAMSINBOUND` | `--limit-config-streams-inbound` | dns | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | global | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | start | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | api | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | service-add | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | service-connect | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | file-receive | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | proxy | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | file-send | `30` | +| `LIMITCONFIGSTREAMSOUTBOUND` | `--limit-config-streams-outbound` | dns | `30` | +| `LIMITENABLE` | `--limit-enable` | global | `false` | +| `LIMITENABLE` | `--limit-enable` | start | `false` | +| `LIMITENABLE` | `--limit-enable` | api | `false` | +| `LIMITENABLE` | `--limit-enable` | service-add | `false` | +| `LIMITENABLE` | `--limit-enable` | service-connect | `false` | +| `LIMITENABLE` | `--limit-enable` | file-receive | `false` | +| `LIMITENABLE` | `--limit-enable` | proxy | `false` | +| `LIMITENABLE` | `--limit-enable` | file-send | `false` | +| `LIMITENABLE` | `--limit-enable` | dns | `false` | +| `LIMITFILE` | `--limit-file` | global | — | +| `LIMITFILE` | `--limit-file` | start | — | +| `LIMITFILE` | `--limit-file` | api | — | +| `LIMITFILE` | `--limit-file` | service-add | — | +| `LIMITFILE` | `--limit-file` | service-connect | — | +| `LIMITFILE` | `--limit-file` | file-receive | — | +| `LIMITFILE` | `--limit-file` | proxy | — | +| `LIMITFILE` | `--limit-file` | file-send | — | +| `LIMITFILE` | `--limit-file` | dns | — | +| `LIMITSCOPE` | `--limit-scope` | global | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | start | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | api | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | service-add | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | service-connect | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | file-receive | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | proxy | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | file-send | `"system"` | +| `LIMITSCOPE` | `--limit-scope` | dns | `"system"` | +| `PEERGATE` | `--peergate` | global | `false` | +| `PEERGATE` | `--peergate` | start | `false` | +| `PEERGATE` | `--peergate` | api | `false` | +| `PEERGATE` | `--peergate` | service-add | `false` | +| `PEERGATE` | `--peergate` | service-connect | `false` | +| `PEERGATE` | `--peergate` | file-receive | `false` | +| `PEERGATE` | `--peergate` | proxy | `false` | +| `PEERGATE` | `--peergate` | file-send | `false` | +| `PEERGATE` | `--peergate` | dns | `false` | +| `PEERGATE_AUTH` | `--peergate-auth` | global | — | +| `PEERGATE_AUTH` | `--peergate-auth` | start | — | +| `PEERGATE_AUTH` | `--peergate-auth` | api | — | +| `PEERGATE_AUTH` | `--peergate-auth` | service-add | — | +| `PEERGATE_AUTH` | `--peergate-auth` | service-connect | — | +| `PEERGATE_AUTH` | `--peergate-auth` | file-receive | — | +| `PEERGATE_AUTH` | `--peergate-auth` | proxy | — | +| `PEERGATE_AUTH` | `--peergate-auth` | file-send | — | +| `PEERGATE_AUTH` | `--peergate-auth` | dns | — | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | global | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | start | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | api | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | service-add | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | service-connect | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | file-receive | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | proxy | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | file-send | `false` | +| `PEERGATE_AUTOCLEAN` | `--peergate-autoclean` | dns | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | global | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | start | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | api | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | service-add | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | service-connect | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | file-receive | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | proxy | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | file-send | `false` | +| `PEERGATE_RELAXED` | `--peergate-relaxed` | dns | `false` | +| `PEERGUARD` | `--peerguard` | global | `false` | +| `PEERGUARD` | `--peerguard` | start | `false` | +| `PEERGUARD` | `--peerguard` | api | `false` | +| `PEERGUARD` | `--peerguard` | service-add | `false` | +| `PEERGUARD` | `--peerguard` | service-connect | `false` | +| `PEERGUARD` | `--peerguard` | file-receive | `false` | +| `PEERGUARD` | `--peerguard` | proxy | `false` | +| `PEERGUARD` | `--peerguard` | file-send | `false` | +| `PEERGUARD` | `--peerguard` | dns | `false` | +| `PROXYDEADINTERVAL` | `--dead-interval` | proxy | `600` | +| `PROXYINTERVAL` | `--interval` | proxy | `120` | +| `PROXYLISTEN` | `--listen` | proxy | `":8080"` | +| `ROUTER` | `--router` | global | — | +| `TRANSIENTCONN` | `--transient-conn` | global | `false` | diff --git a/docs/content/en/docs/reference/ledger-buckets.md b/docs/content/en/docs/reference/ledger-buckets.md new file mode 100644 index 00000000..cc632532 --- /dev/null +++ b/docs/content/en/docs/reference/ledger-buckets.md @@ -0,0 +1,168 @@ +--- +title: "Ledger buckets" +linkTitle: "Ledger buckets" +weight: 45 +description: > + The buckets EdgeVPN keeps in the shared ledger, what their keys mean, and who writes and reads each one. +--- + +The ledger is a two-level map: **bucket → key → value** +(`map[string]map[string]SignedData` in `pkg/blockchain/block.go`). A *bucket* is +just the first level — a namespace. There is no schema, no registration step and +no fixed list: a bucket springs into existence the moment something writes a key +into it, including a `PUT /api/ledger///` from the +[API](../api/) with a name nobody has ever used. + +What follows is the set of buckets **EdgeVPN itself uses**. The names are +declared in `pkg/protocol/protocol.go`; the key semantics below are what the +code that writes and reads them actually does, and they are the part that trips +people up — `machines` is keyed by **IP address**, and `dns` by **regular +expression**, not by hostname. + +## Summary + +| Bucket | Key | Value | Written by | Read by | +|---|---|---|---|---| +| `machines` | VPN IP address (`10.1.0.11`) | `types.Machine` | the VPN service, on every announce | packet routing, DHCP, `/api/machines` | +| `users` | peer ID | `types.User` | a peer before it dials a service, file or egress | the service/file/egress stream handlers, `/api/users` | +| `services` | service name (`--name` / `service-add`) | `types.Service` | the node exposing the service | `service-connect`, `/api/services` | +| `files` | file name (`--name` / `file-send`) | `types.File` | the node sharing the file | `file-receive`, `/api/files` | +| `healthcheck` | peer ID | RFC3339 UTC timestamp, as a string | the alive service, every heartbeat | liveness for every other bucket, `/api/nodes`, relay ACLs | +| `dns` | a **regular expression** | `types.DNS` (`map[dns.Type]string`) | `edgevpn dns`, `POST /api/dns` | the embedded DNS server, `/api/dns` | +| `egress` | peer ID | the literal string `ok` | a node started with the egress service | the HTTP proxy when picking an egress | +| `trustzone` | peer ID | empty string | PeerGuardian, after a peer passes a challenge | PeerGater, when gating gossip | +| `trustzoneAuth` | provider-prefixed name (`ecdsa_1`) | provider data (an ECDSA public key) | **you**, by hand, via the API | the auth providers, when validating challenges | +| `dhcp` | the literal key `leader` | peer ID of the current lease leader | the DHCP service during leader election | the DHCP service | + +`dhcp` is the one bucket with no constant in `pkg/protocol/protocol.go` — it is +written as a bare string literal in `pkg/vpn/dhcp.go`. It still shows up in +`/api/ledger` like any other. + +Values are Go structs serialised with no `json` struct tags, so field names come +back **PascalCase** (`PeerID`, `Hostname`) — see +[response format](../api/#response-format). + +## machines + +Keyed by the node's **VPN IP address without the mask** — `10.1.0.11`, not +`10.1.0.11/24` and not a peer ID. `pkg/vpn/vpn.go` parses `--address` and uses +the resulting IP as the key, re-announcing whenever the entry is missing or +names a different peer. + +The value is `types.Machine`: `PeerID`, `Hostname`, `OS`, `Arch`, `Address`, +`Version`. + +This bucket is the routing table. When the VPN has a packet for `10.1.0.12` it +looks that address up here to find the peer ID to open a stream to; if the +lookup misses, the packet is dropped. It is also what +[DHCP](../../how-to/addressing-and-dhcp/) reads to work out which addresses are +already taken, and what `/api/machines` returns. + +## users + +Keyed by **peer ID**, value `types.User` (`PeerID`, `Timestamp`). + +This is not a user directory, despite the name. It is a "I am about to connect +to you" announcement: `file-receive`, `service-connect` and the egress client +each add their own peer ID here before dialling, and the corresponding stream +handlers on the serving side reset any incoming stream whose remote peer is not +present in this bucket. A node that never consumes anything never appears in it. + +## services + +Keyed by the **service name** you chose (`edgevpn service-add --name mysvc`, or +the `serviceID` argument in the library API), value `types.Service` (`PeerID`, +`Name`). + +The exposing node re-announces the entry whenever it is missing or points at a +different peer, so two nodes exposing the same name will fight over the key — +under `--ownership enforce` the peer that claimed it first keeps it for as long +as it stays alive. The connecting side looks the name up to find which peer to +dial. See +[tunnel TCP services](../../how-to/tunnel-tcp-services/). + +## files + +Keyed by the **file name** you chose (`edgevpn file-send --name myfile`), value +`types.File` (`PeerID`, `Name`). Exactly the same shape as `services`: the +sharing node announces, `file-receive` polls the bucket until the name appears +and then opens a stream to the peer named in the value. The file contents never +enter the ledger. See +[send and receive files](../../how-to/send-and-receive-files/). + +## healthcheck + +Keyed by **peer ID**, value the peer's own clock as an RFC3339 UTC timestamp +string (`pkg/services/alive.go`). + +This is the heartbeat, and it is the bucket every other bucket depends on: a +peer is "alive" if its timestamp here is newer than the liveness window, and +under `--ownership enforce` an entry in `machines`, `services`, `files`, `users`, +`dns` or `egress` is only honoured while its owner is alive. Its own entries age +out on an absolute TTL rather than on liveness, for the obvious reason. +`/api/nodes` and the [relay ACL](../../how-to/relays-and-hop-nodes/) read it too. + +## dns + +Keyed by a **regular expression**, not a hostname. The value is `types.DNS`, a +map from DNS record type to value — `{"A": "10.1.0.11"}`. + +The resolver in `pkg/services/dns.go` walks every key in the bucket, compiles it +as a Go regexp and returns the first entry that matches the queried name. Two +consequences worth internalising: + +- A key of `foo.bar` matches any name *containing* `foo.bar`, because the + pattern is unanchored. Anchor it (`^foo\.bar\.$`) if you want an exact name. + Queried names arrive with the trailing dot, as `foo.bar.`. +- Match order across the bucket is map iteration order, so overlapping patterns + resolve non-deterministically. Keep patterns disjoint. + +See [enable the DNS server](../../how-to/enable-dns/). + +## egress + +Keyed by **peer ID**, value the literal string `ok` — presence is the whole +signal. A node running the egress service re-announces its own key; the HTTP +proxy intersects this bucket with the set of currently alive peers and picks one +at random to forward a request through. See +[HTTP egress and the proxy](../../how-to/http-egress-and-proxy/). + +## trustzone and trustzoneAuth + +These two belong to the experimental `--peerguard` machinery +([trusted networks](../../how-to/trusted-networks/)). Together with `dhcp` they +are the EdgeVPN-defined buckets absent from the policy registry in +`pkg/blockchain/policy.go`, so they take the zero policy: no owner, no expiry, +open to any writer. A node that signs still signs what it writes here — what +the zero policy skips is the *verification*, so an incoming entry is taken +whenever its version is strictly higher, whoever wrote it. + +- **`trustzone`** is keyed by peer ID with an empty value. PeerGuardian adds a + peer's ID once one of the configured auth providers accepts a challenge + response from it; PeerGater turns the key set into the list of senders whose + gossip is accepted. With `autocleanup`, keys for peers no longer seen in the + message hub are deleted. +- **`trustzoneAuth`** holds the *provider* configuration — the material used to + validate challenges, not the peers that passed them. Keys are namespaced by + provider: the ECDSA provider picks up every key containing `ecdsa` and treats + its value as a public key to verify against. This is the bucket the API + reference writes to in its example: + + ```bash + curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/ecdsa_1/' + ``` + + Because the bucket is open, anyone who can reach any node's API — or any peer + already permitted to gossip — can add a key here and thereby authorise new + peers. It is a distribution mechanism for trust anchors, not a protected one. + See [the security model](../../explanation/security-model/). + +## Ownership and expiry + +Whether a bucket is signed, who owns an entry and when it expires is a separate +concern, defined once in `pkg/blockchain/policy.go`. The operator-facing table +is in [ledger ownership](../../how-to/ledger-ownership/); the design note is +[the authenticated ledger](../../explanation/authenticated-ledger/). In short: +`machines`, `services`, `files`, `users`, `egress`, `healthcheck` and `dns` are +owned and expiring; `trustzone`, `trustzoneAuth`, `dhcp` and any bucket you +invent yourself are open and permanent. diff --git a/docs/content/en/docs/Concepts/Token/_index.md b/docs/content/en/docs/reference/network-config.md similarity index 89% rename from docs/content/en/docs/Concepts/Token/_index.md rename to docs/content/en/docs/reference/network-config.md index 5ec4f1b8..e420971a 100644 --- a/docs/content/en/docs/Concepts/Token/_index.md +++ b/docs/content/en/docs/reference/network-config.md @@ -1,11 +1,14 @@ --- -title: "Token" -linkTitle: "Token" -weight: 3 +title: "Network configuration and tokens" +linkTitle: "Network config" +weight: 30 +aliases: + - /docs/concepts/token/ description: > - The edgevpn network token + Every field of the network configuration file, and how it maps to a token. --- + A network token represent the network which edgevpn attempts to establish a connection among peers. A token is created by encoding in base64 a network configuration. @@ -61,7 +64,7 @@ max_message_size: 20971520 The values can be all tweaked to your needs. EdgeVPN uses an otp mechanism to decrypt blockchain messages between the nodes and to discover nodes from DHT, this is in order to prevent bruteforce attacks and avoid bad actors listening on the protocol. -See [the Architecture section]() for more information. +See [the Architecture section](../../explanation/architecture/) for more information. - The OTP keys (`otp.crypto.key`) rotates the cipher key used to encode/decode the blockchain messages. The interval of rotation can be set for both DHT and the Blockchain messages. The length is the cipher key length (AES-256 by default) used by the sealer to decrypt/encrypt messages. - The DHT OTP keys (`otp.dht.key`) rotates the discovery key used during DHT node discovery. A key is generated and used with OTP at defined intervals to scramble potential listeners. diff --git a/docs/content/en/docs/tools/_index.md b/docs/content/en/docs/tools/_index.md new file mode 100644 index 00000000..ea175625 --- /dev/null +++ b/docs/content/en/docs/tools/_index.md @@ -0,0 +1,7 @@ +--- +title: "Tools" +linkTitle: "Tools" +weight: 45 +description: > + Companion applications built around EdgeVPN. +--- diff --git a/docs/content/en/docs/Getting started/gui.md b/docs/content/en/docs/tools/desktop-gui.md similarity index 83% rename from docs/content/en/docs/Getting started/gui.md rename to docs/content/en/docs/tools/desktop-gui.md index 860ab4bc..60870cd3 100644 --- a/docs/content/en/docs/Getting started/gui.md +++ b/docs/content/en/docs/tools/desktop-gui.md @@ -1,11 +1,14 @@ --- -title: "GUI" -linkTitle: "GUI" -weight: 1 +title: "Desktop GUI" +linkTitle: "Desktop GUI" +weight: 10 +aliases: + - /docs/getting-started/gui/ description: > - GUI app + The alpha desktop GUI application for Linux. --- + A Desktop GUI application (alpha) for Linux is available [here](https://github.com/mudler/edgevpn-gui). Note the GUI doesn't require the CLI to be installed. It will automatically prompt to download the latest available version, and offer a version management option. diff --git a/docs/content/en/docs/troubleshooting.md b/docs/content/en/docs/troubleshooting.md new file mode 100644 index 00000000..267554cb --- /dev/null +++ b/docs/content/en/docs/troubleshooting.md @@ -0,0 +1,49 @@ +--- +title: "Troubleshooting" +linkTitle: "Troubleshooting" +weight: 50 +description: > + Common bootstrap failures and poor network performance, and what to do about them. +--- + +## Slow bootstrap or poor network performance + +If during bootstrap you see messages like: + +``` +edgevpn[3679]: * [/ip4/104.131.131.82/tcp/4001] failed to negotiate stream multiplexer: context deadline exceeded +``` + +or + +``` +edgevpn[9971]: 2021/12/16 20:56:34 failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size for details. +``` + +or generally experiencing poor network performance, it is recommended to +increase the maximum buffer size by running: + +``` +sysctl -w net.core.rmem_max=2500000 +``` + +That setting does not survive a reboot. To make it permanent, drop it into +`/etc/sysctl.d/`: + +```bash +echo "net.core.rmem_max=2500000" | sudo tee /etc/sysctl.d/99-edgevpn.conf +sudo sysctl --system +``` + +The systemd unit written by the [install script](../tutorials/install/) already applies +it on every start, via `ExecStartPre=-/bin/sh -c "sysctl -w +net.core.rmem_max=2500000"`. + +## Nodes never see each other + +It might take up time to build the connection between nodes. Wait at least +5 mins, it depends on the network behind the hosts. + +If they still do not connect, check that every node is using the *same* token +or configuration file — a token identifies the network, and two different +tokens are two different networks that will never meet. diff --git a/docs/content/en/docs/tutorials/_index.md b/docs/content/en/docs/tutorials/_index.md new file mode 100644 index 00000000..c802d81e --- /dev/null +++ b/docs/content/en/docs/tutorials/_index.md @@ -0,0 +1,11 @@ +--- +title: "Tutorials" +linkTitle: "Tutorials" +weight: 10 +description: > + Start here. End-to-end walkthroughs that take you from nothing to a working network. +--- + +Tutorials are learning-oriented: each one takes you all the way through a task +and tells you exactly what to type. If you already know what you want and need +the specifics, the [how-to guides](../how-to/) are shorter. diff --git a/docs/content/en/docs/tutorials/decentralized-k3s-cluster.md b/docs/content/en/docs/tutorials/decentralized-k3s-cluster.md new file mode 100644 index 00000000..44189301 --- /dev/null +++ b/docs/content/en/docs/tutorials/decentralized-k3s-cluster.md @@ -0,0 +1,40 @@ +--- +title: "A network-decentralized k3s test cluster" +linkTitle: "Decentralized k3s cluster" +weight: 40 +description: > + Run a multi-node k3s development cluster across machines that are only reachable behind NAT. +--- + +Let's see a practical example, you are developing something for kubernetes and +you want to try a multi-node setup, but you have machines available that are +only behind NAT (pity!) and you would really like to leverage HW. + +If you are not really interested in network performance (again, that's for +development purposes only!) then you could use `edgevpn` + +[k3s](https://github.com/k3s-io/k3s) in this way: + +1) Generate edgevpn config: `edgevpn -g > vpn.yaml` +2) Start the vpn: + + on node A: `sudo IFACE=edgevpn0 ADDRESS=10.1.0.3/24 EDGEVPNCONFIG=vpn.yaml edgevpn` + + on node B: `sudo IFACE=edgevpn0 ADDRESS=10.1.0.4/24 EDGEVPNCONFIG=vpn.yaml edgevpn` +3) Start k3s: + + on node A: `k3s server --flannel-iface=edgevpn0` + + on node B: `K3S_URL=https://10.1.0.3:6443 K3S_TOKEN=xx k3s agent --flannel-iface=edgevpn0 --node-ip 10.1.0.4` + +We have used flannel here, but other CNI should work as well. + +{{% pageinfo color="warning" %}} +This is a development setup. The p2p network is chatty and is not built for +low-latency workloads — see +[when not to use EdgeVPN](../../explanation/when-not-to-use-edgevpn/) before +pointing anything important at it. +{{% /pageinfo %}} + +For a managed, batteries-included version of the same idea, see +[Kairos](https://github.com/kairos-io/kairos), which creates Kubernetes +clusters with k3s automatically using EdgeVPN networks. diff --git a/docs/content/en/docs/tutorials/install.md b/docs/content/en/docs/tutorials/install.md new file mode 100644 index 00000000..68e84afb --- /dev/null +++ b/docs/content/en/docs/tutorials/install.md @@ -0,0 +1,205 @@ +--- +title: "Install EdgeVPN" +linkTitle: "Install" +weight: 10 +description: > + Every way to get the binary — the install script, release archives, Homebrew, the container image, and building from source. +--- + +EdgeVPN is a single statically compiled binary with no runtime dependencies. +Pick whichever route suits the machine; if you just want the shortest path to a +running network, take the [install script](#install-script) and carry on to +[your first network](../your-first-network/). + +## Install script + +The repository ships an installer at +[`install.sh`](https://github.com/mudler/edgevpn/blob/master/install.sh): + +```bash +curl -sfL https://raw.githubusercontent.com/mudler/edgevpn/master/install.sh | sh +``` + +It is **Linux only** — it aborts on any other platform. What it does: + +1. Detects the architecture (`x86_64`, `arm64`, `armv6`, `i386`) and looks up + the latest release tag from the GitHub API. +2. Downloads the matching release archive and installs the `edgevpn` binary + into `/usr/local/bin`, using `sudo` when not running as root. If + `/usr/local/bin` is not writable **and** `/opt/bin` already exists, it uses + `/opt/bin` instead; it will not create that directory. +3. Writes a service unit: a systemd **template** unit + `/etc/systemd/system/edgevpn@.service` if systemd is present, or + `/etc/init.d/edgevpn` under OpenRC. + +The script installs the unit but does not enable or start anything, so nothing +runs until you ask it to. + +Environment variables it honours: + +| Variable | Effect | +|---|---| +| `VERSION` | Install a specific release tag instead of the latest | +| `INSTALL_BIN_DIR` | Where to put the binary (default `/usr/local/bin`) | +| `INSTALL_SYSTEMD_DIR` | Where to write the unit (default `/etc/systemd/system`) | +| `DOWNLOADER` | `curl` (default) or `wget` | +| `ARCH`, `OS` | Override the detected architecture and platform | + +For example: + +```bash +curl -sfL https://raw.githubusercontent.com/mudler/edgevpn/master/install.sh | VERSION=v0.35.3 sh +``` + +Setting `VERSION` yourself is also the workaround when the GitHub API is +unavailable or rate-limits you: the script's version lookup has no working +fallback, so a failed lookup leaves `VERSION` empty, the download URL is +malformed, and the install aborts on the failed download rather than +installing anything wrong. + +{{% pageinfo color="warning" %}} +The installer does **not** verify a checksum or signature on the download — the +script has a `TODO` where that check belongs. If that matters to you, take the +[manual route](#release-archives) and check the archive against the +`edgevpn--checksums.txt` file published with the release. +{{% /pageinfo %}} + +### Running it as a service + +The systemd unit is a template, so one installation can run several networks +side by side. Each instance reads its configuration from an environment file +named after the instance: + +```bash +# /etc/systemd/system.conf.d/edgevpn-home.env +EDGEVPNTOKEN= +ADDRESS=10.1.0.11/24 +IFACE=edgevpn0 +``` + +```bash +sudo systemctl enable --now edgevpn@home +``` + +The unit raises `LimitNOFILE` to 49152 and applies +`sysctl -w net.core.rmem_max=2500000` before starting, which is the buffer-size +tuning described in [troubleshooting](../../troubleshooting/). Create the +`/etc/systemd/system.conf.d/` directory first if it does not exist — the +installer does not create it, and the unit will fail to start without the +environment file. + +See the [environment variables reference](../../reference/environment-variables/) +for everything that can go in that file. + +## Release archives + +Every release publishes statically compiled archives on the +[releases page](https://github.com/mudler/edgevpn/releases), for Linux, macOS, +Windows and FreeBSD on `amd64`, `arm64`, `arm`, `386` and `riscv64`. + +Archives are named `edgevpn---.tar.gz` — for example +`edgevpn-v0.35.3-Linux-x86_64.tar.gz`. A `edgevpn--checksums.txt` file is +published alongside them. + +```bash +tar xvf edgevpn-*-Linux-x86_64.tar.gz +sudo install -m 755 edgevpn /usr/local/bin/edgevpn +edgevpn --version +``` + +## Homebrew (macOS) + +If you're using homebrew in MacOS, you can use the +[edgevpn formula](https://formulae.brew.sh/formula/edgevpn): + +```bash +brew install edgevpn +``` + +## Container image + +Images are published to `quay.io/mudler/edgevpn` for `linux/amd64`, +`linux/arm64` and `linux/arm`. + +{{% pageinfo color="warning" %}} +**`latest` is not the latest release.** The image workflow +(`.github/workflows/images.yml`) pushes `:latest` on **every push to +`master`**, so that tag tracks the development branch. It only re-tags a +release as `latest` when the git tag matches `^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$` +— and every EdgeVPN tag is `v`-prefixed (`v0.35.3`), which never matches. Pin +to a version tag if you want a release. +{{% /pageinfo %}} + +So the tags you can expect are: + +| Tag | What it is | +|---|---| +| `v0.35.3` (a release tag) | The image built from that release | +| `<8-char commit SHA>` | The image built from that exact commit, on both master pushes and tag pushes | +| `latest` | Whatever `master` last pushed — a development build | + +Bringing up the VPN interface from inside a container needs host networking, +the `NET_ADMIN` capability and the TUN device: + +```bash +docker run --rm \ + --network host \ + --cap-add NET_ADMIN \ + --device /dev/net/tun:/dev/net/tun \ + -e EDGEVPNTOKEN= \ + quay.io/mudler/edgevpn:v0.35.3 --address 10.1.0.11/24 +``` + +Sub-commands that do not create an interface — `edgevpn file-send`, +`edgevpn service-connect`, `edgevpn proxy` — need none of that and run in a +plain container. + +### Docker Compose + +There is a +[`docker-compose.yml`](https://github.com/mudler/edgevpn/blob/master/docker-compose.yml) +in the repository with the same settings plus a healthcheck. Using docker is +still experimental as setups can vary wildly, so you'll likely need to edit it +— at minimum the `EDGEVPNTOKEN` and the volume path, both marked `CHANGEME`. +Note that it pins `image: quay.io/mudler/edgevpn:latest`, which is the +development build described above. + +```bash +git clone https://github.com/mudler/edgevpn +cd edgevpn +sudo docker compose up --detach +``` + +## Build from source + +You need [Go](https://golang.org/) 1.26 (the version in `go.mod`), `make`, and +[Node.js](https://nodejs.org/) 20.19 or newer. The web interface is a React +application that is compiled and then embedded into the binary, so a JavaScript +toolchain is part of a full build. + +```bash +git clone https://github.com/mudler/edgevpn +cd edgevpn +make build # compiles the web interface, then the Go binary +``` + +`make build` compiles the web interface first and the Go binary afterwards. +Running `go build` on its own works only when `api/react-ui/dist` already +exists — the interface is embedded with `//go:embed`, so a missing directory is +a compile error rather than a warning. If you are working on the Go side only, +you can stub it out: + +```bash +mkdir -p api/react-ui/dist && touch api/react-ui/dist/index.html +``` + +Building the **documentation site** is the one thing that needs more: Hugo and +Node/npm, both of which `docs/scripts/build.sh` sets up for you. + +```bash +cd docs && make build # one-off build into docs/public +cd docs && make serve # live preview on http://localhost:1313 +``` + +See [contributing](../../contributing/) for the rest of the development +workflow. diff --git a/docs/content/en/docs/tutorials/share-a-service.md b/docs/content/en/docs/tutorials/share-a-service.md new file mode 100644 index 00000000..6529f22d --- /dev/null +++ b/docs/content/en/docs/tutorials/share-a-service.md @@ -0,0 +1,120 @@ +--- +title: "Share a service between two hosts" +linkTitle: "Share a service" +weight: 30 +description: > + Walk through exposing a TCP service on one host and reaching it from another, with no VPN interface and no root. +--- + +In this tutorial you will take a TCP service running on one machine — we will +use an SSH server, but anything that speaks TCP works — and reach it from a +second machine somewhere else, over EdgeVPN. + +Nothing here brings up a VPN interface, so you do not need root on either host. + +You will need: + +- Two hosts, **A** and **B**, that can both reach the internet. They do not + need to see each other, and neither needs a public IP. +- `edgevpn` installed on both. See + [your first network](../your-first-network/) if you have not installed it yet. +- Something listening on a TCP port on host A. We assume `sshd` on + `127.0.0.1:22`. + +If you already know the shape of this and just want the commands, read +[Tunnel TCP services](../../how-to/tunnel-tcp-services/) instead. + +## 1. Generate a token + +Do this once, on either host. The token *is* the network — anyone holding it +joins it, so treat it like a password. + +```bash +$ edgevpn -g -b +b3RwOgogIGRodDoKICAgIGludGVydmFsOiA5MDAwCiAgICBrZXk6IDRPNk5aUUMyTzVRNzdKRlJJT1BCWDVWRUkzRUlKSFdECiAgICBsZW5ndGg6IDMyCiAgY3J5cHRvOgogICAgaW50ZXJ2YWw6IDkwMDAKICAgIGtleTogN1hTUUNZN0NaT0haVkxQR0VWTVFRTFZTWE5ORzNOUUgKICAgIGxlbmd0aDogMzIKcm9vbTogWUhmWXlkSUpJRlBieGZDbklLVlNmcGxFa3BhVFFzUk0KcmVuZGV6dm91czoga1hxc2VEcnNqbmFEbFJsclJCU2R0UHZGV0RPZGpXd0cKbWRuczogZ0NzelJqZk5XZEFPdHhubm1mZ3RlSWx6Zk1BRHRiZGEKbWF4X21lc3NhZ2Vfc2l6ZTogMjA5NzE1MjAK +``` + +Copy that string to both hosts and export it, so you do not have to repeat it +on every command: + +```bash +# on host A, and again on host B +$ export EDGEVPNTOKEN=b3RwOgogIGRodDoK... +``` + +## 2. Expose the service on host A + +`service-add` takes a unique name for the service and the address EdgeVPN +should connect to on your behalf: + +```bash +$ edgevpn service-add "MyCoolService" "127.0.0.1:22" + INFO edgevpn Copyright (C) 2021-2022 Ettore Di Giacinto + This program comes with ABSOLUTELY NO WARRANTY. + This is free software, and you are welcome to redistribute it + under certain conditions. + INFO Version: ... commit: ... + INFO Starting EdgeVPN network + INFO Node ID: 12D3KooWRW4RXSMAh7CTRsTjX7iEjU6DEU8QKJZvFjSosv7zCCeZ + INFO Bootstrapping DHT +``` + +The name — `MyCoolService` here — is how host B will ask for this service. It +is announced on the shared ledger, so it must be unique within the network. + +Leave this running. The process stays in the foreground and acts as the proxy +between the network and `127.0.0.1:22`. + +## 3. Connect from host B + +`service-connect` takes the same name, and a local address to bind: + +```bash +$ edgevpn service-connect "MyCoolService" "127.0.0.1:9090" + INFO edgevpn Copyright (C) 2021-2022 Ettore Di Giacinto + This program comes with ABSOLUTELY NO WARRANTY. + This is free software, and you are welcome to redistribute it + under certain conditions. + INFO Version: ... commit: ... + INFO Starting EdgeVPN network + INFO Node ID: 12D3KooWEyoppNCUx8Yx1oQ4rALCcs2i1p1sc6Bqm2S8LzRvbAcM + INFO Bootstrapping DHT +``` + +Leave this running too. Host B now has a listener on `127.0.0.1:9090` that +forwards to `127.0.0.1:22` on host A. + +Peers find each other over the DHT, which is not instant. It is normal for the +first connection to take a few minutes; on a bad network it can take longer. + +## 4. Use it + +In a third terminal, on host B: + +```bash +$ ssh -p 9090 youruser@127.0.0.1 +``` + +You are now logged into host A. Anything that speaks TCP to `127.0.0.1:9090` +on B reaches port 22 on A. + +If the connection hangs, the two nodes have most likely not discovered each +other yet — wait, and check that both processes are still running and using the +same token. + +## What you did + +- Created a network with a token, with no server and no configuration to + coordinate. +- Published a TCP endpoint on the ledger under a name, from host A. +- Bound a local port on host B that tunnels to it. + +Neither host allocated a `tun` device or needed root, and neither needed to +know the other's address. + +## Next + +- [Tunnel TCP services](../../how-to/tunnel-tcp-services/) — the terse version + of this, for when you come back to it. +- [Your first network](../your-first-network/) — the full VPN, where peers get + virtual IPs and can reach each other directly. diff --git a/docs/content/en/docs/tutorials/your-first-network.md b/docs/content/en/docs/tutorials/your-first-network.md new file mode 100644 index 00000000..8dec59f3 --- /dev/null +++ b/docs/content/en/docs/tutorials/your-first-network.md @@ -0,0 +1,42 @@ +--- +title: "Your first network" +linkTitle: "Your first network" +weight: 20 +aliases: + - /docs/getting-started/ +description: > + Install EdgeVPN, generate a token, and bring up a two-node VPN. +--- + + +## Get EdgeVPN + +Prerequisites: No dependencies. EdgeVPN releases are statically compiled. + +On Linux, the quickest way is the install script: + +```bash +curl -sfL https://raw.githubusercontent.com/mudler/edgevpn/master/install.sh | sh +``` + +Release archives, Homebrew, the container image and building from source are +all covered in [Install EdgeVPN](../install/). + +## Creating Your First VPN + +Let's create our first vpn now and start it: + +```bash +$> EDGEVPNTOKEN=$(edgevpn -b -g) +$> edgevpn --dhcp --api +``` + +That's it! + +You can now access the web interface on [http://localhost:8080](http://localhost:8080). + +To join new nodes in the network, simply copy the `EDGEVPNTOKEN` and use it to start edgevpn in other nodes: + +```bash +$> EDGEVPNTOKEN= edgevpn --dhcp +``` diff --git a/docs/layouts/404.html b/docs/layouts/404.html index 378b7367..812db681 100644 --- a/docs/layouts/404.html +++ b/docs/layouts/404.html @@ -2,7 +2,7 @@

Not found

-

Oops! This page doesn't exist. Try going back to our home page.

+

Oops! This page doesn't exist. Try going back to our home page.

You can learn how to make a 404 page like this in Custom 404 Pages.

diff --git a/docs/superpowers/plans/2026-08-03-docs-restructure.md b/docs/superpowers/plans/2026-08-03-docs-restructure.md new file mode 100644 index 00000000..a1c2aac8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-docs-restructure.md @@ -0,0 +1,1585 @@ +# EdgeVPN Documentation Restructure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restructure EdgeVPN's documentation into a Diátaxis information architecture, generate the CLI and environment-variable reference from the `cli.App` with a CI drift gate, and write the pages for features that are currently invisible. + +**Architecture:** A new `docs/generate` package walks the real `cli.App` — shared with `main.go` via a new `cmd.NewApp()` so it cannot drift — and emits Hugo pages into `docs/content/en/docs/reference/`. Content moves into four Diátaxis sections with Hugo `aliases:` preserving every old URL. Docsy stays; the custom theme is a separate sub-project. + +**Tech Stack:** Hugo (extended, 0.152.2 in CI) + Docsy via Hugo Modules, Go 1.26, urfave/cli v2. + +## Global Constraints + +- **Spec:** `docs/superpowers/specs/2026-08-03-docs-restructure-design.md`. Read it before starting. +- **The CLI is urfave/cli v2.** `go.mod:32` also declares `v3` as a direct dependency, but **no `.go` file imports it**. Never write docs or code against v3. +- **Every command shown in a page must be executed, or explicitly marked unverified in the implementation report.** Undocumented-but-untested command lines are exactly what produced the `--peerguardian` bug. A command needing two hosts or root is marked as such — never silently trusted. +- **Every moved page keeps its old URL** via a Hugo `aliases:` front-matter entry. The site is linked from the README, from Kairos, and from search results. +- **Generated files are never hand-edited.** They carry a banner saying so. +- **Weights must be unique within a section.** The current collisions are a defect being fixed, not a pattern to copy. +- **Do not fix the licence inconsistency.** `LICENSE` is Apache-2.0, the README badge says GPL3, the footer says Apache v2, the CLI banner is GPL-flavoured, ~10 source files carry GPL-2 headers. This is the maintainer's legal call. Flag it; never silently pick one. +- **Out of scope, do not touch:** the `urfave/cli/v3` phantom dep, `cmd/peergate.go`'s `go vet` failure, the echo path-param unescape bug, the custom Hugo theme. +- Branch: `feat/docs-restructure`. Commit after every task. +- **Working-tree hazard — never run a bare `git add -A` or `git add .`.** Two things are loose in the tree and must stay out of every commit: + 1. `api/react-ui/` is untracked here. It belongs to the sibling branch `feat/react-ui-design-system` (a whole React application) and is only present because the working tree was reused. Committing it onto this branch would drag an unrelated feature into a docs PR. + 2. `docs/package.json` and `docs/package-lock.json` are **tracked and simultaneously gitignored**, and `docs/scripts/build.sh` runs `npm install --save`, which rewrites them on **every docs build**. So `cd docs && make build` dirties the tree as a side effect. After building, restore them: `git checkout -- docs/package.json docs/package-lock.json`. Never commit those modifications — they are build noise, and fixing the underlying packaging problem is explicitly out of scope. + + Always `git add` explicit paths. +- **Hugo build baseline: ZERO errors.** Measured on this repo at branch point with the pinned toolchain (`docs/Makefile` → Hugo 0.152.2 extended): the build succeeds, emitting 29 pages, 0 aliases, and exactly three deprecation **warnings** (`params.algolia_docsearch`, the GA4/UA notice, `footer_about_disable`). Any *error* you see is one you introduced. + - An earlier report claimed a 43-error baseline. That was measured on Hugo **0.146.3**, a version this project does not use. Ignore it; do not reintroduce it as a target. + - The three deprecation warnings are pre-existing. Two are addressed incidentally by Task 4 (the GA4 one, when the placeholder analytics ID goes). Do not chase the others. + - **`docs/themes/docsy` is an initialised submodule on this checkout** even though `config.toml` sets no `theme=`. Task 4 removes it; confirm the build still succeeds afterwards rather than assuming. + +--- + +## File Structure + +**Created:** + +| Path | Responsibility | +|---|---| +| `cmd/app.go` | `NewApp(version string) *cli.App` — the single definition of the CLI, used by `main.go` and the generator. | +| `docs/generate/main.go` | Walks the app, emits reference pages. | +| `docs/generate/render.go` | Markdown/front-matter rendering, separated so it is unit-testable without file I/O. | +| `docs/generate/render_test.go` | Tests for the rendering logic. | +| `docs/content/en/docs/{tutorials,how-to,reference,explanation}/_index.md` | Section landing pages. | +| `CONTRIBUTING.md` | Repo root. The contributing page currently links to a 404. | + +**Modified:** `main.go`, `Makefile`, `.github/workflows/pages.yml`, `docs/config.toml`, `.gitmodules`, `README.md`, and every page under `docs/content/en/docs/`. + +**Deleted:** `docs/themes/docsy` submodule, `docs/content/en/community/_index.md`. + +--- + +## Task 1: Share the CLI definition between main.go and the generator + +**Files:** +- Create: `cmd/app.go` +- Modify: `main.go:29-52` +- Test: `cmd/app_test.go` + +**Interfaces:** +- Consumes: existing exported `cmd.MainFlags()`, `cmd.CommonFlags`, `cmd.Start()`, `cmd.API()`, `cmd.ServiceAdd()`, `cmd.ServiceConnect()`, `cmd.FileReceive()`, `cmd.Proxy()`, `cmd.FileSend()`, `cmd.DNS()`, `cmd.Peergate()`, `cmd.Main()`, `cmd.Copyright`. +- Produces: `cmd.NewApp(version string) *cli.App`. Task 3's generator depends on this exact signature. + +**Why:** if the generator built its own `cli.App`, it could drift from the real one and the CI gate would be verifying a copy. Both must read the same definition. + +- [ ] **Step 1: Write the failing test at `cmd/app_test.go`** + +```go +package cmd_test + +import ( + "testing" + + "github.com/mudler/edgevpn/cmd" +) + +func TestNewAppHasAllCommands(t *testing.T) { + app := cmd.NewApp("v0.0.0-test") + want := []string{"start", "api", "service-add", "service-connect", "file-receive", "proxy", "file-send", "dns", "peergater"} + got := map[string]bool{} + for _, c := range app.Commands { + got[c.Name] = true + } + for _, name := range want { + if !got[name] { + t.Errorf("command %q missing from NewApp", name) + } + } + if len(app.Commands) != len(want) { + t.Errorf("got %d commands, want %d", len(app.Commands), len(want)) + } +} + +func TestNewAppHasRootFlags(t *testing.T) { + app := cmd.NewApp("v0.0.0-test") + if len(app.Flags) == 0 { + t.Fatal("NewApp has no root flags") + } + // The root flag set must include both the root-only flags and the + // common flags; --api is root-only, --token is common. + names := map[string]bool{} + for _, f := range app.Flags { + for _, n := range f.Names() { + names[n] = true + } + } + for _, want := range []string{"api", "token", "peerguard"} { + if !names[want] { + t.Errorf("root flag %q missing", want) + } + } +} + +func TestNewAppVersionIsWired(t *testing.T) { + if got := cmd.NewApp("v1.2.3").Version; got != "v1.2.3" { + t.Errorf("Version = %q, want v1.2.3", got) + } +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `go test ./cmd/ -run TestNewApp -v` +Expected: FAIL — `undefined: cmd.NewApp`. + +- [ ] **Step 3: Create `cmd/app.go`** + +Copy the exact app literal currently in `main.go`. Do not change any field. + +```go +package cmd + +import ( + "github.com/urfave/cli/v2" +) + +// NewApp builds the EdgeVPN CLI. +// +// It is the single definition of the command-line surface: main.go runs it, +// and docs/generate walks it to produce the reference documentation. Keeping +// one definition is what makes the generated docs trustworthy — a second copy +// would let the docs drift from the binary while still passing their own +// drift check. +func NewApp(version string) *cli.App { + return &cli.App{ + Name: "edgevpn", + Version: version, + Authors: []*cli.Author{{Name: "Ettore Di Giacinto"}}, + Usage: "edgevpn --config /etc/edgevpn/config.yaml", + Description: "edgevpn uses libp2p to build an immutable trusted blockchain addressable p2p network", + Copyright: Copyright, + Flags: MainFlags(), + Commands: []*cli.Command{ + Start(), + API(), + ServiceAdd(), + ServiceConnect(), + FileReceive(), + Proxy(), + FileSend(), + DNS(), + Peergate(), + }, + Action: Main(), + } +} +``` + +- [ ] **Step 4: Rewrite `main.go` to use it** + +Replace the whole `app := &cli.App{...}` literal with: + +```go + app := cmd.NewApp(internal.Version) +``` + +Remove the now-unused `"github.com/urfave/cli/v2"` import from `main.go` if nothing else there needs it. Keep the `//go:generate`-free state — do not reintroduce one. + +- [ ] **Step 5: Run the tests** + +Run: `go test ./cmd/ -run TestNewApp -v && go build ./... && go vet ./cmd/ .` +Expected: 3 tests PASS, build clean. Note `go vet ./cmd/` will still report the two pre-existing `cmd/peergate.go` non-constant format string errors — that is expected and out of scope. + +- [ ] **Step 6: Verify the binary is unchanged in behaviour** + +Run: `go build -o /tmp/ev-check . && /tmp/ev-check --help | head -30` +Expected: identical help output to before (same commands, same flags). Compare against `git stash && go build -o /tmp/ev-before . && git stash pop` if you want a byte comparison. + +- [ ] **Step 7: Commit** + +```bash +git add cmd/app.go cmd/app_test.go main.go +git commit -m "refactor(cmd): extract NewApp so docs can walk the real CLI" +``` + +--- + +## Task 2: Section skeleton + +**Files:** +- Create: `docs/content/en/docs/tutorials/_index.md`, `docs/content/en/docs/how-to/_index.md`, `docs/content/en/docs/reference/_index.md`, `docs/content/en/docs/explanation/_index.md` +- Modify: `docs/content/en/docs/_index.md` + +**Interfaces:** +- Produces: the four section directories every later task writes into, with the weights they must respect: tutorials 10, how-to 20, reference 30, explanation 40. + +- [ ] **Step 1: Create `docs/content/en/docs/tutorials/_index.md`** + +```markdown +--- +title: "Tutorials" +linkTitle: "Tutorials" +weight: 10 +description: > + Start here. End-to-end walkthroughs that take you from nothing to a working network. +--- + +Tutorials are learning-oriented: each one takes you all the way through a task +and tells you exactly what to type. If you already know what you want and need +the specifics, the [how-to guides](../how-to/) are shorter. +``` + +- [ ] **Step 2: Create `docs/content/en/docs/how-to/_index.md`** + +```markdown +--- +title: "How-to guides" +linkTitle: "How-to" +weight: 20 +description: > + Task-oriented recipes for a specific job — expose a service, run an exit node, lock a network down. +--- + +Each guide assumes you already have a working network. If you don't, start with +[your first network](../tutorials/your-first-network/). +``` + +- [ ] **Step 3: Create `docs/content/en/docs/reference/_index.md`** + +```markdown +--- +title: "Reference" +linkTitle: "Reference" +weight: 30 +description: > + Every command, flag, environment variable and API endpoint. +--- + +The [CLI reference](cli/) and [environment variables](environment-variables/) +pages are generated directly from the source, so they cannot drift from the +binary you are running. +``` + +- [ ] **Step 4: Create `docs/content/en/docs/explanation/_index.md`** + +```markdown +--- +title: "Explanation" +linkTitle: "Explanation" +weight: 40 +description: > + How EdgeVPN works and why it is built this way — architecture, the ledger, and the security model. +--- + +Background reading. Nothing here is required to use EdgeVPN, but the +[security model](security-model/) is worth reading before you deploy it. +``` + +- [ ] **Step 5: Rewrite `docs/content/en/docs/_index.md`** + +Keep its existing front matter (`title`, `linkTitle`, `weight: 20`, `menu.main.weight: 20`) exactly as-is — changing the menu weight moves the top nav. Replace the body with a short "What is EdgeVPN" orientation plus links to the four sections. Draw the description from `README.md`'s opening paragraphs, which are accurate. + +- [ ] **Step 6: Verify the build** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` +Expected: `0`. The docs build clean on Hugo 0.152.2; any error you see is yours. If it increased, you introduced an error — find it before continuing. + +- [ ] **Step 7: Commit** + +```bash +git add docs/content/en/docs/ +git commit -m "docs: add Diataxis section skeleton" +``` + +--- + +## Task 3: The reference generator + +**Files:** +- Create: `docs/generate/main.go`, `docs/generate/render.go`, `docs/generate/render_test.go` +- Modify: `Makefile` + +**Interfaces:** +- Consumes: `cmd.NewApp(version string) *cli.App` from Task 1. +- Produces: `make docs-gen`; generated pages under `docs/content/en/docs/reference/cli/` and `docs/content/en/docs/reference/environment-variables.md`. Task 4's CI gate depends on `make docs-gen` being idempotent. + +**Key API:** flags implement `cli.DocGenerationFlag` (`urfave/cli/v2@v2.27.7/flag.go:130-154`) exposing `TakesValue() bool`, `GetUsage() string`, `GetValue() string`, `GetDefaultText() string`, `GetEnvVars() []string`, `IsVisible() bool`. Type-assert to it; skip flags where the assertion fails or `IsVisible()` is false. + +**Do NOT use `app.ToMarkdown()`.** It exists (`docs.go:19`) but emits one blob with no front matter, no per-command splitting, and no env-var column. + +- [ ] **Step 1: Write the failing test at `docs/generate/render_test.go`** + +```go +package main + +import ( + "strings" + "testing" + + "github.com/urfave/cli/v2" +) + +func TestRenderFlagTableIncludesEnvVars(t *testing.T) { + flags := []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Usage: "Specify an edgevpn token in place of a config file", + EnvVars: []string{"EDGEVPNTOKEN"}, + }, + &cli.BoolFlag{ + Name: "peerguard", + Usage: "Enable peerguard. (Experimental)", + EnvVars: []string{"PEERGUARD"}, + }, + } + out := renderFlagTable(flags) + + for _, want := range []string{"--token", "EDGEVPNTOKEN", "--peerguard", "PEERGUARD", "Enable peerguard"} { + if !strings.Contains(out, want) { + t.Errorf("flag table missing %q\n%s", want, out) + } + } +} + +func TestRenderFlagTableSkipsHiddenFlags(t *testing.T) { + flags := []cli.Flag{ + &cli.StringFlag{Name: "visible", Usage: "shown"}, + &cli.StringFlag{Name: "secret", Usage: "hidden", Hidden: true}, + } + out := renderFlagTable(flags) + if strings.Contains(out, "secret") { + t.Errorf("hidden flag leaked into the table:\n%s", out) + } + if !strings.Contains(out, "visible") { + t.Errorf("visible flag missing:\n%s", out) + } +} + +func TestRenderFlagTableEscapesPipes(t *testing.T) { + // Several real usage strings contain "|" (e.g. the ownership flag lists + // "enforce | observe | off"), which would split the row into extra + // markdown columns. + flags := []cli.Flag{ + &cli.StringFlag{Name: "ownership", Usage: "enforce | observe | off", Value: "enforce"}, + } + out := renderFlagTable(flags) + + var row string + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "ownership") { + row = line + } + } + if row == "" { + t.Fatal("no row rendered for the ownership flag") + } + if !strings.Contains(row, `enforce \| observe \| off`) { + t.Errorf("pipes in usage were not escaped: %q", row) + } + // A well-formed 4-column row has exactly 5 structural pipes; every other + // pipe must be escaped. + if got := strings.Count(row, "|") - strings.Count(row, `\|`); got != 5 { + t.Errorf("row has %d structural pipes, want 5 (4 columns): %q", got, row) + } +} + +func TestRenderPageHasFrontMatterAndBanner(t *testing.T) { + out := renderCommandPage(&cli.Command{ + Name: "proxy", + Usage: "Starts a local http proxy server", + Aliases: []string{}, + Description: "Routes traffic through the p2p network", + Flags: []cli.Flag{&cli.StringFlag{Name: "listen", Usage: "Listen address"}}, + }, 10) + + if !strings.HasPrefix(out, "---\n") { + t.Error("page does not start with front matter") + } + for _, want := range []string{`title: "proxy"`, "weight: 10", "Do not edit", "--listen"} { + if !strings.Contains(out, want) { + t.Errorf("page missing %q\n%s", want, out) + } + } +} + +func TestRenderEnvVarPageMapsBackToFlags(t *testing.T) { + out := renderEnvVarPage(map[string][]envBinding{ + "EDGEVPNTOKEN": {{Flag: "--token", Command: "global", Default: ""}}, + "PROXYLISTEN": {{Flag: "--listen", Command: "proxy", Default: ":8080"}}, + }) + for _, want := range []string{"EDGEVPNTOKEN", "--token", "PROXYLISTEN", "proxy", ":8080"} { + if !strings.Contains(out, want) { + t.Errorf("env var page missing %q\n%s", want, out) + } + } +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `go test ./internal/docsgen/ -v` +Expected: FAIL — undefined `renderFlagTable`, `renderCommandPage`, `renderEnvVarPage`, `envBinding`. + +Note: `docs/` has its own `go.mod` (`github.com/mudler/edgevpn/docs`) for the Hugo module. The generator must be part of the **root** module so it can import `cmd`. Verify `docs/go.mod` does not shadow it — if `go test ./docs/generate/` fails with a module error, place the generator at `docs/generate/` but confirm the root `go.mod` covers it; if the nested `go.mod` interferes, move the generator to `internal/docsgen/` and adjust all paths in this task. Report which you did. + +- [ ] **Step 3: Create `docs/generate/render.go`** + +```go +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/urfave/cli/v2" +) + +const banner = "" + +// envBinding records one place an environment variable is read from. +type envBinding struct { + Flag string + Command string + Default string +} + +// escapeCell makes a string safe inside a markdown table cell. Several real +// usage strings contain "|" (the ownership flag lists its modes that way), +// which would otherwise split the row into extra columns. +func escapeCell(s string) string { + s = strings.ReplaceAll(s, "|", `\|`) + s = strings.ReplaceAll(s, "\n", " ") + return strings.TrimSpace(s) +} + +func flagNames(f cli.Flag) string { + names := f.Names() + out := make([]string, 0, len(names)) + for _, n := range names { + if len(n) == 1 { + out = append(out, "`-"+n+"`") + } else { + out = append(out, "`--"+n+"`") + } + } + return strings.Join(out, ", ") +} + +// renderFlagTable renders a markdown table for the visible flags. +func renderFlagTable(flags []cli.Flag) string { + var b strings.Builder + b.WriteString("| Flag | Default | Environment | Description |\n") + b.WriteString("|---|---|---|---|\n") + + rows := 0 + for _, f := range flags { + df, ok := f.(cli.DocGenerationFlag) + if !ok || !df.IsVisible() { + continue + } + def := df.GetDefaultText() + if def == "" { + def = df.GetValue() + } + if def == "" { + def = "—" + } else { + def = "`" + escapeCell(def) + "`" + } + + env := "—" + if vars := df.GetEnvVars(); len(vars) > 0 { + quoted := make([]string, len(vars)) + for i, v := range vars { + quoted[i] = "`" + v + "`" + } + env = strings.Join(quoted, ", ") + } + + fmt.Fprintf(&b, "| %s | %s | %s | %s |\n", + flagNames(f), def, env, escapeCell(df.GetUsage())) + rows++ + } + if rows == 0 { + return "_This command takes no flags of its own._\n" + } + return b.String() +} + +// renderCommandPage renders one Hugo page for a command. +func renderCommandPage(c *cli.Command, weight int) string { + var b strings.Builder + fmt.Fprintf(&b, "---\ntitle: %q\nlinkTitle: %q\nweight: %d\ndescription: >\n %s\n---\n\n", + c.Name, c.Name, weight, escapeCell(c.Usage)) + b.WriteString(banner + "\n\n") + + if len(c.Aliases) > 0 { + quoted := make([]string, len(c.Aliases)) + for i, a := range c.Aliases { + quoted[i] = "`" + a + "`" + } + fmt.Fprintf(&b, "Aliases: %s\n\n", strings.Join(quoted, ", ")) + } + if c.Description != "" && c.Description != c.Usage { + fmt.Fprintf(&b, "%s\n\n", c.Description) + } + + fmt.Fprintf(&b, "```\nedgevpn %s [options]\n```\n\n", c.Name) + b.WriteString("## Flags\n\n") + b.WriteString(renderFlagTable(c.Flags)) + + for _, sub := range c.Subcommands { + fmt.Fprintf(&b, "\n## `%s %s`\n\n", c.Name, sub.Name) + if sub.Usage != "" { + fmt.Fprintf(&b, "%s\n\n", sub.Usage) + } + b.WriteString(renderFlagTable(sub.Flags)) + } + return b.String() +} + +// renderEnvVarPage renders the environment-variable cross-reference. +func renderEnvVarPage(bindings map[string][]envBinding) string { + var b strings.Builder + b.WriteString("---\ntitle: \"Environment variables\"\nlinkTitle: \"Environment variables\"\nweight: 20\ndescription: >\n Every environment variable EdgeVPN reads, and the flag it corresponds to.\n---\n\n") + b.WriteString(banner + "\n\n") + b.WriteString("Environment variables are read when the corresponding flag is not passed.\n\n") + b.WriteString("| Variable | Flag | Command | Default |\n|---|---|---|---|\n") + + names := make([]string, 0, len(bindings)) + for k := range bindings { + names = append(names, k) + } + sort.Strings(names) + + for _, name := range names { + for _, bind := range bindings[name] { + def := bind.Default + if def == "" { + def = "—" + } else { + def = "`" + escapeCell(def) + "`" + } + fmt.Fprintf(&b, "| `%s` | `%s` | %s | %s |\n", + name, bind.Flag, bind.Command, def) + } + } + return b.String() +} +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `go test ./internal/docsgen/ -v` +Expected: 5 tests PASS. + +- [ ] **Step 5: Create `docs/generate/main.go`** + +```go +// Command generate emits the CLI and environment-variable reference +// documentation from the real cli.App, so the docs cannot drift from the +// binary. Run via `make docs-gen`. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" + + "github.com/mudler/edgevpn/cmd" +) + +const outDir = "docs/content/en/docs/reference" + +func main() { + // The version is irrelevant to the generated output and must be fixed, + // otherwise the CI drift check would fail on every release. + app := cmd.NewApp("") + + if err := run(app); err != nil { + fmt.Fprintln(os.Stderr, "docs generate:", err) + os.Exit(1) + } +} + +func run(app *cli.App) error { + cliDir := filepath.Join(outDir, "cli") + if err := os.MkdirAll(cliDir, 0o755); err != nil { + return err + } + + // Remove previously generated command pages so a deleted command does + // not leave a stale page behind — the drift gate would never catch it. + existing, err := filepath.Glob(filepath.Join(cliDir, "*.md")) + if err != nil { + return err + } + for _, p := range existing { + if err := os.Remove(p); err != nil { + return err + } + } + + bindings := map[string][]envBinding{} + collectEnv(bindings, app.Flags, "global") + + // Index page, carrying the root flag table. + var idx strings.Builder + idx.WriteString("---\ntitle: \"CLI\"\nlinkTitle: \"CLI\"\nweight: 10\ndescription: >\n Every EdgeVPN command and flag.\n---\n\n") + idx.WriteString(banner + "\n\n") + idx.WriteString("Running `edgevpn` with no subcommand starts the VPN.\n\n## Global flags\n\n") + idx.WriteString(renderFlagTable(app.Flags)) + idx.WriteString("\n## Commands\n\n") + for _, c := range app.Commands { + fmt.Fprintf(&idx, "- [`%s`](%s/) — %s\n", c.Name, c.Name, escapeCell(c.Usage)) + } + if err := os.WriteFile(filepath.Join(cliDir, "_index.md"), []byte(idx.String()), 0o644); err != nil { + return err + } + + for i, c := range app.Commands { + page := renderCommandPage(c, (i+1)*10) + path := filepath.Join(cliDir, c.Name+".md") + if err := os.WriteFile(path, []byte(page), 0o644); err != nil { + return err + } + collectEnv(bindings, c.Flags, c.Name) + for _, sub := range c.Subcommands { + collectEnv(bindings, sub.Flags, c.Name+" "+sub.Name) + } + } + + return os.WriteFile( + filepath.Join(outDir, "environment-variables.md"), + []byte(renderEnvVarPage(bindings)), 0o644) +} + +func collectEnv(into map[string][]envBinding, flags []cli.Flag, command string) { + for _, f := range flags { + df, ok := f.(cli.DocGenerationFlag) + if !ok || !df.IsVisible() { + continue + } + names := f.Names() + if len(names) == 0 { + continue + } + def := df.GetDefaultText() + if def == "" { + def = df.GetValue() + } + for _, env := range df.GetEnvVars() { + into[env] = append(into[env], envBinding{ + Flag: "--" + names[0], + Command: command, + Default: def, + }) + } + } +} +``` + +- [ ] **Step 6: Add the Makefile target** + +Add to `Makefile`, and add `docs-gen` to the `.PHONY` line: + +```make +docs-gen: + go run ./docs/generate +``` + +- [ ] **Step 7: Generate and inspect** + +Run: `make docs-gen && ls docs/content/en/docs/reference/cli/ && head -30 docs/content/en/docs/reference/cli/proxy.md` +Expected: `_index.md` plus one page per command (`start`, `api`, `service-add`, `service-connect`, `file-receive`, `proxy`, `file-send`, `dns`, `peergater`). The `proxy.md` page shows front matter, the banner, and a flag table. + +- [ ] **Step 8: Verify idempotency — this is what the CI gate depends on** + +Run: `make docs-gen && git add -A docs/content/en/docs/reference && make docs-gen && git diff --exit-code docs/content/en/docs/reference && echo IDEMPOTENT` +Expected: prints `IDEMPOTENT`. If it does not, something in the output is nondeterministic (map iteration order is the usual culprit) — fix it now, or the CI gate will fail randomly. + +- [ ] **Step 9: Sanity-check coverage against the source** + +Run: +```bash +grep -c 'Name:' cmd/util.go +grep -o '`--[a-z-]*`' docs/content/en/docs/reference/cli/_index.md | sort -u | wc -l +grep -c '^| `' docs/content/en/docs/reference/environment-variables.md +``` +Expected: the global flag table covers the bulk of the 68 flags in `cmd/util.go` plus the 18 root-only ones, and the env table has dozens of rows. Exact numbers will differ (some flags share names, some have no env var) — the point is that the counts are in the right order of magnitude, not zero. Record the real numbers in your report. + +- [ ] **Step 10: Verify the Hugo build** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` +Expected: zero errors. The build is clean on Hugo 0.152.2 — any error is yours. + +- [ ] **Step 11: Commit** + +```bash +git add docs/generate Makefile docs/content/en/docs/reference +git commit -m "docs: generate the CLI and environment variable reference" +``` + +--- + +## Task 4: CI drift gate and docs infrastructure + +**Files:** +- Modify: `.github/workflows/pages.yml`, `docs/config.toml`, `.gitmodules` +- Create: `CONTRIBUTING.md`, `.github/workflows/docs-gen.yml` (or a job added to an existing workflow) +- Delete: `docs/themes/docsy` submodule, `docs/content/en/community/_index.md` + +**Interfaces:** +- Consumes: `make docs-gen` from Task 3. + +- [ ] **Step 1: Add the drift gate** + +Create `.github/workflows/docs-gen.yml`: + +```yaml +name: Docs reference drift + +on: + push: + pull_request: + +jobs: + docs-gen: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: 1.26 + - name: Regenerate the reference + run: make docs-gen + - name: Fail if the generated reference is stale + run: | + if ! git diff --exit-code docs/content/en/docs/reference/; then + echo "::error::The generated CLI reference is out of date. Run 'make docs-gen' and commit the result." + exit 1 + fi +``` + +- [ ] **Step 2: Verify the gate actually catches drift** + +Prove it locally rather than trusting it: + +```bash +# Add a throwaway flag, regenerate, and confirm a diff appears +git stash list +sed -i 's|^var CommonFlags \[\]cli.Flag = \[\]cli.Flag{|&\n\t\&cli.BoolFlag{Name: "drift-canary", Usage: "temporary"},|' cmd/util.go +make docs-gen +git diff --stat docs/content/en/docs/reference/ | tail -1 # expect: changes +git checkout cmd/util.go && make docs-gen +git diff --exit-code docs/content/en/docs/reference/ && echo "RESTORED CLEAN" +``` +Expected: the canary produces a diff, and reverting restores a clean tree. If adding a flag produces **no** diff, the generator is not reading what you think it is — stop and fix it. + +- [ ] **Step 3: Add a PR trigger to the docs build** + +In `.github/workflows/pages.yml`, extend the trigger so docs breakage is caught before merge rather than after. Keep the existing deploy behaviour limited to `master`: + +```yaml +on: + push: + branches: + - master + pull_request: + paths: + - 'docs/**' +``` + +Then guard the deploy step so it only runs on `master` — add `if: github.ref == 'refs/heads/master' && github.event_name == 'push'` to the `JamesIves/github-pages-deploy-action` step. **A pull request must build the docs but must not deploy them.** + +- [ ] **Step 4: Remove the unused docsy submodule** + +`docs/config.toml` sets no `theme=` (verified: 0 matches for `^theme`); Docsy comes from Hugo Modules. The submodule is dead weight that Dependabot keeps bumping. + +```bash +git submodule deinit -f docs/themes/docsy +git rm -f docs/themes/docsy +rm -rf .git/modules/docs/themes/docsy +``` +Then remove the `docs/themes/docsy` entry from `.gitmodules` (delete the whole three-line stanza). If `.gitmodules` becomes empty, delete the file. + +- [ ] **Step 5: Verify the docs still build without the submodule** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` +Expected: still the baseline count. **If the build now fails differently, the submodule was load-bearing after all — restore it and report that.** + +- [ ] **Step 6: Create `CONTRIBUTING.md`** + +`docs/content/en/docs/contribution-guidelines.md` links to +`https://github.com/mudler/edgevpn/blob/master/CONTRIBUTING.md`, which 404s. +Write a short root `CONTRIBUTING.md` covering: how to build (referencing the +Node requirement from the React UI work), how to run tests, the `make docs-gen` +requirement when adding a flag, and how to open an issue or PR. Keep it brief +and link to the docs site rather than duplicating it. + +- [ ] **Step 7: Fix the remaining config defects** + +In `docs/config.toml`: +- Set `breadcrumb_disable = false` (the tree is three levels deep). +- Remove the placeholder `UA-00000000-0` analytics ID, or the `[services.googleAnalytics]` block entirely. Leaving `params.ui.feedback.enable = true` pointed at a dead property sends feedback events nowhere. +- Reconcile `baseURL` with `docs/scripts/build.sh`'s `-b` flag. `config.toml` says `https://mudler.github.io/edgevpn/docs/`; the script passes `https://mudler.github.io/edgevpn`. Make them agree — prefer the script's value, since that is what production actually serves, and record which you changed. + +- [ ] **Step 8: Delete the empty community page** + +`docs/content/en/community/_index.md` is an unfilled Docsy template shell that still occupies a main-nav slot. + +```bash +git rm docs/content/en/community/_index.md +``` +Then remove its `[[menu.main]]` entry from `docs/config.toml` if one exists, so the nav does not link to a 404. + +- [ ] **Step 9: Verify** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` and confirm no increase; then confirm the generated nav has no dead community link by grepping the output HTML: `grep -ri 'community' docs/public/index.html | head -3` (expect nothing, or only unrelated prose). + +- [ ] **Step 10: Commit** + +```bash +git add -A .github CONTRIBUTING.md docs/config.toml .gitmodules docs/content +git commit -m "ci: gate the generated reference on drift; fix docs infrastructure" +``` + +--- + +## Task 5: Move existing pages into the new tree + +**Files:** +- Move (with `git mv`): every page listed in the table below +- Delete: `docs/content/en/docs/Concepts/`, `docs/content/en/docs/Getting started/` (once empty) + +**Interfaces:** +- Consumes: the section skeleton from Task 2. +- Produces: the page paths every later task links to. + +**Every moved page MUST gain an `aliases:` front-matter entry for its old URL.** The site is linked from the README, from Kairos, and from search results. + +| From | To | Alias to add | +|---|---|---| +| `Getting started/_index.md` | `tutorials/your-first-network.md` | `/docs/getting-started/` | +| `Getting started/cli.md` | `how-to/run-as-a-vpn.md` | `/docs/getting-started/cli/` | +| `Getting started/api.md` | `reference/api.md` | `/docs/getting-started/api/` | +| `Getting started/gui.md` | `tools/desktop-gui.md` | `/docs/getting-started/gui/` | +| `Concepts/Overview/dns.md` | `how-to/enable-dns.md` | `/docs/concepts/overview/dns/` | +| `Concepts/Overview/files.md` | `how-to/send-and-receive-files.md` | `/docs/concepts/overview/files/` | +| `Concepts/Overview/services.md` | `how-to/tunnel-tcp-services.md` | `/docs/concepts/overview/services/` | +| `Concepts/Overview/peerguardian.md` | `how-to/trusted-networks.md` | `/docs/concepts/overview/peerguardian/` | +| `Concepts/Overview/_index.md` | `explanation/the-ledger.md` | `/docs/concepts/overview/` | +| `Concepts/Architecture/_index.md` | `explanation/architecture.md` | `/docs/concepts/architecture/` | +| `Concepts/Token/_index.md` | `reference/network-config.md` | `/docs/concepts/token/` | +| `contribution-guidelines.md` | `contributing.md` | `/docs/contribution-guidelines/` | + +- [ ] **Step 1: Confirm the real alias URLs before writing them** + +Do not guess. Build the current site and read the actual paths: + +```bash +cd docs && make build >/dev/null 2>&1 +find public -name index.html | sed 's|^public||;s|index.html$||' | sort +``` +Record the real URLs and use those in the `aliases:` entries. Hugo lowercases and replaces spaces, so `Getting started` becomes `getting-started`, but **verify rather than assume**. + +- [ ] **Step 2: Move each page with `git mv`** + +Use `git mv` (not copy-then-delete) so history follows the file. Example: + +```bash +mkdir -p docs/content/en/docs/{tutorials,how-to,reference,explanation,tools} +git mv "docs/content/en/docs/Getting started/cli.md" docs/content/en/docs/how-to/run-as-a-vpn.md +``` + +- [ ] **Step 3: Update front matter on every moved page** + +For each, set a unique `weight` within its new section, update `title`/`linkTitle` where the new name differs, and add the alias. Example for `how-to/run-as-a-vpn.md`: + +```yaml +--- +title: "Run as a VPN" +linkTitle: "Run as a VPN" +weight: 10 +aliases: + - /docs/getting-started/cli/ +description: > + Join a network as a VPN peer, with automatic or static addressing. +--- +``` + +Assign weights in the order the pages appear in the spec's §5 tree, in tens (10, 20, 30…). **No two pages in a section may share a weight** — the old tree had `Getting started/{_index,api,cli}.md` all at `weight: 1`. + +- [ ] **Step 3b: Split `run-as-a-vpn.md` into its three topics** + +The old `Getting started/cli.md` covers three separate jobs in one page, which +is why it reads as a grab-bag. Split it: + +- **`how-to/run-as-a-vpn.md`** keeps joining a network and the basic VPN flow. +- **`how-to/addressing-and-dhcp.md`** (new file) takes the DHCP section plus + `--address`, and adds `--router` and `--static-peertable`, which are + undocumented today. Read `pkg/vpn/vpn.go` (the `--router` behaviour is + packet-to-single-node routing) and `cmd/util.go` before writing the additions. +- **`how-to/ipv6.md`** (new file) takes the IPv6 section. It currently links + issue #15 and calls IPv6 "very experimental, highly unstable" — keep the + caveat, but check whether the issue is still open and say what is actually + true today rather than copying a claim from an unknown date. + +Both new files need their own unique `weight` and `description`. The alias for +`/docs/getting-started/cli/` stays on `run-as-a-vpn.md` — it is the page that +inherits the old URL's primary topic. + +Also drop the stale version claims while you are here: the page says +"Automatic IP negotiation is available since version `0.8.1`" and pins sample +output to `Version: v0.8.4`. Either update them against `git tag` or remove the +version qualifier — do not leave a claim you have not checked. + +- [ ] **Step 3c: Create `tutorials/share-a-service.md`** + +Reframe `how-to/tunnel-tcp-services.md` as a beginner walkthrough: two hosts, +one exposing a TCP service, one connecting to it, with every command shown in +order and the expected output. The how-to page stays as the terse reference for +someone who already knows the shape. Cross-link them both ways. + +If, on reading the existing services page, a separate tutorial would be pure +duplication rather than a genuinely gentler path, say so in your report and +create it as a stub under Task 12's stub policy instead of padding. + +- [ ] **Step 4: Remove the empty old directories** + +```bash +rmdir "docs/content/en/docs/Getting started" docs/content/en/docs/Concepts/Overview docs/content/en/docs/Concepts/Architecture docs/content/en/docs/Concepts/Token docs/content/en/docs/Concepts 2>/dev/null +git status --porcelain docs/content +``` +Expected: only the intended renames. + +- [ ] **Step 4c: Fix the marketing homepage's links** + +`docs/content/en/_index.html` (the site's front page, not the docs index) +links at lines 74 and 77 to `{{< relref "/docs">}}/getting-started/api/` and +`.../gui/`. The path segment sits *outside* the shortcode, so Hugo does not +error on it — it silently produces a 404 once `Getting started/` moves. + +Update both to the new locations (`reference/api/` and `tools/desktop-gui/`). +Then grep the whole content tree for the same pattern, since anything built +this way is invisible to Hugo's link checking: + +```bash +grep -rn 'relref' docs/content/ +``` + +- [ ] **Step 5: Fix internal links broken by the moves** + +```bash +grep -rn '](/docs/\|](\.\./\|](\./' docs/content/en/docs/ | grep -v aliases +``` +Fix every link that now points at a moved page. Also fix the known dangling link in the old `Concepts/Token/_index.md` (now `reference/network-config.md`): it ends with `See [the Architecture section]()` — an empty target. Point it at `../../explanation/architecture/`. + +- [ ] **Step 6: Verify aliases actually work** + +Run: `cd docs && make build >/dev/null 2>&1 && for u in getting-started/cli concepts/overview/dns concepts/token; do test -f "public/docs/$u/index.html" && echo "OK $u" || echo "MISSING $u"; done` +Expected: all `OK` — Hugo writes a redirect stub at each alias path. **A `MISSING` here means a real 404 for existing inbound links.** + +- [ ] **Step 7: Verify the build and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase over baseline. + +```bash +git add -A docs/content +git commit -m "docs: move existing pages into the Diataxis tree with aliases" +``` + +--- + +## Task 6: Fix the factually wrong content + +**Files:** +- Modify: `docs/content/en/docs/how-to/trusted-networks.md`, `docs/content/en/docs/reference/api.md`, `docs/content/en/docs/explanation/architecture.md` + +**These are the defects that make the current docs actively harmful. Each is verified against the code.** + +- [ ] **Step 1: Fix `--peerguardian` → `--peerguard`** + +`how-to/trusted-networks.md` (formerly `peerguardian.md`) instructs users to run `--peerguardian` on four lines (originally 18, 21, 63, 91). The real flag is `peerguard` (`cmd/util.go:368`). urfave/cli hard-fails on an unknown flag, so every one of those command lines is broken. + +Note the same page already shows `--peerguard` correctly in a pasted help output around line 29 — the page contradicts itself. + +```bash +grep -n 'peerguardian' docs/content/en/docs/how-to/trusted-networks.md +``` +Replace every `--peerguardian` **flag usage** with `--peerguard`. Do **not** blanket-replace the word: "PeerGuardian" as the feature's proper name in prose is correct and should stay. + +- [ ] **Step 2: Verify the corrected commands actually run** + +```bash +go build -o /tmp/ev . && /tmp/ev --peerguard --help >/dev/null 2>&1 && echo "peerguard OK" +/tmp/ev --peerguardian --help 2>&1 | head -2 # expect: flag provided but not defined +``` +Expected: `peerguard OK`, and the old spelling errors out — proving the bug was real. + +- [ ] **Step 3: Fix the `api --api-listen` example** + +`reference/api.md` (formerly `Getting started/api.md`, line 142) shows: + +``` +$ edgevpn api --api-listen "unix://" +``` + +The `api` subcommand takes `--listen`; `--api-listen` exists only on the root command. Correct form for the subcommand: + +``` +$ edgevpn api --listen "unix://" +``` + +The root-command form (`edgevpn --api --api-listen unix://...`) is also valid. Show whichever is clearer, but verify it: + +```bash +/tmp/ev api --listen "unix:///tmp/ev-test.sock" --help >/dev/null 2>&1 && echo "api --listen OK" +/tmp/ev api --api-listen "unix:///tmp/x.sock" 2>&1 | head -2 # expect: not defined +``` + +- [ ] **Step 4: Document the API's undocumented endpoints** + +`reference/api.md` is missing roughly 40% of the routes. Add, verified against `api/api.go`: + +- `GET /api/summary`, `GET /api/files`, `GET /api/nodes`, `GET /api/peerstore` +- the whole `GET /api/metrics` tree: `/api/metrics`, `/api/metrics/protocol`, `/api/metrics/peer`, `/api/metrics/peer/:peer`, `/api/metrics/protocol/:protocol` — noting these are registered **only** when the node has a bandwidth counter, so a 404 means "not enabled", not "broken" +- `GET /debug/pprof/*` when `--debug` is set + +Also state plainly, because it is currently implied nowhere: **the API has no authentication.** Anything that can reach the port can write to the network's ledger via `PUT /api/ledger/:bucket/:key/:value`. Link to the security model page (Task 10). + +Note that responses are PascalCase (`PeerID`, `RateIn`, `BlockChain`) because the Go types carry no `json` struct tags. + +- [ ] **Step 5: Update the stale architecture claims** + +`explanation/architecture.md` predates the authenticated-ledger work. It says the blockchain is "ephemeral and on-memory" and that nodes not on the blockchain "can't talk to each other". Since `pkg/blockchain/{sign,policy,reaper}.go` landed, entries are signed, owner-scoped and TTL-reaped, and a disk store exists (`pkg/blockchain/store_disk.go`). + +Correct the claims and link forward to `explanation/authenticated-ledger.md` (Task 9). Read the actual code before rewriting — do not paraphrase this plan. + +- [ ] **Step 6: Verify and commit** + +Run: `grep -rc 'peerguardian' docs/content/en/docs/ | grep -v ':0' || echo "no stale flag usages"` +Expected: no `--peerguardian` flag usages remain (prose mentions of the feature name are fine). + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase. + +```bash +git add docs/content/en/docs +git commit -m "docs: fix the broken peerguard flag, api example and stale architecture claims" +``` + +--- + +## Task 7: Relocate the README's unique content + +**Files:** +- Create: `docs/content/en/docs/how-to/use-as-a-library.md`, `docs/content/en/docs/tutorials/decentralized-k3s-cluster.md`, `docs/content/en/docs/troubleshooting.md`, `docs/content/en/docs/explanation/when-not-to-use-edgevpn.md` +- Modify: `README.md` + +**This is relocation, not new writing.** The content already exists and is correct; it is moving so the site becomes canonical and the drift ends. + +| README section (line at time of writing) | Destination | +|---|---| +| `:notebook: As a library` (174) | `how-to/use-as-a-library.md` | +| k3s example (153) | `tutorials/decentralized-k3s-cluster.md` | +| `:notebook: Troubleshooting` (240) | `troubleshooting.md` | +| `:question: Is it for me?` (130) + `:warning: Warning!` (149) | `explanation/when-not-to-use-edgevpn.md` | + +- [ ] **Step 1: Locate the sections (line numbers will have shifted)** + +```bash +grep -n '^#\{1,3\} ' README.md +``` + +- [ ] **Step 2: Create each destination page** + +Move the content across with front matter added. Adapt only what must change: GitHub-flavoured emoji headings (`:notebook:`) become plain titles, and relative repo links become site links. **Do not rewrite the prose** — it is correct, and rewriting invites new errors. + +Give each a unique weight in its section and a `description`. + +- [ ] **Step 3: Verify the library example still compiles** + +`how-to/use-as-a-library.md` contains a Go snippet using `edgevpn` as a package. Extract it to a scratch file outside the repo and confirm it builds against the current API: + +```bash +mkdir -p /tmp/evlib && cd /tmp/evlib +# write the snippet as main.go, with a go.mod requiring github.com/mudler/edgevpn +go mod init evlibcheck && go mod edit -replace github.com/mudler/edgevpn=/home/mudler/_git/edgevpn +go mod tidy && go build ./... && echo "LIBRARY EXAMPLE COMPILES" +``` +If it does **not** compile, the README example is already stale. Fix it to match the current API and say so in your report — that is a real find, not a failure. + +- [ ] **Step 4: Verify the troubleshooting commands** + +The troubleshooting section covers `sysctl net.core.rmem_max` and multiplexer negotiation failures. Confirm the sysctl name and syntax are correct on Linux: + +```bash +sysctl net.core.rmem_max +``` +Mark anything you cannot verify (e.g. the multiplexer error text, which needs a real failing peer) as unverified in your report rather than asserting it. + +- [ ] **Step 4b: Create `tutorials/install.md`** + +The site has no installation page of its own, and `install.sh` — the one-liner +installer at the repo root — is never mentioned in the documentation at all. + +Write a fuller install page than the README carries: the `install.sh` +one-liner, downloading a release binary, the container image +(`quay.io/mudler/edgevpn`), and building from source (which now needs Node — +see `CONTRIBUTING.md` and the React UI work). Give it `weight: 10` so it sorts +above `your-first-network.md`. + +The README keeps its own short Installation section, per the spec — it links +here for the fuller version rather than duplicating it. That is deliberate: the +README's job is to get someone running in a minute, and this page's job is to +cover every installation route. + +Verify the one-liner is real before documenting it: + +```bash +head -20 install.sh +grep -n 'curl\|wget' README.md | head -3 +``` + +- [ ] **Step 5: Trim the README** + +Remove the four relocated sections. Replace each with a one-line pointer to the site page. The README keeps: badges, the pitch, the feature list, screenshots, installation, a 5-minute quickstart, projects-using-EdgeVPN, contribution, credits, licence. + +**Do not touch the licence badge or footer.** They are inconsistent with `LICENSE` (see Global Constraints) and resolving that is the maintainer's call. + +- [ ] **Step 6: Verify no content was lost** + +```bash +git show HEAD:README.md | wc -l ; wc -l < README.md +grep -c 'k3s' README.md docs/content/en/docs/tutorials/decentralized-k3s-cluster.md +``` +Confirm every removed section exists at its destination. A section that appears in neither is a real loss — check before committing. + +- [ ] **Step 7: Verify and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase. + +```bash +git add README.md docs/content/en/docs +git commit -m "docs: relocate README-only content to the site and trim the README" +``` + +--- + +## Task 8: HTTP egress and the proxy + +**Files:** +- Create: `docs/content/en/docs/how-to/http-egress-and-proxy.md` +- Modify: `pkg/services/egress.go` (one-line bug fix plus a guard — authorised, see Step 0) + +**An entire feature is invisible.** `grep -ril egress docs/content/` returns 0 files, against 224 lines in `pkg/services/egress.go` plus the `edgevpn proxy` subcommand. + +**Corrections to this plan, established by direct inspection — the earlier draft of this task was wrong in three ways:** + +1. **It is an HTTP forward proxy, not an IP-level exit node.** `ProxyService` (`pkg/services/egress.go:92`) "starts a local http proxy server which redirects requests to egresses into the network". `ServeHTTP` reads an HTTP request, opens a libp2p stream to a chosen egress peer, and writes the request over it; the egress side round-trips it with `http.DefaultTransport`. It does **not** route arbitrary IP traffic, so do not describe it as a VPN exit node or imply "all your traffic". The page is named accordingly. +2. **There is no `CONNECT` handling** anywhere in `ServeHTTP` or the egress handler. The egress side sets `req.URL.Scheme = "https"` in one branch, but no tunnel is established. Determine empirically whether HTTPS works at all and state what you find — do not assume either way. +3. **The `--egress` flag lives in `cmd/main.go:111`**, not `cmd/util.go`. `--egress-announce-time` is at `cmd/main.go:116`, and `cmd/main.go:180-181` wires them to `services.Egress(...)`. + +- [ ] **Step 0: Fix the egress selection panic (authorised code change)** + +`pkg/services/egress.go:158` selects a peer with: + +```go +chosen := availableEgresses[rand.Intn(len(availableEgresses)-1)] +``` + +`rand.Intn` panics for any argument ≤ 0. I reproduced the behaviour directly: + +| available egresses | result | +|---|---| +| 0 | **panic**: invalid argument to Intn | +| 1 | **panic**: invalid argument to Intn | +| 2 | always index 0 — last element unreachable | +| 3 | index 0 or 1 — last element unreachable | + +So `edgevpn proxy` crashes in exactly the setup this page will document — one exit node — and can never select the last egress in any list. + +Fix it: use `rand.Intn(len(availableEgresses))`, and guard the empty case before the call by returning `http.StatusServiceUnavailable` with a clear message rather than panicking. Note `ServeHTTP` already has an `http.Error(..., http.StatusServiceUnavailable)` path lower down; match that style. + +Write a Go test that fails against the current code. The selection logic is inline in `ServeHTTP`, so extracting it into a small helper (e.g. `pickEgress(available []string) (string, bool)`) is the cleanest way to make it testable — do that, keep the change minimal, and cover 0, 1, 2 and many. + +Commit this separately from the documentation, with a message describing it as a bug fix, since it is not docs work. + +- [ ] **Step 1: Establish the remaining facts from source** + +```bash +sed -n '40,100p' pkg/services/egress.go # the egress side +sed -n '130,200p' pkg/services/egress.go # the proxy side +sed -n '25,60p' cmd/proxy.go +sed -n '105,125p' cmd/main.go # --egress and --egress-announce-time +grep -n 'Egress' pkg/protocol/protocol.go +``` +Record: exact flag names and defaults, the `edgevpn proxy` flags (`--listen`, `--interval`, `--dead-interval`, `--debug`), the ledger bucket egress nodes announce into (`protocol.EgressService`), and the protocol ID (`protocol.EgressProtocol`). + +- [ ] **Step 2: Write the page** + +Cover, in this order: + +1. **What it is, stated precisely** — a node started with `--egress` advertises itself as an HTTP egress. A peer running `edgevpn proxy` exposes a local HTTP proxy that forwards requests over libp2p to one of those egress nodes, which performs the request and returns the response. No VPN interface is required on either side, which is what distinguishes this from VPN mode. Say explicitly that this proxies **HTTP requests**, not arbitrary IP traffic — a reader who expects an exit node will otherwise be surprised. +2. **Running an egress node** — the `--egress` flag, `--egress-announce-time`, and their env vars, taken from `cmd/main.go`. +3. **Using one** — `edgevpn proxy --listen :8080`, then pointing a client at that local HTTP proxy (`http_proxy=http://localhost:8080`, or a browser proxy setting). +4. **HTTPS** — state what you actually determined in Step 1. There is no `CONNECT` handling in the code, so if HTTPS does not work, say so plainly; that is a far more useful page than one that stays silent and lets the reader discover it. +5. **A security section, which is mandatory.** Requests leave the network at the egress node's address, so that node's operator sees every URL and can read or alter unencrypted traffic. Any holder of the network token can route through any egress, because EdgeVPN's model is perimeter-only — there is no per-peer authorization. Anyone running an egress is accepting responsibility for that traffic. Link to `explanation/security-model.md`. +6. **Selection behaviour** — an egress is chosen at random per request from those seen alive within `--dead-interval`. Requests are not pinned to one egress, so consecutive requests may exit from different nodes. That matters for anything session-based. + +Include a `weight` unique in `how-to/`, and a `description`. + +- [ ] **Step 3: Verify every command** + +```bash +go build -o /tmp/ev . +/tmp/ev --egress --help >/dev/null 2>&1 && echo "--egress accepted" +/tmp/ev proxy --help +``` +Confirm every flag you documented appears in the real help output, with the defaults you claimed. Any command needing two hosts (actually routing traffic) is marked **unverified** in your report — do not claim you ran it. + +- [ ] **Step 4: Verify and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase. +Run: `grep -ril egress docs/content/ | wc -l` — expect at least 1. + +```bash +git add docs/content/en/docs/how-to/http-egress-and-proxy.md +git commit -m "docs: document HTTP egress and the proxy" +``` + +--- + +## Task 9: Ledger ownership + +**Files:** +- Create: `docs/content/en/docs/how-to/ledger-ownership.md` +- Move: `docs/design/authenticated-ledger.md` → `docs/content/en/docs/explanation/authenticated-ledger.md` + +**Why this matters most for existing users:** `--ownership` defaults to `"enforce"` (`cmd/util.go`), and its own usage string warns *"All nodes on a network must run the same mode/wire format, so flip the whole network together."* A user upgrading into a mixed-version network gets a silently broken ledger, and the only explanation lives in a 369-line design document that has never shipped. + +- [ ] **Step 1: Publish the design document** + +```bash +git mv docs/design/authenticated-ledger.md docs/content/en/docs/explanation/authenticated-ledger.md +``` + +Add Hugo front matter at the top. **Do not rewrite the body** — it is the best writing in the repository. Only adjust what breaks in Hugo: check for `$(...)` or `$VAR` in prose that Hugo's KaTeX renderer parses as math (this is the cause of three pre-existing build errors elsewhere), and for headings that clash with the front-matter title. + +```yaml +--- +title: "The authenticated ledger" +linkTitle: "Authenticated ledger" +weight: 30 +description: > + How ledger entries are signed, owned, versioned and reaped. +--- +``` + +- [ ] **Step 2: Establish the operator-facing facts** + +```bash +grep -n -B2 -A6 '"ownership"' cmd/util.go +grep -n -A6 'ownership-ttl' cmd/util.go +sed -n '1,60p' pkg/blockchain/sign.go +sed -n '1,50p' pkg/blockchain/policy.go +sed -n '1,50p' pkg/blockchain/reaper.go +grep -rn 'SignedData' pkg/blockchain/data.go | head +``` +Record the three modes and exactly what each does, the TTL flag and its default, and what a node logs when it rejects a write. + +- [ ] **Step 3: Write `how-to/ledger-ownership.md`** + +The operator's half. Cover: + +1. **The three modes** — `enforce` (sign, and reject unauthorized writes; the default), `observe` (sign, log violations, accept), `off` (legacy, opt out). +2. **The upgrade hazard, prominently.** Modes are wire-format incompatible. All nodes on a network must run the same mode. Flipping one node at a time produces a network that appears to work while writes are silently rejected. Give the safe path: move the whole network to `observe` first, confirm no violations are logged, then move to `enforce`. +3. **Ephemeral identities** — the runtime warning users will see, and its relationship to `--privkey-cache`. +4. **`--ownership-ttl`** and how reaping interacts with nodes that go offline. +5. A link to `explanation/authenticated-ledger.md` for the design. + +- [ ] **Step 4: Verify the flags and defaults** + +```bash +go build -o /tmp/ev . +/tmp/ev --help 2>&1 | grep -A2 'ownership' +``` +Confirm the modes, the default, and the TTL default match what you wrote. + +- [ ] **Step 5: Verify and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` +Expected: `0`. The published design doc is 369 lines of new content — if it introduces KaTeX math-parse errors, fix them here (escape the `$`), because this page is new and its errors are yours. + +```bash +git add -A docs +git commit -m "docs: document ledger ownership and publish the authenticated ledger design" +``` + +--- + +## Task 10: The security model + +**Files:** +- Create: `docs/content/en/docs/explanation/security-model.md` + +**EdgeVPN ships trust zones, peer gating, ownership enforcement, relay ACLs and an unauthenticated API, and nothing ties them together.** The content exists only scattered across the trusted-networks page, the authenticated-ledger design doc, long usage strings in `cmd/util.go`, and a README warning. + +**This page must be honest above all else.** It is the page a person reads before deciding whether to trust EdgeVPN with their network. Overstating the guarantees here is worse than having no page. + +- [ ] **Step 1: Establish the model from source** + +```bash +sed -n '1,60p' pkg/trustzone/peerguardian.go +sed -n '1,60p' pkg/trustzone/peergater.go +ls pkg/trustzone/authprovider/ecdsa/ +grep -n -A4 'peergate\|peerguard\|whitelist\|blacklist' cmd/util.go | head -40 +sed -n '1,50p' pkg/config/relay_acl.go +grep -n 'crypto\|otp\|sealer' pkg/crypto/*.go | head +``` + +- [ ] **Step 2: Write the page** + +It must state plainly, near the top: + +> **EdgeVPN's security model is perimeter-only.** Anyone holding the network +> token is a fully trusted member of the network. There is no per-peer +> authorization on the data plane, and no audit trail of which peer did what. +> The token *is* the security boundary. + +Then cover: + +1. **What a leaked token grants** — full network membership: join the VPN, read and write the ledger, use any exit node, resolve internal DNS. +2. **Token rotation** — the OTP mechanism, `--key-otp-interval`, and what rotating actually invalidates. +3. **Trust zones / PeerGuardian / PeerGater** — what they add on top (admission control via ECDSA-signed authorization), what they do *not* add (they gate who may join, not what a member may do), and that they are marked Experimental in the flag usage. +4. **Ledger ownership** — what signing adds, linking to Task 9's page. +5. **The API has no authentication.** Anything that can reach the port can write to the ledger. Recommend the unix-socket mode (`unix://`, with `APILISTENUNIXMODE` controlling the file mode, default `0660`) over a TCP listener, and never exposing the TCP port beyond localhost. +6. **Relay ACLs** — `--relay-service-network-only` and what it restricts. +7. **What EdgeVPN does not protect against** — a malicious member, traffic analysis by an exit node, a compromised bootstrap peer. + +- [ ] **Step 3: Cross-link** + +Add links from `how-to/trusted-networks.md`, `reference/api.md` and `how-to/http-egress-and-proxy.md` to this page. Those three pages all raise security questions they should not answer themselves. + +- [ ] **Step 4: Verify and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase. + +```bash +git add docs/content/en/docs +git commit -m "docs: add the security model explanation" +``` + +--- + +## Task 11: Relays, hop nodes, and compatibility + +**Files:** +- Create: `docs/content/en/docs/how-to/relays-and-hop-nodes.md`, `docs/content/en/docs/reference/compatibility.md` + +**`edgevpn start` is undocumented** — `grep -rl 'edgevpn start' docs/content/` returns 0 files, despite `cmd/join.go` describing it as "Useful for setting up relays or hop nodes to improve the network connectivity". + +- [ ] **Step 1: Establish the facts** + +```bash +sed -n '1,60p' cmd/join.go +grep -n -A4 'autorelay\|relay-service' cmd/util.go | head -60 +sed -n '1,60p' pkg/config/relay_acl.go +``` +Record: what `start` does that the root command does not, the autorelay flag family, and the eight `relay-service-*` flags with their defaults. + +- [ ] **Step 2: Write `how-to/relays-and-hop-nodes.md`** + +Cover: why a relay helps (NAT traversal failure modes), `edgevpn start` and how it differs from running the VPN, the autorelay flags (`--autorelay`, `--autorelay-discovery-interval`, `--autorelay-static-only`, `--autorelay-static-peer`), and the relay-service family with its resource limits (`--relay-service-max-circuits`, `--relay-service-max-data`, `--relay-service-max-duration`, `--relay-service-reservation-ttl`, `--relay-service-buffer-size`, `--relay-service-acl-refresh`, `--relay-service-network-only`). + +Point at the generated `reference/cli/start/` page rather than duplicating the flag table. + +- [ ] **Step 3: Write `reference/compatibility.md`** + +A version and wire-format matrix. The driver is `--ownership`: modes are wire-format incompatible, so this page tells an operator whether two versions can share a network and what to do when they cannot. Include the safe upgrade sequence from Task 9 and link to it. + +Be explicit about what you do **not** know: if you cannot determine from the repository which release introduced ownership modes, say so on the page and in your report rather than inventing a version number. Check `git log --oneline -- pkg/blockchain/sign.go | tail -5` and the tags around it. + +- [ ] **Step 4: Verify** + +```bash +go build -o /tmp/ev . && /tmp/ev start --help +``` +Confirm every flag you documented is real, with the defaults you claimed. + +- [ ] **Step 5: Commit** + +```bash +git add docs/content/en/docs +git commit -m "docs: document relays, hop nodes and version compatibility" +``` + +--- + +## Task 12: P1 pages and honest stubs + +**Files:** +- Create: `docs/content/en/docs/how-to/run-with-docker.md`, `docs/content/en/docs/reference/ledger-buckets.md` +- Create as stubs: `docs/content/en/docs/how-to/run-with-systemd.md`, `docs/content/en/docs/how-to/persist-node-identity.md`, `docs/content/en/docs/how-to/tune-for-low-end-devices.md`, `docs/content/en/docs/explanation/discovery-and-nat.md` + +- [ ] **Step 1: Write `how-to/run-with-docker.md`** + +`docker-compose.yml` is linked from the docs but never explained. Read it and cover: `network_mode: host` and why it is required, the `NET_ADMIN` capability, the `/dev/net/tun` device mount, the healthcheck, `--privkey-cache`, and driving configuration through `EDGEVPNTOKEN`. Mention the published image (`quay.io/mudler/edgevpn`, built by `.github/workflows/images.yml`), which the docs never mention. + +- [ ] **Step 2: Write `reference/ledger-buckets.md`** + +`pkg/protocol/protocol.go` defines the bucket namespace — `files`, `machines`, `services`, `users`, `healthcheck`, `dns`, `egress`, `trustzone`, `trustzoneAuth` — plus the protocol IDs. The API docs already tell users to `PUT /api/ledger/trustzoneAuth/...` without ever defining what a bucket is. + +```bash +sed -n '1,50p' pkg/protocol/protocol.go +``` + +Document each bucket: what writes to it, what its keys are (`machines` is keyed by **IP address**, `dns` by **regex** — this trips people up), and what reads it. + +- [ ] **Step 3: Write the four stubs** + +Each stub gets real front matter and an explicit note naming what is missing and where the source is. A stub must not imply content exists. + +```markdown +--- +title: "Run with systemd" +linkTitle: "Run with systemd" +weight: 130 +description: > + Running EdgeVPN as a systemd service, including socket activation. +--- + +{{% pageinfo %}} +This page has not been written yet. + +EdgeVPN supports systemd socket activation for its API — it reads `LISTEN_PID` +and `LISTEN_FDS` (see `api/api.go`) and inherits the listener systemd passes +it, so the socket's user, group and mode are whatever the `.socket` unit +declares. `APILISTENUNIXMODE` sets the mode when EdgeVPN creates the socket +itself instead. None of this is documented. + +Contributions welcome — see [contributing](../../contributing/). +{{% /pageinfo %}} +``` + +Do the same shape for: +- `persist-node-identity.md` — `--privkey-cache`, `--privkey-cache-dir`, `--ledger-state` +- `tune-for-low-end-devices.md` — `--low-profile`, the nine `limit-*` flags, connection water marks +- `discovery-and-nat.md` — DHT, mDNS, OTP rendezvous, holepunching, relay fallback + +Verify `{{% pageinfo %}}` is a real Docsy shortcode before using it: `grep -rn 'pageinfo' $(go env GOMODCACHE)/github.com/google/docsy*/layouts/shortcodes/ 2>/dev/null | head -2`. If it is not available, use a plain blockquote instead. + +- [ ] **Step 4: Verify and commit** + +Run: `cd docs && make build 2>&1 | grep -ciE '^(error|ERROR)'` — no increase. + +```bash +git add docs/content/en/docs +git commit -m "docs: add docker and ledger bucket reference, plus stubs for known gaps" +``` + +--- + +## Task 13: Whole-site verification + +**Files:** none created; this task verifies and fixes what it finds. + +- [ ] **Step 1: Regenerate and confirm the drift gate is clean** + +```bash +make docs-gen && git diff --exit-code docs/content/en/docs/reference/ && echo "GATE CLEAN" +``` +Expected: `GATE CLEAN`. This must pass on the branch that introduces the gate. + +- [ ] **Step 2: Confirm no new build errors** + +```bash +cd docs && make build 2>&1 | grep -iE '^(error|ERROR)' | sort | uniq -c | sort -rn | head +``` +Expected: no output at all — the build is clean. Any **new** error class is yours to fix. Record the before/after counts in your report. + +- [ ] **Step 3: Check every internal link** + +```bash +cd docs && make build >/dev/null 2>&1 +grep -rhoE 'href="(/docs/[^"]*|\.\./[^"]*)"' public/docs --include=index.html \ + | sed 's/href="//;s/"$//' | sort -u > /tmp/links.txt +wc -l /tmp/links.txt +``` +For each site-internal link, confirm a corresponding file exists under `public/`. Report every 404 and fix it. If a link checker is available (`lychee`, `htmltest`), use it and say so. + +- [ ] **Step 4: Confirm every alias resolves** + +```bash +cd docs +for u in getting-started getting-started/cli getting-started/api getting-started/gui \ + concepts/overview concepts/overview/dns concepts/overview/files \ + concepts/overview/services concepts/overview/peerguardian \ + concepts/architecture concepts/token contribution-guidelines; do + test -f "public/docs/$u/index.html" && echo "OK $u" || echo "MISSING $u" +done +``` +Expected: all `OK`. Every `MISSING` is a broken inbound link from the README, Kairos, or search results. + +- [ ] **Step 5: Confirm the factual fixes hold** + +```bash +grep -rn '\-\-peerguardian' docs/content/ && echo "STILL BROKEN" || echo "peerguard fixed" +grep -rn 'api --api-listen' docs/content/ && echo "STILL BROKEN" || echo "api example fixed" +grep -ril egress docs/content/ | wc -l # expect >= 1 +grep -rl 'edgevpn start' docs/content/ | wc -l # expect >= 1 +``` + +- [ ] **Step 6: Confirm no weight collisions** + +```bash +for d in tutorials how-to reference explanation; do + echo "== $d" + grep -h '^weight:' docs/content/en/docs/$d/*.md 2>/dev/null | sort | uniq -d +done +``` +Expected: no duplicate weights printed for any section. + +- [ ] **Step 7: Confirm the whole repo still builds** + +```bash +go build ./... && go test ./cmd/ ./internal/docsgen/ && echo "GO OK" +``` + +- [ ] **Step 8: Commit any fixes** + +```bash +git add docs README.md CONTRIBUTING.md .github +git commit -m "docs: fix links and aliases found in whole-site verification" +``` + +**Never run a bare `git add -A` on this branch** — see the working-tree hazard in Global Constraints. + +--- + +## Verification checklist + +- [ ] `make docs-gen && git diff --exit-code docs/content/en/docs/reference/` is clean +- [ ] Adding a flag to `cmd/util.go` produces a diff in the generated reference (canary test from Task 4) +- [ ] `cd docs && make build` succeeds with **zero** errors (baseline is clean) +- [ ] Every alias in Task 13 Step 4 resolves +- [ ] No internal link 404s +- [ ] `grep -rn '\-\-peerguardian' docs/content/` returns nothing +- [ ] `grep -ril egress docs/content/` returns at least one file +- [ ] `grep -rl 'edgevpn start' docs/content/` returns at least one file +- [ ] No duplicate `weight:` values within any section +- [ ] `go build ./... && go test ./cmd/ ./internal/docsgen/` passes +- [ ] The library example in `how-to/use-as-a-library.md` compiles against the current API +- [ ] Every command shown in a page was executed, or is marked unverified in the report +- [ ] `docs/design/` no longer contains `authenticated-ledger.md` (it moved into `content/`) +- [ ] `CONTRIBUTING.md` exists at the repo root + +--- + +## Out of scope — do not add + +- The custom Hugo theme (sub-project 3) +- Fixing the licence inconsistency — maintainer's legal call +- The `urfave/cli/v3` phantom direct dependency in `go.mod:32` +- `cmd/peergate.go`'s non-constant format string `go vet` failure +- The echo path-param unescape bug affecting DNS-regex ledger deletes +- Renaming environment variables to a consistent scheme (breaking change) +- The 40 pre-existing KaTeX CDN font errors +- `docs/package.json` being tracked and gitignored simultaneously diff --git a/docs/superpowers/specs/2026-08-03-docs-restructure-design.md b/docs/superpowers/specs/2026-08-03-docs-restructure-design.md new file mode 100644 index 00000000..92ae6d84 --- /dev/null +++ b/docs/superpowers/specs/2026-08-03-docs-restructure-design.md @@ -0,0 +1,366 @@ +# EdgeVPN documentation restructure + +**Date:** 2026-08-03 +**Status:** Approved design, ready for implementation planning +**Scope:** Sub-project 2 (docs content restructure). Sub-project 3 (custom Hugo theme) remains deferred. + +--- + +## 1. Programme context + +Second of four sub-projects in the EdgeVPN overhaul. + +| # | Sub-project | Status | +|---|---|---| +| 0 | Design system | done — `api/react-ui/src/styles/tokens.css` | +| 1 | React UI port | done — PR open, branch `feat/react-ui-design-system` | +| 2 | Docs restructure | **this spec** | +| 3 | Custom Hugo theme | deferred; consumes sub-project 0's tokens | + +Sub-project 3 is deliberately excluded. A theme is easier to design once the +real page inventory exists, and the two touch different directories +(`docs/content/` vs `docs/layouts/` + `docs/assets/`). + +--- + +## 2. The problem + +The site is **16 pages, ~3,500 words**, for a project with **~90 CLI flags** and +**~75 environment variables**. That is not primarily a volume problem — it is +three specific failures: + +1. **Actively wrong content.** Verified against the code, not inferred: + + | Defect | Evidence | + |---|---| + | `--peerguardian` does not exist | `cmd/util.go:368` defines `peerguard`. Four broken command lines in `Concepts/Overview/peerguardian.md` (lines 18, 21, 63, 91). urfave/cli hard-fails on an unknown flag. | + | `edgevpn api --api-listen` is wrong | `Getting started/api.md:142`. The `api` subcommand takes `--listen`; `--api-listen` exists only on the root command. | + | Egress / exit nodes / `edgevpn proxy` undocumented | `grep -ril egress docs/content/` → **0 files**, against 224 lines in `pkg/services/egress.go`. | + | `edgevpn start` undocumented | `grep -rl 'edgevpn start' docs/content/` → **0 files**. It is the in-code recommended way to run a relay/hop node. | + | Ledger ownership undocumented | `cmd/util.go` sets `Value: "enforce"` and its own usage string warns *"All nodes on a network must run the same mode/wire format, so flip the whole network together."* Explained only in `docs/design/authenticated-ledger.md`, which is outside `content/` and never ships. | + +2. **An empty reference quadrant.** ~12 of ~90 flags documented; env vars + nowhere. Hand-maintained flag tables are what let `--peerguardian` survive — + a contributor once "fixed a typo" from `--peerguradian` to `--peerguardian`, + i.e. into a flag that still does not exist. + +3. **Broken information architecture.** `Concepts/Overview/` has an `_index.md` + about the blockchain and four children (DNS, files, service tunnelling, + PeerGuardian) that are how-tos, not concepts. Page weights collide — + `Getting started/{_index,api,cli}.md` are all `weight: 1`; `Overview/files.md` + and `Overview/dns.md` are both `weight: 20` — so ordering is arbitrary. + +--- + +## 3. Goals and non-goals + +### Goals + +1. Replace the current tree with a Diátaxis information architecture. +2. Fix every verified factual defect in §2.1. +3. **Generate** the CLI and environment-variable reference from the `cli.App`, + with a CI gate that fails on drift. +4. Write the P0 pages where a whole feature is currently invisible. +5. Publish `docs/design/authenticated-ledger.md`. +6. Make the site canonical: relocate the README's unique content, then trim it. + +### Non-goals + +- **The custom Hugo theme** (sub-project 3). Docsy stays for this round. +- **Rewriting prose that is already correct.** Existing good pages move; they + are not re-authored. +- **The `urfave/cli/v3` phantom dependency** (`go.mod:32` declares it direct; + no `.go` file imports it — the CLI is entirely v2). Separate PR. +- **`cmd/peergate.go`'s non-constant format string** (`go vet` failure at lines + 45 and 47, pre-existing at merge base). Separate PR. +- **The echo path-param unescape bug** affecting DNS-regex ledger deletes. + Separate PR, documented as a known issue. +- **The three deprecation warnings** the build emits (`params.algolia_docsearch`, + the GA4/UA notice, `params.ui.footer_about_disable`). The GA4 one goes away + incidentally when §9 removes the placeholder analytics ID; the other two are + Docsy configuration churn and belong with sub-project 3's theme work. + - Note: an earlier report described three KaTeX math-parse *errors* where + shell `$(...)`/`$VAR` in prose is read as math. Those do not occur on the + pinned Hugo 0.152.2 — the build is clean. Stay alert for the pattern when + adding new pages containing shell snippets, but there is nothing to fix. + +--- + +## 4. Decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | **Diátaxis split** (tutorials / how-to / reference / explanation) | The current site's core failure is that everything is a half-tutorial-half-reference blob under "Concepts", and the reference quadrant is empty. Diátaxis names exactly that problem. | +| D2 | **Generate the CLI + env reference; gate CI on drift** | Rot, not absence, is the disease. A generator that CI enforces is the only option where the docs cannot silently diverge from the code. | +| D3 | **Custom generator, not `app.ToMarkdown()`** | `ToMarkdown` exists in v2.27.7 (`docs.go:19`) but emits one undifferentiated blob: no Hugo front matter, no per-command page splitting, no env-var column. A direct walk is ~120 lines and provides all three. | +| D4 | **Relocate README content, then trim** | The README duplicates ~60% of the site and has drifted, but uniquely holds the k3s walkthrough, `rmem_max` troubleshooting, and library usage. Relocation is not new writing; it ends the drift and makes the site canonical. | +| D5 | **Stub pages for known gaps, with honest notes** | An explicitly empty page beats silent absence: it tells the reader the gap is known and is a hook for contribution. No stub may imply content that does not exist. | +| D6 | **Docsy stays this round** | Changing IA and theme simultaneously makes review harder and couples two independent risks. | + +--- + +## 5. Information architecture + +``` +docs/content/en/docs/ +├─ _index.md "What is EdgeVPN" [rewrite] +│ +├─ tutorials/ weight 10 +│ ├─ install.md [README §Installation + install.sh] +│ ├─ your-first-network.md [Getting started/_index.md, split] +│ ├─ share-a-service.md [Concepts/Overview/services.md, reframed] +│ └─ decentralized-k3s-cluster.md [RELOCATED from README:153] +│ +├─ how-to/ weight 20 +│ ├─ run-as-a-vpn.md [Getting started/cli.md] +│ ├─ addressing-and-dhcp.md [cli.md §DHCP + --address/--router] +│ ├─ ipv6.md [cli.md §IPv6] +│ ├─ enable-dns.md [Concepts/Overview/dns.md] +│ ├─ send-and-receive-files.md [Concepts/Overview/files.md] +│ ├─ tunnel-tcp-services.md [Concepts/Overview/services.md] +│ ├─ exit-nodes-and-proxy.md ★ NEW — --egress + `edgevpn proxy` +│ ├─ relays-and-hop-nodes.md ★ NEW — `edgevpn start`, autorelay +│ ├─ trusted-networks.md [peerguardian.md, --peerguard FIXED] +│ ├─ ledger-ownership.md ★ NEW — operator half of the design doc +│ ├─ run-with-docker.md ★ NEW — docker-compose.yml explained +│ └─ use-as-a-library.md [RELOCATED from README:174] +│ +├─ reference/ weight 30 +│ ├─ cli/ ⚙ GENERATED, one page per command +│ ├─ environment-variables.md ⚙ GENERATED +│ ├─ network-config.md [Concepts/Token/_index.md] +│ ├─ api.md [Getting started/api.md, corrected] +│ ├─ ledger-buckets.md ★ NEW — pkg/protocol/protocol.go +│ └─ compatibility.md ★ NEW — ownership wire-format matrix +│ +├─ explanation/ weight 40 +│ ├─ architecture.md [Concepts/Architecture/_index.md, updated] +│ ├─ the-ledger.md [Concepts/Overview/_index.md] +│ ├─ authenticated-ledger.md ‡ PUBLISHED from docs/design/ +│ ├─ security-model.md ★ NEW +│ └─ when-not-to-use-edgevpn.md [README:130 "Is it for me?" + :149] +│ +├─ tools/desktop-gui.md [Getting started/gui.md] +├─ troubleshooting.md [RELOCATED from README:240] weight 50 +└─ contributing.md [contribution-guidelines.md] weight 60 +``` + +**Weights are unique within each section.** The current collisions +(`Getting started/*` all at 1; `files.md`/`dns.md` both at 20) are a defect to +fix, not a pattern to carry forward. + +**Redirects.** Every moved page gets a Hugo `aliases:` entry for its old URL. +The site is linked from the README, from Kairos, and from search results; moving +16 pages without aliases breaks all of it. + +--- + +## 6. Generated reference + +### Generator + +New package `docs/generate/main.go`, mirroring the `api/generate` pattern the +repo used before this work removed it. + +It imports `github.com/mudler/edgevpn/cmd`, walks: +- `cmd.MainFlags()` — root-only flags (18) +- `cmd.CommonFlags` — shared flags (68) +- each `*cli.Command` in `main.go`'s `Commands` slice, including subcommands + +and emits into `docs/content/en/docs/reference/`: + +- `cli/_index.md` — command index +- `cli/.md` — one page per command: synopsis, aliases, description, + and a flag table (name, type, default, env var, usage) +- `environment-variables.md` — every flag carrying `EnvVars`, as a table of + env var ↔ flag ↔ default ↔ command scope + +Every generated file carries Hugo front matter and a banner: + +``` + +``` + +### Drift gate + +`make docs-gen` regenerates. A CI job runs it and then +`git diff --exit-code docs/content/en/docs/reference/`. Adding a flag without +regenerating fails the build. + +**This is the highest-value item in the spec.** It closes ~90 flags and ~75 env +vars in one mechanism and makes the `--peerguardian` class of defect +structurally impossible. + +### Known follow-up, not fixed here + +The env-var naming is wildly inconsistent — bare (`API`, `DHCP`, `ROUTER`), +`EDGEVPN`-prefixed unseparated (`EDGEVPNTOKEN`, `EDGEVPNMTU`), and +`EDGEVPN_`-prefixed (`EDGEVPN_RELAY_SERVICE`). The generated table will make +this visible for the first time. Renaming is a breaking change and belongs in +its own issue; the table documents what exists. + +Two env vars bypass the flag system entirely and must be documented **by hand** +on `reference/environment-variables.md`, since the generator cannot see them: +- `APILISTENUNIXMODE` (`api/api.go`) — octal mode for the API unix socket +- `LISTEN_PID` / `LISTEN_FDS` (`api/api.go`) — systemd socket activation + +--- + +## 7. New prose + +Eight pages are marked `★ NEW` in §5. Five are **P0** — the docs are actively +wrong or a whole feature is invisible without them — and are listed below in +priority order. The remaining three are **P1**: real gaps, written this round +if the P0 work lands cleanly, otherwise demoted to stubs under §7's stub policy +rather than silently dropped. + +**P1 (new, lower priority):** `how-to/run-with-docker.md` (the repo ships a +`docker-compose.yml` that is linked but never explained — `network_mode: host`, +`NET_ADMIN`, `/dev/net/tun`, the healthcheck, `--privkey-cache`), +`reference/ledger-buckets.md` (the bucket namespace from +`pkg/protocol/protocol.go`, which the API docs already tell users to `PUT` into +without ever defining), and `tutorials/share-a-service.md` where it goes beyond +reframing the existing `services.md`. + +### P0 — five pages, ordered by how much damage their absence does. + +1. **`how-to/exit-nodes-and-proxy.md`** — an entire headline feature is + invisible. Covers `--egress`, `--egress-announce-time`, the `edgevpn proxy` + subcommand, and the security implication that traffic leaves the network at + the exit node's address and any token holder can use it. + +2. **`how-to/ledger-ownership.md`** + **`explanation/authenticated-ledger.md`** — + `--ownership` defaults to `enforce` and is wire-format incompatible across + modes. A user upgrading into a mixed-version network gets a silently broken + ledger. The how-to is the operator half; the explanation is the existing + 369-line design doc, published as-is with front matter added. + +3. **`explanation/security-model.md`** — EdgeVPN ships trust zones, peer gating, + ownership enforcement, relay ACLs and an unauthenticated API, and nothing + ties them together. Must state plainly: the security model is + perimeter-only — any token holder is fully trusted, there is no per-peer + authorization on the data plane, and the API has no authentication. + +4. **`how-to/relays-and-hop-nodes.md`** — `edgevpn start`, autorelay flags, and + the relay-service family (8 flags, the newest and best-commented code in the + repo). + +5. **`reference/compatibility.md`** — version/wire-format matrix, driven by the + ownership modes. + +### Stub policy (D5) + +Gaps not written this round get a stub with real front matter and an explicit +note naming what is missing and pointing at the source. Permitted stubs: +`how-to/run-with-systemd.md`, `how-to/persist-node-identity.md`, +`how-to/tune-for-low-end-devices.md`, `explanation/discovery-and-nat.md`. + +A stub must not imply content exists. No "coming soon" without saying what. + +--- + +## 8. README + +Relocate verbatim (git-mv-shaped, not new writing): + +| README section | Destination | +|---|---| +| `:notebook: As a library` (line 174) | `how-to/use-as-a-library.md` | +| k3s example (line 153) | `tutorials/decentralized-k3s-cluster.md` | +| `:notebook: Troubleshooting` (line 240) | `troubleshooting.md` | +| `:question: Is it for me?` (130) + `:warning: Warning!` (149) | `explanation/when-not-to-use-edgevpn.md` | + +The README then keeps: badges, the one-paragraph pitch, the feature list, +screenshots, installation, a 5-minute quickstart, projects-using-EdgeVPN, +contribution, credits, licence — and links to the site for everything else. + +**Licence inconsistency, flagged not fixed:** `LICENSE` is Apache-2.0, the +README badge says GPL3, the README footer says "Apache License v2", the CLI +banner is GPL-flavoured, and ~10 source files carry GPL-2 headers. This is a +legal question for the maintainer, not a docs edit. The spec records it; the +implementation must not silently pick one. + +--- + +## 9. Infrastructure + +Small, in scope because they are docs-build correctness: + +1. **Add `CONTRIBUTING.md`** at the repo root. `contribution-guidelines.md` + links to it and it does not exist — the contributing page 404s today. +2. **Remove the `docsy` git submodule** and its `.gitmodules` entries. + `docs/config.toml` sets no `theme=` (verified: 0 matches), so the submodule + is unused; Dependabot bumps it regardless. +3. **Add `pull_request` + `paths: [docs/**]` to `.github/workflows/pages.yml`**, + which currently triggers only on push to master — docs breakage is found + only after merge. +4. **Reconcile `baseURL`** — `docs/config.toml` says + `https://mudler.github.io/edgevpn/docs/` while `docs/scripts/build.sh` + passes `-b https://mudler.github.io/edgevpn`. They disagree; local link + prefixes differ from production. +5. **Set `breadcrumb_disable = false`** — the tree is three levels deep. +6. **Fix the empty link** in `Concepts/Token/_index.md` ("See [the Architecture + section]()"). +7. **Delete or fill `community/_index.md`** — an empty Docsy template shell + currently in the main nav. +8. **Remove the placeholder `UA-00000000-0` analytics ID** while + `params.ui.feedback.enable = true` sends feedback events nowhere. + +Not in scope: `docs/package.json` and `package-lock.json` are both tracked +**and** gitignored, so the ignore rules are dead and `build.sh`'s +`npm install --save` dirties the tree on every docs build. Real, but it is a +build-tooling fix that belongs with sub-project 3's theme work. + +--- + +## 10. Verification + +- `cd docs && make build` succeeds, with **no new errors** relative to the + merge base. Measured baseline on the pinned toolchain (Hugo 0.152.2 + extended, per `docs/Makefile`): **zero errors**, 29 pages, three deprecation + warnings. An earlier report's "43 errors" figure came from Hugo 0.146.3, a + version this project does not use. +- **Every command in every page is executed or explicitly marked unverified.** + This is the discipline whose absence produced `--peerguardian`. A command + that cannot be run in CI (needs two hosts, needs root) is marked as such in + the implementation report, not silently trusted. +- `make docs-gen && git diff --exit-code docs/content/en/docs/reference/` is + clean — the gate must pass on the branch that introduces it. +- No internal link 404s. Every moved page has a working `aliases:` entry. +- `grep -ril peerguardian docs/content/` returns nothing. +- `grep -ril egress docs/content/` returns at least the exit-nodes page. +- Every page has unique front matter `weight` within its section. + +--- + +## Appendix A — Verified defect evidence + +Confirmed by direct inspection on 2026-08-03, not taken from the audit report: + +| Claim | Verification | +|---|---| +| `--peerguard` is the real flag | `cmd/util.go:368` `Name: "peerguard"` | +| egress undocumented | `grep -ril egress docs/content/` → 0 | +| `edgevpn start` undocumented | `grep -rl 'edgevpn start' docs/content/` → 0 | +| ownership defaults to enforce | `cmd/util.go` `Value: "enforce"` + wire-format warning in its usage string | +| bad api example | `Getting started/api.md:142` | +| design doc unpublished | `docs/design/authenticated-ledger.md` = 369 lines; 0 matches under `docs/content/` | +| `CONTRIBUTING.md` missing | absent from repo root | +| docsy submodule unused | 3 `.gitmodules` entries; 0 `^theme` lines in `docs/config.toml` | +| `ToMarkdown` exists but unsuitable | `urfave/cli/v2@v2.27.7/docs.go:19` | +| weight collisions | `Getting started/{_index,api,cli}.md` all `weight: 1`; `Overview/{files,dns}.md` both `weight: 20` | + +## Appendix B — Deferred register + +Carried forward, not addressed here: + +1. `go.mod:32` — `urfave/cli/v3` declared direct, imported nowhere. +2. `cmd/peergate.go:45,47` — non-constant format string; fails `go vet`. +3. echo does not unescape path params, so ledger deletes silently no-op for + DNS regexes containing `\ ^ $ /`. Pre-existing; `machines` unaffected. +4. Env-var naming inconsistency (three conventions). +5. Licence signalling inconsistency (§8). +6. `/api/metrics/peer/:peer` uses a raw `peer.ID()` cast, not `peer.Decode`. +7. Nodes co-hosting several peers of one network emit continuous + `ownership violation (rejected): rollback to an older version` logs. +8. `docs/package.json` tracked and gitignored simultaneously. diff --git a/docs/themes/docsy b/docs/themes/docsy deleted file mode 160000 index 01c827ea..00000000 --- a/docs/themes/docsy +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 01c827ea890e8e498f6046a7666a3031f318cc7f diff --git a/internal/docsgen/main.go b/internal/docsgen/main.go new file mode 100644 index 00000000..95026159 --- /dev/null +++ b/internal/docsgen/main.go @@ -0,0 +1,158 @@ +// Command docsgen emits the CLI and environment-variable reference +// documentation from the real cli.App, so the docs cannot drift from the +// binary. Run via `make docs-gen`. +// +// It lives in the root module rather than under docs/ because docs/ carries +// its own go.mod for the Hugo module, which would put a generator placed there +// in a module that cannot import github.com/mudler/edgevpn/cmd. +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/urfave/cli/v2" + + "github.com/mudler/edgevpn/cmd" +) + +const outDir = "docs/content/en/docs/reference" + +func main() { + // The version is irrelevant to the generated output and must be fixed, + // otherwise the CI drift check would fail on every release. + app := cmd.NewApp("") + + if err := run(app); err != nil { + fmt.Fprintln(os.Stderr, "docs generate:", err) + os.Exit(1) + } +} + +func run(app *cli.App) error { + cliDir := filepath.Join(outDir, "cli") + if err := os.MkdirAll(cliDir, 0o755); err != nil { + return err + } + + bindings := map[string][]envBinding{} + collectEnv(bindings, app.Flags, "global") + + // Every page is rendered up front, so the directory is only modified once + // the full output is known to be in hand. Rendering into memory first is + // what lets the prune below skip files it is about to rewrite; a + // delete-then-write pass could empty the directory and then fail. + pages := map[string]string{} + + // Index page, carrying the root flag table. + var idx strings.Builder + idx.WriteString("---\ntitle: \"CLI\"\nlinkTitle: \"CLI\"\nweight: 10\ndescription: >\n Every EdgeVPN command and flag.\n---\n\n") + idx.WriteString(banner + "\n\n") + idx.WriteString("Running `edgevpn` with no subcommand starts the VPN.\n\n## Global flags\n\n") + idx.WriteString(renderFlagTable(app.Flags)) + idx.WriteString("\n## Commands\n\n") + for _, c := range app.Commands { + fmt.Fprintf(&idx, "- [`%s`](%s/) — %s\n", c.Name, c.Name, escapeCell(c.Usage)) + } + pages["_index.md"] = idx.String() + + for i, c := range app.Commands { + pages[c.Name+".md"] = renderCommandPage(c, (i+1)*10) + collectEnv(bindings, c.Flags, c.Name) + for _, sub := range c.Subcommands { + collectEnv(bindings, sub.Flags, c.Name+" "+sub.Name) + } + } + + // Drop pages for commands that no longer exist, so a removed command does + // not leave a stale page behind — the drift gate would never catch it. + fresh := make(map[string]bool, len(pages)) + for name := range pages { + fresh[name] = true + } + foreign, err := pruneStale(cliDir, fresh) + if err != nil { + return err + } + for _, name := range foreign { + fmt.Fprintf(os.Stderr, + "docs generate: leaving %s alone: it is not generated (no %q banner)\n", + filepath.Join(cliDir, name), "Generated by internal/docsgen") + } + + names := make([]string, 0, len(pages)) + for name := range pages { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if err := os.WriteFile(filepath.Join(cliDir, name), []byte(pages[name]), 0o644); err != nil { + return err + } + } + + return os.WriteFile( + filepath.Join(outDir, "environment-variables.md"), + []byte(renderEnvVarPage(bindings)), 0o644) +} + +// pruneStale removes generated markdown pages in dir that are not in fresh. +// +// It only ever deletes files carrying the generator's banner, so a hand-written +// page dropped into the directory survives — the previous unconditional +// `os.Remove` over `*.md` would have destroyed it silently. Files it declines +// to delete are returned by name so the caller can report them rather than let +// them accumulate unnoticed. Files that are about to be rewritten are left in +// place, so a failure part-way through writing cannot leave the tree emptied. +func pruneStale(dir string, fresh map[string]bool) ([]string, error) { + entries, err := filepath.Glob(filepath.Join(dir, "*.md")) + if err != nil { + return nil, err + } + + var foreign []string + for _, p := range entries { + name := filepath.Base(p) + if fresh[name] { + continue + } + content, err := os.ReadFile(p) + if err != nil { + return nil, err + } + if !bytes.Contains(content, []byte(banner)) { + foreign = append(foreign, name) + continue + } + if err := os.Remove(p); err != nil { + return nil, err + } + } + sort.Strings(foreign) + return foreign, nil +} + +func collectEnv(into map[string][]envBinding, flags []cli.Flag, command string) { + for _, f := range flags { + df, ok := documented(f) + if !ok { + continue + } + names := f.Names() + if len(names) == 0 { + continue + } + def := defaultText(df) + for _, env := range df.GetEnvVars() { + into[env] = append(into[env], envBinding{ + Flag: "--" + names[0], + Command: command, + Default: def, + }) + } + } +} diff --git a/internal/docsgen/main_test.go b/internal/docsgen/main_test.go new file mode 100644 index 00000000..83b1b122 --- /dev/null +++ b/internal/docsgen/main_test.go @@ -0,0 +1,128 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/urfave/cli/v2" +) + +// docOnlyFlag implements cli.DocGenerationFlag but deliberately NOT +// cli.VisibleFlag. This is the exact case documented() exists to handle: the +// two interfaces are separate in urfave/cli, and a single type assertion +// against DocGenerationFlag cannot reach IsVisible(). Such a flag has no way to +// declare itself hidden, so it must render. +type docOnlyFlag struct { + name string +} + +func (f *docOnlyFlag) String() string { return f.name } +func (f *docOnlyFlag) Apply(*flag.FlagSet) error { return nil } +func (f *docOnlyFlag) Names() []string { return []string{f.name} } +func (f *docOnlyFlag) IsSet() bool { return false } +func (f *docOnlyFlag) TakesValue() bool { return true } +func (f *docOnlyFlag) GetUsage() string { return "doc only, no visibility interface" } +func (f *docOnlyFlag) GetValue() string { return "somevalue" } +func (f *docOnlyFlag) GetDefaultText() string { return "" } +func (f *docOnlyFlag) GetEnvVars() []string { return []string{"DOCONLYVAR"} } + +// visibleOnlyFlag implements cli.VisibleFlag but NOT cli.DocGenerationFlag. +// There is no usage, default or env var to read off it, so rendering a row +// would produce empty cells. It must be skipped. +type visibleOnlyFlag struct { + name string +} + +func (f *visibleOnlyFlag) String() string { return f.name } +func (f *visibleOnlyFlag) Apply(*flag.FlagSet) error { return nil } +func (f *visibleOnlyFlag) Names() []string { return []string{f.name} } +func (f *visibleOnlyFlag) IsSet() bool { return false } +func (f *visibleOnlyFlag) IsVisible() bool { return true } + +func TestRenderFlagTableRendersDocGenerationFlagWithoutVisibleFlag(t *testing.T) { + out := renderFlagTable([]cli.Flag{&docOnlyFlag{name: "doc-only"}}) + + if !strings.Contains(out, "--doc-only") { + t.Errorf("a DocGenerationFlag that does not implement VisibleFlag was dropped:\n%s", out) + } + for _, want := range []string{"doc only, no visibility interface", "DOCONLYVAR", "somevalue"} { + if !strings.Contains(out, want) { + t.Errorf("row missing %q\n%s", want, out) + } + } +} + +func TestRenderFlagTableSkipsVisibleFlagWithoutDocGeneration(t *testing.T) { + out := renderFlagTable([]cli.Flag{&visibleOnlyFlag{name: "visible-only"}}) + + if strings.Contains(out, "visible-only") { + t.Errorf("a flag with no DocGenerationFlag data was rendered with empty cells:\n%s", out) + } + if !strings.Contains(out, "takes no flags of its own") { + t.Errorf("expected the empty-table placeholder:\n%s", out) + } +} + +// collectEnv must apply the same two-interface rule as renderFlagTable, or the +// env page and the flag tables would disagree about which flags exist. +func TestCollectEnvAppliesTheSameVisibilityRule(t *testing.T) { + bindings := map[string][]envBinding{} + collectEnv(bindings, []cli.Flag{ + &docOnlyFlag{name: "doc-only"}, + &visibleOnlyFlag{name: "visible-only"}, + &cli.StringFlag{Name: "hidden", EnvVars: []string{"HIDDENVAR"}, Hidden: true}, + }, "global") + + if _, ok := bindings["DOCONLYVAR"]; !ok { + t.Errorf("DocGenerationFlag without VisibleFlag was dropped from the env map: %v", bindings) + } + if _, ok := bindings["HIDDENVAR"]; ok { + t.Errorf("hidden flag leaked into the env map: %v", bindings) + } +} + +func TestPruneStaleOnlyRemovesGeneratedPages(t *testing.T) { + dir := t.TempDir() + + write := func(name, content string) string { + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + return p + } + + // A generated page that is about to be rewritten: must survive the sweep, + // so a later write failure cannot leave the directory emptied. + fresh := write("proxy.md", banner+"\ncurrent\n") + // A generated page for a command that no longer exists: must go. + stale := write("removed-command.md", banner+"\nstale\n") + // A hand-written page someone dropped in here: must survive, and be + // reported rather than silently ignored. + handWritten := write("troubleshooting.md", "---\ntitle: notes\n---\nhand written\n") + // A non-markdown file: not our business at all. + other := write("diagram.svg", "") + + foreign, err := pruneStale(dir, map[string]bool{"proxy.md": true}) + if err != nil { + t.Fatal(err) + } + + for _, p := range []string{fresh, handWritten, other} { + if _, err := os.Stat(p); err != nil { + t.Errorf("pruneStale deleted a file it must not touch: %s", filepath.Base(p)) + } + } + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Errorf("pruneStale left the stale generated page %s behind", filepath.Base(stale)) + } + + sort.Strings(foreign) + if len(foreign) != 1 || foreign[0] != "troubleshooting.md" { + t.Errorf("foreign files = %v, want [troubleshooting.md]", foreign) + } +} diff --git a/internal/docsgen/render.go b/internal/docsgen/render.go new file mode 100644 index 00000000..8311afce --- /dev/null +++ b/internal/docsgen/render.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/urfave/cli/v2" +) + +const banner = "" + +// envBinding records one place an environment variable is read from. +type envBinding struct { + Flag string + Command string + Default string +} + +// escapeCell makes a string safe inside a markdown table cell. Several real +// usage strings contain "|" (the ownership flag lists its modes that way), +// which would otherwise split the row into extra columns. +func escapeCell(s string) string { + s = strings.ReplaceAll(s, "|", `\|`) + s = strings.ReplaceAll(s, "\n", " ") + return strings.TrimSpace(s) +} + +// documented reports whether a flag should appear in the reference, and +// returns the documentation view of it. +// +// IsVisible lives on cli.VisibleFlag, not on cli.DocGenerationFlag, so the two +// interfaces have to be asserted separately. A flag that implements neither is +// skipped rather than rendered with empty columns. +func documented(f cli.Flag) (cli.DocGenerationFlag, bool) { + df, ok := f.(cli.DocGenerationFlag) + if !ok { + return nil, false + } + if vf, ok := f.(cli.VisibleFlag); ok && !vf.IsVisible() { + return nil, false + } + return df, true +} + +// machineSpecific lists path prefixes that differ between the machine the docs +// were generated on and the machine the CI drift gate runs on. cmd.stateDir() +// derives --privkey-cache-dir and --lease-dir from the caller's home +// directory, so without this the generated output would never be reproducible. +var machineSpecific = func() [][2]string { + var subs [][2]string + if home, err := os.UserHomeDir(); err == nil && home != "" && home != "/" { + subs = append(subs, [2]string{home, "$HOME"}) + } + if wd, err := os.Getwd(); err == nil && wd != "" && wd != "/" { + subs = append(subs, [2]string{wd, "."}) + } + return subs +}() + +// normalizeDefault strips machine-specific prefixes out of a default value. +func normalizeDefault(s string) string { + for _, sub := range machineSpecific { + s = strings.ReplaceAll(s, sub[0], sub[1]) + } + return s +} + +// defaultText returns the documented default for a flag: the explicit +// DefaultText when the flag sets one, otherwise its value, normalised so the +// output does not depend on where it was generated. It returns "" when the +// flag has no default. +func defaultText(df cli.DocGenerationFlag) string { + def := df.GetDefaultText() + if def == "" { + def = df.GetValue() + } + return normalizeDefault(def) +} + +func flagNames(f cli.Flag) string { + names := f.Names() + out := make([]string, 0, len(names)) + for _, n := range names { + if len(n) == 1 { + out = append(out, "`-"+n+"`") + } else { + out = append(out, "`--"+n+"`") + } + } + return strings.Join(out, ", ") +} + +// renderFlagTable renders a markdown table for the visible flags. +func renderFlagTable(flags []cli.Flag) string { + var b strings.Builder + b.WriteString("| Flag | Default | Environment | Description |\n") + b.WriteString("|---|---|---|---|\n") + + rows := 0 + for _, f := range flags { + df, ok := documented(f) + if !ok { + continue + } + def := defaultText(df) + if def == "" { + def = "—" + } else { + def = "`" + escapeCell(def) + "`" + } + + env := "—" + if vars := df.GetEnvVars(); len(vars) > 0 { + quoted := make([]string, len(vars)) + for i, v := range vars { + quoted[i] = "`" + v + "`" + } + env = strings.Join(quoted, ", ") + } + + fmt.Fprintf(&b, "| %s | %s | %s | %s |\n", + flagNames(f), def, env, escapeCell(df.GetUsage())) + rows++ + } + if rows == 0 { + return "_This command takes no flags of its own._\n" + } + return b.String() +} + +// renderCommandPage renders one Hugo page for a command. +func renderCommandPage(c *cli.Command, weight int) string { + var b strings.Builder + fmt.Fprintf(&b, "---\ntitle: %q\nlinkTitle: %q\nweight: %d\n", c.Name, c.Name, weight) + // An empty folded scalar would leave "description: >" dangling and break + // the front matter, so the key is only emitted when there is a usage line. + if u := escapeCell(c.Usage); u != "" { + fmt.Fprintf(&b, "description: >\n %s\n", u) + } + b.WriteString("---\n\n") + b.WriteString(banner + "\n\n") + + if len(c.Aliases) > 0 { + quoted := make([]string, len(c.Aliases)) + for i, a := range c.Aliases { + quoted[i] = "`" + a + "`" + } + fmt.Fprintf(&b, "Aliases: %s\n\n", strings.Join(quoted, ", ")) + } + if c.Description != "" && c.Description != c.Usage { + fmt.Fprintf(&b, "%s\n\n", c.Description) + } + + fmt.Fprintf(&b, "```\nedgevpn %s [options]\n```\n\n", c.Name) + b.WriteString("## Flags\n\n") + b.WriteString(renderFlagTable(c.Flags)) + + for _, sub := range c.Subcommands { + fmt.Fprintf(&b, "\n## `%s %s`\n\n", c.Name, sub.Name) + if sub.Usage != "" { + fmt.Fprintf(&b, "%s\n\n", sub.Usage) + } + b.WriteString(renderFlagTable(sub.Flags)) + } + return b.String() +} + +// renderEnvVarPage renders the environment-variable cross-reference. +// +// The variable names are sorted so the output is byte-for-byte reproducible; +// Go's randomised map iteration would otherwise make the CI drift gate fail at +// random. +func renderEnvVarPage(bindings map[string][]envBinding) string { + var b strings.Builder + b.WriteString("---\ntitle: \"Environment variables\"\nlinkTitle: \"Environment variables\"\nweight: 20\ndescription: >\n Every environment variable EdgeVPN reads, and the flag it corresponds to.\n---\n\n") + b.WriteString(banner + "\n\n") + b.WriteString("Environment variables are read when the corresponding flag is not passed.\n\n") + b.WriteString("| Variable | Flag | Command | Default |\n|---|---|---|---|\n") + + names := make([]string, 0, len(bindings)) + for k := range bindings { + names = append(names, k) + } + sort.Strings(names) + + for _, name := range names { + for _, bind := range bindings[name] { + def := bind.Default + if def == "" { + def = "—" + } else { + def = "`" + escapeCell(def) + "`" + } + fmt.Fprintf(&b, "| `%s` | `%s` | %s | %s |\n", + name, bind.Flag, bind.Command, def) + } + } + return b.String() +} diff --git a/internal/docsgen/render_test.go b/internal/docsgen/render_test.go new file mode 100644 index 00000000..45aca40b --- /dev/null +++ b/internal/docsgen/render_test.go @@ -0,0 +1,131 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/urfave/cli/v2" +) + +func TestRenderFlagTableIncludesEnvVars(t *testing.T) { + flags := []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Usage: "Specify an edgevpn token in place of a config file", + EnvVars: []string{"EDGEVPNTOKEN"}, + }, + &cli.BoolFlag{ + Name: "peerguard", + Usage: "Enable peerguard. (Experimental)", + EnvVars: []string{"PEERGUARD"}, + }, + } + out := renderFlagTable(flags) + + for _, want := range []string{"--token", "EDGEVPNTOKEN", "--peerguard", "PEERGUARD", "Enable peerguard"} { + if !strings.Contains(out, want) { + t.Errorf("flag table missing %q\n%s", want, out) + } + } +} + +func TestRenderFlagTableSkipsHiddenFlags(t *testing.T) { + flags := []cli.Flag{ + &cli.StringFlag{Name: "visible", Usage: "shown"}, + &cli.StringFlag{Name: "secret", Usage: "hidden", Hidden: true}, + } + out := renderFlagTable(flags) + if strings.Contains(out, "secret") { + t.Errorf("hidden flag leaked into the table:\n%s", out) + } + if !strings.Contains(out, "visible") { + t.Errorf("visible flag missing:\n%s", out) + } +} + +func TestRenderFlagTableEscapesPipes(t *testing.T) { + // Several real usage strings contain "|" (e.g. the ownership flag lists + // "enforce | observe | off"), which would split the row into extra + // markdown columns. + flags := []cli.Flag{ + &cli.StringFlag{Name: "ownership", Usage: "enforce | observe | off", Value: "enforce"}, + } + out := renderFlagTable(flags) + + var row string + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "ownership") { + row = line + } + } + if row == "" { + t.Fatal("no row rendered for the ownership flag") + } + if !strings.Contains(row, `enforce \| observe \| off`) { + t.Errorf("pipes in usage were not escaped: %q", row) + } + // A well-formed 4-column row has exactly 5 structural pipes; every other + // pipe must be escaped. + if got := strings.Count(row, "|") - strings.Count(row, `\|`); got != 5 { + t.Errorf("row has %d structural pipes, want 5 (4 columns): %q", got, row) + } +} + +// cmd.stateDir() derives --privkey-cache-dir and --lease-dir from the calling +// user's home directory. Emitting that verbatim makes the output differ per +// machine, so the CI drift gate would fail on every run even though nothing +// changed. Machine-specific prefixes must be normalised away. +func TestRenderFlagTableNormalizesMachineSpecificDefaults(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil || home == "" || home == "/" { + t.Skip("no usable home directory on this machine") + } + flags := []cli.Flag{ + &cli.StringFlag{ + Name: "privkey-cache-dir", + Usage: "Specify a directory used to store the generated privkey", + Value: filepath.Join(home, ".edgevpn"), + }, + } + out := renderFlagTable(flags) + + if strings.Contains(out, home) { + t.Errorf("machine-specific home path %q leaked into the table:\n%s", home, out) + } + if !strings.Contains(out, "$HOME/.edgevpn") { + t.Errorf("home path was not normalised to $HOME:\n%s", out) + } +} + +func TestRenderPageHasFrontMatterAndBanner(t *testing.T) { + out := renderCommandPage(&cli.Command{ + Name: "proxy", + Usage: "Starts a local http proxy server", + Aliases: []string{}, + Description: "Routes traffic through the p2p network", + Flags: []cli.Flag{&cli.StringFlag{Name: "listen", Usage: "Listen address"}}, + }, 10) + + if !strings.HasPrefix(out, "---\n") { + t.Error("page does not start with front matter") + } + for _, want := range []string{`title: "proxy"`, "weight: 10", "Do not edit", "--listen"} { + if !strings.Contains(out, want) { + t.Errorf("page missing %q\n%s", want, out) + } + } +} + +func TestRenderEnvVarPageMapsBackToFlags(t *testing.T) { + out := renderEnvVarPage(map[string][]envBinding{ + "EDGEVPNTOKEN": {{Flag: "--token", Command: "global", Default: ""}}, + "PROXYLISTEN": {{Flag: "--listen", Command: "proxy", Default: ":8080"}}, + }) + for _, want := range []string{"EDGEVPNTOKEN", "--token", "PROXYLISTEN", "proxy", ":8080"} { + if !strings.Contains(out, want) { + t.Errorf("env var page missing %q\n%s", want, out) + } + } +}