netboot/pixiecore/dhcp.go
David Anderson 8e5c0d0793 pixiecore: Add a basic HTTP status UI.
The UI is optional and need to be explicitly activated, since I
don't want existing users to suddenly have an information-spewing
thing show up on their network.
2016-09-19 03:43:57 -07:00

232 lines
6.8 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) error {
for {
pkt, intf, err := conn.RecvDHCP()
if err != nil {
return fmt.Errorf("Receiving DHCP packet: %s", err)
}
if intf == nil {
return fmt.Errorf("Received DHCP packet with no interface information (this is a violation of dhcp4.Conn's contract, please file a bug)")
}
if err = s.isBootDHCP(pkt); err != nil {
s.debug("DHCP", "Ignoring packet from %s: %s", pkt.HardwareAddr, err)
continue
}
mach, isIpxe, fwtype, err := s.validateDHCP(pkt)
if err != nil {
s.log("DHCP", "Unusable packet from %s: %s", pkt.HardwareAddr, err)
continue
}
spec, err := s.Booter.BootSpec(mach)
if err != nil {
s.log("DHCP", "Couldn't get bootspec for %s: %s", pkt.HardwareAddr, err)
continue
}
if spec == nil {
s.debug("DHCP", "No boot spec for %s, ignoring boot request", pkt.HardwareAddr)
s.machineEvent(pkt.HardwareAddr, machineStateIgnored, "Machine should not netboot")
continue
}
s.log("DHCP", "Offering to boot %s", pkt.HardwareAddr)
if isIpxe {
s.machineEvent(pkt.HardwareAddr, machineStateProxyDHCPIpxe, "Offering to boot iPXE")
} else {
s.machineEvent(pkt.HardwareAddr, machineStateProxyDHCP, "Offering to boot")
}
// Machine should be booted.
serverIP, err := interfaceIP(intf)
if err != nil {
s.log("DHCP", "Want to boot %s on %s, but couldn't get a source address: %s", pkt.HardwareAddr, intf.Name, err)
continue
}
resp, err := s.offerDHCP(pkt, mach, serverIP, isIpxe, fwtype)
if err != nil {
s.log("DHCP", "Failed to construct ProxyDHCP offer for %s: %s", pkt.HardwareAddr, err)
continue
}
if err = conn.SendDHCP(resp, intf); err != nil {
s.log("DHCP", "Failed to send ProxyDHCP offer for %s: %s", pkt.HardwareAddr, err)
continue
}
}
}
func (s *Server) isBootDHCP(pkt *dhcp4.Packet) error {
if pkt.Type != dhcp4.MsgDiscover {
return fmt.Errorf("packet is %s, not %s", pkt.Type, dhcp4.MsgDiscover)
}
if pkt.Options[93] == nil {
return errors.New("not a PXE boot request (missing option 93)")
}
return nil
}
func (s *Server) validateDHCP(pkt *dhcp4.Packet) (mach Machine, isIpxe bool, fwtype Firmware, err error) {
fwt, err := pkt.Options.Uint16(93)
if err != nil {
return mach, false, 0, fmt.Errorf("malformed DHCP option 93 (required for PXE): %s", err)
}
fwtype = Firmware(fwt)
if s.Ipxe[fwtype] == nil {
return mach, false, 0, fmt.Errorf("unsupported client firmware type '%d' (please file a bug!)", 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. The only thing we do with the GUID is
// mirror it back to the client if it's there, so we might as
// well accept these buggy ROMs.
case 17:
if guid[0] != 0 {
return mach, false, 0, errors.New("malformed client GUID (option 97), leading byte must be zero")
}
default:
return mach, false, 0, errors.New("malformed client GUID (option 97), 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]
}
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("%s/%d", mach.MAC, 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, errors.New("no usable unicast address configured on interface")
}