UEFI HTTPBoot Server Setup

Jump to: navigation, search



Please refer to Help:Editing in order to write a quality approved article.

Introduction

HTTPBoot was added into UEFI SPEC since 2.5, and it aims to replace PXE and provides more features. Actually, the concept of HTTPBoot is similar to PXE. It starts with the HTTP URL from the DHCP server and fetches the data with the HTTP protocol. Besides, HTTPBoot also supports DNS. With DNS, the firmware and the bootloader can resolve the domain name so it's possible to pass a well-known HTTP URL to download the image across different domains, while tftp (PXE) is only for the local network. This article provides the HOWTO to set up the HTTPBoot server. We also provides the instructions to create a virtual machine with HTTPBoot support in case you don't have a new enough physical machine.


Preparation

The Server

The HTTPBoot server has to install at least the following packages: dhcp-server, apache2 (or lighttpd), and dnsmasq.

NOTE: This article uses the IP subnets 192.168.111.0/24 (v4) and 2001:db8:f00f:cafe::/64 (v6) and assumes the server IP addresses are 192.168.111.1(v4) and 2001:db8:f00f:cafe::1/64 (v6). Please adjust the related settings in case any conflict exists.

The Client

Physical Machine

Enabling HTTPBoot in the firmware is vendor specific. Please refer to the manual of the mainboard or the machine.

Virtual Machine

To set up a virtual machine with the latest UEFI, the host needs qemu and OVMF. OVMF is the UEFI implementation for qemu. For the better support of HTTPBoot, it's recommended to use ovmf >= r18743.

The latest OVMF is available in https://build.opensuse.org/package/show/Virtualization/ovmf

Just install qemu-ovmf-x86_64 which contains the firmware files for qemu.

OS Image

SUSE Linux Enterprise starts to support HTTP Boot since 12-SP3 and HTTPS Boot since 15. For openSUSE, you need Leap 15.0 or higher to support HTTP Boot and HTTPS Boot.

Network Environment

There are lots of possible networking scenarios. For the beginner, it's recommended to start with the isolated network to avoid breaking the physical network.

Isolated Network Within The Host

There are several networking settings in qemu. The tap networking is the best choice since it utilizes the virtual interface to achieve the two way communication between the host and the guest. It provides a completely isolated network as long as we don't bridge the virtual interface with any real one, so that our testing dhcp server won't mess up the local network.

In this scenario, we assume the host is the HTTPBoot server and the guest is the client.

[host] <--> (tap0) <--> [guest]

First, we set up the a tap interface for the communication between the host and the guest.

 # ip tuntap add mode tap user <username> name tap0

If the firewall is enabled, it's recommended to add tap0 to the "Internal" or "Trusted" zone to avoid the port blocking.

Add the IP address with ip:

 # ip link set dev tap0 up
 # ip addr add 192.168.111.1/24 dev tap0
 # ip addr -6 add 2001:db8:f00f:cafe::1/64 dev tap0

In case ip complains "Permission Denied" when adding the ipv6 address, try to enable ipv6 for tap0 with

 # sysctl net.ipv6.conf.tap0.disable_ipv6=0

NOTE: When using the isolated network, remember to replace eth0 with tap0 in the server configuration later.

Physical Network

A HTTPBoot server acts as a normal DHCP server in the physical network except providing the image download function. Please adapt the related settings in the following sections for your network settings.

For the client, there are two choices: a physical machine or a bridged virtual machine.

Physical Machine As The Client

Just check the firmware manual or UI to see if it supports HTTPBoot or not.

Bridged Virtual Machine As The Client

In case you don't have any new-enough machine as the HTTPBoot client, one choice is to bridge the virtual machine to the physical network. Assume that the host connects to the network with eth0, we can bridge eth0 and tap0 with br0, and then the virtual machine can send/receive packets through br0/eth0.

[host]--(eth0)<-->(br0)<-->(tap0)--[guest]

It's recommended to use YaST to create and manage both tap and bridge interfaces. See the section in Leap document (Managing Network Bridges with YaST) for more information.

Of course, you can do it manually ;-)

First, create the bridge(br0) and tap:

 # ip link add name br0 type bridge
 # ip link set br0 up
 # ip tuntap add mode tap user <username> name tap0

Bridge the interfaces:

 # ip link set eth0 up
 # ip link set tap0 up
 # ip link set eth0 master br0
 # ip link set tap0 master br0

Once the bridge(br0) is set up, configure the network settings on br0 and then both the host and the guest can access the network.


Configure The Server

DNS Server

DNS is optional but it's nice to give your server a well-known name. To set up the DNS server, add the following lines to /etc/dnsmasq.conf

 interface=eth0
 addn-hosts=/etc/dnsmasq.d/hosts.conf

Then, create the mapping of the domain name of the IP address in /etc/dnsmasq.d/hosts.conf

 192.168.111.1 www.httpboot.local
 2001:db8:f00f:cafe::1 www.httpboot.local

Now, it's time to start the DNS server.

 # systemctl start dnsmasq

NOTE: UEFI 2.7 changes the device path of HTTPBoot and inserts a DNS node, and it's recommended to use shim bootloaders from SLE15/openSUSE Leap 15 or newer to avoid the potential errors due to the additional DNS node.

DHCPv4 Server

Before setting the DHCP servers, remember to specify the interface for the DHCP servers in /etc/sysconfig/dhcpd:

 DHCPD_INTERFACE="eth0"
 DHCPD6_INTERFACE="eth0"

Then, the DHCP servers will only provide the service on eth0.

Here we will set up the mixed DHCPv4 server for both PXEBoot and HTTPBoot. Add the following lines to /etc/dhcpd.conf:

 option domain-name-servers 192.168.111.1;
 option routers 192.168.111.1;
 default-lease-time 14400;
 ddns-update-style none;
 subnet 192.168.111.0 netmask 255.255.255.0 {
   range dynamic-bootp 192.168.111.100 192.168.111.120;
   default-lease-time 14400;
   max-lease-time 172800;
   class "pxeclients" {
     match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
     next-server 192.168.111.1;
     filename "/bootx64.efi";
   }
   class "httpclients" {
     match if substring (option vendor-class-identifier, 0, 10) = "HTTPClient";
     option vendor-class-identifier "HTTPClient";
     filename "http://www.httpboot.local/sle/EFI/BOOT/bootx64.efi";
   }
 }

In the example of dhcpd.conf, we firstly match the identifier from the client to tell what kind of service it requests. For HTTPBoot, the DHCPv4 server MUST set the vendor class ID as "HTTPClient" in the DHCPOffer packet since the client use it to identify whether this is a HTTPBoot offer or not.

Start the dhcp daemon:

 # systemctl start dhcpd

NOTE: Due to a bug(*) in shim, you might see "Invalid Parameter" from shim if the gateway (option routers) is not set.

(*) https://github.com/rhboot/shim/pull/136

DHCPv6 Server

To set up the DHCPv6 server, add the following lines to /etc/dhcpd6.conf:

 option dhcp6.bootfile-url code 59 = string;
 option dhcp6.vendor-class code 16 = {integer 32, integer 16, string};
 subnet6 2001:db8:f00f:cafe::/64 {
         range6 2001:db8:f00f:cafe::42:10 2001:db8:f00f:cafe::42:99;
         option dhcp6.bootfile-url "http://www.httpboot.local/sle/EFI/BOOT/bootx64.efi";
         option dhcp6.name-servers 2001:db8:f00f:cafe::1;
         option dhcp6.vendor-class 0 10 "HTTPClient";
 }

First, we define the type of the boot URL and the vendor class and then set the details. Like the DHCPv4 settings, we have to assign the boot URL. Please note that we have to use the IPv6 name in the boot URL. Similar to option 60 in DHCPv4, we have to specify the vendor class. The vendor class option in DHCPv6 consists of the enterprise number and the vendor class data (length and the content). The HTTPBoot driver doesn't care the enterprise number, so we just use 0. The content of the vendor class data has to be "HTTPClient", or the client will just ignore the offer.

For the older HTTPBoot implementation, it doesn't follow RFC 3315 and will need a different setting like this:

 option dhcp6.bootfile-url code 59 = string;
 option dhcp6.vendor-class code 16 = string;
 subnet6 2001:db8:f00f:cafe::/64 {
         range6 2001:db8:f00f:cafe::42:10 2001:db8:f00f:cafe::42:99;
         option dhcp6.bootfile-url "http://www.httpboot.local/sle/EFI/BOOT/bootx64.efi";
         option dhcp6.name-servers 2001:db8:f00f:cafe::1;
         option dhcp6.vendor-class "HTTPClient";
 }

Now start the DHCPv6 daemon.

 # systemctl start dhcpd6

It's also possible to setup a DHCP6 server for both PXEBoot and HTTPBoot. Like this:

 option dhcp6.bootfile-url code 59 = string;
 option dhcp6.vendor-class code 16 = {integer 32, integer 16, string};
 
 subnet6 2001:db8:f00f:cafe::/64 {
         range6 2001:db8:f00f:cafe::42:10 2001:db8:f00f:cafe::42:99;
 
         class "PXEClient" {
                 match substring (option dhcp6.vendor-class, 6, 9);
         }
 
         subclass "PXEClient" "PXEClient" {
                 option dhcp6.bootfile-url "tftp://[2001:db8:f00f:cafe::1]/bootloader.efi";
         }
 
         class "HTTPClient" {
                 match substring (option dhcp6.vendor-class, 6, 10);
         }
 
         subclass "HTTPClient" "HTTPClient" {
                 option dhcp6.bootfile-url "http://www.httpboot.local/sle/EFI/BOOT/bootx64.efi";
                 option dhcp6.name-servers 2001:db8:f00f:cafe::1;
                 option dhcp6.vendor-class 0 10 "HTTPClient";
         }
 }

It could also go further to match the vendor-class for different architecture. For example, "HTTPClient:Arch:00016" means a x86_64 HTTPBoot client, and it can be rewritten as:

         class "HTTPClient" {
                 match substring (option dhcp6.vendor-class, 6, 21);
         }
 
         subclass "HTTPClient" "HTTPClient:Arch:00016" {
                 option dhcp6.bootfile-url "http://www.httpboot.local/sle/EFI/BOOT/bootx64.efi";
                 option dhcp6.name-servers 2001:db8:f00f:cafe::1;
                 option dhcp6.vendor-class 0 10 "HTTPClient";
         }

Then the server can serve different architectures at the same time.

Reference: https://www.mail-archive.com/edk2-devel@lists.01.org/msg14683.html

Firewall

The DHCP6 packets may be dropped by the firewall due to the RP filter in SLE/openSUSE Leap 15+. If you found "rpfilter_DROP" in the firewall log, edit /etc/firewalld/firewalld.conf:

 IPv6_rpfilter=no

TFTP server(Optional)

In case you need to support both PXE and HTTPBoot, a tftp server is necessary. Just install tftp and start the service:

 # systemctl start tftp.socket
 # systemctl start tftp.service

openSUSE/SLE now provide a special package, tftpboot-installation, for PXE. For example, just install tftpboot-installation-openSUSE-Tumbleweed-x86_64 and copy the whole directory to the tftp root directory.

 # cp -r /usr/share/tftpboot-installation/openSUSE-Tumbleweed-x86_64 /srv/tftpboot

Read /usr/share/tftpboot-installation/openSUSE-Tumbleweed-x86_64/README for more information.

HTTP Server

For the complete SLE/openSUSE HTTPBoot installation, copy every file in the first iso image to /srv/www/htdocs/sle/ or /srv/www/htdocs/opensuse/. Then, edit /srv/www/htdocs/sle/EFI/BOOT/grub.cfg to fit your needs. An example of grub.cfg:

 timeout=60
 default=1
 
 menuentry 'Installation IPv4' --class opensuse --class gnu-linux --class gnu --class os {
   set gfxpayload=keep
   echo 'Loading kernel ...'
   linuxefi /sle/boot/x86_64/loader/linux install=http://www.httpboot.local/sle
   echo 'Loading initial ramdisk ...'
   initrdefi /sle/boot/x86_64/loader/initrd
 }
 
 menuentry 'Installation IPv6' --class opensuse --class gnu-linux --class gnu --class os {
   set gfxpayload=keep
   echo 'Loading kernel ...'
   linuxefi /sle/boot/x86_64/loader/linux install=http://www.httpboot.local/sle ipv6only=1 ifcfg=*=dhcp6,DHCLIENT6_MODE=managed
   echo 'Loading initial ramdisk ...'
   initrdefi /sle/boot/x86_64/loader/initrd
 }


The SLE Deployment Guide(*) may provide more information for the installation server setup.

(*) https://www.suse.com/documentation/sles-12/book_sle_deployment/data/part_installserver.html

lighttpd

There are some modification is necessary to make lighttpd support both IPv4 and IPv6. Edit /etc/lighttpd/lighttpd.conf

 ##
 ## Use IPv6?
 ##
 #server.use-ipv6 = "enable"
 $SERVER["socket"] == "[::]:80" {  }

When server.use-ipv6 is enabled, it would make lighttpd only listen to IPv6, so we remove it and add an new IPv6 section with the default options.

Start the lighttpd daemon:

 # systemctl start lighttpd

apache2

Start the apache daemon:

 # systemctl start apache2

SSL Support for HTTP Server(Optional)

The TLS protocol was written in the UEFI spec since 2.5, and the latest OVMF already supports HTTPS Boot. For testing, we can create a self-signed certificate with openssl:

 $ openssl req -newkey rsa:4096 -nodes -keyout server.key -x509 -days 365 -out server.crt

Since we choose "www.httpboot.local" as our domain name, please use "www.httpboot.local" for "Common Name".

Convert the certificate into DER format for the client:

 $ openssl x509 -in server.crt -outform der -out server.der

Please note that this certificate is only for testing and NEVER use this certificate in the production machine.

Enroll The Server Certificate into The Client Firmware

You have to enroll the server certificate (server.der) in the client side before using HTTPS Boot or the client would fail to connect to the server. The method to enroll the server certificate depends on the implementation of the client firmware.

Physical Machine

To enroll the server certificate into a physical machine, you may need to plug in a USB stick which contains the certificate file and enroll it manually with the firmware UI. On the other hand, some high-end servers with Redfish support can enroll the certificate remotely. For example, the HPE iLO5 manual provides the instructions to enroll the certificate through its Restful API.

Virtual Machine

OVMF provides a menu in "Device Manager" -> "Tls Auth Configuration", and the user can enroll the server certificate from a file accessible to OVMF.

Besides enrolling the certificate manually, it's possible to configure the trusted certificates for OVMF with "-fw_cfg". Since 9c7d0d49929, OVMF supports reading the certificate list from the fw_cfg entry, etc/edk2/https/cacerts. The certificate list has to be in the format of Signature Database.

To create the certificate list with efisiglist:

 $ efisiglist -a server.der -o certs.db

After creating the certificate list, append the following command to specify the fw_cfg entry:

 $ qemu ... -fw_cfg name=etc/edk2/https/cacerts,file=certs.db

Check more details in the "HTTPS Boot" section of OVMF README.

lighttpd

Since lighttpd needs the private key and the certificate in the same file, we have to unify them first.

 $ cat server.crt server.key > server-almighty.pem

Copy "server-almighty.pem" to /etc/ssl/private/.

 # cp server-almighty.pem /etc/ssl/private/
 # chown -R root:lighttpd /etc/ssl/private/server-almighty.pem
 # chmod 640 /etc/ssl/private/server-almighty.pem

Then, check /etc/lighttpd/modules.conf whether "mod_openssl" is in the "server.modules" section or not. For example:

 server.modules = (
   "mod_access",
   "mod_openssl",
 )

NOTE: Due to the change of packaging, openSUSE Leap 15.0 or the higher need to enable the openssl module explicitly while lighttpd in openSUSE Leap 42.3 or the lower builds in the openssl module in the lighttpd binary.

Next, add the following lines to "SSL Support" section in /etc/lighttpd/lighttpd.conf:

  # For IPv4
  $SERVER["socket"] == ":443" {
      ssl.engine                 = "enable"
      ssl.pemfile                = "/etc/ssl/private/server-almighty.pem"
  }
  # For IPv6
  $SERVER["socket"] == "[::]:443" {
      ssl.engine                 = "enable"
      ssl.pemfile                = "/etc/ssl/private/server-almighty.pem"
  }

Restart lighttpd to activate SSL support.

 # systemctl restart lighttpd

apache2

Before we start to configure apache, we have to check /etc/sysconfig/apache2 first.

The SSL support of apache is controlled by APACHE_SERVER_FLAGS, so we have to add the SSL flag.

 APACHE_SERVER_FLAGS="SSL"

Besides, make sure that ssl is in APACHE_MODULES. For example:

 APACHE_MODULES="actions alias auth_basic authn_file authz_host authz_groupfile authz_core authz_user autoindex cgi dir env expires include log_config mime negotiation setenvif ssl socache_shmcb userdir reqtimeout authn_core headers proxy proxy_http proxy_wstunnel"

Next, copy the private key and the certificate to /etc/apache2/.

 # cp server.key /etc/apache2/ssl.key/
 # chown wwwrun /etc/apache2/ssl.key/server.key
 # chmod 600 /etc/apache2/ssl.key/server.key
 # cp server.crt /etc/apache2/ssl.crt/

Create the ssl vhost configuration.

 # cd /etc/apache2/vhosts.d
 # cp vhost-ssl.template vhost-ssl.conf

Edit /etc/apache2/vhosts.d/vhost-ssl.conf to change the private key and the certificate:

 SSLCertificateFile /etc/apache2/ssl.crt/server.crt
 SSLCertificateKeyFile /etc/apache2/ssl.key/server.key

Restart apache to activate the SSL support:

 # systemctl restart apache2

Modify DHCP configuration

Last, remember to replace the "http://" prefix with "https://" in the dhcpd.conf/dhcpd6.conf and restart the dhcp server.

 # systemctl restart dhcpd
 # systemctl restart dhcpd6

Changes in grub.cfg

Since we create a self-signed certificate for our HTTPS server, if we specify the HTTPS url in grub.cfg, the SLE/openSUSE installation system may fail to verify the certificate and refuse to download files from our HTTPS server.

There are two possible solutions.

1. Add ssl.certs=0 to disable the certificate verification. For example:

 linuxefi /sle/boot/x86_64/loader/linux install=https://www.httpboot.local/sle ssl.certs=0

2. Create an initrd containing the server certificate. By default, the installation system searches the trusted certificates in /var/lib/ca-certificates/openssl/ and /var/lib/ca-certificates/pem/, so we have to create an initrd containing our server certificate in those directory. Here is the example of commands:

 ## Create new directories containing the target path
 $ mkdir -p initrd-new/var/lib/ca-certificates/openssl/
 $ mkdir -p initrd-new/var/lib/ca-certificates/pem/
 
 ## Copy our server certificate to the target paths
 $ cp server.crt initrd-new/var/lib/ca-certificates/openssl/my-ca.pem
 $ cp server.crt initrd-new/var/lib/ca-certificates/pem/my-ca.pem
 
 ## Create the link file, <hash>.0, to make the certificate be trusted
 $ ln -sr initrd-new/var/lib/ca-certificates/openssl/my-ca.pem initrd-new/var/lib/ca-certificates/openssl/`openssl x509 -hash -noout -in server.crt`.0
 $ ln -sr initrd-new/var/lib/ca-certificates/pem/my-ca.pem initrd-new/var/lib/ca-certificates/pem/`openssl x509 -hash -noout -in server.crt`.0
 
 ## Change the working directory to 'initrd-new'
 $ cd initrd-new
 
 ## Make sure the whole path is owned by root
 $ sudo chown -R root:root var
 
 ## Create initrd (ssl.img)
 $ find . | cpio --quiet -H newc -o | gzip -9 -n > ../ssl.img
 
 ## Back to the upper directory for the further actions
 $ cd ..

You can check the content of ssl.img with lsinitrd. For example:

 $ lsinitrd ssl.img
 Image: ssl.img: 4.0K
 ========================================================================
 Version: 
 
 Arguments: 
 dracut modules:
 ========================================================================
 drwxr-xr-x   3 johndoe  users           0 Sep  3 10:09 .
 drwxr-xr-x   3 root     root            0 Sep  3 16:27 var
 drwxr-xr-x   3 root     root            0 Sep  3 16:27 var/lib
 drwxr-xr-x   3 root     root            0 Sep  3 16:27 var/lib/ca-certificates
 drwxr-xr-x   2 root     root            0 Sep  3 16:28 var/lib/ca-certificates/openssl
 lrwxrwxrwx   1 root     root            9 Sep  3 16:28 var/lib/ca-certificates/openssl/c6bea024.0 -> my-ca.pem
 -rw-r--r--   1 root     root         2114 Sep  3 16:28 var/lib/ca-certificates/openssl/my-ca.pem
 drwxr-xr-x   2 root     root            0 Sep  3 16:28 var/lib/ca-certificates/pem
 lrwxrwxrwx   1 root     root            9 Sep  3 16:28 var/lib/ca-certificates/pem/c6bea024.0 -> my-ca.pem
 -rw-r--r--   1 root     root         2114 Sep  3 16:28 var/lib/ca-certificates/pem/my-ca.pem
 ========================================================================

Now we need to copy ssl.img to the directory of initrd, e.g. /srv/www/htdocs/sle/boot/x86_64/loader/, and modify grub.cfg to add ssl.img. For example:

 echo 'Loading kernel ...'
 linuxefi /sle/boot/x86_64/loader/linux install=https://www.httpboot.local/sle
 echo 'Loading initial ramdisk ...'
 initrdefi /sle/boot/x86_64/loader/initrd /sle/boot/x86_64/loader/ssl.img

NOTE: A simple bash script to create initrd is available: https://github.com/lcp/uefi-fun/blob/master/packcert/packcert.sh

Usage:

 $ sh packcert.sh server.crt ssl.img

Container Services

For the quick test, mightyboot(*) can create the services with containers. Just edit env to match the host network configuration and type

 $ docker-compose up

If the settings are good, it will bring up dhcp, dhcp6, dnsmasq, and lighttpd services on the given network interface and make them ready to test.

(*) https://github.com/lcp/mightyboot


Launch The Client

Physical Machine

If the firmware already supports HTTPBoot, just plug in the cable and choose the correct boot option.

Virtual Machine

Now it's time to set up a virtual machine as the HTTPBoot client. There are a few qemu options needed to create a UEFI virtual machine.

  1. Specify the firmware
 -drive if=pflash,format=raw,readonly,file=/usr/share/qemu/ovmf-x86_64-code.bin \
 -drive if=pflash,format=raw,file=ovmf-x86_64-vars.bin

Please note that ovmf-x86_64-vars.bin must be writable. Just copy /usr/share/qemu/ovmf-x86_64-vars.bin to the working directory.

  1. Specify the network device
 -netdev tap,id=hostnet0,ifname=tap0,script=no,downscript=no \
 -device virtio-net-pci,romfile=,netdev=hostnet0

Since the host uses tap0 to communicate with the guest, we must specify the interface. "romfile=" is to disable the iPXE support so the virtual machine will use the native PXE and HTTPBoot functions from OVMF instead from iPXE.

The complete qemu command would be like this:

 $ qemu-system-x86_64 -enable-kvm \
                      -drive if=pflash,format=raw,readonly,file=/usr/share/qemu/ovmf-x86_64-code.bin \
                      -drive if=pflash,format=raw,file=ovmf-vars.bin \
                      -hda fat:hda-contents/ -monitor stdio \
                      -netdev tap,id=hostnet0,ifname=tap0,script=no,downscript=no \
                      -device virtio-net-pci,romfile=,netdev=hostnet0

If you are using libvirt, then you can use "rom bar" to disable iPXE support. For example:

    <interface type='bridge'>
      <mac address='00:11:22:33:44:55'/>
      <source bridge='br0'/>
      <model type='virtio'/>
      <rom bar='off'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
    </interface>

When the TianoCore logo shows, press "ESC" to enter the firmware menu.

Press "DOWN" to go to "Boot Manager" and press "Enter".

There will be several boot options. Choose the boot option with the "HTTP" prefix.

For example:

HTTPBoot IPv4

HTTPBoot IPv6

Just choose one of the boot options, and the firmware will start to download your UEFI application and execute it!


Another way to test http boot is to download a iso image from a URL. The benefit of this way is that you don't need to setup DHCP on host machine, and it only needs http server.

The first step is putting iso file to /srv/www/htdocs on server side:

e.g. /srv/www/htdocs/openSUSE-Leap-42.1-NET-x86_64.iso

On client code, you should launch UEFI firmware UI:

Device Manager -> Network Device List

e.g. IPv4

Enable DHCP or you can set static ip

Create a boot option by "HTTP Boot Configuration"

Edit "Boot URI", point to the position of ISO file

Select "UEFI HTTP" to boot to the ISO image


See also

Related articles

External links