Sunday, February 21, 2021

Bootable Linux Sparse Virtual Disk Images

The following recipe will get you a bootable sparse disk image that is 20GB in size, but only takes up a minimal amount of disk space (about 4.5MB to start). This process is suitable for creating disk images for Linux virtual machines.

First step is to create your sparse file:

truncate example.img --size 20G

This sets the size to about 20 gigabytes, but in reality it is not taking up any space:

# ls --size --block-size=1 example.img

0 example.img

# stat --format='%s' example.img

21474836480

Since you probably want to install a bootloader in order to make this a bootable image, we are going to need a partition table. I prefer the parted command for this over our old friend fdisk, since parted is a bit easier to script.

# parted example.img mklabel gpt

This creates a GPT partition table in the first 2048 sectors of the image file. The default partition type is MBR, which is fine if you plan on staying under 2TB, and do not mind dealing with extended and logical partitions. I see little to be lost by using GPT, since it part of UEFI, and is backward compatible with legacy BIOS type systems.

# parted example.img print

Model:  (file)
Disk /tmp/example.img: 21.5GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:

Number  Start  End  Size  File system  Name  Flags

Checking on the size, we see that our image file takes up about 40 kbytes, despite still appearing to be 20 gigabytes in size.

# ls --size --block-size=1 example.img

40960 example.img

# stat --format='%s' example.img

21474836480
 

Now we can add a partition. In this case I am only going to create one partition that uses all of the available space and I am going to give it the name "vm-root" to avoid confusion later.

# parted example.img mkpart primary 0% 100%

# parted example.img name 1 vm-root

# parted example.img print

Model:  (file)
Disk /tmp/example.img: 21.5GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:

Number  Start   End     Size    File system  Name     Flags
 1      1049kB  21.5GB  21.5GB               vm-root

Now we format the partition; I generally use ext4 these days. There is a fairly significant limitation to the ext4 mkfs tooling that forces us to use loopback devices at this point.

It was my hope that mkfs would figure out the partition size on its own, or perhaps let me specify it as an argument. But all attempts to do that caused mkfs to overrun the partition boundaries and break the backup GPT partition table.

When you specify fs-size to the mkfs.ext4 command, what you are specifying is the usable space you want, not the actual size of the available volume. The mkfs.ext4 command gets the volume size from the kernel's block layer, and then juggles a lot of complex logic to figure out how much space needs to be burned for meta information like superblocks and inode tables.

I probably could have figured out the math for the fs-size argument and formatted the image file partition directly, but there are too many variables to make me feel like that is a good use of my time. That being said, it would be of nice if the mkfs tooling got a virtual image mode that either allowed you to specify the device size, or detected the partition size from the partition table.

We need to bind our image file and partition to a loopback device:

DEV=$(losetup --show --find --partscan example.img)

 And now we can format our new partition:

# mkfs.ext4 -F ${DEV}p1

mke2fs 1.45.6 (20-Mar-2020)
/dev/loop0p1 contains a ext4 file system
        created on Sun Feb 21 19:32:16 2021
Discarding device blocks: done                            
Creating filesystem with 5242368 4k blocks and 1310720 inodes
Filesystem UUID: ede5cf4d-f7f6-4747-8c7a-28d794f92b92
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
        4096000

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done

 Another size check shows that we are now up to about 4.5 megabytes.

# ls --size --block-size=1 example.img

4505600 example.img

# stat --format='%s' example.img

21474836480

Pretty good so far. Now we can mount our new volume and take a look around.

# mkdir img-mp

# mount ${DEV}p1 img-mp

# df img-mp

Filesystem     1K-blocks  Used Available Use% Mounted on
/dev/loop0p1    20509264 45080  19399328   1% /tmp/img/img-mp

And now for the cleanup:

# umount ${DEV}p1

# losetup -d ${DEV}

To make this a bootable image, mount your volume again and copy the Linux OS file tree of your choice into the mounted volume. Then use the grub-install command to install the boot loader.

# grub-install --modules=part_gpt --root-directory /tmp/img/img-mp ${DEV}

Since this is a sparse image, it will grow larger as more data is written to it until you hit the size limit, but it will not get smaller when data is deleted.

The simplest way to compact this image is to use the zerofree command to zero out the empty space, and then use your VM hypervisor's tools to do the rest, such as virt-sparsify or VirtualBox modifymedium ${FILENAME} --compact.


Tuesday, February 09, 2021

Bash-ism to Display Last Command in xterm Title Bar

I got frustrated at the lack of support for displaying the last command entered in the xterm title bar when I bounced around from host to host, so I came up with a bash-ism to remedy the problem.

Add these to your .bashrc file on any hosts that this matters to you. I suspect there is a cleaner implementation but all of the command escaping was getting too complicated, so I broke it up into two separate functions.

This only works with versions of bash with PS0 support (bash 4.4 or higher) and I have only tested it using the macOS Terminal.app. It seems to work exactly as expected in screen sessions too, which is a nice bonus.

Fixes welcomed and appreciated, particularly those that make this work on earlier versions of bash. Just leave a comment or @ me on twitter.

 

settitle() {
    [[ -z $1 ]] && return
    case ${TERM} in
        xterm*) echo -n -e "\033]0;$1\007";;
    esac
}

lastcmd() {
    echo $(fc -l -1 -1 | sed -e 's/\s*[0-9]*\s*//');
}

export PS0='$(settitle "$(lastcmd)")'

 

Note: macOS does not deliver with bash 4.4 (yet?), so you should make sure to leave the "Active process name" setting checked in your Terminal.app to get this information locally. These are the settings that I am using.






Saturday, January 30, 2021

EdgeRouter Failover Configuration with Partial IPv6

I am in that fortunate first world situation to have two Internet connections wired to my humble abode. One is fast, and the other is pretty slow by modern standards. I considered getting rid of the slow connection, but I got a killer lifetime deal on the price, so it hardly seems worth getting rid of it.

My EdgeRouter supports failover so I figured I would take advantage of the second Internet connection and add a some redundancy to my home network. I wanted to bias things in favor of the faster connection, so it was important to ensure that I was only on the slower connection whenever the faster connection was unavailable. The faster connection also supports IPv6, which I wanted to retain as much as possible.

The basic failover configuration is simple enough that you can use one of the build-in Wizards in the EdgeMAX web UI to set it up. 

 

To ensure that the secondary connection only stands-in when the primary connection is unavailable, simply check this box:

In the EdgeOS config, this sets a failover priority value of 100 on the eth0 interface (the fast connection) and 60 on the eth1 interface (the slow connection). Your mileage may vary, but my experimentation showed that it only took six seconds to fail over to the slow connection, and about 40 seconds to fail back to the fast connection.

This particular configuration is quick and easy, but it omits IPv6, so that part requires some hand tweaking and a few compromises in my situation.

The intention behind IPv6 is that everything on the Internet gets a unique address. In contrast, IPv4 simply does not have enough addresses to go around, so the normal approach is to use a DHCP server to hand out RFC 1918 addresses to stuff inside of your private network.

Supporting IPv6 is great for the health of the Internet, but it complicates things when you want to set your SOHO network up for redundancy. When you failover, your alternate ISP does not recognize the IPv6 address range assigned to all of your devices, so everything stops communicating until each device updates their IPv6 address. In contrast, private IPv4 assignments are typically translated at your firewall, so nothing is required on the client side when you failover.

To fix this problem with IPv6 we have a few tools at our disposal NPTv6NAT66, and ULA. I agree with many other voices on the Internet that NAT66 is a fundamentally broken hack and should not be used. There are privacy concerns with NPTv6, but RFC 4941 seems to address most (all?) of them. ULA solves the problem that RFC 1918 solves with private addresses, and is probably not a significant factor in any SOHO failover design like this.

If I had two connections that supported IPv6, I would probably figure out how to get NPTv6 working, but since I only have one, I really just need to accept a minor compromise. If I choose to support IPv6, I have to accept that I will not have any IPv6 support during a failover. I have no problem with this compromise because we are still in the IPv6 transition phase and virtually everything is available via IPv4. I expect that applications sending traffic over an IPv6 interface will more or less transparently start using the IPv4 interface.

To add IPv6 support to the EdgeOS failover configuration, you can manually add the following sections to support an IPv6 firewall and a DHCPv6 prefix designation.

The firewall configuration (which goes into the firewall section) should look something like this:

    ipv6-name WANv6_IN {
        default-action drop
        description "WAN inbound traffic forwarded to LAN"
        enable-default-log
        rule 10 {
            action accept
            description "Allow established/related sessions"
            state {
                established enable
                related enable
            }
        }
        rule 20 {
            action drop
            description "Drop invalid state"
            state {
                invalid enable
            }
        }
    }
    ipv6-name WANv6_LOCAL {
        default-action drop
        description "WAN inbound traffic to the router"
        enable-default-log
        rule 10 {
            action accept
            description "Allow established/related sessions"
            state {
                established enable
                related enable
            }
        }
        rule 20 {
            action drop
            description "Drop invalid state"
            state {
                invalid enable
            }
        }
        rule 30 {
            action accept
            description "Allow IPv6 icmp"
            protocol ipv6-icmp
        }
        rule 40 {
            action accept
            description "allow dhcpv6"
            destination {
                port 546
            }
            protocol udp
            source {
                port 547
            }
        }
    }

 And this gets added to the eth0 interface configuration:

        dhcpv6-pd {
            pd 0 {
                interface switch0 {
                    host-address ::1
                    prefix-id :1
                    service slaac
                }
                prefix-length /60
            }
            rapid-commit enable
        }

Important Note: The /60 prefix length is provider specific. If you cannot get an IPv6 grant from your provider, you may need to change this value.

And finally, be sure to add the new IPv6 firewall labels to your eth0 interface firewall configuration to bring it all together:

        firewall {
            in {
                ipv6-name WANv6_IN
                name WAN_IN
            }
            local {
                ipv6-name WANv6_LOCAL
                name WAN_LOCAL
            }
         }

Load that configuration back to EdgeOS and you should be all set after a reboot.