diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml new file mode 100644 index 00000000..17d6a589 --- /dev/null +++ b/.github/workflows/installer.yml @@ -0,0 +1,98 @@ +name: Installer + +on: + push: + branches: [main] + paths: + - "install.sh" + - ".github/workflows/installer.yml" + pull_request: + branches: [main] + paths: + - "install.sh" + - ".github/workflows/installer.yml" + # Also catch new releases that break the installer. + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run shellcheck + run: shellcheck install.sh + + # Installs published releases on a real Ubuntu 24.04 VM with systemd, as a + # non-root user with sudo, the way most people will run it. + ubuntu: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install binary only + run: | + sh install.sh --no-service + nylon version + test ! -e /etc/systemd/system/nylon.service + - name: Re-run is a no-op + run: | + version=$(nylon version | sed 's/^Version: //') + sh install.sh --no-service 2>&1 | grep -Fx "==> nylon $version is already installed" + - name: Install service (default) without configs + run: | + # A pre-existing config dir must still end up root-only. + sudo mkdir -m 0755 /etc/nylon + sh install.sh + systemctl is-enabled nylon + test "$(systemctl is-active nylon)" = inactive + test "$(sudo stat -c %a /etc/nylon)" = 700 + - name: Start service with a single-node config + run: | + key=$(nylon key 2>pub) + printf 'id: ci\nkey: %s\nport: 57175\n' "$key" | sudo tee /etc/nylon/node.yaml >/dev/null + printf 'routers:\n - id: ci\n pubkey: %s\n addresses: [10.99.0.1]\n' "$(cat pub)" | sudo tee /etc/nylon/central.yaml >/dev/null + sudo nylon verify /etc/nylon/central.yaml --node /etc/nylon/node.yaml + sh install.sh 2>&1 | grep -Fx "==> started nylon.service" + # Same PID after a while, so it isn't crash-looping. + sleep 2 + pid=$(systemctl show -p MainPID --value nylon) + sleep 10 + test "$pid" != 0 + test "$(systemctl show -p MainPID --value nylon)" = "$pid" + ip -br addr | grep 10.99.0.1 + - name: Re-running leaves it running + run: | + pid=$(systemctl show -p MainPID --value nylon) + sh install.sh 2>&1 | grep -Fx "==> nylon.service is already running" + test "$(systemctl show -p MainPID --value nylon)" = "$pid" + - name: Changing version restarts the service + run: | + sh install.sh --version v0.4.5 2>&1 | grep -Fx "==> restarted nylon.service" + nylon version | grep -Fx "Version: v0.4.5" + sh install.sh 2>&1 | grep -Fx "==> restarted nylon.service" + sleep 2 + pid=$(systemctl show -p MainPID --value nylon) + sleep 10 + test "$pid" != 0 + test "$(systemctl show -p MainPID --value nylon)" = "$pid" + - name: Uninstall + run: | + sh install.sh --uninstall + test "$(systemctl is-active nylon)" != active + if ip link show nylon 2>/dev/null; then exit 1; fi + test ! -e /usr/local/bin/nylon + test ! -e /etc/systemd/system/nylon.service + sudo test -f /etc/nylon/node.yaml + - name: Leaves a hand-written unit alone + run: | + printf '[Service]\nExecStart=/bin/true\n' | sudo tee /etc/systemd/system/nylon.service >/dev/null + if sh install.sh; then exit 1; fi + sh install.sh --uninstall + sudo test -f /etc/systemd/system/nylon.service + - name: Logs + if: failure() + run: sudo journalctl -u nylon --no-pager diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cedf3f93..544f376d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,7 @@ jobs: goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} asset_name: nylon-${{ matrix.goos }}-${{ matrix.goarch }} + sha256sum: TRUE release-windows: name: Release Go Binary (Windows) # Windows requires the wintun driver, so we use a separate action runs-on: ubuntu-latest @@ -53,3 +54,14 @@ jobs: goarch: ${{ matrix.goarch }} extra_files: 'wintun/bin/${{ matrix.goarch }}/wintun.dll' asset_name: nylon-${{ matrix.goos }}-${{ matrix.goarch }} + sha256sum: TRUE + release-installer: + name: Attach install.sh + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Upload install.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" install.sh --clobber diff --git a/README.md b/README.md index 19c8df60..f7bcfc2a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,15 @@ Nylon targets under 10 seconds of convergence time after a link failure, as you ## Getting Started -Download the latest release binary from the [releases page](https://github.com/encodeous/nylon/releases), then head to the [docs](https://nylon.jq.ax) for setup instructions. +Install the latest release on Linux, along with a systemd service that runs it with the configs in `/etc/nylon`: + +```bash +curl -fsSL https://raw.githubusercontent.com/encodeous/nylon/main/install.sh | sh +``` + +You can also download binaries directly from the [releases page](https://github.com/encodeous/nylon/releases). + +Then head to the [docs](https://nylon.jq.ax) for setup instructions. > **[Read the full documentation at nylon.jq.ax](https://nylon.jq.ax)** > includes configuration reference, guides for connecting WireGuard clients, port forwarding, and comparisons with Tailscale/Nebula. diff --git a/docs/guides/getting-started.mdx b/docs/guides/getting-started.mdx index 625eba5c..3edb4314 100644 --- a/docs/guides/getting-started.mdx +++ b/docs/guides/getting-started.mdx @@ -17,7 +17,13 @@ graph LR ## Prerequisites - Two machines with UDP port `57175` open (can configure with a different port). -- The `nylon` binary downloaded on both machines from the [releases page](https://github.com/encodeous/nylon/releases). +- The `nylon` binary installed on both machines. On Linux: + + ```bash + curl -fsSL https://raw.githubusercontent.com/encodeous/nylon/main/install.sh | sh + ``` + + On other platforms, download it from the [releases page](https://github.com/encodeous/nylon/releases). :::note The Linux and macOS versions are well tested, but the Windows TUN interface has known stability issues. For Windows, we recommend using [WireGuard for Windows](https://www.wireguard.com/install/) and connecting to a Linux/macOS machine as a [passive client](/guides/wg-clients). @@ -96,6 +102,17 @@ The Linux and macOS versions are well tested, but the Windows TUN interface has After a few seconds, the nodes will discover each other and establish a secure tunnel. You should be able to ping `10.0.0.2` from `node-1`. + :::tip[Running as a service] + On Linux with systemd, the installer also set nylon up as a service. To run it that way instead, stop the nylon you started above (Ctrl+C), then run: + + ```bash + sudo mv node.yaml central.yaml /etc/nylon/ + sudo systemctl start nylon + ``` + + The service reads its configs from `/etc/nylon` (readable only by root) and starts on boot. Follow its logs with `journalctl -u nylon -f`. Re-running the installer upgrades nylon and restarts the service if it's running. + ::: + ## Next Steps diff --git a/example/nylon.service b/example/nylon.service index d0cbd1cf..ecabf6ec 100644 --- a/example/nylon.service +++ b/example/nylon.service @@ -1,10 +1,15 @@ +# Sample unit; install.sh --service installs an equivalent one. [Unit] -Description=Nylon Service +Description=nylon mesh network +Documentation=https://nylon.jq.ax After=network-online.target +Wants=network-online.target [Service] -ExecStart=/usr/bin/nylon run -WorkingDirectory=/home/encodeous +ExecStart=/usr/local/bin/nylon run -c /etc/nylon/central.yaml -n /etc/nylon/node.yaml +WorkingDirectory=/etc/nylon +Restart=on-failure +RestartSec=5 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..91335e9b --- /dev/null +++ b/install.sh @@ -0,0 +1,228 @@ +#!/bin/sh +# nylon installer +# Installs the latest nylon release to /usr/local/bin as well as a systemd +# unit to enable nylon as a service. opt out with --no-service +# Re-running upgrades nylon and restarts the service. Tested on Ubuntu 24.04. + +set -eu + +REPO="encodeous/nylon" +BIN="/usr/local/bin/nylon" +CONFIG_DIR="/etc/nylon" +UNIT="/etc/systemd/system/nylon.service" + +usage() { + cat <] [--no-service] [--uninstall] + + --version Install a specific release, e.g. v0.4.6 (default: latest) + --no-service Don't install the systemd service that runs nylon with the + configs in $CONFIG_DIR (installed by default) + --uninstall Remove the service and binary (keeps $CONFIG_DIR) +EOF +} + +info() { echo "==> $*" >&2; } +die() { + echo "error: $*" >&2 + exit 1 +} + +as_root() { + if [ "$(id -u)" = 0 ]; then + "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + die "need root to run: $*" + fi +} + +detect_arch() { + case "$(uname -m)" in + x86_64 | amd64) ARCH=amd64 ;; + aarch64 | arm64) ARCH=arm64 ;; + i386 | i686) ARCH=386 ;; + *) die "unsupported architecture: $(uname -m)" ;; + esac +} + +# resolves latest to a tag +resolve_version() { + case "$VERSION" in + latest) + VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/$REPO/releases/latest" | sed -n 's#.*/tag/##p')" + [ -n "$VERSION" ] || die "could not determine the latest release" + ;; + v*) ;; + *) VERSION="v$VERSION" ;; + esac +} + +installed_version() { + if [ -x "$BIN" ]; then + "$BIN" version 2>/dev/null | sed -n 's/^Version: //p' + fi +} + +download_and_install() { + asset="nylon-linux-$ARCH.tar.gz" + base="https://github.com/$REPO/releases/download/$VERSION" + + info "downloading $asset ($VERSION)" + curl -fsSL -o "$TMP/$asset" "$base/$asset" || die "failed to download $base/$asset" + + # check md5 for releases predating this pr (they didn't publish sha256) + if curl -fsSL -o "$TMP/sum" "$base/$asset.sha256" 2>/dev/null; then + actual="$(sha256sum "$TMP/$asset" | cut -d' ' -f1)" + elif curl -fsSL -o "$TMP/sum" "$base/$asset.md5" 2>/dev/null; then + actual="$(md5sum "$TMP/$asset" | cut -d' ' -f1)" + else + die "no checksum published for $asset; refusing to install" + fi + [ "$(cut -d' ' -f1 <"$TMP/sum")" = "$actual" ] || die "checksum mismatch for $asset" + + tar -xzf "$TMP/$asset" -C "$TMP" nylon + "$TMP/nylon" version >/dev/null 2>&1 || + die "the downloaded binary does not run on this system (if $(dirname "$TMP") is mounted noexec, set TMPDIR to another directory)" + + # atomic replace + as_root install -m 0755 "$TMP/nylon" "$BIN.new" + as_root mv -f "$BIN.new" "$BIN" + info "installed $BIN ($VERSION)" +} + +MARKER="# Managed by nylon's install.sh" + +service_running() { + systemctl is-active --quiet nylon +} + +# unit is only managed if it was installed by this script +managed_unit() { + grep -qF "$MARKER" "$UNIT" 2>/dev/null +} + +install_service() { + cat >"$TMP/unit" <