netboot/pixiecore/dhcp.go
David Anderson e440f01741 pixiecore: new version of Pixiecore, using netboot libraries.
This version is still *extremely* barebones, and the unstable tag
in the README applies triple for it, but the code in its current
state is capable of booting x86 and UEFI clients correctly, and
that was hard enough to merit a snapshot checkpoint.
2016-08-09 00:19:08 -07:00

215 lines
5.5 KiB
Go

// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pixiecore
import (
"errors"
"fmt"
"net"
"go.universe.tf/netboot/dhcp4"
)
func (s *Server) serveDHCP(conn *dhcp4.Conn) {
for {
pkt, intf, err := conn.RecvDHCP()
if err != nil {
fmt.Println(err)
return
}
if intf == nil {
fmt.Println("no interface")
continue
}
mach, isIpxe, fwtype, err := s.validateDHCP(pkt)
if err != nil {
fmt.Println(err)
continue
}
spec, err := s.Booter.BootSpec(mach)
if err != nil {
fmt.Println(err)
continue
}
if spec == nil {
fmt.Println("no spec")
continue
}
// Machine should be booted.
serverIP, err := interfaceIP(intf)
if err != nil {
fmt.Println(err)
continue
}
resp, err := s.offerDHCP(pkt, mach, serverIP, isIpxe, fwtype)
if err != nil {
fmt.Println(err)
continue
}
if err = conn.SendDHCP(resp, intf); err != nil {
fmt.Println(err)
continue
}
}
}
func (s *Server) validateDHCP(pkt *dhcp4.Packet) (mach Machine, isIpxe bool, fwtype Firmware, err error) {
if pkt.Type != dhcp4.MsgDiscover {
return mach, false, 0, errors.New("not DHCPDISCOVER")
}
fwt, err := pkt.Options.Uint16(93)
if err != nil {
return mach, false, 0, fmt.Errorf("malformed arch: %s", err)
}
fwtype = Firmware(fwt)
if s.Ipxe[fwtype] == nil {
return mach, false, 0, fmt.Errorf("unsupported firmware type %d", fwtype)
}
guid := pkt.Options[97]
switch len(guid) {
case 0:
// A missing GUID is invalid according to the spec, however
// there are PXE ROMs in the wild that omit the GUID and still
// expect to boot.
case 17:
if guid[0] != 0 {
return mach, false, 0, errors.New("malformed GUID (leading byte must be zero)")
}
default:
return mach, false, 0, errors.New("malformed GUID (wrong size)")
}
// iPXE options
if len(pkt.Options[175]) > 0 {
bs := pkt.Options[175]
for len(bs) > 0 {
if len(bs) < 2 || len(bs)-2 < int(bs[1]) {
return mach, false, 0, errors.New("Malformed iPXE option")
}
switch bs[0] {
case 19:
// This iPXE build supports HTTP, so is appropriate
// for going straight into the OS kernel, no need to
// chainload our own.
isIpxe = true
}
bs = bs[2+int(bs[1]):]
}
}
mach.MAC = pkt.HardwareAddr
mach.Arch = fwToArch[fwtype]
return mach, isIpxe, fwtype, nil
}
func (s *Server) offerDHCP(pkt *dhcp4.Packet, mach Machine, serverIP net.IP, isIpxe bool, fwtype Firmware) (*dhcp4.Packet, error) {
resp := &dhcp4.Packet{
Type: dhcp4.MsgOffer,
TransactionID: pkt.TransactionID,
Broadcast: true,
HardwareAddr: mach.MAC,
RelayAddr: pkt.RelayAddr,
ServerAddr: serverIP,
Options: make(dhcp4.Options),
}
resp.Options[dhcp4.OptServerIdentifier] = serverIP
// says the server should identify itself as a PXEClient vendor
// type, even though it's a server. Strange.
resp.Options[dhcp4.OptVendorIdentifier] = []byte("PXEClient")
if pkt.Options[97] != nil {
resp.Options[97] = pkt.Options[97]
}
// TODO: for maximum support, need to also send a BINL response to
// UEFI clients, or they might ignore this ProxyDHCP response.
if isIpxe {
resp.BootFilename = fmt.Sprintf("http://%s:%d/_/ipxe?arch=%d&mac=%s", serverIP, s.HTTPPort, mach.Arch, mach.MAC)
} else {
resp.BootServerName = serverIP.String()
resp.BootFilename = fmt.Sprintf("%d", fwtype)
}
if fwtype == FirmwareX86PC {
// In theory, the PXE boot options are required by PXE
// clients. However, some UEFI firmwares don't actually
// support PXE properly, and will ignore ProxyDHCP responses
// that include the option.
//
// On the other hand, seemingly all firmwares support a
// variant of the protocol where option 43 is not
// provided. They behave as if option 43 had pointed them to a
// PXE boot server on port 4011 of the machine sending the
// ProxyDHCP response. Looking at TianoCore sources, I believe
// this is the BINL protocol, which is Microsoft-specific and
// lacks a specification. However, empirically, this code
// seems to work.
//
// But this code block is for classic old BIOS, which does
// behave and want option 43 to tell it how to boot.
pxe := dhcp4.Options{
// PXE Boot Server Discovery Control - bypass, just boot from filename.
6: []byte{8},
}
bs, err := pxe.Marshal()
if err != nil {
return nil, fmt.Errorf("failed to serialize PXE vendor options: %s", err)
}
resp.Options[43] = bs
}
return resp, nil
}
func interfaceIP(intf *net.Interface) (net.IP, error) {
addrs, err := intf.Addrs()
if err != nil {
return nil, err
}
// Try to find an IPv4 address to use, in the following order:
// global unicast (includes rfc1918), link-local unicast,
// loopback.
fs := [](func(net.IP) bool){
net.IP.IsGlobalUnicast,
net.IP.IsLinkLocalUnicast,
net.IP.IsLoopback,
}
for _, f := range fs {
for _, a := range addrs {
ipaddr, ok := a.(*net.IPNet)
if !ok {
continue
}
ip := ipaddr.IP.To4()
if ip == nil {
continue
}
if f(ip) {
return ip, nil
}
}
}
return nil, fmt.Errorf("interface %s has no usable server addresses", intf.Name)
}