the notebook

memories of bygone projects


github | about | explore | rss

Wired VR on Apple silicon: setup and reverse engineering

A wired VR headset can run directly from an M1 MacBook Air. The setup here uses an HP Reverb G1, a powered USB hub, an ordinary USB-C-to-DisplayPort adapter, and a native Swift/Metal application. The Mac drives the headset’s display and talks to its USB devices itself.

This provides a general approach to wired VR on Apple silicon: get the Mac’s display pipeline to send a usable signal, handle the headset’s USB protocol, and render the two eye views. The Reverb G1 is the working implementation described here.

The source repository includes both demonstrations: native macOS desktop output and a rotating cube rendered in stereo at 4320×2160, targeting 90 Hz.

Running the demo

The tested machine is an M1 MacBook Air running macOS 15.6.1 (24G90), with Swift 6.1.2 from Apple’s Command Line Tools. The headset is an HP Reverb G1 (VR1000-2xxx). Its USB lead connects through a powered Anker hub. Its DisplayPort lead connects through a Cable Matters USB-C-to-DisplayPort adapter, model 201086, in the Mac’s other USB-C port.

System configuration

Disable System Integrity Protection (SIP): shut down the Mac, hold the power button until startup options appear, choose Options, and continue into Recovery. Authenticate as an administrator when prompted. Open Utilities → Terminal, run the following, follow its prompts, and restart:

csrutil disable

Back in macOS, check SIP and read the current boot arguments:

csrutil status
nvram boot-args

The configuration needs two boot arguments:

  • hdcp-fuse-mask=0 disables HDCP on the display connection. This is the fix for the headset’s repeated blanking.
  • amfi_get_out_of_my_way=1 allows the ad-hoc entitled helper used for connection recovery. It does not fix HDCP.

Set both, retaining any other boot arguments needed by the machine:

sudo nvram boot-args="amfi_get_out_of_my_way=1 hdcp-fuse-mask=0"
nvram boot-args

boot-args is one string: writing it replaces the previous value. Restart, then verify:

sysctl -n kern.bootargs

It should include both flags. As a one-time check before connecting the headset:

ioreg -l -w0 | grep '"HDCPTXCapabilities"'

The Protocols list should be empty: {"Protocols"=()}, rather than (1,2). The separate HDCPCapabilityMask field can remain 2 or 3.

SIP and AMFI changes reduce system protections. Disabling HDCP affects protected video playback on external displays. To undo the HDCP change, remove only hdcp-fuse-mask=0 from the boot-argument string and restart.

Clone and build

Install Apple’s Command Line Tools if needed, then clone the source and build:

xcode-select --install
git clone https://umhau.org/code/reverb-m1.git
cd reverb-m1
zsh tools/build.sh

The build produces the stereo application, recovery helper, USB wake tool, and passive display observer in .build/debug.

Connect the headset’s USB lead through the powered hub, and its DisplayPort lead through the adapter on the other USB-C port. If macOS asks Allow accessory to connect?, choose Allow. Until permission is granted, HID calls fail with 0xe00002cd (kIOReturnNotOpen).

Native macOS desktop output

Run the wake tool and leave it running:

.build/debug/hmdwake

In System Settings → Displays, select the headset and choose an extended display or mirroring. Windows placed on that display appear inside the headset.

macOS sends one desktop image across the entire display surface. It does not duplicate the desktop for each eye or apply lens correction. The stereo demo below supplies those operations for its own scene.

A passive observer can run in another terminal:

.build/debug/displaywatch

It reports display connections and pixel modes through CoreGraphics without capturing screen pixels. The wake tool itself does not run automatic connection recovery.

Stop hmdwake with Ctrl-C before starting the stereo application. Both programs use the headset’s HID devices and must not run together.

Stereo 3D demo

.build/debug/VRWorkstation --run-seconds 600

Omit --run-seconds to run indefinitely. The scene contains a rotating colored cube, a floor grid, and reference objects at several depths. Each eye gets a separate rendered view with factory lens correction. The viewpoint stays fixed as the headset moves.

The Headset optics window on the Mac provides scene and grid patterns, color-correction controls, and Quit. The factory setting is 100% for each color control; the reset button restores it. Adjustments save automatically to ~/Library/Application Support/ReverbM1Demo/optics-adjustments.json.

On first launch, the application reads the headset’s factory calibration over USB before enabling the rendering session. It validates and caches the result at ~/Library/Application Support/ReverbM1Demo/factory.json. Later launches reuse the validated file. To select another calibration:

.build/debug/VRWorkstation --calibration /path/to/factory.json

Invalid calibration stops startup. Each eye renders with its asymmetric field of view to a 3024×3024 target. A final Metal pass applies Polynomial-3K lens correction separately to red, green, and blue, producing the native 4320×2160 output at 90 Hz. Eye spacing is 63 mm. The floor uses a filtered procedural grid to avoid unstable subpixel lines at distance.

For a stationary angular grid, use --optics-grid. For an image rendered through the same correction without opening the headset, use --snapshot /tmp/calibrated.png.

The USB activation sequence, configuration-store decoding, and optical model are based on Monado’s Windows Mixed Reality driver.

Why the display went dark

Before the boot-argument fix, the Mac detected the Reverb as a 4320×2160 display. The headset would turn its panels on, turn them off about 300 ms later, drop the DisplayPort link, and retry roughly every eight seconds.

Power, the hub, refresh rate, resolution, color depth, the proximity sensor, and activation commands were plausible causes. The same headset and hub worked on Linux. macOS already sent 8-bit RGB, and the headset blanked even when no HID commands were sent.

The difference was encryption. A couple of seconds after hotplug, macOS’s display coprocessor started HDCP 2.x encryption on the external DisplayPort link. The headset could not handle it. The Linux setup did not enable that encryption.

Finding the HDCP switch

The search started with the Mac’s own arm64 kernelcache, under:

/System/Volumes/Preboot/<uuid>/boot/<hash>/System/Library/Caches/com.apple.kernelcaches/kernelcache

After extraction and decompression with pyimg4 img4 extract and pyimg4 im4p extract, HDCP strings provided entry points into the disassembly. Capstone and an ADRP/ADD cross-reference scanner connected those strings to code.

Two consumers of hdcp-fuse-mask explained the behavior:

  • AppleDCPDPTXProxy calls PE_parse_boot_argn("hdcp-fuse-mask", &mask, 4). Its default is the hardware fuse value masked with 3. Apple’s own “HDCP suppressed per disable-av-content-protection” path sets that mask to zero.
  • AppleSEPHDCPManager parses the same argument and replaces each HDCP channel’s protocol bits before passing them to the Secure Enclave:
caps = (caps & ~0x18) | ((mask & 3) << 3);

The external display coprocessor, dcpext, owns HDCP channels 5 and 6. A zero mask leaves the transmitter advertising no HDCP protocols. After rebooting with hdcp-fuse-mask=0, the protocol list became empty and the macOS desktop stayed visible inside the headset. First light was on October 20, 2026.

Activating the headset

With encryption disabled, the headset still needs its USB wake sequence. The wake tool sends:

  • To the QHMD control device, 03f0:0c6a: four feature-report sets of [0x50, 0x01], each followed by a get of 0x50; gets of 0x09, 0x08, and 0x06; then [0x04, 0x01] to turn the screen on.
  • To the HoloLens Sensors device, 045e:0659: output report [0x02, 0x07], zero-padded to 64 bytes, to enable the IMU.

Within about 1.5 seconds, the DisplayPort link trains at HBR3, 8.1 Gbps across four lanes, and macOS adds the display. The application keeps its IOHIDManager objects alive: releasing a manager closes its devices.

The headset’s 0x05 status report is useful but does not measure presentation rate reliably. Byte 5 varied even while video was visibly running. The native display mode and actual Metal presentation counts provide the relevant measurements.

Recovering a dropped connection

Disabling HDCP made the display usable. Recovering a lost DisplayPort connection required understanding the Mac’s USB-C controller interface.

Some connection losses healed by themselves. Others left the adapter stuck, and another HID screen-on command did not restore the display. The target was a software reset of the adapter’s data path that would retrain DisplayPort without unplugging the cable.

Locating the controller interface

The reverse-engineering target was the decompressed arm64e fileset kernelcache from macOS 15.6.1 (24G90). Addresses below refer to that build. Chained pointers decode with:

BASE = 0xfffffe0007004000
pointer = BASE + (raw & 0xffffffff)

The relevant executable ranges were:

Component __TEXT_EXEC start Size
com.apple.driver.AppleHPM 0xfffffe0009b3d8c0 0x51f00
com.apple.iokit.IOAccessoryManager 0xfffffe000aba2ab0 0xf2d9c
com.apple.iokit.IOPortFamily — 0x84, a stub

AppleHPM talks to the USB-C “Ace” controller, in the TI CD3217 / TPS6598x family. Its AppleHPMLib.plugin exposes register reads, writes, commands, and vendor-defined messages.

The registry node used to identify the port is not the node on which to open the plugin. In the tested layout, AppleHPMDevice with RID 1 has child Port-USB-C@2. Open the plugin on its AppleHPMARMI2C parent. Trying IOCreatePlugInInterfaceForService on the device node returned 0xe00002c7; the plugin’s personality expects IOProviderClass = AppleHPM, which the parent satisfies.

Decoding user-client access

An initial reading of AppleHPMUserClient::initWithTask suggested that both root and an entitlement were required. Following the branches showed an OR condition instead:

AppleHPMUserClient::initWithTask @ 0xfffffe0009b49570

rootResult = IOUserClient::clientHasPrivilege(token, "root")
entitlement = copyClientEntitlement(task, "com.apple.USBCEntitlement")

allow = (rootResult == 0) || (entitlement == kOSBooleanTrue)

The deciding branches are at 0x9b49614, 0x9b4961c, 0x9b49620, and 0x9b49624. Hardware testing agreed: an unsigned non-root process failed, while a non-root process signed with com.apple.USBCEntitlement=true could read the controller. With AMFI disabled, an ad-hoc signature can carry that entitlement.

This permits a small entitled helper while the rendering application runs as an ordinary user.

Mapping the external methods

The dispatch at 0xfffffe0009b497dc uses a getTargetAndMethodForIndex-style switch rather than an IOExternalMethodDispatch array. A shared argument checker at user-client vtable +0x990 (0xfffffe0009b49b10) returns 0xe00002c2, kIOReturnBadArgument, for count mismatches.

sel plugin vtbl name handler checkArguments forwards to provider
0 +0x28 iecsRead 0x9b49c24 struct-out path reg read
1 +0x30 iecsWrite 0x9b49e3c scalarIn 5 provider +0x218 (mem) then write
2 +0x38 iecsCommand 0x9b4a044 scalarIn 3 provider +0x8f0(route,cmd,flags,5)
3 +0x40 sendVDM 0x9b4a118 scalarIn 5 provider +0x990(route,SOP,md,len,flags)
4 +0x48 receiveVDM 0x9b4a314 — provider +0x998-ish
5 +0x50 receiveVDMAttention 0x9b4a540 — —
6 +0x58 forceMode 0x9b4a76c scalarIn 2 provider +0x938(mode)
7 +0x60 forceUpdateMode 0x9b4a834 scalarIn 4 provider +0x940
8 +0x68 IECSAtomicCommand 0x9b4a908 scalarIn 2 provider +0x948(route,u8)
9 +0x70 EnableOptions 0x9b4a9d4 scalarIn 0xb up to 4 copyin buffers
10 +0x78 (reserved) 0x9b4b0e4 — —
default         kIOReturnUnsupported 0xe00002c7 @0x9b49a88

The useful command paths are:

  • iecsWrite, selector 1: scalar inputs [route, register, length, userBufferPointer, flags]. Length is limited to 64 bytes. This exposes the controller’s raw host interface.
  • iecsCommand, selector 2: scalar inputs [route, command, flags]. The handler validates three scalar inputs and calls provider+0x8f0(route, command, flags, 5).
  • sendVDM, selector 3: scalar inputs [route, SOPtype, length, userBufferPointer, flags]. It copies the message buffer and forwards it to provider+0x990.
  • forceMode, selector 6, and forceUpdateMode, selector 7: entry points into controller mode logic.

Selector 2 has no command allowlist in the user-client handler. That mattered because early command attempts returned 0xe00002c6, kIOReturnBadMessageID. The error needed to be traced farther down the path.

Finding a reset command

The driver’s own AppleHPMInterface::exitPDMode, at 0xfffffe0009b5bc44, supplied the strongest lead. It reads “Exit PD Mode TX Identity” and “Exit PD Mode DRST Data” properties, then invokes the controller command executor with DRST:

provider+0xb10(this, reg=8, &DRST, drstData[0x40], length=0x40)

The raw host interface uses register 0x08 for CMD1 and 0x09 for DATA1. A command with payload writes DATA1 first, then the four command bytes to CMD1. The controller clears CMD1 when finished; DATA1 contains the task result.

Other candidates appeared in the disassembly:

Path Intended effect Result for this project
DRST USB-PD Data Reset and renewed alternate-mode negotiation Accepted; restored the display
DP Status / Exit Mode / Enter Mode VDMs Reassert status or restart only DP alternate mode Investigated as alternatives; not needed for the working recovery
forceMode / forceUpdateMode Re-run controller mode logic Identified entry points; not the implemented recovery
Gaid / GAID Controller reset Earlier attempts used incorrect byte order; they did not establish whether this path works
HRST Hard-reset candidate Rejected by this firmware with correct encoding

The driver also sends VDMs through AppleHPMInterface at 0xfffffe0009b5d7bc. Its internal DATA1 payload contains [u32 VDM header][u8 SOP type][VDOs…]. DisplayPort uses SVID 0xFF01, with command 6 for Status Update, 5 for Exit Mode, 4 for Enter Mode, and 7 for Configure. These were useful leads, but the accepted DRST command made further VDM experiments unnecessary.

The byte-order trap

Early DRST attempts failed because constants copied from driver-internal code were wrong for the user-client boundary.

Inside the driver, 0x54535244 stored as a little-endian integer produces the bytes DRST. Selector 2 expects the opposite integer representation: 0x44525354. Its provider byte-swaps the input before writing CMD1.

Layer Handling of the command
Plugin iecsCommand, vtable +0x38 Passes the integer unchanged to selector 2
User-client handler, 0x9b4a044 Passes it unchanged to provider +0x8f0
Provider, 0x9b8c830 Executes rev w8, w23 at 0x9b8c970, then writes four bytes to CMD1
Driver-internal helpers Can store literal command bytes directly; their constants are not selector-2 inputs

The provider mapping was confirmed by a chained-pointer vtable scan, including the matching PAC discriminator 0x191d.

For selector 2, the command values are:

DRST = 0x44525354
HRST = 0x48525354
Gaid = 0x47616964

Sending 0x54535244 through that path puts TSRD on the wire. The controller rejects it as an unknown command. Correcting the encoding made DRST return success in about 50 ms.

Tracing the return codes

The provider first reads CMD1 and can clear an outstanding command. It writes the byte-swapped input, then normally polls CMD1 every 100 microseconds. With flags bit 0 clear, the limit is 1,000 attempts; setting that bit raises it to 100,000. Flags bit 1 selects a fire-and-forget path. The recovery helper uses flags zero.

Controller result IOReturn Meaning
CMD1 contains !CMD 0xe00002c6 Firmware did not recognize the command
CMD1 clears to zero 0 Command accepted/completed; read DATA1 byte 0 for task result
Polling limit expires 0xe00002d6 Timeout

This located the earlier BadMessageID error: the provider translates the firmware’s !CMD response into it. With correct encoding, HRST still returned that error; DRST did not.

In the successful recovery test, the display was stuck after an induced screen-off event. Calling iecsCommand(route=0, command=0x44525354, flags=0) on RID 1 returned zero in 50 ms, with CMD1 zero and DATA1 byte 0 zero. CoreGraphics added the 4320×2160 display about 2.209 seconds later. No physical replug was needed.

Why the other display interfaces were insufficient

IOAccessoryManager contains the OS-side DisplayPort transport logic. IOPortTransportStateDisplayPort exposes setHPDState at 0xabe5adc, triggerHPDIRQ at 0xabe5f98, and pin-assignment controls. These change or propagate the state seen by the display pipeline. They do not by themselves make the USB-C controller renegotiate alternate mode with the adapter.

The controller drives this state through AppleHPM’s communicateDPHPDChange. Its sendDpStatusUpdates at 0xfffffe0009b58490 extracts HPD from bit 15 of the cached DP status word and propagates changes to the transport.

Other examined interfaces included IOPortTransportUserClient, gated by com.apple.iokit.IOPort.user-access, and IOPortFeatureLDCMUserClient, gated by com.apple.iokit.IOPortFeature.LDCM. Accessory-ID bus reset methods reset a different bus. None supplied the working USB-C alternate-mode recovery path.

AppleHPM’s diagnostic boot arguments—such as hpmI2CRetries, hpmI2CRetryInterval, and hpmlog—also did not provide a DisplayPort reset switch. The working route was the controller command interface.

The working recovery implementation

The application uses a small signed hpmctl helper. Before resetting anything, it finds RID 1 and verifies the Port-USB-C@2 child, then opens the plugin on the parent. In the tested wiring, that is the DisplayPort adapter’s port. RID 0 carries the hub, headset USB, and Mac power; the helper refuses to reset it. Port identity must be verified before adapting the helper to another layout.

On display loss, the application allows a two-second grace period for spontaneous recovery. If the display remains absent, the helper sends DRST with route zero and flags zero. The application imposes a three-second helper timeout and a limit of three resets per minute. A display-add event cancels a pending reset.

The helper checks both IOReturn and DATA1’s task result. DATA1 0x04 has occurred after an accepted reset that nevertheless restored the display, so the application’s decisive evidence is the display returning, rather than the helper exit status alone.

Keeping recovery off the main thread

During display reconfiguration, macOS’s SkyLight code could block the main thread for about ten seconds in SLSWaitThroughReconfigurationCycleForcingNotificationHandling. Recovery queued on that thread waited with it.

The implementation separates the work:

  • AppKit manages windows and display arrangement on the main thread.
  • A dedicated rendering thread runs CAMetalDisplayLink and presents frames using its supplied drawable.
  • A separate serial recovery queue receives CoreGraphics display events before main-thread work is scheduled.

Moving recovery to that queue reduced measured forced-outage duration from about 14.4 seconds to about 4.4 seconds: the two-second grace period plus roughly 2.2 seconds for reset, retraining, and display addition. A brief lane-sync retrain could follow the reset and was handled automatically.

The renderer uses a non-activating borderless panel at native 4320×2160, 90 Hz, without exclusive display capture. It restores the mode if macOS changes it. Mode checks use CoreGraphics; recovery does not repeatedly enumerate IOPortTransportStateDisplayPort registry properties.

Checking the result

Every ten seconds, the application logs submitted and presented frames, skipped frames, presentation rate, presentation age, GPU time, GPU errors, and IMU sample count. Presented frames count Metal callbacks with actual presentation timestamps. A healthy settled line looks like:

fps=90.00 presentAge=0.01s GPUerrors=0

A recorded native-resolution soak ran for 649.965 settled seconds with 58,496 presented frames: 89.9987 fps, zero GPU errors, and zero DP resets. There was a 0.637-second self-heal during startup, followed by no drops. The repository contains the measurements in evidence/verified-90hz-soak.json and its accompanying log. A separate calibrated-rendering trial sustained 89.996 presented fps over its settled interval with no display drops or GPU errors.

Offline calibration and recovery checks run with:

zsh tools/check.sh

Those checks exercise the calibration model and recovery logic. The desktop-output and stereo commands above provide the hardware demonstrations.

Co-authored with AI assistance from GPT-6.1 Sol low.


Network Interface Madness

Well, this was fun to write. I’m trying to do some server automation; part of the process is automatically figuring out which network interfaces are available, and on which networks. I have a machine with 4+ ports: I really don’t want to go plugging and unplugging each one just to figure out which one is connected where.

You might say, “that’s easy! just run ifconfig, get the subnet associated with each interface, and parse the results. Bing, bang, boom, done.” But there’s a few problems with that approach. First among them is that we aren’t assuming the existence of any kind of L3 network. In that case, how do you know which of your interfaces are on the same switch? What if the machine is on multiple L2 networks? What if you only care about VPN interfaces, or want to ignore wifi? What if there’s no subnet?

Anyway, that’s what I had to deal with.

To solve the problem, I wrote a very ugly script. If you don’t like it….well, constructive analysis always welcome. Pull requests better still. To use it, specify the “types” of interfaces you want to analyze and about how much time it should take.

iface-groups -i eth -t 3

The output is a little strange. The problem with the data involved is that it’s inherently 2-dimensional; I want to know which interfaces are grouped together. That doesn’t lend itself well to a linear output. So, on every line sent to STDOUT, I’m printing a space-separated list of all interfaces that are connected on the same L2 network. For example:

$ iface-groups -i eth -t 3

eth0 eth2
eth1 eth3 eth4

You can parse this pretty well in a while read loop, like so:

iface-groups -i eth -t 3 | while read group
do

    echo "these interfaces can see only each other: $group"

done

Finally, you may be wondering about the available interface types. Well, you can read the script, or I’ll just list them out below. You can guess which is which, because I really don’t feel like specifying them right now. I just wrote this whole thing, gosh darn it!

Available interface types:
  eth, wlan, bridge, vlan, bond, tap, dummy, ib, ibchild, ppp
  tipip, ip6tnl, lo, sit, gre, irda, wlan_aux, tun, isdn, mip6mnha

Anyway, that’s all a nice little prelude to actually dumping the script here. I got no idea what the license is; the type identification was pulled from stack overflow and the original source link is dead. So whatever. If you want an updated version, you’ll have to hunt through my github repos: I probably won’t update this post, and the project it’s for is still under wraps. You’ll have to check back in a few years when it’s finally presentable.

#!/bin/bash
# written by me on October 1, 2022. Do what you will, for I have already won.

# enumerates the properties of each network interface
# eventually, it may be possible to assign rules on a per-network basis
# i.e., all ifaces on 10.111.13/24 should request DHCP
# unless, that's better done as a set of logic outside this program

# we need to know:
# - which interfaces are wired
# - which interfaces are connected
# - which interfaces are on the same L2 network

# ASSUMPTION: one interface is NOT connected to multiple L2 switches

set -e

help () {

    echo -e "usage:"
    echo -e "\t-i , --ifacetype=[TYPE]"
    echo -e "\t-t , --timelimit=[seconds]"
    echo -e "Multiple interface types may be specified, like so:"
    echo -e "\tiface-groups -i eth -i tun -t 5"
    echo -e "Available interface types:"
    echo -e "\teth, wlan, bridge, vlan, bond, tap, dummy, ib, ibchild, ppp,"
    echo -e "\tipip, ip6tnl, lo, sit, gre, irda, wlan_aux, tun, isdn, mip6mnha"

}

timelimit="$1" ; [ -z "$timelimit" ] && timelimit='4'

arp_period='1'

ip_address_prefix='203.0.113'

#         +----------------------+----------------------------+
#         | Attribute            | Value                      |
#         +----------------------+----------------------------+
#         | Address Block        | 203.0.113.0/24             |
#         | Name                 | Documentation (TEST-NET-3) |
#         | RFC                  | [RFC5737]                  |
#         | Allocation Date      | January 2010               |
#         | Termination Date     | N/A                        |
#         | Source               | False                      |
#         | Destination          | False                      |
#         | Forwardable          | False                      |
#         | Global               | False                      |
#         | Reserved-by-Protocol | False                      |
#         +----------------------+----------------------------+
# 
#         Table 14: TEST-NET-3

ifacetype=eth timelimit=3 lopts=ifacetype:,timelimit: sopts=i:t:

PARSED=$(getopt --options=$sopts --longoptions=$lopts --name "$0" -- "$@")

eval set -- "$PARSED"

while true; do case "$1" in

    -i|--ifacetype) 

        allowed_iface_types="${allowed_iface_types:+$allowed_iface_types } $2"
        shift 2
        ;;

    -t|--timelimit) timelimit="$2"  ; shift 2 ;;

    --) shift ; break ;;
    *)  help ; exit 1 ;;

esac done

[ -z "$allowed_iface_types" ] && help && exit 1

tempfile=/tmp/iface_enumeration
groupingfolder=/tmp/iface_enumeration_groups

rm -rf $tempfile $groupingfolder
mkdir -p $tempfile
mkdir -p $groupingfolder

if ! command -v tcpdump &>/dev/null ; then echo "tcpdump required" ; exit 2 ; fi

sudo -v

get_iface_type () {
    
    # function came from a dead link via stackoverflow
    # http://gitorious.org/opensuse/sysconfig/blobs/master/scripts/functions

    local IF=$1 TYPE
    test -n "$IF" || return 1
    test -d /sys/class/net/$IF || return 2
    case "`cat /sys/class/net/$IF/type`" in
            1)
                TYPE=eth
                # Ethernet, may also be wireless, ...
                if test -d /sys/class/net/$IF/wireless -o \
                        -L /sys/class/net/$IF/phy80211 ; then
                    TYPE=wlan
                elif test -d /sys/class/net/$IF/bridge ; then
                    TYPE=bridge
                elif test -f /proc/net/vlan/$IF ; then
                    TYPE=vlan
                elif test -d /sys/class/net/$IF/bonding ; then
                    TYPE=bond
                elif test -f /sys/class/net/$IF/tun_flags ; then
                    TYPE=tap
                elif test -d /sys/devices/virtual/net/$IF ; then
                    case $IF in
                      (dummy*) TYPE=dummy ;;
                    esac
                fi
                ;;
           24)  TYPE=eth ;; # firewire ;; # IEEE 1394 IPv4 - RFC 2734
           32)  # InfiniBand
            if test -d /sys/class/net/$IF/bonding ; then
                TYPE=bond
            elif test -d /sys/class/net/$IF/create_child ; then
                TYPE=ib
            else
                TYPE=ibchild
            fi
                ;;
          512)  TYPE=ppp ;;
          768)  TYPE=ipip ;; # IPIP tunnel
          769)  TYPE=ip6tnl ;; # IP6IP6 tunnel
          772)  TYPE=lo ;;
          776)  TYPE=sit ;; # sit0 device - IPv6-in-IPv4
          778)  TYPE=gre ;; # GRE over IP
          783)  TYPE=irda ;; # Linux-IrDA
          801)  TYPE=wlan_aux ;;
        65534)  TYPE=tun ;;
    esac
    # The following case statement still has to be replaced by something
    # which does not rely on the interface names.
    case $IF in
        ippp*|isdn*) TYPE=isdn;;
        mip6mnha*)   TYPE=mip6mnha;;
    esac
    test -n "$TYPE" && echo $TYPE && return 0
    return 3
}

# for every interface on the system
for interface in /sys/class/net/*
do

    interface=$(basename $interface)

    # figure out what it is: wifi? ethernet? virtual bridge? infiniband?
    iface_type=$(get_iface_type $interface)

    # for each of the useful interface types, specified above (or in an arg)
    for allowed_iface_type in ${allowed_iface_types[@]}
    do

        # if the interface we're looking at is one of those allowed
        if [ "$iface_type" == "$allowed_iface_type" ]
        then

            # if the interface is connected to something: switch, router, etc
            if tcpdump --list-interfaces | grep $interface | grep -q 'Running'
            then

                # add it do the list of viable interfaces
                viable_ifaces="${viable_ifaces:+$viable_ifaces } $interface"

            fi

            # don't check more types after a match: "there can be only one"
            break

        fi

    done

done

[ -z "${viable_ifaces[@]}" ] && echo "no viable interfaces discovered" && exit 1

ip_iterator=0

declare -A iface_ip

starttime=$(date +%s)

# for each viable interface we found
for interface in ${viable_ifaces[@]}
do

    # create a unique, unused ip address (taken from TEST-NET-3, RFC 6890)
    ip_iterator=$((ip_iterator+1))
    ip_address="$ip_address_prefix.$ip_iterator"

    # associate that ip address with the current interface
    iface_ip+=(["$ip_address"]="$interface")

    # start listening on that interface for ARP requests
    sudo timeout $timelimit tcpdump --immediate-mode -i $interface arp > $tempfile/$interface 2>/dev/null &

    # start sending ARP requests for the unique IP address on that interface
    sudo arping -I $interface -w $timelimit $ip_address &>/dev/null &

done

# total time it took to start the listeners and senders
endtime=$(date +%s)
initializationtime=$(( $endtime - $starttime ))

# wait for the listeners and senders to finish
sleep $(($timelimit + $initializationtime + 1))

# for each viable interface, get the file with the results from the listener
for ifacefile in $tempfile/*
do

    ifacename=$(basename $ifacefile)

    # if the iface is not already in a group of ifaces that can see each other
    if ! grep -q $ifacename $groupingfolder/* 2>/dev/null
    then

        # create a new group with just that interface
        echo $ifacename > $groupingfolder/$(date +%s%N)

    fi

    # for each ARP request seen by this interface, get the IP address requested
    grep $ip_address_prefix $ifacefile | cut -d ' ' -f 5 | while read ipaddress
    do

        # get the group file of the interface that saw the request
        groupfile=$(grep -l $ifacename $groupingfolder/* 2>/dev/null)

        # convert the ip address to the sender interface and add it to the group
        echo ${iface_ip[$ipaddress]} >> $groupfile

    done

done

# for each group of interfaces that we collected
for group in $groupingfolder/*
do 

    # filter out duplicate interfaces within the group and print to stdout 
    echo $(sort $group | uniq | xargs)

done



Set up Xen on Alpine Linux

So I’m starting out on this new project. The first iteration is just VMs on a box - but they’re Xen VMs, and they’re on an Alpine box. Later iterations shall be revealed, all in good time. Much work has been done behind the scenes, and much work is yet to be done.

session 1: the host

Anyway, Xen vms on Alpine. Start with a clean alpine installation, then enable the community repository. Note that I used this source for most of the content here, but as written, that source has a couple bugs. i.e., my walkthrough works, and theirs doesn’t - quite.

First become root. You should remain root for the duration of this walkthrough.

su
cat /etc/os-release

repository_server="dl-cdn.alpinelinux.org"
alpine_version="v3.16"

echo "https://$repository_server/alpine/$alpine_version/main" > /etc/apk/repositories

Install the hypervisor, and related packages.

apk add xen xen-hypervisor seabios xen-qemu # ovmf
apk add grub grub-bios

Load the necessary kernel modules.

echo "xen-netback" >> /etc/modules
echo "xen-blkback" >> /etc/modules
echo "tun" >> /etc/modules

And add the xen daemons.

rc-update add xenconsoled
rc-update add xendomains
rc-update add xenqemu
rc-update add xenstored

Then reboot; because kernel mods don’t immediately take effect, and those daemons haven’t autostarted yet.

reboot

Now we get to do the tricky stuff: GRUB modifications. If you recall your Xen, you’ll remember that it’s got a special Dom0 that manages the other VMs on the host. We need to specify some configs for that VM, and the place to do that is in GRUB, before things like “RAM size” are determined.

Add the following to the bottom of the grub config. Note that you may want to increase the size of the dom0 RAM; just change 1024M to some larger number.

vim /etc/default/grub

# You need to set the amount of RAM to allocate to the Dom0 Alpine install so that
# our future virtual machines will have enough memory.
GRUB_CMDLINE_XEN_DEFAULT="dom0_mem=1024M,max:1024M"

GRUB_DEFAULT="saved"
GRUB_SAVEDEFAULT="true"

Then apply the new config.

grub-mkconfig -o /boot/grub/grub.cfg
grub-set-default "$(grep ^menuentry /boot/grub/grub.cfg | grep Xen | cut -d \' -f 2 | head -1)"

That sets the default entry in GRUB to the first entry containing ‘Xen’. Run this every time you upgrade Alpine or Xen.

One last thing. Do you plan to use more than 8 VMs? Probably. If so, be sure to increase the maximum number of loop devices.

In Alpine Linux, you will need to add the max_loop option to the loop module, then add the loop module to your initramfs.

touch /etc/modprobe.d/loop.conf
echo "options loop max_loop=32" > /etc/modprobe.d/loop.conf

Then update the modules list and reboot.

mkinitfs
reboot

If you want to confirm that the dom0 management vm is running, execute this:

xl list

session 2: the guests

Alpine docs won’t help so much here. I’m working from the xenproject now.