Configures a Keenetic router: SSH access, package installation, cron jobs, and optional xray/TProxy and network-boot (PXE) deployments.
Targets Entware on KeeneticOS. ansible_python_interpreter must point at
/opt/bin/python3, and the role bootstraps that interpreter over raw on first
contact if it is missing.
Network boot (pxe.yml) is opt-in via keenetic_pxe_enabled, false by
default, so a first-time run does not suddenly stand up a TFTP and HTTP
server.
- name: keenetic
hosts: keenetic
ignore_unreachable: true
gather_facts: true
roles:
- role: keenetic
tags:
- keenetic| Variable | Description | Example |
|---|---|---|
keenetic_user |
Root user config: name, password, authorized_ssh_keys (public key basenames under ~/.ssh/ on the controller) |
Definition example in defaults/main.yml |
keenetic_cron_jobs |
Cron jobs to install via ansible.builtin.cron |
Definition example in defaults/main.yml |
keenetic_packages |
opkg packages installed unconditionally | Definition example in defaults/main.yml |
keenetic_repo_path |
Directory the custom opkg repo .conf files are written to |
/opt/etc/opkg |
keenetic_repos |
Extra opkg repos to add: name, src, packages |
Definition example in defaults/main.yml |
keenetic_xray_enabled |
Whether this role manages xray at all (installs/updates config and binary) | false |
keenetic_xray_service_state |
started | stopped -- whether xray should actually be running, distinct from keenetic_xray_enabled |
started |
keenetic_xray_version |
xray version pinned for this router; compared against the installed binary for equality, not substring | 26.7.28 |
keenetic_xray |
Paths, TProxy port, fwmark, routing table, LAN interface, log file, corp dummy range and download URL | Definition example in defaults/main.yml |
keenetic_xray_config_src |
Path on the controller to the rendered client profile copied to the router | {{ inventory_dir }}/files/clients/{{ keenetic_xray_server_name }}-{{ inventory_hostname }}.json |
keenetic_xray_server_name |
Name of the xray server host this router's client profile was generated against | my-vps |
keenetic_pxe_enabled |
Whether this role manages the PXE daemons at all | false |
keenetic_pxe_service_state |
started | stopped — whether the PXE daemons should be running, distinct from keenetic_pxe_enabled |
started |
keenetic_pxe |
Paths, LAN interface, HTTP port, bootfile name and the optional tftpd remap file | Definition example in defaults/main.yml |
keenetic_pxe_next_server |
Address handed to clients and bound by both daemons; defaults to the keenetic_pxe.lan_iface address |
192.168.1.1 |
keenetic_pxe_dhcp_pool |
KeeneticOS DHCP pool that receives the boot fields; empty string leaves the router's DHCP configuration untouched | _WEBADMIN |
keenetic_pxe_loaders |
iPXE binaries staged into the TFTP root: name, url, optional checksum |
Definition example in defaults/main.yml |
keenetic_pxe_menu |
Boot menu entries rendered into autoexec.ipxe: id, label, key, kernel, initrd, optional args |
Definition example in defaults/main.yml |
keenetic_pxe_images |
Boot payloads staged into keenetic_pxe.http_root: name (may nest), url, checksum |
Definition example in defaults/main.yml |
keenetic_user.authorized_ssh_keys names public keys under ~/.ssh on the
controller, without the .pub suffix — tasks/connect.yml
appends it. id_ed25519 reads ~/.ssh/id_ed25519.pub.
All 28 rows — including the two ansible_* connection variables the role rewrites
| Fact | Description |
|---|---|
keenetic_ssh_port |
Set by connect.yml when the router is reached on a non-default port; used to render dropbear.j2 and to move ansible_port |
keenetic_default_ssh_port |
Result of the port-22 probe, used to decide whether the router is still on the stock port |
keenetic_custom_ssh_port |
Result of the keenetic_ssh_port probe, used to decide whether ansible_port may move onto it |
keenetic_check_connection_result |
Result of the initial ping, used to pick the bootstrap credentials |
keenetic_controller_ssh_keys |
Public key material read off the controller, rendered into authorized_keys.j2 |
keenetic_python3_bootstrap |
Result of the raw python3 bootstrap, used only for its changed_when |
keenetic_root_password |
keenetic_user.password, held no_log for the user module to hash |
keenetic_update_result |
Result of opkg update in the repos | update cache handler, which tolerates rc 1 |
keenetic_opkg_architecture |
Raw opkg print-architecture output; authoritative because busybox uname -m cannot tell mips from mipsel |
keenetic_xray_arch |
Derived from the above; used to build the xray download URL in keenetic_xray.repo |
keenetic_xt_tproxy |
Whether the firmware ships xt_TPROXY.ko for the running kernel |
keenetic_rci_http |
The /rci/ip/http response the port-443 precondition is read from |
keenetic_https_on_443 |
Whether the web UI still holds 443, which TProxy needs free |
keenetic_xray_installed / keenetic_xray_installed_version |
xray version output and the version token extracted from it |
keenetic_xray_stage / keenetic_xray_download |
Controller-side staging directory and release download |
keenetic_xray_profile |
Whether keenetic_xray_config_src has been generated yet |
keenetic_xray_status |
S24xray status output, compared against keenetic_xray_service_state |
keenetic_pxe_running_config |
ndmc -c "show running-config" output, the boot fields are diffed against it |
keenetic_pxe_pool_block |
The single ip dhcp pool block extracted from the above, so a sibling pool's settings cannot be mistaken for this one's |
keenetic_pxe_boot_fields_write |
Result of the DHCP boot-field write loop, used to gate the post-write verification below on whether anything actually changed |
keenetic_pxe_running_config_after |
ndmc -c "show running-config" re-read after a boot-field write, to verify the write actually took |
keenetic_pxe_pool_block_after |
The pool block re-extracted from the above, asserted to contain all four boot fields |
keenetic_pxe_tftpd_status |
S59tftpd status output, compared against keenetic_pxe_service_state |
keenetic_pxe_loader_download |
Result of the iPXE loader downloads, used for its until retry |
keenetic_pxe_httpd_status |
S82pxehttpd status output, compared against keenetic_pxe_service_state |
keenetic_pxe_image_download |
Result of the boot image downloads, used for its until retry |
ansible_port |
Rewritten to 22 while bootstrapping, then to keenetic_ssh_port once dropbear has moved |
ansible_password |
Rewritten to the stock password when the first connection is refused |
| Tag | Purpose |
|---|---|
keenetic.cron |
Cron job management |
keenetic.packages |
Base opkg package installation |
keenetic.repo |
Custom opkg repo setup |
keenetic.ssh |
Dropbear config, authorized_keys, ssh port detection/change |
keenetic.user |
Root password and connection bootstrap |
keenetic.xray |
Preflight checks and xray install/config/service state |
keenetic.pxe |
TFTP and HTTP boot services, and the router's DHCP boot fields |
detect.yml carries all seven tags, so any single tag still runs the fact
gathering it depends on — preflight.yml reads ansible_facts.kernel for the
xt_TPROXY path, which is why keenetic.xray is in that list too.
connect.yml carries only keenetic.ssh and keenetic.user. A keenetic.cron,
keenetic.packages, keenetic.repo, keenetic.xray or keenetic.pxe run
therefore does not re-run the connection bootstrap, and relies on
ansible_port and the credentials in inventory already being correct for the
router as it stands.
That is deliberate: the bootstrap changes the root password and rewrites
dropbear's config, which no other tag should imply.
keenetic.xray additionally requires keenetic_xray_enabled: true — the
preflight and xray includes are gated on it, so a tagged run against a host
with it false correctly does nothing.
Tags do not discriminate within install.yml. Taggable.tags is
extend=True, so its tasks inherit both of the include's tags on top of their
own, and --tags keenetic.packages and --tags keenetic.repo each run the whole
file. This is the same shape as flyoverhead.server's packages include and is
left alone for consistency with it; the per-task tags there document intent
rather than gate execution.
- Stop xray with
keenetic_xray_service_state: stopped, never a bareS24xray stop. The service state is written to a flag file that survives reboots, deploys and the geofile cron; stopping it out-of-band leaves that flag out of sync and a later restart handler can bring xray back up unexpectedly. - While xray is running, the router proxies its entire LAN via tproxy, so taking it down (or leaving it down when it should be up) affects every client on the network, not just this host.
preflight.ymlmust run beforexray.yml-- the xray download URL depends on thekeenetic_xray_archfactpreflight.ymlsets -- and both must run afterinstall.yml, which suppliesca-certificates(needed by xray'sget_url),iptablesandip(needed by the netfilter hook).xray.ymlwill not render a client profile for you. It assertskeenetic_xray_config_srcexists and tells you the exact command to generate it, which needs both--tags xray.clients,xray.tuningagainst the xray server host.- Stop the PXE daemons with
keenetic_pxe_service_state: stopped, never a bareS59tftpd stop. Both init scripts read one flag file underkeenetic_pxe.conf_dir; stopping either out-of-band leaves that flag out of sync and the next deploy or reboot brings the daemon back. - Neither
keenetic_pxe_service_state: stoppednorkeenetic_pxe_enabled: falseun-configures the router's DHCP pool -- the first only stops the two daemons, and the second skips the whole file, including the pool write. Left alone, the pool keeps advertising anext-server/bootfilefor a TFTP server that is no longer answering, and every PXE-capable client on the LAN stalls for its TFTP timeout at each cold boot. This is the same shape as xray, which likewise does not uninstall itself when disabled, but it is worth writing down because the symptom lands on every client on the network, not just this host. To actually remove the boot fields:ndmc -c "no ip dhcp pool <pool> bootfile" ndmc -c "no ip dhcp pool <pool> next-server" ndmc -c "no ip dhcp pool <pool> option 66" ndmc -c "no ip dhcp pool <pool> option 67" ndmc -c "system configuration save" dnsmasq-fullis inkeenetic_packageson every host, it ships/opt/etc/init.d/S56dnsmasqwithENABLED=yes, and its packageddnsmasq.confis entirely comments -- so a running instance answers DNS on :53 besidendnsproxy. This role does not manage it. Check/opt/etc/init.d/S56dnsmasq checkbefore assuming the router's DNS path is what you think it is, and do not build PXE on that daemon: withoutport=0and with anydhcp-range, it competes with the router's own DHCP server for the whole LAN.- The
lighttpdpackage likewise shipsS80lighttpdwithENABLED=yesand a config that sets noserver.port, so a stock instance binds :80 -- the web UI's port.pxe.ymlsetsENABLED=nothere and runs its own instance fromkeenetic_pxe.conf_dir/httpd.conf. Do not re-enable it. next-serverandbootfileare the fields that make network boot work, not options 66/67. Many PXE option ROMs read the BOOTPsiaddr/fileheader fields and ignore option 66 entirely, and KeeneticOS populates only what it is asked for. The role sets all four.- Router CLI changes made through
ndmclive in the running configuration only. Thepxe | save router configurationhandler runssystem configuration save; if it is skipped, the boot fields survive until the next reboot and then silently vanish. - One
bootfileper DHCP pool means one client architecture. The default is UEFI x64 (ipxe.efi).undionly.kpxeis staged alongside it for a future option-60/user-class DHCP change, not sokeenetic_pxe.bootfilecan be repointed at it today. The loop-breaking mechanism this design relies on -- iPXE fetchingautoexec.ipxefrom the TFTP server it just booted from, before re-requesting DHCP -- only exists in iPXE's EFI build.undionly.kpxehas no equivalent, so pointingbootfileat it produces the exact infinite chainload loop this design exists to avoid. Serving both architectures needs DHCP class matching on option 60, which is untested on KeeneticOS. - TFTP and the image HTTP server bind
keenetic_pxe_next_server, which defaults to thebr0address. Neither is authenticated. On a router whose LAN bridge carries more than one address, set the variable explicitly rather than letting it pick. - Both the bind address in
S59tftpd/pxe-httpd.conf.j2and the advertisednext-serverin the DHCP pool are baked in at deploy time fromkeenetic_pxe_next_server. Renumbering the LAN from the KeeneticOS web UI changes neither -- re-run this role afterwards, or both daemons keep listening on the old address and the pool keeps pointing clients at it. - Both roots live under
/opt, the external drive. An unplugged stick meansS59tftpdandS82pxehttpdrefuse to start rather than serving an empty tree -- deliberately loud. pxe.yml's DHCP boot-field write is only as idempotent asndmc's own rendering. It compares eachnext-server/bootfile/option 66/option 67 value againstshow running-configassuming that output is unquoted and sits directly after the verb (option 66 ascii 192.168.1.1); if a firmware version quotes option values instead, those two loop items will reportchangedon every run even though the router already has them set.
Check mode — what --check --diff covers, the ten probes that opt out of it, and two things it cannot tell you
--check --diff reports drift in dropbear.conf, authorized_keys, the opkg
repo files, the cron jobs, the xray config, the netfilter hook and S24xray.
Ten probes carry check_mode: false, because they only read and later tasks
branch on their output. Left to be skipped, each would fabricate a result that
reads as a definite answer rather than "unknown":
- The two
wait_forport checks in tasks/connect.yml.wait_fordeclares no check mode support, and thewhenbeneath each reads.msg | default(""), so a skip resolves to "the port answered" — forcingansible_portto 22 on a router whose sshd is elsewhere. opkg print-architectureand the/rci/ip/httpGET in tasks/preflight.yml.commandfabricates rc 0 with empty stdout under--check, which makeskeenetic_xray_archresolve tounknown;uriis skipped outright before the module runs, which would silentlywhen-skip the port-443 assertion and report a green play with its single most consequential precondition unchecked.xray versionandS24xray statusin tasks/xray.yml, which otherwise read as "nothing installed" and "not running" and make every dry run claim a binary install and a service change.- Two
ndmc -c "show running-config"reads,S59tftpd statusandS82pxehttpd statusin tasks/pxe.yml.commandfabricates rc 0 with empty stdout under--check; for the initial running-config read that would fail the dhcp-pool assertion on every dry run, even against a router that is already configured correctly, for the re-read (after boot-field write) that would fail the boot-field verification assertion, and for the two service-status reads it reads as "not running", making both converge tasks claim a change on every dry run.
Two things a check run cannot tell you:
- It does not work against a router that has not been bootstrapped yet.
The python3 bootstrap is
ansible.builtin.raw, which is skipped under--check, and every module task after it needs the interpreter that task installs. Run the role for real once first. - The service state is reported, not converged.
xray | set the administrative service stateusestouch, which reportschangedon every real run by design and so carrieschanged_when: false; the flag file therefore appears unchanged in a check run whatever the declared state is.pxe | set the administrative service statehas the identical property. Readxray | converge the service to its declared state(or, for PXE,pxe | converge tftpd to its declared state/pxe | converge httpd to its declared state) instead — those are the tasks that would act.
The preflight assertions do run, so a check against a bootstrapped aarch64
router is a genuine way to verify the xt_TPROXY and port-443 preconditions
without touching anything.
GPL-3.0-only
fLy0v3rH34d