mirror of
https://github.com/danderson/netboot.git
synced 2025-10-17 02:21:20 +02:00
Also introduces a "Trace" member to the API, for the future addition of extremely verbose and complete tracing, to be used for bug reports. But for now, only Log is used.
223 lines
6.5 KiB
Go
223 lines
6.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 {
|
|
s.logf("receiving DHCP packet: %s", err)
|
|
// TODO: fatal errors that return from one of the handler
|
|
// goroutines should plumb the error back to the
|
|
// coordinating goroutine, so that it can do an orderly
|
|
// shutdown and return the error from Serve(). This "log +
|
|
// randomly stop a piece of pixiecore" is a terrible
|
|
// kludge.
|
|
return
|
|
}
|
|
if intf == nil {
|
|
s.logf("received DHCP packet with no interface information (this is a violation of dhcp4.Conn's contract)")
|
|
return
|
|
}
|
|
|
|
mach, isIpxe, fwtype, err := s.validateDHCP(pkt)
|
|
if err != nil {
|
|
s.logf("DHCP packet from %s not usable: %s", pkt.HardwareAddr, err)
|
|
continue
|
|
}
|
|
|
|
spec, err := s.Booter.BootSpec(mach)
|
|
if err != nil {
|
|
s.logf("couldn't get bootspec for %s: %s", pkt.HardwareAddr, err)
|
|
continue
|
|
}
|
|
if spec == nil {
|
|
s.logf("booter gave no spec for %s, ignoring boot request", pkt.HardwareAddr)
|
|
continue
|
|
}
|
|
|
|
// Machine should be booted.
|
|
serverIP, err := interfaceIP(intf)
|
|
if err != nil {
|
|
s.logf("want to boot %s on %s, but couldn't find a unicast source address on that interface: %s", pkt.HardwareAddr, intf.Name, err)
|
|
continue
|
|
}
|
|
|
|
resp, err := s.offerDHCP(pkt, mach, serverIP, isIpxe, fwtype)
|
|
if err != nil {
|
|
s.logf("failed to construct ProxyDHCP offer for %s: %s", pkt.HardwareAddr, err)
|
|
continue
|
|
}
|
|
|
|
if err = conn.SendDHCP(resp, intf); err != nil {
|
|
s.logf("failed to send ProxyDHCP offer for %s: %s", pkt.HardwareAddr, 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 a DHCPDISCOVER packet")
|
|
}
|
|
|
|
if pkt.Options[93] == nil {
|
|
return mach, false, 0, errors.New("not a PXE boot request (missing option 93)")
|
|
}
|
|
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("%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, errors.New("no usable source address")
|
|
}
|