So, recently, I wanted to start dual-booting Windows and Linux. I mainly use Windows for playing vidya. All coding is remote through an SSH client. I wanted a better local testing solution than spinning up a VM in VirtualBox…

But, everyone knows that trying to install Linux alongside Windows is a horrible idea. Most times, installing Linux after Windows will manage to clobber the Windows bootloader and give you many, many issues (source: experience). How can we solve this?

I’ve heard “oh just run Linux in a VM, it will work fine!” > Nope. Not gonna happen. Been there already.

So I had a neat thought. I’ve got a NAS (WD EX2). I’ve got a pfSense router that serves DNS. The EX2 supports iSCSI, NFS, etc… pfSense has a TFTP package and some advance DHCP options…. I’m sure you see where this is going…

So, I decided to build a “diskless” Linux environment that I can netboot via PXE and run on my tower without touching my local disks. I’m going to document this a little bit to help other people get this stuff together, because it was a tedious process.

Here’s a brief overview of the process:

So, here’s the “long” version:

Setting up pfSense for TFTP and PXE.

This step should be swift and relatively “easy”, as pfSense’s UI is pretty fabulous and straight-forward.

First, install the System Patches and TFTPd packages from the pfSense package manager.

Set up the TFTPd package to serve on the LAN and upload your iPXE undionly.kpxe can be generated here (with the UNDI only / kpxe output option).

Next, we need to patch the DHCPd to classify users for iPXE. We want to make sure the boot agent doesn’t go into a recursive fetch loop, which will happen without the DHCP PXE agent classification.

DHCPd Patch (via System -> Patches):

--- dhcpd.conf 2016-01-20 16:25:28.000000000 -0600
+++ dhcpd.conf.override 2016-01-20 16:25:12.000000000 -0600
@@ -24,6 +24,8 @@
        option tftp-server-name "";
+       include "/etc/pxe-class.conf";
 host s_lan_0 {

--- /dev/null   2016-01-20 16:26:26.000000000 -0600
+++ pxe-class.conf      2016-01-16 16:54:38.000000000 -0600
@@ -0,0 +1,5 @@
+if exists user-class and option user-class = "iPXE" {
+    filename "tftp://";
+} else {
+    filename "undionly.kpxe";

--- /dev/null   2016-01-20 16:26:26.000000000 -0600
+++ /usr/local/etc/rc.d/dhcpoverride.sh 2016-01-16 16:35:03.000000000 -0600
@@ -0,0 +1,5 @@
+killall -3 dhcpd
+cp -vf /var/dhcpd/etc/dhcpd.conf.override /var/dhcpd/etc/dhcpd.conf
+/usr/local/sbin/dhcpd -user dhcpd -group _dhcp -chroot /var/dhcpd -cf /etc/dhcpd.conf -pf /var/run/dhcpd.pid re0

NOTE: You may need to modify the TFTP server location to point to your router. The DHCPd configuration patch may need to be modified to insert the pxe-class.conf into the dhcpd.conf properly.

Next, you’ll want to patch the TFTPd to work properly.

TFTP Patch (via System -> Patches):

--- /usr/local/pkg/tftp.inc     2016-01-16 12:48:29.000000000 -0600
+++ /usr/local/pkg/tftp.inc     2016-01-16 10:53:39.000000000 -0600
@@ -88,7 +88,7 @@

        // Add tftpd daemon to inetd
        $inetd_fd = fopen("/var/etc/inetd.conf", "a+");
-       fwrite($inetd_fd, "tftp\t\tdgram\tudp\twait\t\troot\t/usr/libexec/tftpd\ttftpd /tftpboot\n");
+       fwrite($inetd_fd, "tftp\t\tdgram\tudp\twait\t\troot\t/usr/libexec/tftpd\ttftpd -w -s /tftpboot -u nobody -l -d DEBUG_SIMPLE|DEBUG_ACCESS /\n");

        if (!empty($config['installedpackages']['tftpd']['config'][0]['tftpdinterface'])) {

NOTE: Make sure auto-apply is enabled on both of these patches, as pfSense will regenerate the configurations on service restart and system reboot.

Build an iSCSI target on the NAS.

Simple, handled through the EX2 web UI. Differs based on your NAS model. The lower-end WD MyCloud NAS devices do not have iSCSI support, but they may have NFS support which may also work. Depends on OS support and NFS quirks.

Setting up the iPXE configuration suite.

This part is relatively simple. First, you’ll need to generate an UNDI-only iPXE image, which you can generate here through the Standard page.

Place that in your TFTP server root and point your DHCP server’s next-server and network boot settings here. Your network boot settings should look like this:

pfSense DHCP Netboot Settings

Next, in your TFTP root, create a default.ipxe with the following contents:


# Global variables used by all other iPXE scripts
chain boot.ipxe.cfg ||

# Boot <boot-url>/<boot-dir>/hostname-<hostname>.ipxe
# if hostname DHCP variable is set and script is present
isset ${hostname} && chain ${boot-dir}hostname-${hostname}.ipxe ||

# Boot <boot-url>/<boot-dir>/uuid-<UUID>.ipxe
# if SMBIOS UUID variable is set and script is present
isset ${uuid} && chain ${boot-dir}uuid-${uuid}.ipxe ||

# Boot <boot-url>/<boot-dir>/mac-010203040506.ipxe if script is present
chain ${boot-dir}mac-${mac:hexraw}.ipxe ||

# Boot <boot-url>/<boot-dir>/pci-8086100e.ipxe if one type of
# PCI Intel adapter is present and script is present
chain ${boot-dir}pci-${pci/${busloc}.0.2}${pci/${busloc}.2.2}.ipxe ||

# Boot <boot-url>/<boot-dir>/chip-82541pi.ipxe if one type of
# PCI Intel adapter is present and script is present
chain ${boot-dir}chip-${chip}.ipxe ||

# Boot <boot-url>/menu.ipxe script if all other options have been exhausted
chain ${menu-url} ||

As well as a boot.cfg.ipxe like this, but modified to your liking:


# OPTIONAL: NFS server used for menu files and other things
# Must be specified as IP, as some distros don't do proper name resolution
set nfs-server
set nfs-root /nfs/boot

# OPTIONAL: Base URL used to resolve most other resources
# Should always end with a slash
#set boot-url nfs://${nfs-server}${nfs-root}
set boot-url tftp://

# OPTIONAL: What URL to use when sanbooting
# Usually ${boot-url} is used, but required until NFS supports block device API
# Should always end with a slash
set sanboot-url

# OPTIONAL: Relative directory to boot.ipxe used to
# override boot script for specific clients
set boot-dir boot/

# REQUIRED: Absolute URL to the menu script, used by boot.ipxe
# and commonly used at the end of simple override scripts
# in ${boot-dir}.
set menu-url ${boot-url}menu.ipxe

# OPTIONAL: iSCSI server location and iSCSI IQNs
# Must be specified as an IP, some clients have issues with name resolution
# Initiator IQN is also calculated to use hostname, if present
set iscsi-server
set base-iqn iqn.2013-03.com.wdc
set base-iscsi iscsi:${iscsi-server}::::${base-iqn}
isset ${hostname} && set initiator-iqn ${base-iqn}:${hostname} || set initiator-iqn ${base-iqn}:${mac}

Follow that up with a menu.ipxe:


# Variables are specified in boot.ipxe.cfg

# Some menu defaults
set menu-timeout 5000
set submenu-timeout ${menu-timeout}
isset ${menu-default} || set menu-default exit

# Figure out if client is 64-bit capable
cpuid --ext 29 && set arch x64 || set arch x86
cpuid --ext 29 && set archl amd64 || set archl i386

###################### MAIN MENU ####################################

menu iPXE boot menu for ${initiator-iqn}
item --gap --             ------------------------- Operating systems ------------------------------
item --key t testenv      Boot testing environment from iSCSI
item --key l menu-live    Live environments...
item --gap --             ------------------------- Tools and utilities ----------------------------
item --key b netboot-xyz  Chain to boot.netboot.xyz
item --gap --             ------------------------- Advanced options -------------------------------
item --key c config       Configure settings
item shell                Drop to iPXE shell
item reboot               Reboot computer
item --key x exit         Exit iPXE and continue BIOS boot
choose --timeout ${menu-timeout} --default ${menu-default} selected || goto cancel
set menu-timeout 0
goto ${selected}

echo You cancelled the menu, dropping you to a shell

echo Type 'exit' to get the back to the menu
set menu-timeout 0
set submenu-timeout 0
goto start

echo Booting failed, dropping to shell
goto shell



goto start

set submenu-timeout 0
clear submenu-default
goto start

############ MAIN MENU ITEMS ############

echo Booting testing environment from iSCSI for ${initiator-iqn}
set root-path ${base-iscsi}:cloud:linbox
sanboot ${root-path} || goto failed
goto start

echo Booting to boot.netboot.xyz...
chain http://boot.netboot.xyz

Your PXE setup is mostly done! Make sure you change the boot.cfg.ipxe to reflect your environment setup. Same goes for the menu items in menu.ipxe.

Partitioning & bootstrapping the iSCSI Target.

Now we’re getting into the funner steps as we get closer to the finish line. We’ll start with partitioning first, as it seems to be the logical first step.

First, make sure you’ve got open-iscsi installed on your bootstrapper machine. Obviously, this differs between package managers, but it should be something similar.

Start by making sure your LUN is configured and exposed properly by your NAS:

# iscsiadm -m discovery -t sendtargets -p,1 iqn.2013-03.com.wdc:cloud:linbox

If you see your LUN listed like the above, you’re going in the right direction. Next, open a session with the iSCSI portal:

# iscsiadm -m node -T 'iqn.2013-03.com.wdc:cloud:linbox' -p -l
Logging in to [iface: default, target: iqn.2013-03.com.wdc:cloud:linbox, portal:,3260] (multiple)
Login to [iface: default, target: iqn.2013-03.com.wdc:cloud:linbox, portal:,3260] successful.

Now that we’re logged in to the LUN, your system will be able to pick up the disk. dmesg will show output like the following (just without the sdc* partitions and most likely, a different drive letter):

[508455.879000] scsi12 : iSCSI Initiator over TCP/IP
[508456.140857] scsi 12:0:0:0: Direct-Access     WD       FILEIO           4.0  PQ: 0 ANSI: 5
[508456.141503] sd 12:0:0:0: Attached scsi generic sg2 type 0
[508456.142478] sd 12:0:0:0: [sdc] 250000001 512-byte logical blocks: (128 GB/119 GiB)
[508456.145915] sd 12:0:0:0: [sdc] Write Protect is off
[508456.145927] sd 12:0:0:0: [sdc] Mode Sense: 3b 00 10 00
[508456.147142] sd 12:0:0:0: [sdc] Write cache: enabled, read cache: enabled, supports DPO and FUA
[508456.196057]  sdc: sdc1 sdc2 sdc3 sdc4
[508456.201907] sd 12:0:0:0: [sdc] Attached SCSI disk

Start partitioning! I chose to partition the main disk with btrfs because I like the snapshotting and other advanced capabilities it has. I attempted to follow the “typical” Linux partitioning style, although it may not be completely necessary. Start by opening gdisk on your iSCSI disk:

# gdisk /dev/sdc
GPT fdisk (gdisk) version 0.8.8

Type device filename, or press <Enter> to exit: /dev/sdc
Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.

Command (? for help):

Your partition table scan may look a little different, but we’ll fix that real quick. Type o into the gdisk prompt to create a new GPT and give it confirmation that you want to burn away what’s not on your iSCSI LUN:

Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): y

Now, your partition table scan will look remarkably similar to the above! Start by adding a BIOS boot partition. You don’t need a particularly large BIOS boot partition, just enough space for GRUB to store all of its data.

Command (? for help): n
Partition number (1-128, default 1): 
First sector (34-14680030, default = 2048) or {+-}size{KMGTP}: 
Last sector (2048-14680030, default = 14680030) or {+-}size{KMGTP}: +64K
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): ef02
Changed type of partition to 'BIOS boot partition'

If you’ve got space to spare and want a separate /boot partition, we’d set that up now. Remember, you’ll need space to store some configs and kernel updates, so it should be a little bit bigger than the BIOS boot partition. 1G should be enough, but I recommend going a little bigger in case you have extra space handy.

Command (? for help): n
Partition number (2-128, default 2): 
First sector (34-14680030, default = 4096) or {+-}size{KMGTP}: 
Last sector (4096-14680030, default = 14680030) or {+-}size{KMGTP}: +1G
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): 
Changed type of partition to 'Linux filesystem'

Next, we’ll be creating the main root filesystem. It’s a lot like the /boot partition setup, but we’ll be doing some extra stuff to it afterwards.

Command (? for help): n
Partition number (3-128, default 3): 
First sector (34-67108830, default = 2101248) or {+-}size{KMGTP}: 
Last sector (2101248-67108830, default = 67108830) or {+-}size{KMGTP}: -4G
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): 
Changed type of partition to 'Linux filesystem'

Next is swap, which is optional but recommended.

Command (? for help): n
Partition number (4-128, default 4): 
First sector (34-67108830, default = 58720256) or {+-}size{KMGTP}: 
Last sector (58720256-67108830, default = 67108830) or {+-}size{KMGTP}: 
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): 8200
Changed type of partition to 'Linux swap'

Now, review our partitioning scheme and write it if everything looks good.

Command (? for help): p
Disk /dev/sdc: 67108864 sectors, 32.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 7E16CC34-B216-4A70-AA02-A873C1805A5A
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 67108830
Partitions will be aligned on 2048-sector boundaries
Total free space is 3967 sectors (1.9 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            2175   64.0 KiB    EF02  BIOS boot partition
   2            4096         2101247   1024.0 MiB  8300  Linux filesystem
   3         2101248        58720222   27.0 GiB    8300  Linux filesystem
   4        58720256        67108830   4.0 GiB     8200  Linux swap

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING

Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to /dev/sdc.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot.
The operation has completed successfully.

And we’re done partitioning! Now all you should need to do is issue the following commands and we’ll be in business:

# sync
# partprobe /dev/sdc
### Verify with:
# ls -la /dev/sdc*
brw-rw---- 1 root disk 8, 32 Jan 24 16:54 /dev/sdc    # Raw disk device
brw-rw---- 1 root disk 8, 33 Jan 24 16:50 /dev/sdc1   # BIOS boot
brw-rw---- 1 root disk 8, 34 Jan 24 16:50 /dev/sdc2   # /boot
brw-rw---- 1 root disk 8, 35 Jan 24 16:50 /dev/sdc3   # /
brw-rw---- 1 root disk 8, 36 Jan 24 16:50 /dev/sdc4   # swap

Now, we need to actually create the filesystems on the disk. We don’t need to touch sdc1 as GRUB will handle that.

# mkfs.ext4 /dev/sdc2
mke2fs 1.42.9 (4-Feb-2014)
Discarding device blocks: done                            
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
65536 inodes, 262144 blocks
13107 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=268435456
8 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks: 
	32768, 98304, 163840, 229376

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

# mkfs.btrfs /dev/sdc3  # OR, use `mkfs.ext4` if you're not feeling risky.

WARNING! - see http://btrfs.wiki.kernel.org before using

Performing full device TRIM (27.00GiB) ...
Turning ON incompat feature 'extref': increased hardlink limit per file to 65536
fs created label (null) on /dev/loop0p3
	nodesize 16384 leafsize 16384 sectorsize 4096 size 27.00GiB
Btrfs v3.12

# mkswap /dev/sdc4
Setting up swapspace version 1, size = 4194280 KiB
no label, UUID=de3863e9-9926-4ebb-8d2c-b4c987efa8ee

Setting up your OS on the Chroot.

Filesystems are up and going! Now, let’s do our mounts and debootstrap the system. I used Debian Testing/Stretch because I like to be on the bleeding edge, but feel free to use jessie or wheezy instead if you want a more stable experience.

# cd /mnt
# mkdir chroot
# mount /dev/sdc3 chroot
# debootstrap testing chroot
I: Keyring file not available at /usr/share/keyrings/debian-archive-keyring.gpg; switching to https mirror https://mirrors.kernel.org/debian
I: Retrieving Release 
I: Retrieving Packages
--- snip ---
I: Base system installed successfully.
### Set up the chroot
# cd chroot
# mount /dev/sdc2 boot
# mount -t proc none proc
# mount -t sysfs none sys
# mount --bind /dev dev
# chroot . /bin/bash
### Install some extra packages
# apt-get install -y locales tasksel open-iscsi initramfs-tools grub2 vim-nox openssh-server
### Reconfigure timezone and locales
# dpkg-reconfigure tzdata
# dpkg-reconfigure locales

NOTE: Make sure that you install GRUB to the correct disk… For example, our LUN’s block device, /dev/sdc.

Configuring your OS.

There’s a bit of setup that has to go in to configuring the OS for disless boot. Obviously, I’m going to use Debian here as the example since that is what I’m running.

NOTE: This needs to be done before you can boot your OS for the first time.

Set up your mtab:

# cp /proc/mounts /etc/mtab
# sed -i '\|^/dev/sdc3|,$!d' /etc/mtab

NOTE: Remember to replace sdc3 with your partition node, if it is different..

Set up fstab:

# blkid /dev/sdc{2,3,4}
/dev/sdc2: UUID="70f7d8de-68de-4b25-9f9d-d57348be6c12" TYPE="ext4" PARTLABEL="Linux filesystem" PARTUUID="e608b54d-0985-4879-984f-4ce879dcf930"
/dev/sdc3: UUID="042e69f8-b0d6-410d-8f1d-febcd584dde3" UUID_SUB="a444d825-5969-4bba-9f42-db2ed21f4f8b" TYPE="btrfs" PARTLABEL="Linux filesystem" PARTUUID="cc434614-f42d-4cc7-833b-59660ef7f7aa"
/dev/sdc4: UUID="9970794d-0a2c-4c0e-9d8b-0fcd3c33234f" TYPE="swap" PARTLABEL="Linux swap" PARTUUID="4dae3e79-c371-4c89-b9da-63bca1cb1663"
# echo "UUID=042e69f8-b0d6-410d-8f1d-febcd584dde3 / btrfs defaults,_netdev 0 1" >> /etc/fstab
# echo "UUID=70f7d8de-68de-4b25-9f9d-d57348be6c12 /boot ext4 defaults,_netdev 0 1" >> /etc/fstab
# echo "UUID=9970794d-0a2c-4c0e-9d8b-0fcd3c33234f none swap sw,_netdev 0 0" >> /etc/fstab
# cat fstab
UUID=042e69f8-b0d6-410d-8f1d-febcd584dde3 	/	btrfs 		defaults,_netdev 	0	1
UUID=70f7d8de-68de-4b25-9f9d-d57348be6c12       /boot   ext4            defaults,_netdev	0	1
UUID=9970794d-0a2c-4c0e-9d8b-0fcd3c33234f 	none	swap		sw,_netdev		0	0

Set up the iSCSI initiator:

# echo "InitiatorName=iqn.2016-01.com.maiome.roaming:client" > /etc/iscsi/initiatorname.iscsi

Change the following in your /etc/default/grub:

GRUB_CMDLINE_LINUX="root=UUID=042e69f8-b0d6-410d-8f1d-febcd584dde3 ip=dhcp ipby=dhcp"  # Set root to your / UUID!
GRUB_GFXMODE=1920x1080  # Set to your native screen resolution.

NOTE: Debian Sid adds support for consistent network device naming. I had issues with this for some reason. It can be disabled with biosdevname=0 and net.ifnames=0. If you have one interface, you shouldn’t have problems with device name allocation. For Docker and Radeon, I’ve also got these kernel boot options: cgroup_enable=memory swapaccount=1 radeon.dpm=1.

Configure your iSCSI target options (/etc/iscsi/iscsi.initramfs):


Do some research on your hardware and make sure the drivers you need get loaded on boot via /etc/modules and /etc/initramfs-tools/modules. In my case, both my modules files look like this:

radeon modeset=1

Configure the initramfs-tools (/etc/initramfs-tools/initramfs.conf):

# initramfs.conf
# Configuration file for mkinitramfs(8). See initramfs.conf(5).
# Note that configuration options from this file can be overridden
# by config files in the /etc/initramfs-tools/conf.d directory.

# MODULES: [ most | netboot | dep | list ]
# most - Add most filesystem and all harddrive drivers.
# dep - Try and guess which modules to load.
# netboot - Add the base modules, network modules, but skip block devices.
# list - Only include modules from the 'additional modules' list


# BUSYBOX: [ y | n ]
# Use busybox if available.


# KEYMAP: [ y | n ]
# Load a keymap during the initramfs stage.


# COMPRESS: [ gzip | bzip2 | lzma | lzop | xz ]


# NFS Section of the config.

# DEVICE: ...
# Specify a specific network interface, like eth0
# Overridden by optional ip= bootarg

DEVICE=eth1  # Set this to your boot interface!

# NFSROOT: [ auto | HOST:MOUNT ]


Configure your network interfaces (/etc/network/interfaces):

# interfaces(5) file used by ifup(8) and ifdown(8)
# Include files from /etc/network/interfaces.d:
source-directory /etc/network/interfaces.d

auto eth0
    iface eth0 inet manual

auto eth1
    iface eth1 inet manual

NOTE: Make sure your boot interface is set to manual in your network interfaces config. Otherwise, this will blow things up.

Now, update your initrd and grub!

# update-initramfs -u
# update-grub

Next steps…

Your “diskless” system should be ready to boot now. Unmount the chroot from your “helper” machine and give it a shot.

NOTE: If you have problems unmounting the chroot, make sure nothing is still running within the chroot. If necessary, force your helper system to “lazy unmount” the chroot: umount -lfv /mnt/chroot/{proc,dev,sys,}

To complete your install, remember that you should set up your graphics drivers for X11 (xserver-xorg-video-intel, xserver-xorg-video-radeon, xserver-xorg-video-nouveau, etc). That process is left as an exercise for the reader.

For troubleshooting, check some of the references (or Google) below.


These resources were instrumental in getting this setup built. I simply took bits and pieces of this information and compiled it all in one place to make things easier.


2/6/16 - Added some modules to both lists, moved ISCSI params out of /etc/default/grub.

8/21/16 - With the newest updates to pfSense (2.3.x+), the TFTP package is no longer maintained and is gone from the package repository. It also seems that restarting the DHCP service from the web panel rewrites the DHCP config, making the pxe-class patch worthless. It seems that the better solution would be to configure a TFTP server on a Raspberry Pi with the same iPXE scripts and use Rom-o-Matic to build a KPXE build with an embedded script which chains the default.ipxe from the TFTP server. Here is an example config. Good luck!

5/1/17 - A while back, I created a Docker image (pirogoeth/rpi-tftpd-hpa) to provide a TFTP server on a RPi. This is what I use now instead of the old pfSense TFTP package. It has been very stable, at least in my usage.