Who needs social media when you can dd if=/dev/brainfart of=/dev/blograndom !?
Created ยท Updated
2026-09-13
arachnopress 1.0.65
Introducing LCARS-inspired palette themes
Thanks to the fine work of Matthew Leonawicz in the trekcolours palettes (MIT licensed)
Added LCARS_2357, LCARS_2369, LCARS_2375, and LCARS_2379 variants
Mapped RGB values to interface and syntax roles, added filled LCARS chrome, and tuned background opacity
The mappings need a bit more fine-tuning in some instances, but adding these has introduced a couple of useful CSS features that can be used
to tune existing themes
There are many additional trekcolours palettes available, I'll look at integrating more in future versions
2026-09-11
VLC/tvOS samba config
Apparently VLC really doesn't like transitioning from a plain SMB session using
server smb encrypt = if_required in [global] to an encrypted SMB share using
server smb encrypt = required in [private].
The work-around for this is to use desired in [global] and continue using required in
[private], so encryption is all or nothing and there's no renegotiation.
Now my LG Smart TV
doesn't know how obsessed with 90s Star Trek I am (technically anything ≤2005, beyond that just isn't Star Trek. Also, the TV probably still knows!)
- which reminds me, I must add an LCARS theme to arachnopress.
I've listened to the Malazan Book of the Fallen audiobooks on a few
devices, and annoyingly they have small embedded 20-25 minute sections rather than full chapter breaks, let alone chapter titles - I think this
is because they are quite old and there were chapter length limitations in some audiobook platforms back in the 2010s.
Additionally to that, many audio players (including Rockbox) don't even support the embedded section markers.
I decided to fix it with CUE files, which are supported by Rockbox and many enthusiast DAP devices, but bumped into a couple of other issues along the way.
Limitations (Rockbox, but may apply to other devices)
Supports <99 CUE sections per file, the Malazan audiobooks are massive and all contain >100 sections
AAC-HE (High-Efficiency) codec misinterprets some timing table entries, causing incorrect FF/RW seeking and jumps to CUE chapter positions
32-bit timing range limitation, high sample-rate means lower permitted maximum playback duration
Fixes / Work-arounds
Chapter counts are closer to ~24, and the (>100) section boundries always have a boundry that starts where a chapter begins
Used the Audnexus API to find real Section/Chapter times and titles for the 10 Malazan audiobooks
Matched each section/chapter timestamp against the already embedded sections breaks (extracted with ffprobe)
Created a ".chapters.txt" mapping file for each audiobook with chapter names and the index number of the embedded start section
Created m4b2cue.sh which builds a CUE file (Rockbox compatible) for each audiobook assisted by the mapping files
The AAC-LC (Low-Complexity) codec doesn't suffer from the timing table entry issue
Created m4b2aac.sh which uses ffmpeg to transcode the original AAC-HE files to AAC-LC
Parallel processing was implemented because transcoding is bound to a single core, running 10 audiobooks in parallel took my M2 Max macbook about 40 minutes
with fans running full speed!
Ensured album-art and existing metadata was preserved in the transcoded files
Added options for bitrate and channels, with sane defaults if unset.
m4b2aac.sh uses a fixed 24kHz sample rate, which keeps below the 32-bit timing range limitation, providing a max of ~49.7 hours
After dropping the CUE files alongside the AAC-LC .m4b files and enabling the CUE file support setting in the Rockbox menu,
everything worked perfectly with accurate FF/RW, chapter navigation, and chapter titles. I've provided the finished CUE files here,
and included the chapter mapping files just in case they might be useful.
I'll update this to include Novels of the Malazan Empire as soon as I purchase them from libro.fm.
Downloadmalazan_cue_chapters.zipReady to use CUE files for Malazan Book of the Fallen12 KiBSHA256 (rockbox/malazan_cue_chapters.zip) = aa9531e0635b3626f9bf82e1ec7a00edd548444a44bbb706a194163ac23a8abf
The next task is too see what can be fixed in the Rockbox source to remove these limitations . . .
Added to scripts article
These are POSIX sh scripts, and should be good templates for further (parallel) ffmpeg/ffprobe conversion/generator scripts.
Requirements are ffmpeg/ffprobe. m4b2cue.sh makes substantial use of jq.
m4b2aac.sh - Parallel batch Conversion of m4a/m4b to AAC-LC
m4b2cue.sh - Extract CUE file from m4a/m4b, optionally guided by Chapter metadata files
2026-09-06
Innioasis Y1 Rockbox
Innioasis Y1Y1 running Rockbox (AA for scale)rawโ
The Innioasis Y1 is a relatively cheap android DAP, which seems to be based on
mobile-type hardware - Importantly it has a relatively good speaker, which I believe is the sound-dampening ball filled type.
I bumped into it online whilst looking for a replacement Rockbox capable DAP to replace my ancient Sandisk Sansa Clip. I was also
looking for something with an internal speaker, as I plan to use it for listening to audiobooks whilst going to sleep.
Flashing to Rockbox using rockbox-y1/rockbox was a painless process via the
Mediatek Flash and Repair Utility - although I did end up doing it on debian, as
macOS's raw USB access needed elevated privileges that I didn't want to provide (certainly not via sudo and a bunch of Python PyPI packages!)
The original firmware was pretty terrible in terms of sleep-timer configuration and fast-forward/rewind acceleration. Rockbox
has both of these (and most everything else!) tuned/configurable to perfection - I have automatic resume and sleep timer enabled on boot, as well
as bookmarking features.
The touch wheel works surprisingly well, and it looks like it can run Quake! Although, right now this is a dedicated
Malazan Book of the Fallen device.
I recommend libro.fm for all your DRM-free audiobook needs.
2026-09-05
arachnopress 1.0.64
Enhancements to create/modify date handling
Added optional data-created and data-modified attributes to h2/h3 tags
Dates conditionally roll-up to parents and are used for better Atom feed ordering and article index sorting
A rule-based comparison mechanism is used for optimal handling of create/modify dates if/where provided
2026-09-04
arachnopress 1.0.63
Enhanced Atom feed with data-feed-sections article attribute values:
none (or unset), does not include article
h1, article title, plain-text summary, link to article
h2, "article title - section heading", link to section
h3, "article title - section heading", link to sub-section(s)
2026-09-03
arachnopress 1.0.62
Refined Atom 1.0 Feed support
โ๏ธ button now has the correct mime-type (set for .atom files, and configured in httpd.conf example)
2026-09-02
arachnopress 1.0.61
Atom 1.0 Feed support added!
Article index browser-native tooltips with summary text and metadata
Article Creation/Updated dates now have default values and "auto" option
Theme mode selector configurable at build: off/exact/auto/light/dark
2026-08-30
Config files article
Added a config files article, as an archive for myself and maybe useful to a passing wanderer.
I think the universe wants me to join the drooling mass of smooth grey goo that is all TikTok and Meta-platform users,
but I intend to persist into the next aeon by becoming such an obscenely supermassive black hole that, after a
googol years of Hawking away on the cosmic back burner, my final evaporation crosses the conformal boundary as
a Hawking point - thus remaining, in the weakest possible sense, available for the next universe.
ngircd.conf - Chrooted ngIRCd IRC server config for OpenBSD, became redundant.
smb.conf - Samba config for MacOS/iPadOS/iOS and MS Windows clients on OpenBSD/FFS2
Finally gave up struggling to read small-print . . .
Porsche Design Reading Tool P8801
Weight: ~16 g
Lens width: 48 mm
Lens height: ~21-22 mm
Bridge: ~21 mm
Temple: 150 mm
Overall width: ~126 mm
Front: stainless steel
Temples: high-tech polyamide / RXP
Lenses: polycarbonate (AR/UV)
Finding a decent pair of half-moon reading glasses that I can leave sat on the end of my nose all day turned out to be quite difficult
until I found the Porsche Design "Reading Tool". I had to give up on my idea of actual half-moon lenses and gold plated frames for
reading cosmic horror by candle light, but it still turned out well.
Apparently Porsche have been making this exact design for the last 20 years, which is pretty weird considering I could only find two
videos of them on the less cosmic but certainly horrific YouTube. I can see why they stuck with it though, my first time needing
glasses and I don't even notice they're there - I can actually read the tiny date window on my Seiko now, after forgetting it even had one!
2026-08-08
arachnopress 1.0.58
Image blocks needed a few enhancements, mainly alignment issues
Very pleased with it, this was my first time adjusting a pin & collar bracelet, and I think the easiest so far to size -
Lower end split pins need too much force (link tool / hammer) and higher end screw bar/tube systems
always bother me due to tightening force and damaging the thread or screw-head (not forgetting decent screwdrivers).
arachnopress 1.0.57
I think it's finished . . .
Technical reference
arachnopress
Build a zero-JS static article site from hand-written HTML fragments.
Created ยท Updated
DESCRIPTION
arachnopress reads articles/slug/article.html files and builds
a single-page or multi-page static article site in site/.
make build is the normal interface.
It runs in POSIX-like BSD, Linux, and macOS environments. Generated output
is served as static files and may also be opened through
file://. It requires no server-side processing.
FEATURES
File-backed code, download, and image blocks with raw links, checksums,
image sizing, framing, alignment, text flow, and native enlarged-image
popovers.
Pygments or Source-highlight syntax highlighting with escaped-source
fallback and reduced span markup.
Minimal, hand-edited article fragments with arbitrary HTML; no Markdown
or template pipeline.
Single-page or per-article output, optional unlisted articles, and HTML
or extensionless internal URLs.
Opt-in Atom entries for articles or sections, with summaries, updated
dates, and section/subsection links.
Responsive CSS navigation, sortable title and date indexes, and article
creation and modification metadata.
Selectable upstream colour palettes with Exact, Auto, Light, and Dark
modes.
SVG logo and favicon support, theme-aware inline SVG, and optional fixed
or randomised SVG backdrops. A single Unicode symbol supplies the
logo/favicon fallback when no SVG icon is configured.
An HTML and CSS interface without JavaScript, cookies, browser storage,
or server-side processing.
Native theme, contact, and license popovers; marked GET contact requests
for access-log collection.
POSIX-style sh and make builds for BSD, Linux,
and macOS, with optional versioned release packaging.
Run make build from the project root. It builds every source
article and replaces site/. Supply settings through the
environment or as make command-line variables.
Do not store source files in or run make from
site/; each successful build replaces it.
REQUIREMENTS
Required utilities
The build requires a POSIX-like operating environment with
sh, make, awk, sed,
grep, sort, tr, date,
dirname, pwd,
printf, mktemp, mkdir, rm,
wc, cat, cksum,
chmod, mv, cp, and find.
The maintainer targets additionally require cmp,
tar, and gzip support in tar.
mktemp is widely available but is not specified by POSIX.
Optional utilities
pygmentize
Provides Pygments syntax highlighting. This is the default highlighter
when the command is available.
source-highlight
Provides GNU Source-highlight syntax highlighting when selected.
sha256, sha256sum, shasum, or openssl
Provides SHA256 values for download blocks. The first successful
implementation is used. Downloads remain usable without a checksum.
Browser features
The generated interface requires browser support only for HTML and CSS.
It uses CSS :has(), :target,
prefers-color-scheme, native
details/summary, and the HTML popover API. The
optional randomised article backdrop requires CSS random();
unsupported browsers retain the SVG's base fit. The bundled backdrop uses
a centred cover fit.
SOURCE LAYOUT
Maintained files
Makefile
Defines build, release, and full,
their defaults, and their explicit inclusion lists.
styles.css
Defines layout, themes, syntax colours, responsive behaviour, popovers,
and generated block presentation. Targets copy it into their fresh site
output but do not modify it.
THIRD_PARTY_NOTICES.txt
Identifies the upstream colour schemes, sources, authors, and licences.
The license popover and optional Theme popover link to this file.
licenses/
Contains the retained third-party copyright and licence notices.
tools/build.sh
Discovers, validates, orders, and renders articles. It generates
the selected page layout, optional Atom feed, favicon.svg,
automatic-theme rules, and embedded navigation rules in the selected
build root.
tools/build-profile.sh
Stages, builds, publishes, cleans, and packages target output.
tools/theme-menu.html
Supplies theme controls and optional data-auto-dark and
data-auto-light pair mappings. Its IDs are reserved as page
anchors.
tools/license.txt
Supplies the authoritative static project-license text. Its first non-blank
line is the displayed title.
tools/html-fragment.outlang
Configures Source-highlight to emit an HTML fragment.
articles/<slug>/article.html
Supplies one article. The directory name is the article slug and must
match the article element ID.
articles/<slug>/*
Supplies article-local code, download, and image files. These paths are
copied below site/articles/ and linked from the page.
The example tree shows maintained source and make build
output. site/ contains deployed files and copied articles. The
example contains only the arachnopress article.
make build copies the configured files and articles into a
private staging tree, renders the complete site, then replaces
site/. A failure before publication preserves the previous
output.
site/ is generated output. All targets replace the same
directory and must not run concurrently. The maintainer release targets
also manage the generator download marker and versioned archive.
ARTICLE FORMAT
Root metadata
Each article begins with a one-line article element. Its
class list must include article. Its id must
equal the containing directory name. data-title is required.
Use double-quoted values for attributes read by the generator.
data-created and data-modified accept a value
beginning with a valid YYYY-MM-DD date or auto:
data-created: missing, empty, or auto uses
the current UTC build date.
data-modified: missing or empty uses the resolved
creation date; auto uses the build date.
Explicit dates remain unchanged. Use explicit dates on published
articles and dated headings to keep metadata and ordering stable.
data-listed defaults to true and accepts only
true or false.
SITE_MODE_UNLISTED=false excludes articles marked
data-listed=false from generated HTML and navigation in
either output mode.
SITE_MODE_UNLISTED=true enables unlisted output.
data-feed-sections selects none, h1,
h2, or h3. Unset or empty means none.
See Atom feed modes under NAVIGATION AND THEMES.
Unlisted output:
Single-page: embedded after listed articles, absent from article
indexes, and accessible through article, section, and subsection
fragments.
Multi-page: written as slug.html, absent from listed-page
indexes, and included only in its own index. Its document has
noindex, nofollow.
Single-page articles share index.html and have no
separate robots directive.
Unlisted output remains publicly accessible. Targets still copy
included article directories to site/articles/.
At least one listed article is required. Multi-page mode reserves
index.
articles/getting-started/article.htmlComplete example articleraw
<articleclass="article"id="getting-started"data-title="Getting Started"data-created="2026-07-09"data-modified="2026-07-09"data-feed-sections="h1"><headerclass="article-header"><pclass="kicker">Guide</p><h1>Getting Started</h1><pclass="summary">
A short article built from one editable HTML fragment.
</p></header><section><h2>Example</h2><p>Article text is normal HTML.</p><h3>Subsection</h3><p>Plain one-line h3 headings appear under their preceding section.</p></section></article>
Header metadata
The source header contains an h1 and summary. The build inserts
Created and a time element before its closing tag. Updated is
the newest resolved article or heading date and appears when it differs
from Created. Unlisted articles are prefixed with Unlisted; fields use
middle-dot separators.
Indexed headings
Use h2 for major sections and h3 for genuine
subdivisions. Indexable headings contain plain text and close on the same
source line. An h3 must follow an h2. The build
preserves a valid explicit ID or derives a unique ID from the heading and
article.
Article, heading, theme, generated control, and generated block IDs share
one collision registry. Explicit IDs and slugs may contain ASCII letters,
digits, dots, underscores, and hyphens.
Every source h2 and h3 is indexed. IDs on other
author-supplied elements are preserved but are not registered; keep them
unique and clear of generated IDs.
Indexable headings may use data-created and
data-modified. Values follow the article-date format and may
use auto. Their resolved dates contribute to Updated metadata,
modification ordering, and Atom dates. They do not reorder section or
subsection indexes.
Generated markers are empty, file-backed elements contained on one source
line. data-src is relative to the article directory. Missing
or invalid sources render visible missing blocks. Use double-quoted
marker attributes.
data-header="false": always visible, without a summary.
data-footer: true or false
overrides the default. Normal blocks hide the footer for source line
counts at or below CODE_FOOTER_LINES; headerless blocks
hide it unless explicitly enabled.
Raw links and sizes refer to the original file. Only the rendered code
is HTML-escaped.
<preclass="code-block"data-src="src/example.c"data-title="example.c"data-note="A short C example"data-lang="c"></pre><preclass="code-block"data-src="build.sh"data-lang="sh"data-open="false"data-footer="false"></pre><preclass="code-block"data-src="output.txt"data-lang="text"data-header="false"></pre><preclass="code-block"data-src="output.txt"data-title="output.txt"data-lang="text"data-header="false"data-footer="true"></pre>
Download blocks
A download marker names exactly one file. data-title changes
only its display title and data-note adds a qualifier. The
link continues to target data-src. The block shows the raw
size and, when a supported checksum program is available, its SHA256.
The obsolete data-downloads attribute is rejected; use one
data-src marker per file.
The generator article has exactly one marker carrying
data-release="generator". The release and full targets replace
that complete line with the validated current archive name; a normal
build leaves it unchanged.
Download block markersOne source file per markerraw
data-alt: alternative text. data-caption:
caption.
data-title and data-note: header labels.
data-open and data-header: as for code blocks.
data-footer: shown with a normal header unless
false; a headerless image requires true to
show it.
data-scale caps inline size: small (20rem),
medium (32rem), large (48rem), or
full (article width, default). Images retain their aspect
ratio, do not upscale, and remain within the viewport.
data-border: full (default),
fit, or none. fit shrink-wraps
the block with one width for its header, image border, and footer.
none removes the frame, uses the opaque article
background, and suppresses both bars.
data-align: start, center
(default), or end.
Other scale, border, and alignment values are fatal.
Images use lazy loading and asynchronous decoding.
Text flow:
Above 760px, following prose and text lists may flow beside start- or
end-aligned fit or borderless blocks, capped at 55% of the containing
section's width and by the configured size limit.
Headings, generated blocks, preformatted blocks, tables, horizontal
rules, consecutive images, and section ends clear the flow.
Centered, full-frame, and narrower layouts keep images on their own row.
Image viewer:
Selecting an image opens a themed viewer over the article pane.
Intrinsic size and aspect ratio are retained, scaling down to fit.
The enlarged image, X button, Escape, or light dismissal closes it.
A missing or invalid source renders a missing block. A non-empty or
multiline marker is fatal.
SYNTAX HIGHLIGHTING
HIGHLIGHTER selects pygments,
source-highlight, or none. Use an explicit
data-lang for predictable lexer selection, or omit it for
automatic selection.
Pygments
Pygments is the default. An omitted data-lang requests
filename inference. data-lang="auto" may also guess from
content.
Source-highlight
Source-highlight uses tools/html-fragment.outlang. An omitted
or automatic language lets the program infer its input language.
Fallback and optimisation
A missing program, unknown lexer, failed invocation, missing output
definition, or empty highlighted result for non-empty input falls back to
escaped raw source for that block. HIGHLIGHTER=none always
uses that path.
Successful highlighted output is filtered to unwrap whitespace-only
Pygments span.w and Source-highlight
span.sh-normal elements. Their whitespace remains in the
preformatted code content.
NAVIGATION AND THEMES
Article and section selection
The build writes title, creation-date, and modification-date order lists.
ARTICLE_ORDER selects the initial list; CSS radio controls
select another.
In single-page mode, URL fragments and CSS
:target/:has() select articles, sections, and
subsections. The first listed article is visible without a target.
index.html embeds the generated navigation rules.
In multi-page mode, index.html contains the first listed
article and each article has slug.html. Each file contains one
article, its section index, and its generated navigation rules. A slug
page title is its article title followed by SITE_TITLE; the
index page title is SITE_TITLE.
Title order is case-insensitive by title, then slug. Creation order uses
the resolved article creation date. Modification order uses the newest
resolved article or heading date. Date orders are newest first, then slug.
Article-title links provide the article summary and resolved Created and
Updated dates as native hover text, with the dates on the final line. If a
summary cannot be reduced to clean text, the hover text contains only the
dates.
Responsive index
Wide landscape layouts place article and section navigation in a vertical
left pane. Narrow portrait layouts use independent horizontal article,
section, and conditional subsection rows. Short and narrow layouts hide
descriptive build metadata while retaining the generator identity,
Contact, and license controls.
Theme state
DEFAULT_THEME selects the initial exact variant.
DEFAULT_THEME_MODE selects the initial mode:
off: omit the mode control; apply
DEFAULT_THEME unchanged.
exact: apply the selected theme unchanged.
auto: select the mapped variant using the browser preference.
light or dark: select the corresponding
mapped variant.
Every value except off renders all four choices. An
unmapped theme remains fixed in every mode.
DEFAULT_THEME_SELECTOR enables the selector from
tools/theme-menu.html. Single-page selections span all
articles until reload. Multi-page selections reset when another page
loads. The mode control is independent of the Theme selector.
Light and dark pairs are the data-auto-light and
data-auto-dark attributes in
tools/theme-menu.html. Edit those attributes to change a
pairing.
SITE_FEED=true writes feed.atom, feed discovery
metadata, and a text-form Atom link before the Theme control. Listed
articles opt in through data-feed-sections:
none
No entries. Also the default when unset or empty.
h1
One entry with the article title, link, and plain-text summary.
h2
One entry per h2, titled "Article title - Section heading",
with a link to that section instead of the article summary.
h3
As h2, with a linked list of its h3 headings
in each entry body.
Feed rules:
h2 and h3 fall back to h1
without h2 headings. In h3 mode, an
h2 without child headings has the same content as
h2 mode.
Section bodies contain generated HTML links, not article prose.
Unlisted articles never enter the feed. Other mode values are fatal.
An h1 entry uses the newest article or heading date. An
h2 entry uses its data-modified, then its
data-created, then the article's resolved modification date.
A dated h3 updates its parent entry when newer. An undated
h3 has no effect. Article-level auto rolls into
h1 entries; in section modes it is only the fallback for an
undated h2. Feed dates use midnight UTC.
Article groups are ordered by their newest entry. Sections and linked
subsections retain source order. An empty feed uses the UTC build date.
The feed identifies the generator and uses the versioned site favicon.
Links follow SITE_MODE and SITE_URL_STYLE.
IDs remain SITE_URL/#article-id or
SITE_URL/#section-id, independent of dates and output
mode.
Explicit heading IDs are optional and preserve identity across renames
and reordering of duplicate headings. New h2 headings
create entries; section edits and added h3 links retain the
parent entry ID.
Enabling section entries introduces existing sections as new entries.
Revisions retain their IDs; read/unread state is reader-controlled.
Branding and backdrop
SITE_ICON supplies the Unicode header logo and generated
favicon when SITE_ICON_PATH is empty.
SITE_ICON_PATH supplies an SVG copied unchanged as the
favicon and used as the header logo.
SITE_ICON_PATH_THEME=true embeds the header copy and maps its
colour-out and
colour-in classes to the current article background and title
colours. See Custom site icon under BUILD for class assignment.
SITE_BACKGROUND enables a fixed article-pane SVG backdrop.
SITE_BACKGROUND_RANDOM lets supporting browsers select its
scale and crop with CSS random() when loading the document.
Other browsers retain the SVG's base fit. The bundled SVG uses a centred
cover crop. Its colour-out class is not painted;
colour-in uses the panel background colour at 25% opacity.
Code, download, and image block surfaces remain opaque.
CONTACT AND LICENSE
Contact request
Contact opens a native popover:
Reply address: optional, at most 254 characters.
Message: required, at most 500 characters.
Submission: GET to the current page, targeting
contact-confirmation.
The first query item is a lowercase SITE_TITLE identifier
followed by _contact=1. Runs outside letters, digits, dots,
underscores, and hyphens become one underscore; edge underscores are
removed. Identifiers without ASCII letters or digits become
arachnopress. Defaults are
arachnopress_contact=1 for release and
arachnogoat_contact=1 for build and full.
Contact request shapeValues are URL-encoded by the browserraw
Browser URL with SITE_TITLE="Example Site":
https://host.example/?example_site_contact=1&reply=operator%40example.com&message=Short+message#contact-confirmation
HTTP request target logged by a server that retains query strings:
GET /?example_site_contact=1&reply=operator%40example.com&message=Short+message
Contact privacy
HTTPS encrypts the request in transit. The address and message remain in
the URL, browser history, and logs that record the query string. Do not
submit confidential information. The static site provides no separate
message delivery or storage.
The operator extracts and processes marked requests from the server log.
arachnopress provides no log-processing component.
Project license
The license popover HTML-escapes and embeds
tools/license.txt. Its first non-blank line is the title.
THIRD_PARTY_NOTICES.txt maps bundled colour palettes to
notices below licenses/.
TARGETS
build
Routine site-generation target. Replaces site/ with the
deployment files, every source article, and newly generated output.
Leaves release markers and archives unchanged.
release
Maintainer target for a generator release. Builds only the arachnopress
article, marks its archive download as missing, and replaces
site/. It creates the versioned archive in the project root,
removes all arachnopress_*.tar.gz source article archives,
and updates the source marker.
full
Optional maintainer target that installs the matching root release
archive in a full staged site. It replaces site/, removes
all arachnopress_*.tar.gz source article archives, and
updates the source marker.
No clean, install, serve, or watch target is defined.
VARIABLES
RELEASE_SITE_ENV defines release site defaults.
PUBLIC_SITE_ENV defines build and full site defaults.
BUILD_ENV defines shared defaults. Environment and
command-line make values override them.
Site identity
SITE_TITLE
Page title and displayed brand. It also forms the contact request marker
prefix. Defaults: arachnopress for release and
direct tools/build.sh use; arachnogoat for
build and full.
SITE_FEED
true writes feed.atom, its discovery metadata,
and a header link. Listed articles opt in with
data-feed-sections. false omits them.
Defaults: false for release and direct
tools/build.sh use;
true for build and full.
SITE_URL
Canonical site root used for Atom links. SITE_FEED=true
requires an absolute HTTP or HTTPS URL without a query or fragment. A
trailing slash is removed. Defaults: empty for release and
direct tools/build.sh use;
https://arachnogoat.com for build and
full.
SITE_AUTHOR
Atom feed author. Defaults: SITE_TITLE for
release and direct tools/build.sh use;
musol@arachnogoat.com for build and
full.
SITE_ICON
Character shown beside SITE_TITLE and rendered into the
generated SVG favicon. Defaults: U+4DD6 for release and
direct tools/build.sh use; U+4DEA for build
and full.
SITE_ICON_COLOUR
Optional fixed colour for the Unicode header icon and generated
favicon. The value must be #rgb or #rrggbb.
When empty, the header icon follows the current article-title colour
and the generated favicon uses the SVG default text fill. It is unused
when SITE_ICON_PATH is set. Default: empty.
SITE_ICON_PATH
Optional relative path to an SVG in the selected build tree. The build
copies it unchanged to site/favicon.svg and uses it beside
SITE_TITLE. SITE_ICON and
SITE_ICON_COLOUR are then unused. The path must not name
the generated favicon.svg. Defaults: empty for
release;
articles/arachnopress/arachnogoatsundual.svg for
build and full. Direct
tools/build.sh execution defaults to empty. Restore the text
icon with SITE_ICON_PATH= and
SITE_ICON_PATH_THEME=false.
SITE_ICON_PATH_THEME
true embeds the SITE_ICON_PATH SVG in the
header and maps its colour-out and colour-in
classes to the current article background and title colours. The
favicon remains an unchanged copy. true requires
SITE_ICON_PATH. false uses the SVG as an external
header image. Defaults: false for release and
direct tools/build.sh use; true for
build and full.
SITE_BACKGROUND
Article-pane backdrop. true embeds the
SITE_BACKGROUND_PATH SVG. The bundled SVG uses a centred
cover crop; a replacement's root SVG attributes determine its base fit.
Defaults: false for release and direct
tools/build.sh execution; true for
build and full.
SITE_BACKGROUND_RANDOM
Backdrop crop. true lets browsers with CSS
random() support select a scale and crop on page load;
unsupported browsers retain the base fit. false always
uses the base fit. Default: true.
SITE_BACKGROUND_PATH
Relative path to the backdrop SVG in the selected build tree. When
SITE_BACKGROUND=true, the file must define
colour-out and colour-in classes. The former is
not painted; the latter uses the active theme's panel background colour
at 25% opacity. Default:
articles/arachnopress/arachnogoatsundual.svg. Use
preserveAspectRatio="xMidYMid slice" for a centred cover
fit.
GENERATOR_LABEL
Maintainer setting for the generator name displayed in site metadata
and the Atom feed. Default: arachnopress.
GENERATOR_VERSION
Maintainer setting displayed after GENERATOR_LABEL and used
in release archive names and Atom generator metadata. It contains
dot-separated digits. Default: 1.0.64.
Navigation and rendering
DEFAULT_THEME
Exact input ID from tools/theme-menu.html, with or without
the theme- prefix. Defaults: solarized-dark for
release and direct tools/build.sh use;
everforest-hard-dark for build and
full.
DEFAULT_THEME_MODE
Theme mode. off omits the control and applies
DEFAULT_THEME unchanged. exact,
auto, light, or dark render all
four choices and select that initial mode. Auto follows the browser
preference; Light and Dark use the selected theme's mapping. An
unmapped theme remains fixed. Defaults: off for
release and direct tools/build.sh use;
exact for build and full.
DEFAULT_THEME_SELECTOR
Theme selector state. true renders the selector in either
output mode; false fixes the selection to
DEFAULT_THEME. Default: true.
SITE_MODE
Output layout. single-page writes all generated articles
to index.html. multi-page writes
index.html plus one slug.html per article.
Default: single-page.
SITE_MODE_UNLISTED
Unlisted article generation. true enables unlisted output
using the selected SITE_MODE; false excludes
unlisted articles from generated HTML and navigation. Defaults:
false for release and direct
tools/build.sh use; true for build
and full.
SITE_URL_STYLE
Page-link format. html retains .html links.
extensionless keeps generated .html files but
uses slug and ./ for multi-page navigation, home
links, and contact actions. Defaults: html for
release and direct tools/build.sh use;
extensionless for build and full.
Extensionless output requires a matching web-server rewrite.
Single-page output is unchanged.
ARTICLE_ORDER
Initial order: title, created, or
modified. Default: title.
HIGHLIGHTER
pygments, source-highlight, or
none. Default: pygments.
CODE_FOOTER_LINES
Non-negative line threshold for automatically hiding code block
footers. Default: 23.
ENVIRONMENT
PATH
Locates required utilities and optional highlighters and checksum
programs.
TMPDIR
Parent for private static-site-build.* rendering
directories. Default: /tmp. Target staging uses
.site-build.* below the project root.
LC_ALL is set to C.
SOURCE_HIGHLIGHT_DATADIR is unset so Source-highlight uses
its installed data files and the project output definition.
BUILD
Routine site build
make build is the normal target. Run it from the project root
after changing articles, assets, styles, tool inputs, or build settings.
Routine build profilesRun from the project rootraw
make build
# Per-article pages with system light/dark selection and no theme menu.
make build SITE_MODE=multi-page DEFAULT_THEME_MODE=auto \DEFAULT_THEME_SELECTOR=false# Per-article files for file:// navigation, using a fixed theme.
make build SITE_MODE=multi-page SITE_URL_STYLE=html \DEFAULT_THEME=solarized-dark DEFAULT_THEME_MODE=off \DEFAULT_THEME_SELECTOR=false# Published articles only, without theme controls or an article backdrop.
make build SITE_MODE_UNLISTED=falseSITE_BACKGROUND=false\DEFAULT_THEME_MODE=off DEFAULT_THEME_SELECTOR=false# Override the public Atom feed identity.
make build SITE_FEED=trueSITE_URL=https://example.com \SITE_AUTHOR='Example Author'
Custom site icon
Store the SVG below an included article. For theme colouring, assign
colour-out and colour-in classes to its drawable
elements or groups. Edit the class attributes directly or with Inkscape's
XML Editor. Restore the Unicode icon with SITE_ICON_PATH=
and SITE_ICON_PATH_THEME=false.
Custom site icon buildReplace article-slug after adding an SVGraw
# Run each command separately from the project root.# Custom SVG after adding the required classes.
make build SITE_ICON_PATH=articles/article-slug/site-icon.svg \SITE_ICON_PATH_THEME=true# Restore the Unicode icon.
make build SITE_ICON_PATH=SITE_ICON_PATH_THEME=false
Generator release
Maintainers changing the generator update GENERATOR_VERSION
and run make release. This builds only the arachnopress
article and creates the root archive. Copy that archive into
articles/arachnopress/, then use make build for a
normal site. The optional make full target performs the
archive integration within its staged output.
Highlighter variants
HIGHLIGHTER=none selects escaped source. Select either
installed highlighter explicitly to test its output.
Highlighter buildsRun separately from the project rootraw
# Run each command separately from the project root.
make build HIGHLIGHTER=none
make build HIGHLIGHTER=pygments
make build HIGHLIGHTER=source-highlight
REBUILD AND RECOVERY
Re-run make build after changing maintained input.
Stylesheet and favicon cache tokens change with their resources.
After failure or interruption, correct the diagnostic and rerun the
target. Normal failure and signal handling remove unpublished
temporary files and preserve the previous site before publication.
After an untrappable termination, wait until no build is running.
Inspect .site-build.* below the project root and
static-site-build.* below TMPDIR before
removing leftovers.
Remove generated output
No clean target is defined. Remove site/ without changing
maintained source. Release replaces the current-version root archive but
does not remove older root archives. Remove obsolete root archives by
exact name.
Remove generated outputRun from the project rootraw
# Run from the project root when no build is running.
rm -rf site
FILES
site/
Fresh published output from the most recent target. Do not store
maintained articles or run make in this directory.
site/index.html
Generated entry point. It contains every generated article in
single-page mode or the first listed article in multi-page mode.
site/<slug>.html
Multi-page output for each listed article and each enabled unlisted
article.
site/favicon.svg
Generated fallback or unchanged copy of SITE_ICON_PATH.
Its link contains a cache version token.
site/feed.atom
Site-wide Atom feed generated when SITE_FEED=true.
site/theme-auto.css
Generated when DEFAULT_THEME_MODE is not off
and a reachable theme has an automatic mapping. It contains light/dark
rules derived from styles.css and the mappings in
tools/theme-menu.html. Its link contains the shared
stylesheet cache token.
site/styles.css
Copied site stylesheet.
THIRD_PARTY_NOTICES.txt
Maintained notice for bundled colour palettes. Targets copy it into
site/ and release archives.
licenses/
Retained third-party licence notices. Targets copy the directory into
site/ and release archives.
articles/<slug>/*
Maintained article source and published raw, download, and image assets.
Targets copy them to site/articles/<slug>/.
arachnopress_GENERATOR_VERSION.tar.gz
Maintainer release archive produced by make release and
consumed by make full from the same project root. Its
top-level directory has the same name without the .tar.gz
suffix.
README.arachnopress
The compact operator and interface reference.
EXIT STATUS
A target exits zero after its complete staged tree is published as
site/, after release packaging when applicable, and after
managed source cleanup. It exits non-zero on invalid settings or article
structure, missing required tool inputs, duplicate or invalid IDs, no
articles, or an unhandled command or filesystem failure.
Optional highlighting and checksum failures are not fatal. The build
either emits escaped raw source or omits the checksum.
DIAGNOSTICS
Missing release archive
Run make release with the same generator version before
make full. Run both commands from the same project root, not
from site/.
DEFAULT_THEME does not exist in theme menu / Automatic theme mapping
must name a theme option
Use theme radio IDs from tools/theme-menu.html. Define
data-auto-dark and data-auto-light together,
and include the source theme in its pair.
No listed articles found
Add or select at least one article not marked
data-listed="false".
Extensionless article directory names must not contain dots /
Extensionless article URL conflicts with a site root path
Rename the article directory and matching article ID. The slug must not
contain a dot or match another generated root path.
Unsafe SITE_ICON_PATH / SITE_ICON_PATH_THEME requires SVG class
Use a readable relative .svg file in the staged tree. A
themed icon must contain colour-out and
colour-in class tokens.
Unsafe SITE_BACKGROUND_PATH / SITE_BACKGROUND requires SVG class
Enable the backdrop with a readable relative .svg file in
the staged tree containing colour-out and
colour-in class tokens.
Article, heading, or generated-marker format error
Apply the ARTICLE FORMAT and GENERATED BLOCKS rules. Keep indexed
headings and empty generated markers on one source line.
Missing or invalid generated block files appear as visible missing blocks.
They do not produce these fatal diagnostics.
SERVING
Deploy site/ as static files without changing its relative
paths. It may instead be opened as site/index.html through
file://; multi-page file access requires
SITE_URL_STYLE=html.
Contact submission is for HTTP or HTTPS deployment. Serve contact forms
over HTTPS and retain query strings in the access log to receive their
contents.
OpenBSD httpd example
The OpenBSD httpd(8) example serves .atom as
application/atom+xml and rewrites extensionless article
paths. Replace its host and document-root paths, validate the
configuration, and reload httpd.
Its final rule appends .html internally to a missing final
path component without a dot. Keep specific rules before it.
/etc/httpd.confReplace host, certificate, and document-root pathsrawโ
types {
include "/usr/share/misc/mime.types"
application/atom+xml atom
}
server "arachnogoat.com" {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location * {
block return 301 "https://$HTTP_HOST$REQUEST_URI"
}
}
server "arachnogoat.com" {
listen on * tls port 443
tls {
certificate "/etc/ssl/arachnogoat.com.fullchain.pem"
key "/etc/ssl/private/arachnogoat.com.key"
}
root "/htdocs/arachnogoat.com"
# Required when arachnopress uses SITE_URL_STYLE=extensionless.
location not found match "/[^./]+$" {
request rewrite "$DOCUMENT_URI.html"
}
}
CAVEATS
Trusted input
Article HTML, a SITE_ICON_PATH SVG embedded by
SITE_ICON_PATH_THEME, and a
SITE_BACKGROUND_PATH SVG are trusted author content. The
build escapes generated text and code, but does not sanitize author HTML
or embedded SVG markup. Review them before building. The zero-JS
guarantee applies to the generated interface; author HTML may add its own
active content or external dependencies.
Single-page scale
Single-page output contains every generated article and highlighted code
span. Large code-heavy collections increase HTML size and DOM cost. Use
multi-page mode, focused excerpts, raw downloads, or
HIGHLIGHTER=none when that cost becomes material.
Browser compatibility
Older clients may not support every CSS or popover feature. See REQUIREMENTS.
LICENSE
arachnopress is distributed under the BSD 3-Clause license. See
tools/license.txt or the License control in the site metadata.
Bundled colour palettes retain their upstream values and third-party
licences; see
THIRD_PARTY_NOTICES.txt and licenses/.
Patch Notes
bsddd.rd
OpenBSD/amd64 parallel disk-overwrite ramdisk and miniroot targets.
The patch adds separate bsddd.rd and
minirootXX_dd.img targets. The ramdisk waits for one eligible
sd(4) or wd(4) disk to disappear, then overwrites
the remaining disks concurrently. Two console confirmations or an
optional timeout instead select every eligible attached disk.
/dev/zero is the default input. /dev/urandom is
selectable at build time. Console messages and interval-based bells
report status.
The patch adds files only. Stock source files, Makefiles,
bsd.rd, minirootXX.img, and
cdXX.iso targets are unchanged. Enhanced targets are
available only through Makefile.bsddd.
INTENDED USE
bsddd.rd supports batch whole-disk overwriting when preparing
decommissioned systems for disposal or reuse, particularly where
device-native sanitize or secure-erase facilities are unavailable or
impractical. It runs from RAM, allowing boot media and optional console
equipment to be moved between machines while wipes continue.
PATCH
The aggregate patch contains the complete build overlay and README.
--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/Makefile.bsddd@@ -0,0 +1,233 @@+# $OpenBSD$++.include <bsd.own.mk>++# Stock ramdisk_cd outer-image size, retained as an override point.+BSDDD_FSSIZE?= 11392+BSDDD_DD_IF?= /dev/zero+BSDDD_WIPE_TIMEOUT?= 0++BSDDD_BASEDIR= ${.CURDIR}/..+BSDDD_TOP= ${.CURDIR}/../../../..+BSDDD_UTILS= ${BSDDD_BASEDIR}/../../miniroot+BSDDD_MTREE= ${BSDDD_UTILS}/mtree.conf+BSDDD_EFIBOOT= ${DESTDIR}/usr/mdec/BOOTX64.EFI \+ ${DESTDIR}/usr/mdec/BOOTIA32.EFI+BSDDD_MOUNT_ARGS_MSDOS= -o-s++BSDDD_RAMDISK= RAMDISK_CD_DD+BSDDD_KERNEL= ${.OBJDIR}/bsd+BSDDD_FS= miniroot${OSrev}_dd.img+BSDDD_BUILDOBJDIR= ${.OBJDIR}/build+BSDDD_KERNELOBJDIR= ${BSDDD_BUILDOBJDIR}/kernel/${BSDDD_RAMDISK}+BSDDD_INSTBIN= ${BSDDD_BUILDOBJDIR}/instbin+BSDDD_RDOBJDIR= ${.OBJDIR}/rdobj+BSDDD_MOUNT_POINT= ${.OBJDIR}/mnt+BSDDD_VND= ${.OBJDIR}/vnd+BSDDD_BOOT= ${.OBJDIR}/boot+BSDDD_OBJCHECK= ${.OBJDIR}/.bsddd-obj-ok+BSDDD_MAKEFILE= ${.CURDIR}/Makefile.bsddd+BSDDD_KERNELCONF= ${.CURDIR}/${BSDDD_RAMDISK}+BSDDD_BASECONF= ${BSDDD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD+BSDDD_KERNELCONFDEPS= ${BSDDD_BASECONF}+BSDDD_LISTS= ${BSDDD_BASEDIR}/list ${.CURDIR}/list.dd+BSDDD_SCRIPTSRC= ${.CURDIR}/bsddd+BSDDD_SCRIPT= ${BSDDD_RDOBJDIR}/bsddd+BSDDD_PROFILE= ${.CURDIR}/dot.profile+# Stock 7360-block rdroot; keep paired with RAMDISK_CD_DD MINIROOTSIZE.+BSDDD_RDROOT_SIZE?= 3680k+BSDDD_MRMAKEFSARGS?= -s ${BSDDD_RDROOT_SIZE} \+ -o rdroot,minfree=0,bsize=4096,fsize=512,density=4096++.PHONY: bsddd+bsddd: bsddd.rd ${BSDDD_FS}++${BSDDD_OBJCHECK}:+ @if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \+ echo "private object directory is not active;" >&2; \+ echo "run 'make -f Makefile.bsddd obj' separately first" >&2; \+ exit 1; \+ fi+ touch $@++${BSDDD_KERNEL}: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE} \+ ${BSDDD_KERNELCONF} ${BSDDD_KERNELCONFDEPS}+ install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${BSDDD_KERNELOBJDIR}+ su ${BUILDUSER} -c \+ 'config -b ${BSDDD_KERNELOBJDIR} -s ${BSDDD_TOP}/sys \+ ${BSDDD_KERNELCONF} && cd ${BSDDD_KERNELOBJDIR} && \+ MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} clean && \+ exec env MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'+ cp -p ${BSDDD_KERNELOBJDIR}/bsd $@++bsddd.gz: bsddd.rd+ objcopy -g -x -R .comment -R .SUNW_ctf \+ -K rd_root_size -K rd_root_image \+ bsddd.rd bsddd.strip+ gzip -9cn bsddd.strip > bsddd.gz++.PHONY: bsddd-stock-deps bsddd-instbin+bsddd-stock-deps: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE}+ @_objroot=`cd ${BSDDD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \+ if [ ! -d "$$_objroot" ]; then \+ echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \+ echo "create it before building bsddd" >&2; \+ exit 1; \+ fi+ cd ${BSDDD_TOP}/lib && ${MAKE} obj+ cd ${BSDDD_TOP}/distrib/special && ${MAKE} obj+ cd ${BSDDD_BASEDIR} && ${MAKE} obj+ @_srcdir=`cd ${BSDDD_BASEDIR} && pwd`; \+ _objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \+ if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \+ echo "normal ramdisk_cd object directory is not active" >&2; \+ exit 1; \+ fi+ cd ${BSDDD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}++bsddd-instbin: bsddd-stock-deps+ cd ${BSDDD_BASEDIR} && ${MAKE} ${MFLAGS} instbin+ install -d ${BSDDD_BUILDOBJDIR}+ @_objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \+ if [ ! -f "$$_objdir/instbin" ]; then \+ echo "stock ramdisk_cd instbin was not built" >&2; \+ exit 1; \+ fi; \+ cp -p "$$_objdir/instbin" ${BSDDD_INSTBIN}++bsddd.rd: bsddd-files bsddd-instbin ${BSDDD_KERNEL}+ install -d ${BSDDD_RDOBJDIR}+ rm -f ${BSDDD_RDOBJDIR}/instbin \+ ${BSDDD_RDOBJDIR}/mr.fs \+ ${BSDDD_RDOBJDIR}/bsd.rd+ cp -p ${BSDDD_INSTBIN} ${BSDDD_RDOBJDIR}/instbin+ rm -rf ${BSDDD_RDOBJDIR}/mr.fs.d+ install -d -o root -g wheel ${BSDDD_RDOBJDIR}/mr.fs.d+ mtree -def ${BSDDD_MTREE} -p ${BSDDD_RDOBJDIR}/mr.fs.d -u+ CURDIR=${BSDDD_BASEDIR} OBJDIR=${BSDDD_RDOBJDIR} OSrev=${OSrev} \+ TARGDIR=${BSDDD_RDOBJDIR}/mr.fs.d UTILS=${BSDDD_UTILS} \+ RELEASEDIR=${RELEASEDIR} sh ${BSDDD_UTILS}/runlist.sh \+ ${BSDDD_LISTS}+ rm ${BSDDD_RDOBJDIR}/mr.fs.d/instbin+ makefs ${BSDDD_MRMAKEFSARGS} ${BSDDD_RDOBJDIR}/mr.fs \+ ${BSDDD_RDOBJDIR}/mr.fs.d+ cp -p ${BSDDD_KERNEL} ${BSDDD_RDOBJDIR}/bsd.rd+ rdsetroot ${BSDDD_RDOBJDIR}/bsd.rd ${BSDDD_RDOBJDIR}/mr.fs+ cp ${BSDDD_RDOBJDIR}/bsd.rd $@++${BSDDD_FS}: bsddd.gz+ -umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1+ @if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \+ rm -f ${BSDDD_VND}; \+ fi+ @if [ -e ${BSDDD_VND} ]; then \+ echo "stale private vnd state; run" \+ "'make -f Makefile.bsddd unconfig-bsddd' first" >&2; \+ exit 1; \+ fi+ install -d ${BSDDD_MOUNT_POINT}+ dd if=/dev/zero of=${BSDDD_FS} bs=512 count=${BSDDD_FSSIZE}+ vnconfig -v ${.OBJDIR}/${BSDDD_FS} > ${BSDDD_VND}+ fdisk -yi -l ${BSDDD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \+ `cat ${BSDDD_VND}`+ echo '/ *' | disklabel -wAT- `cat ${BSDDD_VND}`+ newfs -t msdos /dev/r`cat ${BSDDD_VND}`i+ mount ${BSDDD_MOUNT_ARGS_MSDOS} /dev/`cat ${BSDDD_VND}`i \+ ${BSDDD_MOUNT_POINT}+ mkdir -p ${BSDDD_MOUNT_POINT}/efi/boot+ cp ${BSDDD_EFIBOOT} ${BSDDD_MOUNT_POINT}/efi/boot+ umount ${BSDDD_MOUNT_POINT}+ newfs -O 1 -m 0 -o space -i 524288 -c ${BSDDD_FSSIZE} \+ /dev/r`cat ${BSDDD_VND}`a+ mount /dev/`cat ${BSDDD_VND}`a ${BSDDD_MOUNT_POINT}+ objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${BSDDD_BOOT}+ installboot -v -r ${BSDDD_MOUNT_POINT} `cat ${BSDDD_VND}` \+ ${DESTDIR}/usr/mdec/biosboot ${BSDDD_BOOT}+ install -c -m 555 -o root -g wheel bsddd.gz \+ ${BSDDD_MOUNT_POINT}/bsd+ df -i ${BSDDD_MOUNT_POINT}+ umount ${BSDDD_MOUNT_POINT}+ vnconfig -u `cat ${BSDDD_VND}`+ rm -f ${BSDDD_VND}++.PHONY: bsddd-check bsddd-files+bsddd-check:+ @case "${BSDDD_DD_IF}" in \+ /dev/zero|/dev/urandom) ;; \+ *) echo "BSDDD_DD_IF must be /dev/zero or /dev/urandom" >&2; exit 1;; \+ esac+ @case "${BSDDD_WIPE_TIMEOUT}" in \+ ''|*[!0-9]*|0[0-9]*) \+ echo "BSDDD_WIPE_TIMEOUT must be 0 or a positive decimal integer" >&2; \+ exit 1;; \+ esac+ @if ! [ "${BSDDD_WIPE_TIMEOUT}" -le 2147483647 ]; then \+ echo "BSDDD_WIPE_TIMEOUT exceeds 2147483647" >&2; \+ exit 1; \+ fi+ test -s ${BSDDD_SCRIPTSRC}+ test -s ${BSDDD_PROFILE}+ test -s ${.CURDIR}/list.dd++bsddd-files: ${BSDDD_OBJCHECK} bsddd-check ${BSDDD_SCRIPTSRC} \+ ${BSDDD_PROFILE} ${.CURDIR}/list.dd+ install -d ${BSDDD_RDOBJDIR}+ rm -f ${BSDDD_SCRIPT}.tmp+ sed -e 's|^DD_IF=.*|DD_IF=${BSDDD_DD_IF}|' \+ -e 's|^WIPE_TIMEOUT=.*|WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}|' \+ ${BSDDD_SCRIPTSRC} > \+ ${BSDDD_SCRIPT}.tmp+ grep -Fqx 'DD_IF=${BSDDD_DD_IF}' ${BSDDD_SCRIPT}.tmp+ grep -Fqx 'WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}' ${BSDDD_SCRIPT}.tmp+ mv ${BSDDD_SCRIPT}.tmp ${BSDDD_SCRIPT}++.PHONY: unconfig-bsddd+unconfig-bsddd:+ -umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1+ @if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \+ rm -f ${BSDDD_VND}; \+ elif [ -f ${BSDDD_VND} ]; then \+ _vnd=`cat ${BSDDD_VND}`; \+ _unit=$${_vnd#vnd}; \+ if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \+ echo "invalid private vnd state: $$_vnd" >&2; exit 1; \+ fi; \+ case "$$_unit" in \+ *[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \+ esac; \+ _info=`vnconfig -l "$$_vnd"` || exit 1; \+ case "$$_info" in \+ "$$_vnd: not in use") rm -f ${BSDDD_VND} ;; \+ "$$_vnd: covering ${.OBJDIR}/${BSDDD_FS} on "*) \+ vnconfig -u "$$_vnd" && rm -f ${BSDDD_VND} ;; \+ *) echo "refusing to detach vnd not owned by bsddd: $$_info" >&2; \+ exit 1 ;; \+ esac; \+ fi++.ifdef RELEASEDIR+.PHONY: install-bsddd+install-bsddd: bsddd.gz ${BSDDD_FS}+ cp bsddd.gz ${RELEASEDIR}/bsddd.rd+ cp ${BSDDD_FS} ${RELEASEDIR}+ chmod a+r ${RELEASEDIR}/bsddd.rd ${RELEASEDIR}/${BSDDD_FS}+.endif++.PHONY: clean-bsddd clean cleandir+clean-bsddd: unconfig-bsddd+ rm -f bsddd.rd bsddd.gz bsddd.strip ${BSDDD_FS} \+ ${BSDDD_KERNEL} ${BSDDD_BOOT} ${BSDDD_OBJCHECK}+ rm -rf ${BSDDD_BUILDOBJDIR} ${BSDDD_RDOBJDIR}+ -rmdir ${BSDDD_MOUNT_POINT}++clean cleandir: clean-bsddd++.PHONY: prepare-unpatch-bsddd+prepare-unpatch-bsddd: clean-bsddd+ @if [ -L ${.CURDIR}/obj ]; then \+ _obj=`readlink ${.CURDIR}/obj`; \+ echo "empty private object directory may be removed: $$_obj"; \+ rm -f ${.CURDIR}/obj; \+ fi++.include <bsd.obj.mk>--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/RAMDISK_CD_DD@@ -0,0 +1,12 @@+# $OpenBSD$++include "arch/amd64/conf/RAMDISK_CD"++# Retain an explicit, stock-sized rdroot override for later adjustment.+option BSDDD_RDROOT+rmoption MINIROOTSIZE+option MINIROOTSIZE=7360+rmoption BSDDD_RDROOT++pcppi0 at isa?+spkr0 at pcppi?--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/README.bsddd@@ -0,0 +1,238 @@+BSDDD.RD bsddd.rd build notes BSDDD.RD++NAME+ bsddd.rd - OpenBSD/amd64 parallel disk-overwrite ramdisk++DESCRIPTION+ The patch adds separate bsddd.rd and minirootXX_dd.img targets. The+ ramdisk waits for one eligible sd(4) or wd(4) disk to disappear, then+ overwrites the remaining disks concurrently. Two console confirmations+ or an optional timeout instead select every eligible attached disk.++ /dev/zero is the default input. /dev/urandom is selectable at build+ time. Console messages and interval-based bells report status.++ The patch adds files only. Stock source files, Makefiles, bsd.rd,+ minirootXX.img, and cdXX.iso targets are unchanged. The enhanced targets+ are available only through Makefile.bsddd.++INTENDED USE+ bsddd.rd supports batch whole-disk overwriting when preparing+ decommissioned systems for disposal or reuse, particularly where+ device-native sanitize or secure-erase facilities are unavailable or+ impractical. It runs from RAM, allowing boot media and optional console+ equipment to be moved between machines while wipes continue.++SYNOPSIS+ Build on the matching OpenBSD/amd64 release after completing the normal+ source and release prerequisites. BSDOBJDIR and the amd64 boot files+ below ${DESTDIR}/usr/mdec must exist. The documented commands require+ root privileges.++ Apply the patch below /usr/src, create the private object link, then build:++ cd /usr/src+ patch -p1 < /root/bsddd.rd.patch+ cd distrib/amd64/ramdisk_cd/bsddd+ make -f Makefile.bsddd obj+ make -f Makefile.bsddd++ Select pseudorandom input instead:++ make -f Makefile.bsddd BSDDD_DD_IF=/dev/urandom++ Enable a 300-second all-disk timeout:++ make -f Makefile.bsddd BSDDD_WIPE_TIMEOUT=300++ Install release artifacts when RELEASEDIR is set:++ make -f Makefile.bsddd install-bsddd \+ RELEASEDIR=/path/to/release++TARGETS+ bsddd+ Builds bsddd.rd and minirootXX_dd.img.++ install-bsddd+ Defined when RELEASEDIR is set. Installs compressed bsddd.rd and+ minirootXX_dd.img.++ unconfig-bsddd+ Recovers private mount and vnd(4) state after an interrupted+ media build. An unowned vnd(4) is not detached.++ clean-bsddd, clean, cleandir+ Remove private outputs. Stock instbin objects remain under+ normal OpenBSD clean ownership.++ prepare-unpatch-bsddd+ Cleans private outputs, prints the private object path, and+ removes the source obj symlink.++BUILD MODEL+ make obj creates a private object link below BSDOBJDIR. The custom+ kernel, rdroot, instbin copy, and media objects remain below that path.++ The wrapper builds stock ramdisk_cd instbin objects in their normal object+ directories, then copies instbin into private state. Stock source,+ configuration, installed use, and cleanup ownership are unchanged. Do+ not run bsddd concurrently with a build or clean using those objects.++ The private rdroot replaces .profile, installs /bsddd, and creates+ /dev/speaker. It adds no userland binaries. The private kernel retains+ the stock rdroot size and adds pcppi(4) and speaker(4).++ No parent Makefile or SUBDIR list is changed. Normal builds and cleans do+ not enter the bsddd source directory or its private objects.++DISK SELECTION+ /bsddd reads hw.disknames with sysctl(8) and selects names matching+ sd[0-9]* and wd[0-9]*. This includes softraid(4) volumes, their backing+ disks, and disks presented through common amd64 storage controllers.++ Wiping begins when the current set equals the initial set minus exactly+ one name. Other changes leave the ramdisk waiting. The accepted set is+ frozen; later arrivals are excluded. The interlock cannot identify the+ boot medium. Two explicit y responses or timeout expiry select every+ eligible attached disk. The timer continues during both prompts.++ Other device classes are excluded. After the interlock, /bsddd detaches+ auto-assembled softraid(4) volumes and removes their names from the set.+ Their backing disks remain selected. Inspection or detach failure stops+ before writes.++ MAKEDEV(8) creates device nodes. disklabel(8) supplies sector size and+ total sectors. dd(1) writes each whole-disk raw character device in 1 MiB+ transfers followed by an exact sector-sized tail. Disks run concurrently.+ One device failure does not stop other jobs. A status failure waits for+ active jobs. sync(8) precedes the final result.++FEEDBACK+ Console-bell counts and intervals identify each state:++ three short bells waiting without a timeout+ four rapid bells waiting with an all-disk timeout+ two separated bells disk writes started+ one bell wipe progress+ five short bells all selected disks completed+ three separated bells setup, status, or disk-write failure++ Ready patterns repeat every 10 seconds. Completion and failure patterns+ repeat every 30 seconds. Progress is reported every 60 seconds. A+ console bell is used if speaker output fails. The result loop continues+ until power-off or reset.++USE+ Write minirootXX_dd.img to removable media. This replaces the selected+ device's partition table and filesystems. Verify the output device. For+ example, as root on OpenBSD:++ dd if=./miniroot79_dd.img of=/dev/rsdXc bs=1m+ sync++ Boot the target. At the ready pattern, remove the boot medium. Removal+ of any eligible disk satisfies the name-based interlock. Two separated+ bells mark write startup. Wait for the repeating completion pattern+ before power-off. Stock amd64 BIOS and UEFI boot programs are used.+ Secure Boot is unsupported.++ A boot loader or matching OpenBSD/amd64 pxeboot(8) may load the compressed+ bsddd.rd directly. Without removable media, use the console confirmations.++REBUILD AND CLEAN+ Rebuild private outputs:++ cd /usr/src/distrib/amd64/ramdisk_cd/bsddd+ make -f Makefile.bsddd clean+ make -f Makefile.bsddd++ Recover an interrupted media build first:++ make -f Makefile.bsddd unconfig-bsddd++ Before reversing the patch:++ make -f Makefile.bsddd prepare-unpatch-bsddd+ cd /usr/src+ patch -R -p1 < /root/bsddd.rd.patch+ rmdir distrib/amd64/ramdisk_cd/bsddd++VARIABLES+ BSDDD_DD_IF+ Build-time dd(1) input device. /dev/zero is the default.+ /dev/urandom supplies non-blocking pseudorandom data but may limit+ throughput. No other value is accepted.++ BSDDD_WIPE_TIMEOUT+ Build-time automatic all-disk deadline in seconds. The default,+ 0, disables it. Values from 1 through 2147483647 enable it.+ Expiry selects every eligible attached disk, including boot media.++ BSDDD_FSSIZE+ Outer miniroot size in 512-byte blocks. Default: 11392, matching+ stock ramdisk_cd.++ BSDDD_RDROOT_SIZE+ Private rdroot size. Default: 3680k, matching 7360 512-byte+ blocks. An override must match MINIROOTSIZE in RAMDISK_CD_DD.++ BSDDD_MRMAKEFSARGS+ makefs(8) arguments for the private rdroot. The default uses+ BSDDD_RDROOT_SIZE and stock filesystem parameters.++ BSDOBJDIR+ Normal OpenBSD object root and base for the private obj symlink.+ Default: /usr/obj.++ DESTDIR+ Optional prefix for installed amd64 boot files under usr/mdec.+ Default: empty.++ RELEASEDIR+ Enables install-bsddd and names its destination directory.++FILES+ distrib/amd64/ramdisk_cd/bsddd/Makefile.bsddd+ Private build wrapper.++ distrib/amd64/ramdisk_cd/bsddd/RAMDISK_CD_DD+ Private amd64 kernel configuration.++ distrib/amd64/ramdisk_cd/bsddd/list.dd+ Ramdisk overlay list.++ distrib/amd64/ramdisk_cd/bsddd/dot.profile+ Single-user profile that replaces itself with /bsddd.++ distrib/amd64/ramdisk_cd/bsddd/bsddd+ Disk enumeration, parallel overwrite, and status script.++ obj/bsddd.rd+ Uncompressed ramdisk kernel.++ obj/bsddd.gz+ Stripped and compressed kernel installed as bsddd.rd.++ obj/minirootXX_dd.img+ Bootable BIOS/UEFI whole-device image.++CAVEATS+ This operation is irreversible. Console confirmation and timeout expiry+ select every eligible attached disk, including boot media.++ A single logical overwrite pass is not a hardware secure erase. Remapped+ sectors, flash translation layers, controller caches, hidden regions, and+ inaccessible or unsupported devices may retain data. Use device-native+ sanitize, secure-erase, or cryptographic-erasure procedures when the+ required disposal policy covers those areas.++ Audible feedback requires a working PC speaker or console bell. Verify+ feedback and storage-controller support before unattended use.++SEE ALSO+ bioctl(8), dd(1), disklabel(8), make(1), MAKEDEV(8), makefs(8), pcppi(4),+ pxeboot(8), random(4), rdsetroot(8), speaker(4), sync(8), sysctl(8),+ vnconfig(8)++BSDDD.RD August 18, 2026 BSDDD.RD--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/bsddd@@ -0,0 +1,365 @@+#!/bin/ksh++set +o sh+export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/++POLL_INTERVAL=2+READY_INTERVAL=10+PROGRESS_INTERVAL=60+RESULT_INTERVAL=30+DD_IF=/dev/zero+WIPE_TIMEOUT=0++trap '' HUP INT QUIT TSTP++beep() {+ if [[ -c /dev/speaker ]] &&+ print -nr -- 'T240 O4 L8 C' >/dev/speaker 2>/dev/null; then+ return+ fi+ print -n '\a'+}++beep_pattern() {+ typeset _count=$1 _interval=$2++ while ((_count > 0)); do+ beep+ _count=$((_count - 1))+ ((_count > 0)) && sleep "$_interval"+ done+}++result_loop() {+ typeset _message=$1 _count=$2 _interval=$3++ while :; do+ echo+ echo "$_message"+ beep_pattern "$_count" "$_interval"+ sleep $RESULT_INTERVAL+ done+}++get_disks() {+ typeset _entry _disk _disks='' _oldifs=$IFS++ IFS=,+ for _entry in $(sysctl -n hw.disknames); do+ _disk=${_entry%%:*}+ case $_disk in+ sd+([0-9])|wd+([0-9]))+ _disks="${_disks}${_disks:+ }$_disk"+ ;;+ esac+ done+ IFS=$_oldifs+ print -r -- "$_disks"+}++compare_disks() {+ typeset _disk++ _removed=''+ _removed_count=0+ _added_count=0+ for _disk in $_initial_disks; do+ case " $_current_disks " in+ *" $_disk "*) ;;+ *)+ _removed="${_removed}${_removed:+ }$_disk"+ _removed_count=$((_removed_count + 1))+ ;;+ esac+ done+ for _disk in $_current_disks; do+ case " $_initial_disks " in+ *" $_disk "*) ;;+ *)+ _added_count=$((_added_count + 1))+ ;;+ esac+ done+}++start_reader() {+ rm -f /tmp/bsddd.answer+ (+ typeset _answer+ IFS= read -r _answer </dev/console || _answer=+ if [[ $_answer == [yY] ]]; then+ print -n 'Wipe every attached sd/wd disk, including boot media? [y/N] ' \+ >/dev/console+ IFS= read -r _answer </dev/console || _answer=+ fi+ print -r -- "$_answer" > /tmp/bsddd.answer+ ) &+ _reader=$!+}++stop_reader() {+ kill "$_reader" 2>/dev/null || :+ wait "$_reader" 2>/dev/null || :+ rm -f /tmp/bsddd.answer+}++wipe_disk() {+ typeset _disk=$1 _geometry _sector_size _sectors+ typeset _sectors_per_mib _full _tail _raw++ (cd /dev && sh MAKEDEV "$_disk") || return 1+ _raw=/dev/r${_disk}c+ [[ -c $_raw ]] || return 1++ _geometry=$(disklabel -d "$_disk" 2>/dev/null) || return 1+ _sector_size=$(print -r -- "$_geometry" |+ sed -n 's/^bytes\/sector:[[:space:]]*//p')+ _sectors=$(print -r -- "$_geometry" |+ sed -n 's/^total sectors:[[:space:]]*\([0-9][0-9]*\).*/\1/p')++ case $_sector_size in+ ''|*[!0-9]*) return 1 ;;+ esac+ case $_sectors in+ ''|*[!0-9]*) return 1 ;;+ esac+ ((_sector_size > 0 && _sectors > 0)) || return 1++ echo "$_disk: overwriting $_sectors sectors at $_sector_size bytes"++ if ((1048576 % _sector_size != 0)); then+ dd if="$DD_IF" of="$_raw" bs="$_sector_size" count="$_sectors" \+ status=none || return 1+ else+ _sectors_per_mib=$((1048576 / _sector_size))+ _full=$((_sectors / _sectors_per_mib))+ _tail=$((_sectors % _sectors_per_mib))++ if ((_full > 0)); then+ dd if="$DD_IF" of="$_raw" bs=1m count="$_full" \+ status=none || return 1+ fi+ if ((_tail > 0)); then+ dd if="$DD_IF" of="$_raw" bs="$_sector_size" \+ seek="$((_full * _sectors_per_mib))" count="$_tail" \+ status=none || return 1+ fi+ fi++ echo "$_disk: complete"+}++_initial_disks=$(get_disks)+if [[ -z $_initial_disks ]]; then+ result_loop 'ERROR: no eligible sd(4) or wd(4) disks were found.' 3 1+fi++echo+echo "bsddd.rd is resident in memory."+echo "Detected disks: $_initial_disks"+echo "Remove exactly one disk to begin automatically."+_timeout_deadline=0+if ((WIPE_TIMEOUT > 0)); then+ echo "Automatic all-disk wipe in $WIPE_TIMEOUT seconds."+ _timeout_deadline=$((SECONDS + WIPE_TIMEOUT))+fi+echo "Proceed without detected device removal? [y/N]"++start_reader+_ready_elapsed=$READY_INTERVAL+_last_disks=$_initial_disks+_override=no++while :; do+ _current_disks=$(get_disks)+ compare_disks+ if ((_removed_count == 1 && _added_count == 0)); then+ stop_reader+ _disks=$_current_disks+ echo+ echo "Removed disk: $_removed"+ break+ fi++ if [[ $_current_disks != "$_last_disks" ]]; then+ echo "Waiting; current disks: ${_current_disks:-none}"+ _last_disks=$_current_disks+ fi++ if [[ -s /tmp/bsddd.answer ]]; then+ _answer=$(</tmp/bsddd.answer)+ wait "$_reader" 2>/dev/null || :+ rm -f /tmp/bsddd.answer+ if [[ $_answer == [yY] ]]; then+ _override=console+ _disks=$(get_disks)+ break+ fi+ echo "Proceed without detected device removal? [y/N]"+ start_reader+ elif ! kill -0 "$_reader" 2>/dev/null; then+ wait "$_reader" 2>/dev/null || :+ start_reader+ fi++ if ((_timeout_deadline > 0 && SECONDS >= _timeout_deadline)); then+ stop_reader+ _override=timeout+ _disks=$(get_disks)+ break+ fi++ if ((_ready_elapsed >= READY_INTERVAL)); then+ if ((_timeout_deadline > 0)); then+ beep_pattern 4 0.15+ else+ beep_pattern 3 0.25+ fi+ _ready_elapsed=0+ fi+ _sleep=$POLL_INTERVAL+ if ((_timeout_deadline > 0)); then+ _remaining=$((_timeout_deadline - SECONDS))+ ((_remaining < _sleep)) && _sleep=$_remaining+ ((_sleep > 0)) || continue+ fi+ sleep "$_sleep"+ _ready_elapsed=$((_ready_elapsed + _sleep))+done++if ! _softraid_output=$(bioctl softraid0 2>/dev/null); then+ result_loop 'ERROR: cannot inspect softraid volumes.' 3 1+fi+_softraid_volumes=$(print -r -- "$_softraid_output" |+ sed -n 's/^softraid0.*\(sd[0-9][0-9]*\).*/\1/p')+for _volume in $_softraid_volumes; do+ echo "$_volume: detaching softraid volume"+ if ! bioctl -d "$_volume" >/dev/null 2>&1; then+ result_loop "ERROR: cannot detach softraid volume: $_volume" 3 1+ fi+done++_wipe_disks=+for _disk in $_disks; do+ case " $_softraid_volumes " in+ *" $_disk "*) ;;+ *) _wipe_disks="${_wipe_disks}${_wipe_disks:+ }$_disk" ;;+ esac+done+_disks=$_wipe_disks+if [[ -z $_disks ]]; then+ result_loop 'ERROR: no eligible sd(4) or wd(4) disks remain.' 3 1+fi++_status_dir=/tmp/bsddd.status+if ! rm -rf "$_status_dir" || ! mkdir "$_status_dir"; then+ result_loop 'ERROR: cannot initialize wipe status storage.' 3 1+fi+for _disk in $_disks; do+ _status_file=$_status_dir/$_disk+ _status_ok=${_status_file}.ok+ _status_failed=${_status_file}.failed+ if ! print -r -- running > "$_status_file" ||+ ! print -r -- ok > "$_status_ok" ||+ ! print -r -- failed > "$_status_failed" ||+ [[ $(<"$_status_file") != running ]] ||+ [[ $(<"$_status_ok") != ok ]] ||+ [[ $(<"$_status_failed") != failed ]]; then+ result_loop 'ERROR: cannot initialize wipe status storage.' 3 1+ fi+done++echo+if [[ $_override == console ]]; then+ echo "Console override confirmed."+elif [[ $_override == timeout ]]; then+ echo "Wipe timeout expired; all attached sd/wd disks selected."+fi+echo "Wiping disks in parallel: $_disks"+echo "Wipe input: $DD_IF"+echo "DO NOT POWER OFF."+beep_pattern 2 1++_jobs=+_total=0+for _disk in $_disks; do+ (+ _status_file=$_status_dir/$_disk+ if wipe_disk "$_disk"; then+ mv "${_status_file}.ok" "$_status_file" || exit 2+ exit 0+ else+ mv "${_status_file}.failed" "$_status_file" || exit 2+ exit 1+ fi+ ) &+ _jobs="${_jobs}${_jobs:+ }$!:$_disk"+ _total=$((_total + 1))+done++_elapsed=0+_status_error=no+while :; do+ _done=0+ for _job in $_jobs; do+ _pid=${_job%%:*}+ _disk=${_job#*:}+ _status_file=$_status_dir/$_disk+ if [[ ! -r $_status_file ]]; then+ _status_error=yes+ break+ fi+ _status=$(<"$_status_file")+ case $_status in+ ok|failed)+ _done=$((_done + 1))+ ;;+ running)+ if ! kill -0 "$_pid" 2>/dev/null; then+ _status_error=yes+ break+ fi+ ;;+ *)+ _status_error=yes+ break+ ;;+ esac+ done+ [[ $_status_error == yes ]] && break+ ((_done == _total)) && break++ sleep $POLL_INTERVAL+ _elapsed=$((_elapsed + POLL_INTERVAL))+ if ((_elapsed >= PROGRESS_INTERVAL)); then+ echo "Wipe in progress: $_done/$_total disks complete."+ beep_pattern 1 0+ _elapsed=0+ fi+done++if [[ $_status_error == yes ]]; then+ echo "ERROR: wipe status unavailable; waiting for disk jobs."+fi+for _job in $_jobs; do+ _pid=${_job%%:*}+ wait "$_pid" 2>/dev/null || :+done+sync++if [[ $_status_error == yes ]]; then+ result_loop 'ERROR: wipe status unavailable.' 3 1+fi++_failed=+for _disk in $_disks; do+ _status_file=$_status_dir/$_disk+ [[ $(<"$_status_file") == ok ]] ||+ _failed="${_failed}${_failed:+ }$_disk"+done++if [[ -n $_failed ]]; then+ result_loop "ERROR: wipe failed: $_failed" 3 1+fi++result_loop 'WIPE COMPLETE: all selected disks were overwritten.' 5 0.25--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/dot.profile@@ -0,0 +1,16 @@+set +o sh+export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/++if ! mount -u /dev/rd0a /; then+ echo "Cannot remount rd0a read-write."+ while :; do+ sleep 3600+ done+fi++exec /bsddd++echo "Cannot start /bsddd."+while :; do+ sleep 3600+done--- /dev/null+++ b/distrib/amd64/ramdisk_cd/bsddd/list.dd@@ -0,0 +1,7 @@+# $OpenBSD$++# bsddd.rd overlay.+SCRIPT ${CURDIR}/bsddd/dot.profile .profile+SCRIPT ${OBJDIR}/bsddd bsddd+SPECIAL chmod 555 bsddd+SPECIAL cd dev; sh MAKEDEV speaker
SYNOPSIS
Build on the matching OpenBSD/amd64 release after completing the normal
source and release prerequisites. BSDOBJDIR and the amd64
boot files below ${DESTDIR}/usr/mdec must exist. The
documented commands require root privileges.
Apply the patch below /usr/src, create the private object link, then build.
Installs compressed bsddd.rd and the miniroot when RELEASEDIR is set.
unconfig-bsddd
Recovers private mount and vnd(4) state after an interrupted media build. An unowned vnd(4) is not detached.
clean-bsddd, clean, cleandir
Remove private outputs without taking ownership of stock instbin objects.
prepare-unpatch-bsddd
Cleans private outputs, prints the private object path, and removes the source obj symlink.
BUILD MODEL
make obj creates a private object link below
BSDOBJDIR. The custom kernel, rdroot, instbin copy, and media
objects remain below that path.
The wrapper builds stock ramdisk_cd instbin objects in their normal
object directories, then copies instbin into private state. Stock source,
configuration, installed use, and cleanup ownership are unchanged. Do
not run bsddd concurrently with a build or clean using those
objects.
The private rdroot replaces .profile, installs
/bsddd, and creates /dev/speaker. It adds no
userland binaries. The private kernel retains the stock rdroot size and
adds pcppi(4) and speaker(4).
No parent Makefile or SUBDIR list is changed. Normal builds
and cleans do not enter the bsddd source directory or its
private objects.
# $OpenBSD$
include "arch/amd64/conf/RAMDISK_CD"
# Retain an explicit, stock-sized rdroot override for later adjustment.
option BSDDD_RDROOT
rmoption MINIROOTSIZE
option MINIROOTSIZE=7360
rmoption BSDDD_RDROOT
pcppi0 at isa?
spkr0 at pcppi?
set +o sh
exportPATH=/sbin:/bin:/usr/bin:/usr/sbin:/
if ! mount -u /dev/rd0a /;thenecho"Cannot remount rd0a read-write."while :;do
sleep 3600donefiexec /bsddd
echo"Cannot start /bsddd."while :;do
sleep 3600done
DISK SELECTION
/bsddd reads hw.disknames with
sysctl(8). It selects names matching sd[0-9]*
and wd[0-9]*. This includes softraid(4) volumes,
their backing disks, and disks presented through common amd64 storage
controllers.
Wiping begins when the current set equals the initial set minus exactly
one name. Other changes leave the ramdisk waiting. The accepted set is
frozen; later arrivals are excluded. The interlock cannot identify the
boot medium. Two explicit y responses or timeout expiry
select every eligible attached disk. The timer continues during both
prompts.
Other device classes are excluded. After the interlock,
/bsddd detaches auto-assembled softraid(4)
volumes and removes their names from the set. Their backing disks remain
selected. Inspection or detach failure stops before writes.
MAKEDEV(8) creates device nodes. disklabel(8)
supplies sector size and total sectors. dd(1) writes each
whole-disk raw character device in 1 MiB transfers followed by an exact
sector-sized tail. Disks run concurrently. One device failure does not
stop other jobs. A status failure waits for active jobs.
sync(8) precedes the final result.
Ready patterns repeat every 10 seconds. Completion and failure patterns
repeat every 30 seconds. Progress is reported every 60 seconds. A
console bell is used if speaker output fails. The result loop continues
until power-off or reset.
USE
As root, write the miniroot to removable media. This replaces the
selected device's partition table and filesystems. Verify the output
device.
set -e
dd if=./miniroot79_dd.img of=/dev/rsdXc bs=1m
sync
Boot the target. At the ready pattern, remove the boot medium. Removal of
any eligible disk satisfies the name-based interlock. Two separated bells
mark write startup. Wait for the repeating completion pattern before
power-off. Stock amd64 BIOS and UEFI boot programs are used. Secure Boot
is unsupported.
A boot loader or matching OpenBSD/amd64 pxeboot(8) may load
the compressed bsddd.rd directly. Without removable media,
use the console confirmations.
set -e
cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd prepare-unpatch-bsddd
cd /usr/src
patch -R -p1 < /root/bsddd.rd.patch
rmdir distrib/amd64/ramdisk_cd/bsddd
VARIABLES
BSDDD_DD_IF
Build-time dd(1) input device. Default: /dev/zero. /dev/urandom supplies non-blocking pseudorandom data but may limit throughput. No other value is accepted.
BSDDD_WIPE_TIMEOUT
Build-time automatic all-disk deadline in seconds. Default: 0 (disabled). Values from 1 through 2147483647 enable it. Expiry selects every eligible attached disk, including boot media.
BSDDD_FSSIZE
Outer media size in 512-byte blocks. Default: 11392, matching stock ramdisk_cd.
BSDDD_RDROOT_SIZE
Private rdroot size. Default: 3680k, matching 7360 512-byte blocks. Overrides must match MINIROOTSIZE.
BSDDD_MRMAKEFSARGS
makefs(8) arguments for the private rdroot. The default uses BSDDD_RDROOT_SIZE and stock filesystem parameters.
BSDOBJDIR
Normal object root and base for the private object link. Default: /usr/obj.
DESTDIR
Optional prefix for amd64 boot files under usr/mdec. Default: empty.
RELEASEDIR
Enables install-bsddd and names its destination.
FILES
obj/bsddd.rd
Uncompressed ramdisk kernel.
obj/bsddd.gz
Compressed kernel installed as bsddd.rd.
obj/minirootXX_dd.img
Bootable BIOS/UEFI whole-device image.
CAVEATS
This operation is irreversible. Console confirmation and timeout expiry
select every eligible attached disk, including boot media.
A single logical overwrite pass is not a hardware secure erase. Remapped
sectors, flash translation layers, controller caches, hidden regions,
and inaccessible or unsupported devices may retain data. Use
device-native sanitize, secure-erase, or cryptographic-erasure procedures
when the required disposal policy covers those areas.
Audible feedback requires a working PC speaker or console bell. Verify
feedback and storage-controller support before unattended use.
The patch adds separate bsdsshd.rd and
minirootXX_sshd.img targets. The ramdisk configures
networking from /auto_install.conf, starts sshd, and leaves
the installer environment available to the remote root user.
The default retains the local installer menu and shell.
RDSSHD_CONSOLE_LOCK=yes removes the local installer userland
session. Boot-loader and kernel console output remain visible.
The target supports remote storage preparation and installation,
including softraid(4) work with bioctl(8). Use
autoinstall(8), install.site(5), and
siteXX.tgz for fully unattended installation.
The patch adds files only. Stock source files, Makefiles,
bsd.rd, minirootXX.img, and
cdXX.iso targets are unchanged. Enhanced targets are
available only through Makefile.rdsshd.
PATCH
The aggregate patch contains the build wrapper, kernel configurations,
overlay patches, ramdisk files, and plain-text reference.
Private rdsshd paths:
obj/build/kernel enhanced kernel objects
obj/build/instbin copy of stock instbin
obj/sshobj static OpenSSH objects
obj/initsrc optional patched init source
obj/initobj optional private init objects
obj/stage generated and overlay files
obj/rdobj ramdisk assembly
obj/bsd* copied enhanced kernels
obj/boot miniroot boot file
obj/mnt miniroot mount point
obj/vnd vnd ownership record
Normal OpenBSD paths used by stock instbin:
${BSDOBJDIR}/distrib/amd64/ramdisk_cd
${BSDOBJDIR}/distrib/special
${BSDOBJDIR}/lib
The enhanced kernel, OpenSSH programs, optional init, staging tree,
ramdisk, media, mount point, and vnd record remain private. The kernel
uses config(8) -b. OpenSSH uses explicit private
MAKEOBJDIR paths. No directory below
sys/arch/amd64/compile is added or used.
Stock instbin
Installer instbin is unmodified. The wrapper runs the normal
obj targets for lib,
distrib/special, and ramdisk_cd. It builds stock
distrib/special/libstubs, invokes the stock
instbin target, then copies the result to
obj/build/instbin below the private object directory.
Component objects, reduced libraries, and crunchgen output retain their
normal paths below ${BSDOBJDIR}. The wrapper does not rewrite
generated crunchgen files. Normal OpenBSD clean targets own these stock
objects. clean-rdsshd owns the private copy and enhanced
outputs only. Enhanced and normal builds or cleans must not run
concurrently when they share instbin objects.
No parent Makefile or SUBDIR list is changed. Normal
top-level builds and cleans do not enter the rdsshd source
directory.
Object checks and media state
BSDOBJDIR must exist with normal OpenBSD ownership and
permissions. The default is /usr/obj, owned by
build:wobj with mode 770. The build rejects source-directory
fallback for both the wrapper and ramdisk_cd.
Normal and enhanced media use distinct boot files, image names, mount
points, and vnd state. Cleanup detaches only the recorded vnd covering
the absolute enhanced image path. A missing, malformed, or reused vnd is
reported and retained.
KERNEL
RAMDISK_CD_SSHD includes stock RAMDISK_CD. It
changes only the inherited stack-protection option, rdroot reservation,
and pty count.
# $OpenBSD$
include "arch/amd64/conf/RAMDISK_CD"
# Keep config(8)'s option append pointer valid while removing the tail.
option RDSSHD_RDROOT
rmoption MINIROOTSIZE
option MINIROOTSIZE=40960
rmoption RDSSHD_RDROOT
pseudo-device pty 16
MINIROOTSIZE=40960 reserves a 20 MiB rdroot. Sixteen ptys
support ssh sessions. SMALL_KERNEL remains enabled.
The temporary RDSSHD_RDROOT option protects
config(8)'s option-list append pointer while inherited tail
entries are removed. It is absent from the final configuration.
Stock installer programs remain in instbin. OpenSSH is built through its
normal Makefiles as separate static PIE executables with zlib disabled.
The ssh(1) client is not included.
The separate executables are /usr/sbin/sshd,
/usr/bin/ssh-keygen,
/usr/libexec/sshd-session, and
/usr/libexec/sshd-auth.
# $OpenBSD$
# bsdsshd.rd overlay.
MKDIR usr/libexec
MKDIR etc/ssh
MKDIR root
MKDIR root/.ssh
COPY ${OBJDIR}/../stage/sshd_config etc/ssh/sshd_config
SPECIAL test ! -s ${OBJDIR}/../stage/ssh_host_ed25519_key || install -c -m 600 -o root -g wheel ${OBJDIR}/../stage/ssh_host_ed25519_key etc/ssh/ssh_host_ed25519_key
COPY ${OBJDIR}/../stage/auto_install.conf auto_install.conf
SCRIPT ${OBJDIR}/../stage/dot.profile .profile
SCRIPT ${OBJDIR}/../stage/install.sub install.sub
SPECIAL chmod 755 install.sub
COPY ${OBJDIR}/../stage/master.passwd etc/master.passwd
COPY ${OBJDIR}/../stage/group etc/group
SPECIAL pwd_mkdb -p -d etc master.passwd; rm etc/master.passwd
COPY ${CURDIR}/rdsshd/root.profile root/.profile
COPY ${OBJDIR}/../stage/authorized_keys root/.ssh/authorized_keys
SPECIAL chmod 700 root root/.ssh; chmod 600 root/.ssh/authorized_keys
SPECIAL cd dev; sh MAKEDEV pty0 ptm
COPY ${OBJDIR}/../stage/bin/sshd usr/sbin/sshd
SPECIAL chmod 511 usr/sbin/sshd
COPY ${OBJDIR}/../stage/bin/ssh-keygen usr/bin/ssh-keygen
SPECIAL chmod 555 usr/bin/ssh-keygen
COPY ${OBJDIR}/../stage/bin/sshd-session usr/libexec/sshd-session
SPECIAL chmod 511 usr/libexec/sshd-session
COPY ${OBJDIR}/../stage/bin/sshd-auth usr/libexec/sshd-auth
SPECIAL chmod 511 usr/libexec/sshd-auth
COPY ${CURDIR}/../../../etc/etc.amd64/login.conf etc/login.conf
The overlay adds sshd configuration, authorized keys, the root profile,
login classes, early network answers, the sshd account, and pty devices.
Root retains stock instbin's -sh argv link. The root profile
leaves sh mode, sets the installer environment, and selects
TERM=vt220. The sshd Port directive is substituted only in
private staging.
The outer image follows the stock amd64 BIOS and EFI miniroot layout. It
uses private boot, mount, image, and vnd paths. The default size is 32768
512-byte blocks, or 16 MiB.
sshd permits root public-key authentication only. Forwarding, user rc,
interactive authentication, passwords, and compression are disabled.
Static PIE executables retain normal OpenBSD OpenSSH compiler and linker
protections.
Port ${RDSSHD_PORT}
HostKey /etc/ssh/ssh_host_ed25519_key
AllowUsers root
PermitRootLogin prohibit-password
AuthorizedKeysFile .ssh/authorized_keys
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
Compression no
PermitUserRC no
PrintMotd no
PrintLastLog no
DisableForwarding yes
Authorized keys
RDSSHD_AUTHORIZED_KEYS is mandatory. It may contain one or
more public keys. Blank and comment lines are removed. Every retained
line must pass ssh-keygen(1) public-key validation.
The installed file has mode 0600. /root and
/root/.ssh have mode 0700.
sed -e '/^[ ]*$$/d' -e '/^[ ]*#/d'\
< "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
test -s ${RDSSHD_STAGE}/authorized_keys
@_n=0;whileIFS=read -r _key;do\_n=$$((_n + 1));\ if ! printf '%s\n' "$$_key" | \ ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \ echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \ exit 1; \ fi; \ done < ${RDSSHD_STAGE}/authorized_keys
Build-time configuration test
If RDSSHD_HOST_KEY is empty, the build generates
obj/stage/ssh_host_ed25519_key.test and its public key. These
files exist only to give the newly built sshd -t a host key
while it validates the generated configuration.
The build removes the test configuration and key pair after validation.
They never enter the ramdisk. An interrupted build may leave them below
obj/stage; the next staging pass or cleandir
removes them.
No host key is embedded by default. First boot generates
/etc/ssh/ssh_host_ed25519_key. Normal installer CGI fetches
call feed_random before generation. An unreachable fetch
delays sshd until the normal CGI timeout.
Embedded host key
RDSSHD_HOST_KEY may name an Ed25519 host private key without
a passphrase. The build validates it and copies it to private staging
with mode 0600. The newly built sshd validates the generated configuration
against that staged key. Build-host ssh configuration and keys are not
read or modified.
An embedded key is recoverable from private objects, kernels, images,
and release copies. Key-bearing kernel and media artifacts use mode 0600.
Startup
sshd starts after donetconfig. A live numeric PID greater
than one in /var/run/sshd.pid suppresses restart. The ramdisk
has no process-inspection utility, so this checks liveness only.
install.sub creates /var/run/rdsshd.ready only
after sshd starts. Profiles use this marker because installer exit status
does not establish readiness.
The initial profile invokes install -af with early network
answers. Static IPv4 configuration includes a netmask and default route.
autoconf, dhcp, and none omit them.
Static IPv4 response fileDocumentation addresses use RFC 5737 spaceraw
System hostname = rdinstall
Network interface to configure = em0
IPv4 address for em0 = 192.0.2.10
Netmask for em0 = 255.255.255.0
Default IPv4 route = 192.0.2.1
IPv6 address for em0 = none
Network interface to configure = done
DNS domain name = example.com
DNS nameservers = 192.0.2.53
Default console
Automatic setup starts after the normal installer-menu timeout unless a
local operator selects another action. The profile returns to the local
ramdisk shell after sshd starts.
With RDSSHD_CONSOLE_LOCK=yes, a private static init replaces
/sbin/init only in the enhanced ramdisk. It is built through
the stock distrib/special/init Makefile using the dedicated
wrapper and stock pathnames.h. Patched source and objects stay
private.
The bootstrap child retains inherited /dev/null descriptors.
It never acquires /dev/console as a controlling terminal. The
profile branches before terminal setup, retries setup until the ready
marker exists, then sleeps. It presents no menu or shell. Init restarts
it after exit. The default uses stock init from instbin.
After ssh login, run install. Complete storage preparation
first. At network prompts, select done to retain the active
configuration. Reconfiguration can drop the ssh session.
CAVEATS
Console locking removes the interactive local installer userland. The
boot-loader and kernel consoles remain active. A local operator can still
change early boot state, reset, halt, or deny remote access.
RDSSHD_KERNEL_STACK_PROTECTOR=no deliberately retains
NO_PROPOLICE. This may reduce image size and weakens
mitigation of kernel stack corruption.
Protect an embedded host key, the object tree, every key-bearing image,
and installed copies. Use a distinct host key for each machine identity.
RDSSHD_FSSIZE is not auto-sized. A value too small for the
compressed kernel and boot files causes miniroot assembly to fail.
Network variables become installer response-file answers. The build does
not probe the interface or validate reachability. Incorrect values can
prevent ssh access. A locked image then has no local installer userland
with which to repair the configuration.
BUILD
Prerequisites and base build
Build on OpenBSD/amd64 with src.tar.gz and
sys.tar.gz matching the installed OpenBSD release. Normal
source-build prerequisites apply. The media target requires root for vnd,
mount, device, and ownership operations. Kernel compilation runs as
BUILDUSER.
Apply the patch below /usr/src. The configured
BSDOBJDIR root, normally /usr/obj, must already
exist.
cd /usr/src
patch -p1 < /root/bsdsshd.rd.patch
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
Run obj separately. OpenBSD make selects
.OBJDIR at startup and rejects a combined
obj rdsshd invocation. No top-level
/usr/src make obj is required. The wrapper creates only the
normal object links required by stock instbin. Custom components retain
private paths.
Static network
Supply netmask and route with a static IPv4 address. The documentation
addresses below use the reserved 192.0.2.0/24 range.
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \RDSSHD_KERNEL_STACK_PROTECTOR=no
ssh-keygen -q -t ed25519 -N '' -f /root/rdsshd_host_ed25519_key
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \RDSSHD_HOST_KEY=/root/rdsshd_host_ed25519_key
Use a distinct host key per machine identity. Protect its source, object
tree, and all resulting images. Derive its public-key fingerprint before
deployment and compare it when establishing host trust.
Repeat rdsshd. Normal dependency rules reuse current stock
instbin components, OpenSSH, private init, and the selected kernel.
Staging, ramdisk, and media are regenerated.
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
Source or toolchain changes
Clean both ownership domains after source, compiler, flag, or Makefile
changes. A top-level cleandir may replace the three stock
cleans. The next enhanced build recreates required normal object
directories. The wrapper clean never removes stock objects.
cd /usr/src/distrib/amd64/ramdisk_cd
make cleandir
cd /usr/src/distrib/special
make cleandir
cd /usr/src/lib
make cleandir
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd cleandir
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
Failed instbin trace link
Clean ramdisk_cd before retrying. Its
instbin.map and reduced archives may be incomplete.
RDSSHD_AUTHORIZED_KEYS?=RDSSHD_HOST_KEY?=RDSSHD_CONSOLE_LOCK?= no
RDSSHD_KERNEL_STACK_PROTECTOR?= yes
RDSSHD_PORT?=22RDSSHD_AI_IF?= em0
RDSSHD_AI_HOSTNAME?= rdinstall
RDSSHD_AI_IPV4?= autoconf
RDSSHD_AI_NETMASK?=255.255.255.0
RDSSHD_AI_ROUTE?= none
RDSSHD_AI_IPV6?= none
RDSSHD_AI_DOMAIN?= my.domain
RDSSHD_AI_DNS?= none
RDSSHD_FSSIZE?=32768RDSSHD_MRMAKEFSARGS?= -s 20m \
-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096rdsshd-check:
@if [ -z "${RDSSHD_AUTHORIZED_KEYS}"];then\echo"set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2;\exit1;\fi
@case "${RDSSHD_CONSOLE_LOCK:L}"in\
yes|no);;\
*)echo"RDSSHD_CONSOLE_LOCK must be yes or no" >&2;exit1;;\esac
@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}"in\
yes|no);;\
*)echo"RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2;exit1;;\esac
USE
Prepare a non-interactive next boot from the running system.
No boot prompt or console access is assumed.
Whole-device image
minirootXX_sshd.img is a complete disk image. Writing it to
a block device replaces the device's partition table and filesystems.
Verify the output device before writing it.
# Linux example. This destroys the existing contents of /dev/sda.
dd if=./miniroot79_sshd.img of=/dev/sda bs=512
sync
reboot
The running OS or storage stack may deny raw writes to the initial
sectors of the device backing its active root filesystem. Use the
boot-loader method if this cannot be changed remotely.
The image boots in UEFI or BIOS/CSM mode with Secure Boot disabled.
Firmware must already select the target device. The rdroot runs from
memory, so the installer can reuse that device as its target.
GRUB one-shot boot
Copy the uncompressed ramdisk kernel. Do not rely on gzio.
This example was tested in BIOS/CSM mode. UEFI is untested and may lack
display console output. Secure Boot is unsupported. part_gpt
and ext2 match the tested /boot filesystem. The
target layout may require different modules. The GRUB build must provide
the bsd module and
kopenbsd command. Other boot loaders are untested.
Network booting
The matching OpenBSD/amd64 pxeboot(8) can load the
compressed bsdsshd.rd over the network in place of
bsd.rd.
Remote session
After the enhanced kernel boots, connect as root when the configured
address accepts ssh. Perform required storage preparation, then start the
interactive installer.
ssh -i /path/to/private_key root@host.example
# Perform the required storage setup.# Enter "done" at the network prompt to preserve network configuration.
install
RDSSHD_CONSOLE_LOCK=yes presents no local installer menu or
shell.
REMOVE PATCH
Prepare private state before reversal. The target prints the recorded
object path and removes the source obj symlink.
/usr/obj is the default; use the printed path when different.
patch(1) removes added files but leaves their empty parent
directory. rmdir refuses non-empty directories.
A place for config files to hangout and configure things
Created ยท Updated
ngIRCd
An OpenBSD chroot orientated config for the awesome ngIRCd IRC daemon, likely useful on other platforms.
Note that there's a small DoS issue in the packaged version of ngIRCd for OpenBSD 7.9, so you might
want to build a newer version from OpenBSD ports.
There also seems to be some great work being done on pledge/unveil integration upstream.
ngircd.confChrooted ngIRCd config for OpenBSDrawโ
# Hardened for the OpenBSD net/ngircd package (ngIRCd 27).## When ChrootDir is enabled, prefer "rcctl restart ngircd" over reload.# A reload is safe only when the same configuration is readable at# /var/ngircd/etc/ngircd/ngircd.conf from inside the jail.[Global]# Placeholder: Name = <IRC_SERVER_FQDN># Example: irc.example.comName=irc.arachnogoat.com# Placeholder: Info = <SERVER_DESCRIPTION># Example: Example private IRC serverInfo=arachnogoatIRCserver# Placeholder: Network = <IRC_NETWORK_NAME> (ASCII without spaces)# Example: ExampleIRCNetwork=ArachnoIRC# Placeholder: Password = <64_HEX_RANDOM_SECRET># Generate one with: openssl rand -hex 32# Configure this as the IRC server password in every authorized client.# This shared password controls admission to the whole server; ngIRCd 27's# OpenBSD package does not provide a built-in per-user account database.Password=example_password# Disable the default unencrypted listener on port 6667.Ports=# OpenBSD rc.d does not require a PID file. Leaving this empty also lets the# OpenBSD pledge(2) patch omit its cpath promise and keeps the jail read-only.PidFile=ServerUID=_ngircdServerGID=_ngircd# Placeholder: MotdPhrase = <WELCOME_MESSAGE> (maximum 126 characters)# Example: Welcome to the example IRC server.MotdPhrase=WelcometothearachnogoatIRCserver.# Placeholder: Listen = <PUBLIC_IPV4>,<PUBLIC_IPV6># Example using RFC documentation addresses (do not deploy these addresses):# Listen = 192.0.2.10,2001:db8::10# If omitted, ngIRCd listens on every local IPv4 and IPv6 address.Listen=51.83.6.53# These limits are intentionally low[Limits]MaxConnections=10MaxConnectionsIP=5MaxJoins=1MaxListSize=1[Options]# Placeholder: ChrootDir = <CHROOT_DIRECTORY># OpenBSD package example: /var/ngircdChrootDir=/var/ngircd# Only the predefined channel below may exist.AllowedChannelTypes=AllowRemoteOper=noDNS=noIdent=noDefaultUserModes=iIMorePrivacy=yesScrubCTCP=yes# Hide client IP addresses, user names, and real names from other clients.# The cloak hash salt is intentionally generated afresh at each server start.# Placeholder: CloakHost = hidden-%x.<CLOAK_SUFFIX># Example: hidden-%x.example-ircCloakHost=hidden-%x.arachnoircCloakUserToNick=yesNoticeBeforeRegistration=noOperCanUseMode=noOperChanPAutoOp=noOperServerMode=noWebircPassword=SyslogFacility=daemon[SSL]# Placeholder: CertFile = <FULL_CHAIN_CERTIFICATE_FILE># OpenBSD acme-client example: /etc/ssl/irc.example.com.fullchain.pemCertFile=/etc/ssl/irc.arachnogoat.com.fullchain.pem# Placeholder: KeyFile = <PRIVATE_KEY_FILE># OpenBSD acme-client example: /etc/ssl/private/irc.example.com.keyKeyFile=/etc/ssl/private/irc.arachnogoat.com.key# TLS 1.3 plus forward-secret AEAD suites for TLS 1.2 (LibreSSL syntax).CipherList=TLSv1.3:ECDHE+AESGCM:ECDHE+CHACHA20# Placeholder: Ports = <TLS_PORT># Standard encrypted IRC example: Ports = 6697Ports=6697[Channel]# Placeholder: Name = <CHANNEL_NAME>, including its # prefix# Example: #arachnochatName=#arachnochat# Placeholder: Topic = <CHANNEL_TOPIC># Example: Private chatTopic=PrivatechatModes=+sntzAutojoin=yes# Optional per-user channel keys (disabled)# -----------------------------------------## NOTE: This per-user example configuration is untested in my environment,# Fully test and verify if you intend to follow these instructions.## This is only a limited approximation of user accounts. Nicknames are not# registered identities, and every client must still supply the shared global# Password above before registering with the server.## To enable separate channel-entry keys for user1, user2, and user3:## 1. Generate four independent secrets with "openssl rand -hex 32": one# undisclosed master channel key plus one key for each user.# 2. Replace the active channel settings above with:## Modes = +sntzNk <RANDOM_MASTER_CHANNEL_KEY># Autojoin = no# KeyFile = /etc/ngircd/arachnochat.keys## Mode +k enables channel-key checking, and +N prevents nickname changes# while a user is in the channel. Autojoin must be disabled because it# cannot provide an individual channel key.# 3. Create key file inside the chroot:## /var/ngircd/etc/ngircd/arachnochat.keys## Its format is "user:nick:key", without spaces. Because this configuration# uses "CloakUserToNick = yes", match any user name and the exact nickname:## *:user1:<RANDOM_KEY_FOR_USER1># *:user2:<RANDOM_KEY_FOR_USER2># *:user3:<RANDOM_KEY_FOR_USER3>## 4. Secure the file as root-owned, readable by _ngircd, and not writable by# the daemon:## chown root:_ngircd /var/ngircd/etc/ngircd/arachnochat.keys# chmod 0440 /var/ngircd/etc/ngircd/arachnochat.keys## 5. Restart ngIRCd. Each client chooses its assigned nickname and joins with:## /JOIN #arachnochat <THE_USER'S_CHANNEL_KEY>## Security limitation: channel members can retrieve the +k master channel key# using MODE and could share it. This mechanism provides convenience and# separately revocable user keys, but it is not a strong account/ACL system.
Samba
An OpenBSD (on FFS2 filesystem) optimised config for Samba built to work with MacOS/iPadOS/iOS and MS Windows clients.
# Samba 4.22 on OpenBSD/FFS2.[global]serverrole=standaloneserverworkgroup=WORKGROUPsecurity=userpassdbbackend=tdbsamntlmauth=ntlmv2-onlymaptoguest=neverguestok=nousersharemaxshares=0# Enumerate only shares accessible to the authenticated user.accessbasedshareenum=yesdisablenetbios=yessmbports=445serverminprotocol=SMB3_00servermaxprotocol=SMB3_11# Per-share encryption; sign unencrypted traffic.serversmbencrypt=desiredserversigning=mandatoryunixcharset=UTF-8casesensitive=auto# No experimental SMB3 POSIX extensions.smb3unixextensions=nowidelinks=nogetwdcache=yes# Parent permissions override creation masks.inheritpermissions=yesdeletereadonly=yesdosfilemode=yesdosfiletimes=yes# Directory leases require userspace oplocks and imply strict rename.oplocks=yeslevel2oplocks=yeskerneloplocks=nosmb2leases=yessmb3directoryleases=yes# POSIX lock interoperability; disables durable handles.kernelsharemodes=noposixlocking=yesstrictlocking=autoaioreadsize=1aiowritesize=1# Honour flushes without fsync on every write.strictsync=yessyncalways=no# Avoid FFS2 preallocation overhead.strictallocate=no# Unsupported on OpenBSD.servermultichannelsupport=nousesendfile=no# OpenBSD mkdir workaround (Samba bug 15801).vfsmkdirusetmpname=no# No native FFS2 xattrs; preserve existing ADS/EA backends and paths.vfsobjects=streams_depotxattr_tdbeasupport=yesstoredosattributes=yesstreams_depot:directory=/storage/samba/.streams_depotxattr_tdb:file=/storage/var/db/samba/xattr.tdb# Retain displaced streams as lost-* instead of deleting.streams_depot:delete_lost=nomulticastdnsregister=noloadprinters=nodisablespoolss=yesprintcapname=/dev/nulllogging=fileloglevel=0maxlogsize=1000[share]path=/storage/samba/sharereadonly=novalidusers=fileshareforceuser=fileshareforcegroup=smbusersserversmbencrypt=desired[work]# Note: This share optimised for Windows clientspath=/storage/samba/workreadonly=novalidusers=fileworkforceuser=fileworkforcegroup=smbusers# Avoid depot/TDB overhead; no ADS or persistent DOS attributes.vfsobjects=easupport=nostoredosattributes=no# Do not map DOS flags onto Unix modes.maparchive=nomaphidden=nomapsystem=nomapreadonly=no[private]path=/storage/samba/privatereadonly=novalidusers=fileprivateforceuser=fileprivateforcegroup=smbusersserversmbencrypt=required
various dotfiles
dotfiles
A place for dotfiles to hangout and do dotfile things
" ~/.vimrc" Syntax and filetype handling." Enable highlighting, indentation, and HTML-aware % matching.syntaxonfiletype plugin indent on
packadd! matchit
" Security." Ignore Vim settings embedded inside edited files.setnomodeline" Interface." Show useful position information with a little context around the cursor.setnumbersetrulersetlaststatus=2setscrolloff=3" Searching." Search as you type, highlight matches, and ignore case unless capitals are used.setincsearchsethlsearchsetignorecasesetsmartcasesetshowmatch" Indentation." Use two-space soft tabs and insert spaces instead of literal tab characters.settabstop=2setshiftwidth=2setsofttabstop=2setexpandtab" Terminal." Disable the bell and assume a dark terminal background.setvisualbellsett_vb=setbackground=dark
various scripts
scripts
A place for scripts to hangout and script things
Created ยท Updated
Blur optimised ffmpeg frame extractor
Who doesn't want rotating wallpaper sets extracted from their favourite videos!?
frame_extract.shA blur optimised frame to image extractorrawโ
#!/bin/sh# Extracts the sharpest still frame near each regular time interval.# Candidates within WINDOW are scored with FFmpeg blurdetect; the nearest is# used if none has a finite score. Exact selected frames are written as lossless# PNGs, optionally Lanczos-scaled to cover and centre-cropped to an exact size.# No sharpening, frame interpolation, or deinterlacing; existing paths are not overwritten.set -eu
LC_ALL=C
export LC_ALL
usage(){printf'Usage: %s [-w WINDOW] [-o OUTDIR] [-s WIDTHxHEIGHT] INTERVAL INPUT\n'"$0" >&2exit2}window=0.25
out=frames
size=size_set=0whilegetopts'w:o:s:' opt;docase$optin
w)window=$OPTARG;;
o)out=$OPTARG;;
s)size=$OPTARG;size_set=1;;
*) usage ;;esacdoneshift$((OPTIND-1))["$#" -eq 2]|| usage
interval=$1input=$2if["$size_set" -eq 1];thencase$sizein
*[!0-9x]* | *x*x* | x* | *x) usage ;;esacwidth=${size%%x*}height=${size#*x}case$widthin *[1-9]*);; *) usage ;;esaccase$heightin *[1-9]*);; *) usage ;;esacelsewidth=height=fi
awk -v i="$interval" -v w="$window"' BEGIN { re = "^[0-9]+([.][0-9]+)?$" exit !(i ~ re && w ~ re && i > 0 && w >= 0 && w < i / 2) }'||{printf'%s\n'\"INTERVAL must be > 0; WINDOW must be >= 0 and < INTERVAL/2" >&2exit2}command -v ffmpeg >/dev/null 2>&1||{printf'%s\n'"ffmpeg not found" >&2exit127}# Force relative pathnames to be treated as local files, not FFmpeg URLs.case$inputin /*);; *)input=./$input;;esaccase$outin /*);; *)out=./$out;;esac[ -f "$input"]||{printf'Input is not a regular file: %s\n'"$input" >&2exit1}[ ! -e "$out"]&&[ ! -L "$out"]||{printf'Output path already exists: %s\n'"$out" >&2exit1}tmpbase=${TMPDIR:-/tmp}case$tmpbasein /*);; *)tmpbase=./$tmpbase;;esactmp=$(mktemp -d "$tmpbase/bestframes.XXXXXX")||{printf'%s\n'"Could not create temporary directory" >&2exit1}[ -n "$tmp"]&&[ -d "$tmp"]||{printf'%s\n'"mktemp did not create a temporary directory" >&2exit1}
cleanup(){
rm -f "$tmp/meta""$tmp/picks""$tmp/filter"2>/dev/null || :
rmdir "$tmp"2>/dev/null || :
}trap cleanup 0trap'exit 1' HUP INT TERM
printf'[1/2] Analysing candidate frames...\n' >&2
ffmpeg -nostdin -hide_banner -loglevel error \
-stats -stats_period 2\
-i "$input"\
-map 0:v:0 \
-vf "setpts=PTS-STARTPTS,select='lte(abs(t-round(t/$interval)*$interval)\,$window)',blurdetect=block_width=32:block_height=32:block_pct=80,metadata=mode=print:key=lavfi.blur:file=-"\
-f null /dev/null \
>"$tmp/meta"
awk -v interval="$interval"' /^frame:/ { pts = time = "" for (i = 1; i <= NF; i++) { if ($i ~ /^pts:/) { pts = $i sub(/^pts:/, "", pts) } else if ($i ~ /^pts_time:/) { time = $i sub(/^pts_time:/, "", time) } } next } /^lavfi\.blur=/ && pts != "" && time != "" { blur = $0 sub(/^[^=]*=/, "", blur) b = int(time / interval + 0.5) dist = time - b * interval if (dist < 0) dist = -dist # Fallback: nearest candidate if no finite blur score is available. if (!(b in nearpts) || dist < neardist[b]) { nearpts[b] = pts neardist[b] = dist } if (blur ~ /^[+-]?(([0-9]+([.][0-9]*)?)|([.][0-9]+))([eE][+-]?[0-9]+)?$/) { score = blur + 0 if (!(b in bestpts) || score < bestblur[b] || (score == bestblur[b] && dist < bestdist[b])) { bestpts[b] = pts bestblur[b] = score bestdist[b] = dist } } } END { for (b in nearpts) { if (b in bestpts) print bestpts[b] else print nearpts[b] } }'"$tmp/meta" >"$tmp/picks"count=$(awk 'END { print NR + 0 }'"$tmp/picks")["$count" -gt 0]||{printf'%s\n'"No suitable frames found" >&2exit1}printf'[1/2] Analysis complete: %s frames selected.\n'"$count" >&2# Build a balanced expression tree. This avoids FFmpeg parser-depth# problems from a long flat eq()+eq()+eq()+... expression. If requested,# scale to cover the target with square pixels, then crop exactly at centre.
awk -v width="$width" -v height="$height"' { a[NR] = "eq(pts\\," $1 ")" } END { n = NR while (n > 1) { m = 0 for (i = 1; i <= n; i += 2) { m++ if (i < n) b[m] = "(" a[i] "+" a[i + 1] ")" else b[m] = a[i] } for (i = 1; i <= m; i++) a[i] = b[i] n = m } filter = "setpts=PTS-STARTPTS,select=" a[1] if (width != "") filter = filter ",scale=w=" width ":h=" height \ ":force_original_aspect_ratio=increase" \ ":flags=lanczos+accurate_rnd+full_chroma_int:reset_sar=1" \ ",crop=w=" width ":h=" height ":exact=1" print filter }'"$tmp/picks" >"$tmp/filter"
mkdir "$out"||{printf'Could not create output directory: %s\n'"$out" >&2exit1}printf'[2/2] Extracting %s lossless PNG frames...\n'"$count" >&2if ! ffmpeg -nostdin -hide_banner -loglevel error \
-stats -stats_period 2 -n \
-i "$input"\
-map 0:v:0 \
-/filter:v "$tmp/filter"\
-fps_mode passthrough \
-start_number 0\"$out/frame_%06d.png"thenprintf'Extraction failed; partial output may remain in: %s\n'"$out" >&2exit1fiactual=0for file in"$out"/frame_*.png;do[ -f "$file"]||continueactual=$((actual+1))done["$actual" -eq "$count"]||{printf'Expected %s frames but wrote %s; output left in: %s\n'\"$count""$actual""$out" >&2exit1}printf'Done: %s frames written to %s/\n'"$count""$out" >&2
ffmpeg parallel AAC-LC converter
The Malazan audiobooks are too huge for Rockbox when using AAC-HE encoding
Irssi normally places server status and the joined channel in separate
windows. Use /WIN 1 for the first window and
/WIN 2 for the second. The status bar shows the actual window
numbers when they differ.
AUTOMATIC CONNECTION
Add a named network, its server, and the channel to Irssi, then save the
configuration. Irssi will reconnect and join #chat on later
starts.
Persistent Irssi configurationReplace SERVER_PASSWORD before useraw