# gen-rescue-netscript

Generates a shell script to configure a rescue system's network according
to the current network configuration of the running system.

`gen-rescue-netscript` inspects a running Linux system's network
configuration via `ip -json` and emits a standalone shell script that
recreates that configuration.  This is intended for situations such as
booting a rescue image that needs to bring a box back onto its usual
network without DHCP being available or desired there.

The generated script itself has no dependency on `gen-rescue-netscript` or
Python It is only a POSIX shell using common command line tools.

## Features

- **Scoped to one interface.** By default, only the interface carrying the
  default route is analyzed - considering both IPv4 and IPv6. If they
  disagree, a manual override is necessary.
- **Sensible address filtering.** Keeps static and DHCP addresses. DHCP
  addresses are pinned as static so that the rescue system does not need
  a DHCP lcient. SLAAC-derived, privacy ("temporary") IPv6 addresses, and
  the kernel's auto-generated EUI-64 link-local addresses are dropped.
- **Static routes to off-link networks are kept.**
- **Writes `/etc/resolv.conf`,** including the systemd-resolved-under-
  `/run` symlink case. Nameservers are obtained by `resolvectl` before
  falling back to the current `/etc/resolv.conf`, before falling back to
  `1.1.1.1`.
- **Generated script is idempotent and editable.** Uses `ip addr/route
  replace` (create-or-update), with a defensive `add` retry if `replace`
  somehow doesn't create a missing object. Addresses, gateways, and
  nameservers are space-separated shell variables at the top of the
  script, so a 3am hand-edit means changing one line, not hunting
  through several.
- **Small set of well-known dependencies.** Geared to use only stdlib
  python modules, suitable for a minimal rescue system. The *generated*
  rescue script itself has no dependency on either - just a POSIX shell and
  the common Linux command line utils

## Installation

```console
$ pip install .
```

or, for editable development:

```console
$ pip install -e '.[dev]'
```

This installs the `gen-rescue-netscript` console script. A Unix man page is
provided at `man/gen-rescue-netscript.1`.

No installation is also an option — the tool is a single self-contained
module and can be run directly:

```console
$ PYTHONPATH=src python3 -m gen_rescue_netscript
```

## Usage

```console
$ gen-rescue-netscript                        # print the rescue script to stdout
$ gen-rescue-netscript --output rescue-net.sh # write it to a file
$ gen-rescue-netscript --efi                  # write it to the EFI partition, as rescue-net/network.sh
$ gen-rescue-netscript --efi --output boot/net.sh  # ...or a custom path on that partition
$ gen-rescue-netscript --interface eth1       # configure eth1 regardless of routing
$ gen-rescue-netscript --prefer ipv4          # prefer IPv4's default-route interface if v4/v6 disagree
```

See `gen-rescue-netscript(1)` for full option and behavior documentation.

### Configuration file

`/etc/gen-rescue-netscript.conf` is an optional ini-style file with a
single section [gen-rescue-netscript] (parsed with configparser). It
currently supports these keys (all optional):

```ini
[gen-rescue-netscript]
interface=eth0
outputrescue-net/network.sh
prefer=ipv6
efi=False
```

`interface` is the default-route override - `--interface` on the command
line takes priority over it. `output` is the default for `--output` when
combined with `--efi`. `prefer` sets which family's default route wins when
IPv4 and IPv6 disagree on the interface (`ipv4` or `ipv6`; default `ipv6`).

### `--efi`

Looks for a partition whose filesystem label or GPT partition label is
`EFI` or `ESP` (or all lower-case versions. Filesystem label checked first,
then partition label), mounts it to a temporary mountpoint (if it isn't
already mounted), and writes the script there. `--output` composes with
`--efi` rather than conflicting with it: without `--efi` it's an absolute
output path; with `--efi` it's relative to that partition's root,
defaulting to `rescue-net/network.sh` (or the config file's `output` key)
if not given.

## Example output

Run the plain script without root privileges and see what it generates.

## Future work

The following things should be there but are not implemented yet. Patches
and Merge Requests appreciated.

- **better tests.** The existing tests are kind of rudimentary.
- **VLANS.** Useful if management happens over a tagged VLAN.
- **Wifi.** Useful if connection to a Wifi network is needed for
  management.
- **network_config should be a dataclass.** Currently, it's a plain dict.
  Things would be cleaner if that also was a dataclass.
- **fec0:0:0:ffff::1 nameservers.** It might make sense to add
  those well-known local IPv6 nameservers in addition to 1.1.1.1 as the
  final fallback when generating resolv.conf. in that case, the query timeout
  should be lowered as well.
- **nameservers set from config or command line.** If the local system
  uses a local resolver that is not systemd-resolved, the generated
  `resolv.conf` will point to the local resolver that won't be running
  in the rescue system. A --nameserver command line option and config file
  option would be helpful.
- **IP policy routing (`ip rule`).** This might be important for IPv6
  source address selection, but probably not if you just want ssh access.
  TCP always answers from the IP address that was accessed by the client.
- **Multiple default routes.** It might make sense to take multiple default
  routes into the generated script (for example, if they use different
  metrics). If you have such a setup, please consider implementing this
  and testing the behavior.
- **Multiple interfaces.** The Interface dict is already keyed by interface
  name (`Dict[str, List[Address]]`) rather than being a flat list,
  specifically so that a future version (or a separate network-config module
  built from the dataclasses here — see "Design notes") could populate and
  emit more than one interface without a data-model rework. Today, exactly
  one key is ever populated.

## Requirements

- `ip(8)` with `-json` support (iproute2 ≥ 4.9, released 2017).
- Python ≥ 3.8 with stdlib (argparse, configparser, ipaddress, json,
  os, socket, subprocess, sys, dataclasses, datetime, pathlib, typing)
- `--efi` additionally requires `blkid(8)` and `mount(8)`/`umount(8)`.
- `resolvectl(1)` is used opportunistically if present. Its absence
  is not an error, falling back to reading `/etc/resolv.conf`.
  Only its plain-text output is parsed — `resolvectl --json=` exists on
  recent systemd, but is too new to be reliably present across an
  arbitrary fleet.

## Development

```console
$ pip install -e '.[dev]'
$ pytest
$ pylint src/gen_rescue_netscript tests
$ vulture src/gen_rescue_netscript/__init__.py --min-confidence 60
```

The test suite stubs all network-affecting subprocess calls (`ip`,
`resolvectl`, `blkid`, `mount`, `umount`) via monkeypatching, so it
doesn't require root, real block devices, or a real network stack to run.

## Design notes

- **Why filter SLAAC but keep DHCP addresses?** SLAAC-derived
  addresses (and their privacy-extension siblings) regenerate on their
  own from router advertisements; pinning them as static config would
  just create stale entries once they roll over. DHCP leases won't
  regenerate in a rescue environment without a DHCP client, so
  they're pinned.
- **Why distinguish auto vs. explicit link-local addresses?** The kernel
  always assigns exactly one link-local address on interface bring-up.
  Anything else present is assumed to have been added on purpose. Interfaces
  using RFC 7217 stable-privacy link-local generation won't match the EUI-64
  computation and will be kept as if explicit.
- **Why is this all one file?** Deliberately — a single self-contained
  module is easy to vendor or run without installing anything. The
  dataclass definition near the top are kept free of CLI- or
  script-emission-specific coupling on purpose, so they'd be reasonable to
  lift into a standalone network-config module later if a need for one ever
  comes up

## Copyright / License

Copyright (C) 2026 by Marc Haber <mh+debian-packages@zugschlus.de>

GPL-2.0-or-later. See [`LICENSE`](LICENSE).
