|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## What is AWL |
| 6 | + |
| 7 | +Anywherelan (awl) is a peer-to-peer mesh VPN written in Go. It uses libp2p for P2P networking, WireGuard's TUN driver for the virtual network interface, and supports SOCKS5 proxy (exit nodes). There is also a fuller human-facing dev guide in `AGENTS.md` (general) and `AGENTS_test.md` (tests). |
| 8 | + |
| 9 | +## Repository layout — Go modules |
| 10 | + |
| 11 | +The repo contains **two** Go modules. They have independent dependency graphs and must be tidied separately: |
| 12 | + |
| 13 | +- `go.mod` — root module `github.com/anywherelan/awl`: all of the core code (application, p2p, vpn, service, api, cli, config, …) plus `cmd/awl` and `cmd/gomobile-lib`. |
| 14 | +- `cmd/awl-tray/go.mod` — desktop tray app. Separate module so that the tray/GUI dependencies (systray, webview, zenity bindings) don't leak into the headless server binary. You **cannot** `go build ./cmd/awl-tray` from root; use `./build.sh awl-tray` or `cd cmd/awl-tray && go build`. |
| 15 | + |
| 16 | +Always run `go mod tidy -compat=1.26` (not `-compat=1.x`) in whichever module you changed. |
| 17 | + |
| 18 | +## Commands |
| 19 | + |
| 20 | +### Build |
| 21 | + |
| 22 | +```bash |
| 23 | +# Desktop binary (awl-tray) for current platform — this handles the separate awl-tray module |
| 24 | +./build.sh awl-tray |
| 25 | + |
| 26 | +# Headless server binary (root module) |
| 27 | +go build github.com/anywherelan/awl/cmd/awl |
| 28 | + |
| 29 | +# Web UI assets (requires Flutter + the sibling repo awl-flutter) |
| 30 | +./build.sh web |
| 31 | + |
| 32 | +# Before first build on Windows (or cross-compiling to Windows), download wintun: |
| 33 | +./build.sh deps |
| 34 | +``` |
| 35 | + |
| 36 | +Version is injected at build time: `-ldflags "-X github.com/anywherelan/awl/config.Version=${VERSION}"`. The default at `config/version.go` is `DevVersion = "dev"`; `config.IsDevVersion()` / `Config.DevMode()` gate dev-only behavior. |
| 37 | + |
| 38 | +The web UI is served from `//go:embed static` in `application.go`. Because Go's `embed` refuses to compile if the directory is missing, **tests and local builds both require `static/` to exist**, even if empty. If a fresh clone has no `static/`: |
| 39 | + |
| 40 | +```bash |
| 41 | +mkdir -p static && touch static/index.html |
| 42 | +``` |
| 43 | + |
| 44 | +### Swagger / OpenAPI |
| 45 | + |
| 46 | +API types are documented inline with `swag` annotations. After changing API request/response structs or handlers, regenerate: |
| 47 | + |
| 48 | +```bash |
| 49 | +go generate ./... |
| 50 | +# equivalent: cd <root> && go run github.com/swaggo/swag/cmd/swag@latest init --parseDependency -g application.go |
| 51 | +``` |
| 52 | + |
| 53 | +The `go:generate` directive lives at the top of `application.go`. |
| 54 | + |
| 55 | +### Test |
| 56 | + |
| 57 | +```bash |
| 58 | +go test -count=1 ./... |
| 59 | +go test -race -count=1 ./... |
| 60 | + |
| 61 | +# Single test |
| 62 | +go test -run TestName ./... |
| 63 | + |
| 64 | +# Benchmarks |
| 65 | +go test -bench=. ./... |
| 66 | +``` |
| 67 | + |
| 68 | +Tests spin up real `Application` instances against real libp2p + DHT. See "Test Infrastructure" below. |
| 69 | + |
| 70 | +#### Simulated network performance tests |
| 71 | + |
| 72 | +Perf tests for both the VPN tunnel and SOCKS5 proxy live in `application_simnet_test.go`. They use `simlibp2p` + `simnet` to simulate network conditions with controlled latency and bandwidth. They take ~30s each and are skipped by default. Enable with `AWL_PERF_TESTS=1`: |
| 73 | + |
| 74 | +```bash |
| 75 | +# Run all perf tests |
| 76 | +AWL_PERF_TESTS=1 go test -run TestSimulated -v . |
| 77 | + |
| 78 | +# Run a specific scenario |
| 79 | +AWL_PERF_TESTS=1 go test -run TestSimulatedSOCKS5ProxyPerformance/LongDistCable_10Mbps_300ms -v . |
| 80 | +AWL_PERF_TESTS=1 go test -run TestSimulatedTunnelPerformance/LongDistCable_10Mbps_300ms -v . |
| 81 | +``` |
| 82 | + |
| 83 | +Tests output a table with throughput, utilization, and TTFB metrics. |
| 84 | + |
| 85 | +### Lint |
| 86 | + |
| 87 | +```bash |
| 88 | +golangci-lint run |
| 89 | +gofmt -d . |
| 90 | +go mod tidy -compat=1.26 # in whichever module(s) you touched |
| 91 | +``` |
| 92 | + |
| 93 | +Linter config: `.golangci.yml`. `go fmt` + `go vet` are enforced. |
| 94 | + |
| 95 | +## Architecture |
| 96 | + |
| 97 | +### Application lifecycle (`application.go`) |
| 98 | + |
| 99 | +The correct init sequence — order matters, `Init` will panic if the logger/config aren't set up first, and it needs a platform-specific `tun.Device`: |
| 100 | + |
| 101 | +```go |
| 102 | +app := awl.New() // zero-valued struct; does NOT set logger/config |
| 103 | +app.SetupLoggerAndConfig() // required before Init: loads config_awl.json |
| 104 | + // (falls back to config.NewConfig on error), |
| 105 | + // wires the ring-buffer log sink |
| 106 | +tunDevice, err := tun.CreateTUN("awl0", vpn.InterfaceMTU) // platform-specific |
| 107 | +if err != nil { /* ... */ } |
| 108 | +err = app.Init(ctx, tunDevice) // brings up P2P, VPN, DNS, SOCKS5, API, metrics |
| 109 | +// ... app.Ctx() is cancelled on app.Close() |
| 110 | +app.Close() // saves config, cancels ctx, shuts down services |
| 111 | +``` |
| 112 | + |
| 113 | +Entry points wrap this slightly differently: |
| 114 | + |
| 115 | +- `cmd/awl/main.go` — headless server: builds its own TUN via `vpn.NewTunDevice`. |
| 116 | +- `cmd/awl-tray/main.go` — desktop tray: same core init, plus systray wiring and update checks. |
| 117 | +- `cmd/gomobile-lib/` — Android AAR built with `gomobile`; the Android app passes in an externally-managed `tun.Device` (VpnService file descriptor). |
| 118 | + |
| 119 | +### Core packages |
| 120 | + |
| 121 | +- **`p2p/`** — libp2p host. DHT routing, connection management, metrics. Transports: **QUIC (with native TLS 1.3) and TCP+TLS**, both enabled (`p2p/p2p.go:171-172`). Default listen port is probed at startup. |
| 122 | +- **`vpn/`** — WireGuard `tun.Device` wrapper with platform-specific constructors (`iface_linux.go`, `iface_darwin.go`, `iface_windows.go`, `iface_android.go`, `iface_other.go`). Packet batching; `InterfaceMTU = 3500`, `maxContentSize = InterfaceMTU + 100`. |
| 123 | +- **`service/`** — glue between P2P streams and app logic: |
| 124 | + - `tunnel.go` — routes IP packets between TUN and per-peer P2P streams. Non-blocking send (`select` with `default`) on the per-peer channel; drops on full. |
| 125 | + - `auth_status.go` — friend-request/accept flow, background retry, peer status exchange. |
| 126 | + - `socks5.go` — local SOCKS5 listener and remote exit-node proxying (thin wrapper; real server impl lives in `socks5/`). |
| 127 | +- **`socks5/`** — standalone SOCKS5 server/client implementation (`server.go`, `client.go`, `conn.go`). Owned by awl, not a third-party import. |
| 128 | +- **`api/`** — Echo HTTP REST API on port 8639. Handlers split across `api.go`, `peers.go`, `settings.go`, `debug.go`, `metrics.go`. `api/apiclient/` is the typed Go client used by the CLI and tests. pprof is mounted under `/api/v0/debug/pprof/`. Prometheus metrics at `/metrics`. Swagger served from `/swagger/`. |
| 129 | +- **`config/`** — JSON config (`config_awl.json`), `sync.RWMutex`-guarded, auto-saves on every mutation via `Config.save()`. Field-level docs are in `config/config.go`; defaults in `config/other.go::setDefaults`. |
| 130 | +- **`awldns/`** — **.awl domain resolver** built on `anywherelan/ts-dns` (Tailscale's DNS fork). The DNS *service* (lifecycle, OS DNS takeover, mapping refresh) lives in `application.go` as `DNSService` / `NewDNSService`, not here. |
| 131 | +- **`awlevent/`** — internal event bus on top of libp2p's eventbus. Events: `KnownPeerChanged`, `ReceivedAuthRequest`. Use `awlevent.WrapSubscriptionToCallback` to subscribe with a callback. |
| 132 | +- **`metrics/`** — Prometheus registry and custom AWL metrics; `StartBackgroundUpdater` is launched from `Application.Init`. |
| 133 | +- **`cli/`** — terminal CLI via `urfave/cli/v2` (`cli.go`, `me.go`, `peers.go`). Talks to a running daemon over the HTTP API via `api/apiclient`. Compiled into both `cmd/awl` and `cmd/awl-tray`. |
| 134 | +- **`protocol/`** — libp2p stream protocol IDs. Base path `/awl/0.3.0/`, methods: `auth/`, `status/`, `tunnel/`, `socks5/`, **`socks5-noauth/`**. Also the packet framing helpers (`AppendPacketToBuf`, `ReadUint64`). |
| 135 | +- **`update/`** — self-update client (via `GrigoryKrasnochub/updaterini`); drives `awl cli update` and the tray's "check for updates". |
| 136 | +- **`ringbuffer/`** — in-memory ring buffer used as a log sink so the API can return the last N log lines (`/api/v0/debug/log`). |
| 137 | +- **`entity/`** — shared DTOs between api/handlers and api/apiclient (keeps the two sides type-safe). |
| 138 | + |
| 139 | +### Config file |
| 140 | + |
| 141 | +`config_awl.json` lookup order (see `config/other.go::CalcAppDataDir`): |
| 142 | + |
| 143 | +1. `$AWL_DATA_DIR` if set. |
| 144 | +2. Directory of the running executable, **only if** `config_awl.json` already exists there. |
| 145 | +3. OS user config dir: `~/.config/anywherelan/` (Linux), `%AppData%\anywherelan\` (Windows), `~/Library/Application Support/anywherelan/` (macOS). |
| 146 | + |
| 147 | +The resolved path is printed at startup: |
| 148 | + |
| 149 | +``` |
| 150 | +INFO awl Initializing app in <path> directory |
| 151 | +``` |
| 152 | + |
| 153 | +`config.LoadConfig` falls back to `config.NewConfig` if the file is missing or unparseable, so the app always boots with a valid config. See the README for a populated example and field semantics. |
| 154 | + |
| 155 | +### Test infrastructure |
| 156 | + |
| 157 | +Tests spin up real `Application` instances with a mock TUN (`TestTUN`) that exposes `Outbound` / `Inbound` Go channels in place of a real interface: |
| 158 | + |
| 159 | +```go |
| 160 | +// Integration tests (real libp2p, real DHT, mock TUN) |
| 161 | +ts := NewTestSuite(t) |
| 162 | +peer := ts.NewTestPeer(false) |
| 163 | + |
| 164 | +// Perf tests (simulated network with controlled latency/bandwidth) |
| 165 | +ts := NewSimnetTestSuite(t) |
| 166 | +peer1, peer2 := ts.NewSimnetPeerPair(latency, bandwidthBps, socks5Conf1, socks5Conf2) |
| 167 | + |
| 168 | +// Injecting and observing packets via TestTUN |
| 169 | +peer.tun.Outbound <- packet // inject outbound |
| 170 | +count := peer.tun.InboundCount() // received on this peer |
| 171 | +``` |
| 172 | + |
| 173 | +Key files: |
| 174 | + |
| 175 | +- `test_suite_test.go` — test infrastructure, `NewTestSuite` / `NewTestPeer` / `TestTUN`. |
| 176 | +- `application_test.go` — integration tests (tunnel, auth flow, SOCKS5, DNS, API). |
| 177 | +- `application_simnet_test.go` — perf/simnet scenarios, gated on `AWL_PERF_TESTS=1`. |
| 178 | +- `api_handlers_test.go`, `cli_test.go` — HTTP API and CLI tests (both hit a real `Application`). |
| 179 | + |
| 180 | +## Code conventions |
| 181 | + |
| 182 | +- Config access: `sync.RWMutex` on `Config`, with `Lock`/`Unlock` / `RLock`/`RUnlock` always paired in the same function. Mutations call `Config.save()` while holding the lock. |
| 183 | +- Hot paths (tunnel packet forwarding, socks5) use non-blocking channel sends (`select` with `default`) to drop rather than back-pressure the caller. See `service/tunnel.go`. |
| 184 | +- Every package gets its own logger: `log.Logger("awl/<pkg>")` (ipfs/go-log/v2). Log levels for libp2p sub-loggers are tuned in `application.go::SetupLoggerAndConfig` (`swarm2`, `relay`, `connmgr`, `autonat` are pinned to warn). |
| 185 | +- Context plumbing: `Application.Init` stores a cancellable context on the app; long-running goroutines take `a.ctx` and exit on `Close()`. |
| 186 | +- Packet channels are sized per-peer, not global — see `service.Tunnel`. |
| 187 | + |
| 188 | +## Monitoring |
| 189 | + |
| 190 | +- Prometheus metrics: `http://localhost:8639/metrics` |
| 191 | +- pprof: `http://localhost:8639/api/v0/debug/pprof/` |
| 192 | +- Docker Compose stack with Grafana dashboards, Pyroscope, and Alloy in `monitoring/` — see `monitoring/README.md`. |
0 commit comments