Merge pull request #1544 from kernelkit/misc

This commit is contained in:
Joachim Wiberg
2026-06-22 08:39:14 +02:00
committed by GitHub
31 changed files with 478 additions and 323 deletions
+1 -1
View File
@@ -272,7 +272,7 @@ other contributions that are not aligned to this Code of Conduct."*
[PEP-8]: https://peps.python.org/pep-0008/
[RDD]: https://tom.preston-werner.com/2010/08/23/readme-driven-development
[cbeams]: https://cbea.ms/git-commit/#seven-rules
[conduct]: CODE-OF-CONDUCT.md
[conduct]: CODE_OF_CONDUCT.md
[DCO]: https://developercertificate.org/
[closing]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
[gpg-verify]: https://docs.github.com/en/authentication/managing-commit-signature-verification
+1 -7
View File
@@ -113,12 +113,6 @@ jobs:
pattern: "artifact-*"
merge-multiple: true
- name: Create checksums ...
run: |
for file in *.tar.gz; do
sha256sum $file > $file.sha256
done
- uses: ncipollo/release-action@v1
with:
allowUpdates: true
@@ -128,7 +122,7 @@ jobs:
prerelease: true
tag: "latest-boot"
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: "*.tar.gz*"
artifacts: "*.tar.gz"
- name: Summary
run: |
+1 -10
View File
@@ -223,15 +223,6 @@ jobs:
output/images/*-emmc.img*
retention-days: 30
- name: Create checksums
run: |
cd output/images/
for file in *-sdcard.img *-emmc.img; do
if [ -f "$file" ]; then
sha256sum "$file" > "$file.sha256"
fi
done
- name: Upload to release
uses: ncipollo/release-action@v1
with:
@@ -242,7 +233,7 @@ jobs:
prerelease: true
tag: "latest-boot"
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: "output/images/*-sdcard.img*,output/images/*-emmc.img*"
artifacts: "output/images/*-sdcard.img,output/images/*-emmc.img"
- name: Generate summary
run: |
-88
View File
@@ -1,88 +0,0 @@
name: Manny the Manager
on:
workflow_dispatch:
inputs:
checkout:
required: true
type: boolean
cleanup:
required: true
type: boolean
peek:
required: true
type: boolean
jobs:
inventory:
runs-on: ubuntu-latest
steps:
- name: Disk usage ...
run: |
cd
du -hs .[^.]*
- name: Disk inventory (1/2) ...
run: |
echo "df -h ========================================================================="
df -h
echo "mounts ========================================================================"
mount
- name: File inventory (1/2) ...
run: |
echo "Current directory: $(pwd)"
echo "Files in $HOME ================================================================"
ls $HOME
echo "Find $HOME ===================================================================="
find $HOME
- name: Container inventory ...
run: |
echo "Available container images: ==================================================="
docker images
echo "Available containers: ========================================================="
docker ps -a
checkout:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: kernelkit/actions/cache-restore@v1
with:
target: x86_64
dl-prefix: dl-netconf
- name: Disk inventory (2/2) ...
run: |
echo "df -h ========================================================================="
df -h
echo "mounts ========================================================================"
mount
- name: File inventory (2/2) ...
run: |
echo "Current directory: $(pwd)"
echo "Files in $HOME ================================================================"
ls $HOME
echo "Find $HOME ===================================================================="
find $HOME
peeky:
if: ${{ inputs.peek }}
runs-on: ubuntu-latest
steps:
- name: Peek & Poke ...
run: |
whoami
ls -l /mnt/
cat /mnt/DATALOSS_WARNING_README.txt
sudo mkdir /mnt/x-aarch64
sudo chown $(id -un):$(id -gn) /mnt/x-aarch64
ls -l /mnt/
cleanup:
if: ${{ inputs.cleanup }}
needs: [inventory, peeky]
runs-on: ubuntu-latest
steps:
- name: Cleaning up cruft ...
run: |
docker image prune -af
docker volume prune -f
docker container prune -f
+1 -7
View File
@@ -16,12 +16,6 @@ jobs:
pattern: "artifact-*"
merge-multiple: true
- name: Create checksums ...
run: |
for file in *.tar.gz; do
sha256sum $file > $file.sha256
done
- uses: ncipollo/release-action@v1
with:
allowUpdates: true
@@ -32,7 +26,7 @@ jobs:
prerelease: true
tag: "latest"
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: "*.tar.gz*"
artifacts: "*.tar.gz"
- name: Summary
run: |
+1 -12
View File
@@ -83,17 +83,6 @@ jobs:
pattern: "artifact-*"
merge-multiple: true
- name: Create checksums ...
run: |
for file in *.tar.gz; do
sha256sum $file > $file.sha256
done
if ls *.qcow2 &>/dev/null; then
for file in *.qcow2; do
sha256sum "$file" > "$file.sha256"
done
fi
- name: Extract ChangeLog entry ...
run: |
cat doc/ChangeLog.md | ./utils/extract-changelog.sh > release.md
@@ -106,7 +95,7 @@ jobs:
makeLatest: ${{ steps.rel.outputs.latest }}
discussionCategory: ${{ steps.rel.outputs.cat }}
bodyFile: release.md
artifacts: "*.tar.gz*,*.qcow2*"
artifacts: "*.tar.gz,*.qcow2"
- name: Summary
run: |
+1 -12
View File
@@ -27,17 +27,6 @@ jobs:
pattern: "artifact-*"
merge-multiple: true
- name: Create checksums
run: |
for file in *.tar.gz; do
sha256sum $file > $file.sha256
done
if ls *.qcow2 &>/dev/null; then
for file in *.qcow2; do
sha256sum "$file" > "$file.sha256"
done
fi
- uses: ncipollo/release-action@v1
with:
tag: latest
@@ -54,7 +43,7 @@ jobs:
**Commit:** ${{ github.sha }}
**Built:** ${{ github.run_id }}
artifacts: "*.tar.gz*,*.qcow2*"
artifacts: "*.tar.gz,*.qcow2"
- name: Summary
run: |
+116 -93
View File
@@ -1,63 +1,21 @@
[![License Badge][]][License] [![Release Badge][]][Release] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan] [![Discord][discord-badge]][discord-url]
[![License Badge][]][License] [![Release Badge][]][Release] [![GitHub Status][]][GitHub] [![Discord][discord-badge]][discord-url]
<img align="right" src="doc/logo.png" alt="Infix — Immutable.Friendly.Secure" width=480 padding=10>
<img align="right" src="doc/logo.png" alt="Infix — Immutable.Friendly.Secure" width=380 padding=10>
Turn any ARM or x86 device into a powerful, manageable network appliance
in minutes. From $35 Raspberry Pi boards to enterprise switches — deploy
routers, IoT gateways, edge devices, or custom network solutions that
just work.
Infix turns an ARM or x86 device into a managed network appliance. The
same OS runs on a $35 Raspberry Pi and on enterprise switching hardware,
so you can build a router, an IoT gateway, or an edge device on whatever
you have on hand.
## Our Values
More in-depth material is available in our blog and User Guide:
**🔒 Immutable**
Your system never breaks. Read-only filesystem with atomic upgrades
means no configuration drift, no corrupted updates, and instant rollback
if something goes wrong. Deploy once, trust forever.
- <https://www.kernelkit.org/>
- <https://www.kernelkit.org/infix/>
**🤝 Friendly**
Actually easy to use. Auto-generated CLI from standard YANG models comes
with built-in help for every command — just hit <kbd>?</kbd> or
<kbd>TAB</kbd> for context-aware assistance.
## See it in action
Familiar NETCONF & RESTCONF APIs and [comprehensive documentation][4]
mean you're never stuck. Whether you're learning networking or managing
enterprise infrastructure.
**🛡️ Secure**
Built with security as a foundation, not an afterthought. Minimal
attack surface, separation between system and data, and container
isolation. Sleep better knowing your infrastructure is protected.
## Why Choose Infix
**Hardware Flexibility**: Start with a $35 Raspberry Pi, scale to
enterprise switching hardware. Same OS, same tools, same reliability.
**Standards-Based**: Built around YANG models and IETF standards. Learn
once, use everywhere - no vendor lock-in.
**Container Ready**: Run your applications alongside networking
functions. GPIO access, dedicated Ethernet ports, custom protocols —
your device, your rules.
## Use Cases
1. **Home Labs & Hobbyists**:
Transform a Raspberry Pi into a full-featured router with WiFi
1. **IoT & Edge Computing**:
Bridge devices to the cloud with reliable, updatable gateways
1. **Small Business Networks**:
Enterprise-grade features without the complexity or cost
1. **Developers & Makers**:
Test networking concepts, prototype IoT solutions, or build custom
appliances
1. **Network Professionals**:
Consistent tooling from development to production deployment.
How about a digital twin using raw Qemu or [GNS3](https://gns3.com/infix)!
## Quick Example
Configure an interface in seconds - the CLI guides you with built-in help:
The CLI is generated from the [YANG models][inside], so it guides you with
built-in help. Here's setting an IP address on an interface:
<pre><code>admin@infix-12-34-56:/> <b>configure</b>
admin@infix-12-34-56:/config/> <b>edit interface eth0</b>
@@ -94,38 +52,94 @@ eth0 ethernet UP 52:54:00:12:34:56
admin@infix-12-34-56:/> <b>copy running startup</b>
</code></pre>
Notice how <kbd>TAB</kbd> completion shows available options, `show`
displays current config, and `diff` shows exactly what changed before
you commit your changes with the `leave` command.
<kbd>TAB</kbd> completes available options and <kbd>?</kbd> shows online help
for each option and argument. `show` displays the current config, and `diff`
shows exactly what changed before you commit it with `leave`. See the [CLI
documentation][3] for more.
For more information, see [CLI documentation][3].
## Web interface
## Get Started
If the CLI isn't your style, the same configuration is available through the
web interface. Log in from a browser, keep an eye on your device from the
Status dashboard and use the Configure > Interface setup wizard to create more
advanced setups, or just fold out an interface to add an IP address.
Get [pre-built images][5] for your hardware. Use the CLI, web
interface, or standard NETCONF/RESTCONF tools, e.g., `curl`. Add
containers for any custom functionality you need.
<p>
<a href="doc/img/webui-login.png"><img src="doc/img/webui-login.png" alt="Login" align="top" width=220></a>
<a href="doc/img/webui-dashboard.png"><img src="doc/img/webui-dashboard.png" alt="Dashboard" width=290></a>
<a href="doc/img/webui-wizard.png"><img src="doc/img/webui-wizard.png" alt="Setup wizard" width=260></a>
</p>
### Supported Platforms
The web interface is built on the same concepts as the CLI, so operational
status and state are kept separate from configuration and commands.
- **Raspberry Pi 2B/3B/4B/CM4** - Perfect for home labs, learning, and prototyping
- **Banana Pi-R3** - Your next home router and gateway
- **NanoPi R2S** - Compact dual-port router in a tiny package
- **x86_64** - Run in VMs or on mini PCs for development and testing
- **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM64 platforms
- **Microchip SparX-5i** - Enterprise switching capabilities
- **Microchip SAMA7G54-EK** - ARM Cortex-A7
- **NXP i.MX8MP EVK** - Highly capable ARM64 SoC
- **StarFive VisionFive2** - RISC-V architecture support
## Try it in 5 minutes
*Why start with Raspberry Pi?* It's affordable, widely available, has
built-in WiFi + Ethernet, and runs the exact same Infix OS you'd deploy
in production. Perfect for learning, prototyping, or even small-scale
deployments.
You don't need hardware to get started:
- **In a virtual lab** — run a full topology in [GNS3][gns3-post] and test
networks entirely in software.
- **From source** — [build it and `make run`][build-post] to boot Infix in
QEMU, from `git clone` to pinging the internet.
- **On real hardware** — grab a [pre-built image][5] for your board, or run
the `x86_64` image in any VM.
Log in with `admin` / `admin` on the virtual and pre-built images. On
shipped products the factory-reset credentials are customizable — we
typically provision a unique per-device password stored in EEPROM/VPD.
## Supported hardware
- **Raspberry Pi 2B/3B/4B/CM4** - a good starting point; built-in WiFi and Ethernet
- **Banana Pi-R64/R3/R3 Mini/R4** - multi-port routers and gateways
- **NanoPi R2S** - compact dual-port router
- **x86_64** - VMs and mini PCs, for development or production
- **Marvell CN9130 CRB, EspressoBIN** - ARM64 development boards
- **Microchip SparX-5i** - enterprise switching
- **Microchip SAMA7G54-EK** - ARM Cortex-A7 evaluation kit
- **NXP i.MX8MP EVK** - ARM64 SoC evaluation kit
- **StarFive VisionFive2** - RISC-V board
*Why start with Raspberry Pi?* It's cheap, easy to get hold of, has
built-in WiFi and Ethernet, and runs the same Infix you'd deploy in
production — so what you learn on it carries straight over.
> [!TIP]
> 📖 **[Complete documentation][4]** • 💬 **[Join our Discord][discord-url]**
## Technical Details
## Why Infix
**🔒 Immutable**
Read-only filesystem with atomic upgrades. An update either applies
cleanly or rolls back, so a failed upgrade or a power cut midway through
won't leave you with a half-broken system.
**🤝 Friendly**
The CLI is generated from the YANG models, so every command carries its
own help — hit <kbd>?</kbd> or <kbd>TAB</kbd> to see what's available.
The same models are reachable over NETCONF and RESTCONF, with
[documentation][4] for when you get stuck.
**🛡️ Secure**
A small attack surface, separation between system and data, and
container isolation. Since the system partition is read-only, a
compromised service or container can't rewrite the OS underneath it.
## Use cases
1. **Home labs & hobbyists**:
Turn a Raspberry Pi into a router with WiFi
1. **IoT & edge**:
Build gateways you can update in the field
1. **Small business networks**:
Routing, firewalling, and VLANs on affordable hardware
1. **Developers & makers**:
Prototype networking ideas, or build a custom appliance with containers
1. **Network professionals**:
The same tooling from lab to production — spin up a digital twin in raw
Qemu or [GNS3](https://gns3.com/infix)
## Under the hood
<a href="https://bitsign.se">
<picture>
@@ -135,27 +149,29 @@ deployments.
</picture>
</a>
Built on proven open-source foundations: [Linux][0], [Buildroot][1], and
[sysrepo][2] — for reliability you can trust:
Built on [Linux][0], [Buildroot][1], and [sysrepo][2]:
- **Immutable OS**: Read-only filesystem, atomic updates, instant rollback
- **YANG Configuration**: Industry-standard models with auto-generated tooling
- **Hardware Acceleration**: Linux switchdev support for wire-speed packet processing
- **Container Integration**: Docker support with flexible network and hardware access
- **Memory Efficient**: Runs comfortably on devices with as little as 256 MB RAM
- **Code Signing**: Releases are cryptographically signed for integrity verification
- **Immutable OS**: read-only filesystem, atomic updates, rollback on failure
- **YANG configuration**: standard models with an auto-generated CLI and APIs
- **Hardware acceleration**: switchdev offload for wire-speed forwarding
- **Container integration**: Docker, with access to host network and hardware
- **Memory efficient**: runs on devices with as little as 256 MB RAM
- **Code signing**: releases are cryptographically signed
Perfect for everything from resource-constrained edge devices to
high-throughput network appliances.
With the entire system modeled in YANG, scalability is no longer an
issue, be it in development, testing, or end users deploying and
monitoring their devices. All knobs and dials are accessible from the
CLI (console/SSH), or remotely using the native NETCONF or RESTCONF
APIs.
Because the whole system is modeled in YANG, every setting is reachable
the same way: from the CLI over console or SSH, or remotely over the
native NETCONF and RESTCONF APIs. The same models drive development,
testing, and day-to-day monitoring.
> Check the *[Latest Build][]* for bleeding-edge features.
## Contributing
Bug reports, ideas, and pull requests are welcome. Start with
[CONTRIBUTING][contributing] and the [code of conduct][coc]. Found a
security issue? Follow the [security policy][security]. Need a hand?
See [support options][support] or [join us on Discord][discord-url].
---
<div align="center">
@@ -171,6 +187,13 @@ APIs.
[3]: https://www.kernelkit.org/infix/latest/cli/introduction/
[4]: https://www.kernelkit.org/infix/
[5]: https://github.com/kernelkit/infix/releases/latest
[inside]: https://www.kernelkit.org/posts/inside-infix/
[gns3-post]: https://www.kernelkit.org/posts/infix-in-gns3/
[build-post]: https://www.kernelkit.org/posts/building-infix-from-source/
[contributing]: .github/CONTRIBUTING.md
[coc]: .github/CODE_OF_CONDUCT.md
[security]: .github/SECURITY.md
[support]: .github/SUPPORT.md
[Latest Build]: https://github.com/kernelkit/infix/releases/tag/latest "Latest build"
[License]: https://en.wikipedia.org/wiki/GPL_license
[License Badge]: https://img.shields.io/badge/License-GPL%20v2-blue.svg
+2
View File
@@ -30,6 +30,8 @@ All notable changes to the project are documented in this file.
### Fixes
- Fix #941: a VETH pair can now connect two containers directly, with both
ends assigned to containers.
- Enabling IP masquerading in the firewall no longer enables IP forwarding on
all interfaces. This has been an issue ever since the firewall support was
introduced in v25.10.0
+3 -5
View File
@@ -668,11 +668,9 @@ set:
For an example of both, see the next section.
> [!IMPORTANT]
> **VETH Pair Limitation:** When using VETH pairs with containers, at least
> one side of the pair must remain in the host namespace. It is currently
> not possible to create VETH pairs where both ends are assigned to different
> containers. One end must always be accessible from the host.
> [!TIP]
> Both ends of a VETH pair may be assigned to containers, connecting two
> containers directly without involving the host namespace.
[^3]: Something which the container bridge network type does behind the
scenes with one end of an automatically created VETH pair.
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+18 -5
View File
@@ -544,6 +544,13 @@ def mdns(args: List[str]) -> None:
cli_pretty(data, "show-mdns")
# Sensor names that represent the SoC/CPU temperature (not per-port PHYs).
# Matches "cpu"/"soc"/"core", and Marvell CN913x "ap-*" / "cp<N>-*" zones.
# Note the hyphen after "cp<N>" so mangled PHY names like "cp0busbus…" never
# match.
SOC_TEMP_RE = re.compile(r'^(cpu|soc|core|ap-|cp\d+-)')
def system(args: List[str]) -> None:
# Get system state from sysrepo
data = get_json("/ietf-system:system-state")
@@ -562,6 +569,7 @@ def system(args: List[str]) -> None:
fan_rpm = None
if hardware_data and "ietf-hardware:hardware" in hardware_data:
components = hardware_data.get("ietf-hardware:hardware", {}).get("component", [])
soc_temps = []
for component in components:
sensor_data = component.get("sensor-data", {})
if not sensor_data:
@@ -570,16 +578,21 @@ def system(args: List[str]) -> None:
name = component.get("name", "")
value_type = sensor_data.get("value-type")
# Only capture CPU/SoC temperature (ignore phy, sfp, etc.)
# Different platforms use different names: cpu, soc, core, etc.
if value_type == "celsius" and name in ("cpu", "soc", "core") and cpu_temp is None:
temp_millidegrees = sensor_data.get("value", 0)
cpu_temp = temp_millidegrees / 1000.0
# Capture SoC/CPU temperature, ignoring per-port phy, sfp, etc.
# Platforms name the zone differently: a plain "cpu"/"soc"/"core",
# or, on Marvell CN913x, an "ap-*" (application processor) or
# "cp<N>-*" (communication processor) cluster. Collect them all
# and report the hottest as the representative SoC temperature.
if value_type == "celsius" and SOC_TEMP_RE.match(name):
soc_temps.append(sensor_data.get("value", 0) / 1000.0)
# Capture fan speed if available
elif value_type == "rpm" and fan_rpm is None:
fan_rpm = sensor_data.get("value", 0)
if soc_temps:
cpu_temp = max(soc_temps)
if cpu_temp is not None:
runtime["cpu_temp"] = cpu_temp
if fan_rpm is not None:
+10
View File
@@ -414,6 +414,16 @@ int cni_netdag_gen_iface(struct dagger *net, const char *ifname,
return -EIO;
fprintf(fp, "container -a -f delete network %s >/dev/null\n", ifname);
/* If this end belongs to a veth pair, the kernel keeps the pair
* alive after CNI host-device returns the interface to the host
* namespace. Remove it here, once the container is gone, so the
* pair does not linger and block a later re-creation. Tolerant:
* the peer's teardown may already have removed it.
*/
if (lydx_get_child(dif, "veth"))
fprintf(fp, "ip link del dev %s 2>/dev/null || true\n", ifname);
fclose(fp);
if (cni_type == IFT_BRIDGE)
+15 -10
View File
@@ -21,23 +21,28 @@
bool veth_is_primary(struct lyd_node *cif)
{
struct lyd_node *peer, *veth;
bool self_cni, peer_cni;
const char *peername;
veth = lydx_get_child(cif, "veth");
peername = lydx_get_cattr(veth, "peer");
peer = lydx_find_by_name(lyd_parent(cif), "interface", peername);
/* At the moment, CNI code relies on one side of the pair
* remaining in the host namespace, and that that interface
* takes care of creating the pair.
*/
if (lydx_get_child(cif, "container-network"))
return false;
if (lydx_get_child(peer, "container-network"))
return true;
self_cni = lydx_get_child(cif, "container-network") != NULL;
peer_cni = lydx_get_child(peer, "container-network") != NULL;
return strcmp(lydx_get_cattr(cif, "name"),
lydx_get_cattr(veth, "peer")) < 0;
/* When exactly one end is handed to a container (CNI host-device),
* the other end stays in the host namespace and creates the pair.
*/
if (self_cni != peer_cni)
return peer_cni;
/* Neither or both ends are container interfaces: pick a stable
* primary by name so exactly one end creates the pair. When both
* ends are containers the pair is still created in the host
* namespace first, then moved into each container by CNI host-device.
*/
return strcmp(lydx_get_cattr(cif, "name"), peername) < 0;
}
int ifchange_cand_infer_veth(sr_session_ctx_t *session, const char *path)
+36 -1
View File
@@ -518,6 +518,14 @@ static int veth_gen_del(struct lyd_node *dif, FILE *sh)
if (!veth_is_primary(dif))
return 0;
/* When the primary end is itself a container interface it currently
* lives in the container's namespace, so a host-namespace delete here
* would fail and abort the teardown. Its removal is handled after the
* container is gone, see cni_netdag_gen_iface().
*/
if (lydx_get_child(dif, "container-network"))
return 0;
return link_gen_del(dif, sh);
}
@@ -571,6 +579,28 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
return 0;
}
/*
* Both ends of a veth pair can be handed to containers, leaving no
* host-side interface to create the pair. Have the primary end create it
* in the host namespace early (NETDAG_INIT_PHYS, before the container is
* (re)started); CNI host-device then moves each end into its container.
*/
static int veth_gen_host(struct dagger *net, struct lyd_node *dif, struct lyd_node *cif)
{
const char *ifname = lydx_get_cattr(cif, "name");
FILE *ip;
int err;
ip = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PHYS, "init.ip");
if (!ip)
return -EIO;
err = veth_gen(dif, cif, ip);
fclose(ip);
return err;
}
static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype)
{
if (!strcmp(iftype, "infix-if-type:ethernet")) {
@@ -604,8 +634,13 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
if ((err = cni_netdag_gen_iface(net, ifname, dif, cif))) {
/* error or managed by CNI/podman */
if (err > 0)
if (err > 0) {
err = 0; /* done, nothing more to do here */
if (op == LYDX_OP_CREATE && lydx_get_child(cif, "veth") &&
veth_is_primary(cif))
err = veth_gen_host(net, dif, cif);
}
goto err;
}
+1 -5
View File
@@ -59,11 +59,7 @@ submodule infix-if-container {
identity host {
base container-network;
description "Host device, e.g., one end of a VETH pair or other host interface.
Note: When using VETH pairs, at least one side must remain in the
host namespace. Both ends of a VETH pair cannot be assigned to
different containers.";
description "Host device, e.g., one end of a VETH pair or other host interface.";
}
/*
+1 -5
View File
@@ -13,11 +13,7 @@ submodule infix-if-veth {
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Linux virtual Ethernet pair extension for ietf-interfaces.
Note: When using VETH pairs with containers, at least one side
of the pair must remain in the host namespace. Both ends of a
VETH pair cannot be assigned to different containers.";
description "Linux virtual Ethernet pair extension for ietf-interfaces.";
revision 2023-06-05 {
description "Initial revision.";
+17 -4
View File
@@ -916,7 +916,12 @@ class Sensor:
# Standalone sensor without description: use name as-is
display_name = self.name
row = f"{indent_str}{display_name:<{PadSensor.name - len(indent_str)}}"
# Truncate over-long names so they never spill into the VALUE column
# (e.g. unmapped switch-PHY hwmon names derived from the DT path).
field = PadSensor.name - len(indent_str)
if len(display_name) >= field:
display_name = display_name[:field - 2] + ""
row = f"{indent_str}{display_name:<{field}}"
# For colored value, pad manually to account for ANSI codes
value_str = self.get_formatted_value()
# Count visible characters (strip ANSI codes for length calculation)
@@ -2245,6 +2250,13 @@ def show_services(json):
service_table.print()
def sensor_sort_key(component):
"""Natural sort key for sensor names: digit runs compare numerically so
e2 sorts before e10, while keeping ap-cpu/cp0-ic/sfp groups together."""
name = component.get("name", "")
return [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', name)]
def show_hardware(json):
if not json.get("ietf-hardware:hardware"):
print("Error, top level \"ietf-hardware:component\" missing")
@@ -2418,15 +2430,16 @@ def show_hardware(json):
print(f"\n{module_name}:")
if module_name in children:
for child in sorted(children[module_name], key=lambda c: c.get("name", "")):
for child in sorted(children[module_name], key=sensor_sort_key):
sensor = Sensor(child)
sensor.print(indent=1)
# Display standalone sensors (no parent)
# Display standalone sensors (no parent), naturally sorted so port
# temperatures read e1, e2, ... e28 rather than e1, e10, e11, ...
if standalone:
if modules:
print() # Add blank line between modules and standalone
for component in sorted(standalone, key=lambda c: c.get("name", "")):
for component in sorted(standalone, key=sensor_sort_key):
sensor = Sensor(component)
sensor.print()
+41
View File
@@ -149,6 +149,36 @@ def normalize_sensor_name(name):
return name
def _dt_phandle(path):
"""Read a device-tree phandle cell as a normalized hex string.
phandle/phy-handle properties are 4-byte big-endian cells. Read them via
od(1) so the binary content survives the text-based HOST transport (works
both locally and over the ssh-style remote transport).
"""
out = HOST.run(("od", "-An", "-tx1", path), default="")
return "".join(out.split()) if out else None
def phy_handle_to_ifname():
"""Map a PHY's device-tree phandle to the interface it drives.
DSA user ports carry a "phy-handle" pointing at the PHY that serves them.
The reverse map lets us name a switch PHY's hwmon temperature sensor after
the front-panel port (e.g. e1) instead of the unreadable name the kernel
derives from the full device-tree path (cp0busbusf2000000mdio...).
"""
mapping = {}
for ifname in HOST.run(("ls", "/sys/class/net"), default="").split():
handle_path = os.path.join("/sys/class/net", ifname, "of_node", "phy-handle")
if not HOST.exists(handle_path):
continue
handle = _dt_phandle(handle_path)
if handle:
mapping[handle] = ifname
return mapping
def get_wifi_phy_info():
"""
Discover WiFi PHYs using iw list command.
@@ -218,6 +248,7 @@ def hwmon_sensor_components():
"""
components = []
device_sensors = {} # Track {device_base_name: [list of sensor components]}
phy_ifname = phy_handle_to_ifname()
def add_sensor(base_name, sensor_component):
"""Helper to track sensors per device"""
@@ -244,6 +275,16 @@ def hwmon_sensor_components():
base_name = normalize_sensor_name(device_name)
# Switch PHYs get an hwmon name derived from their full
# device-tree path (e.g. cp0busbusf2000000mdio12a200switch2mdio01).
# If this PHY drives a known port, name the sensor after that
# port (e1, e2, ...) instead.
phandle_path = os.path.join(hwmon_path, "device", "of_node", "phandle")
if HOST.exists(phandle_path):
ifname = phy_ifname.get(_dt_phandle(phandle_path))
if ifname:
base_name = ifname
# Helper to create sensor component with human-readable description
def create_sensor(sensor_name, value, value_type, value_scale, label=None):
component = {
+3
View File
@@ -18,6 +18,9 @@
- name: Container with VETH Pair
case: veth/test.py
- name: VETH Pair Between Two Containers
case: internal_link/test.py
- name: Container Volume Persistence
case: volume/test.py
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,33 @@
=== VETH Pair Between Two Containers
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/internal_link]
==== Description
Verify that a VETH pair can connect two containers directly, with *both*
ends handed to containers and neither remaining in the host namespace.
....
.------------. .------------.
| left | | right |
| veth0a ===|========= veth ===========|=== veth0b |
'------------' 10.0.0.1 10.0.0.2 '------------'
....
The pair is created in the host namespace then each end is moved into
its container when starting up. Connectivity is verified by pinging
across the pair, from inside one container's network namespace to the
other end's address.
==== Topology
image::topology.svg[VETH Pair Between Two Containers topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Create VETH pair with both ends assigned to containers
. Verify both containers have started
. Verify {LEFT} reaches {RIGHT} over the internal VETH pair
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
r"""VETH Pair Between Two Containers
Verify that a VETH pair can connect two containers directly, with *both*
ends handed to containers and neither remaining in the host namespace.
....
.------------. .------------.
| left | | right |
| veth0a ===|========= veth ===========|=== veth0b |
'------------' 10.0.0.1 10.0.0.2 '------------'
....
The pair is created in the host namespace then each end is moved into
its container when starting up. Connectivity is verified by pinging
across the pair, from inside one container's network namespace to the
other end's address.
"""
import infamy
from infamy.util import until
# Regression test for #941: previously, when both ends of a pair were
# assigned to a container, neither side created the pair.
with infamy.Test() as test:
LEFT, IFACE_LEFT, IP_LEFT = "left", "veth0a", "10.0.0.1"
RIGHT, IFACE_RIGHT, IP_RIGHT = "right", "veth0b", "10.0.0.2"
IMAGE = f"oci-archive:{infamy.Container.HTTPD_IMAGE}"
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
tgtssh = env.attach("target", "mgmt", "ssh")
if not target.has_model("infix-containers"):
test.skip()
with test.step("Create VETH pair with both ends assigned to containers"):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": IFACE_LEFT,
"type": "infix-if-type:veth",
"enabled": True,
"infix-interfaces:veth": {"peer": IFACE_RIGHT},
"ipv4": {
"address": [{"ip": IP_LEFT, "prefix-length": 24}]
},
"container-network": {}
},
{
"name": IFACE_RIGHT,
"type": "infix-if-type:veth",
"enabled": True,
"infix-interfaces:veth": {"peer": IFACE_LEFT},
"ipv4": {
"address": [{"ip": IP_RIGHT, "prefix-length": 24}]
},
"container-network": {}
},
]
}
},
"infix-containers": {
"containers": {
"container": [
{
"name": LEFT,
"image": IMAGE,
"command": "/usr/sbin/httpd -f -v -p 91",
"network": {"interface": [{"name": IFACE_LEFT}]}
},
{
"name": RIGHT,
"image": IMAGE,
"command": "/usr/sbin/httpd -f -v -p 91",
"network": {"interface": [{"name": IFACE_RIGHT}]}
},
]
}
}
})
c = infamy.Container(target)
with test.step("Verify both containers have started"):
until(lambda: c.running(LEFT), attempts=60)
until(lambda: c.running(RIGHT), attempts=60)
with test.step(f"Verify {LEFT} reaches {RIGHT} over the internal VETH pair"):
pid = tgtssh.runsh(f"sudo podman inspect --format '{{{{.State.Pid}}}}' {LEFT}").stdout.strip()
assert pid.isdigit(), f"failed to get pid for container {LEFT}: {pid!r}"
def reachable():
return tgtssh.runsh(f"sudo nsenter -t {pid} -n ping -c 2 -w 5 {IP_RIGHT}").returncode == 0
until(reachable, attempts=30)
test.succeed()
+1
View File
@@ -0,0 +1 @@
../../../infamy/topologies/1x1.dot
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+15 -10
View File
@@ -22,10 +22,12 @@ with infamy.Test() as test:
target = env.attach("target", "mgmt")
_, data_if = env.ltop.xlate("target", "data")
_, mgmt_if = env.ltop.xlate("target", "mgmt")
_, unused_if = env.ltop.xlate("target", "unused")
_, host_data = env.ltop.xlate("host", "data")
TARGET_IP = "192.168.1.1"
HOST_IP = "192.168.1.42"
# A dummy interface stands in for an unused port: it is not placed
# in any zone, so it must fall back to the default zone.
UNUSED_IF = "dummy0"
with test.step("Configure basic end-device firewall"):
target.put_config_dicts({
@@ -41,6 +43,11 @@ with infamy.Test() as test:
"prefix-length": 24
}]
}
},
{
"name": UNUSED_IF,
"type": "infix-if-type:dummy",
"enabled": True
}
]
}
@@ -119,16 +126,14 @@ with infamy.Test() as test:
assert "http" in public_zone["service"]
with test.step("Verify unused interface assigned to default zone"):
data = target.get_data("/infix-firewall:firewall")
fw = data["firewall"]
def unused_in_default_zone():
data = target.get_data("/infix-firewall:firewall")
fw = data["firewall"]
assert fw["default"] == "public-untrusted-net", "Default zone should be 'public-untrusted-net'"
zones = {zone["name"]: zone for zone in fw["zone"]}
return UNUSED_IF in zones["public-untrusted-net"].get("interface", [])
assert fw["default"] == "public-untrusted-net", "Default zone should be 'public-untrusted-net'"
zones = {zone["name"]: zone for zone in fw["zone"]}
public_zone = zones["public-untrusted-net"]
assert unused_if in public_zone["interface"], \
f"Unused interface {unused_if} should be in default zone 'public-untrusted-net', got interfaces: {public_zone['interface']}"
until(unused_in_default_zone, attempts=10)
with infamy.IsolatedMacVlan(host_data) as ns:
ns.addip(HOST_IP)
+5 -12
View File
@@ -1,4 +1,4 @@
graph "1x3" {
graph "1x2" {
layout = "neato";
overlap = false;
esep = "+30";
@@ -8,23 +8,16 @@ graph "1x3" {
host [
label="host | { <mgmt> mgmt | <data> data }",
pos="10,10.95!",
pos="10,10!",
requires="controller"
];
target [
label="{ <mgmt> mgmt | <data> data | <unused> unused } | target",
pos="30,10!",
requires="infix",
];
dummy [
label="{ <link> link } | dummy",
pos="29.8,00!",
label="{ <mgmt> mgmt | <data> data } | target",
pos="40,10!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data -- target:data [color=black, fontcolor=black, taillabel="192.168.1.42/24"]
target:unused -- dummy:link [color="gray", style="dashed"]
host:data -- target:data [color=black]
}
+20 -36
View File
@@ -2,57 +2,41 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x3 Pages: 1 -->
<svg width="381pt" height="191pt"
viewBox="0.00 0.00 381.03 190.72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 186.72)">
<title>1x3</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-186.72 377.03,-186.72 377.03,4 -4,4"/>
<!-- Title: 1x2 Pages: 1 -->
<svg width="299pt" height="55pt"
viewBox="0.00 0.00 299.02 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 295.02,-51 295.02,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-136.22 0,-182.22 100,-182.22 100,-136.22 0,-136.22"/>
<text text-anchor="middle" x="25" y="-155.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-136.22 50,-182.22 "/>
<text text-anchor="middle" x="75" y="-167.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-159.22 100,-159.22 "/>
<text text-anchor="middle" x="75" y="-144.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="241.03,-112.51 241.03,-181.51 373.03,-181.51 373.03,-112.51 241.03,-112.51"/>
<text text-anchor="middle" x="274.03" y="-166.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="241.03,-158.51 307.03,-158.51 "/>
<text text-anchor="middle" x="274.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="241.03,-135.51 307.03,-135.51 "/>
<text text-anchor="middle" x="274.03" y="-120.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
<polyline fill="none" stroke="black" points="307.03,-112.51 307.03,-181.51 "/>
<text text-anchor="middle" x="340.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
<polygon fill="none" stroke="black" points="175.02,-0.5 175.02,-46.5 291.02,-46.5 291.02,-0.5 175.02,-0.5"/>
<text text-anchor="middle" x="200.02" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="175.02,-23.5 225.02,-23.5 "/>
<text text-anchor="middle" x="200.02" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="225.02,-0.5 225.02,-46.5 "/>
<text text-anchor="middle" x="258.02" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-171.22C100,-171.22 241.03,-170.01 241.03,-170.01"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-35.5C100,-35.5 175.02,-35.5 175.02,-35.5"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;target:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-147.22C100,-147.22 241.03,-147.01 241.03,-147.01"/>
<text text-anchor="middle" x="159" y="-151.02" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
</g>
<!-- dummy -->
<g id="node3" class="node">
<title>dummy</title>
<polygon fill="none" stroke="black" points="250.46,-0.5 250.46,-36.5 358.46,-36.5 358.46,-0.5 250.46,-0.5"/>
<text text-anchor="middle" x="275.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="300.46,-0.5 300.46,-36.5 "/>
<text text-anchor="middle" x="329.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">dummy</text>
</g>
<!-- target&#45;&#45;dummy -->
<g id="edge3" class="edge">
<title>target:unused&#45;&#45;dummy:link</title>
<path fill="none" stroke="gray" stroke-width="2" stroke-dasharray="5,2" d="M274.03,-112.01C274.03,-112.01 275.46,-36.5 275.46,-36.5"/>
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 175.02,-11.5 175.02,-11.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB