1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
|
package bsdp
import (
"errors"
"fmt"
"net"
"github.com/insomniacslk/dhcp/dhcpv4"
)
// MaxDHCPMessageSize is the size set in DHCP option 57 (DHCP Maximum Message Size).
// BSDP includes its own sub-option (12) to indicate to NetBoot servers that the
// client can support larger message sizes, and modern NetBoot servers will
// prefer this BSDP-specific option over the DHCP standard option.
const MaxDHCPMessageSize = 1500
// AppleVendorID is the string constant set in the vendor class identifier (DHCP
// option 60) that is sent by the server.
const AppleVendorID = "AAPLBSDPC"
// ReplyConfig is a struct containing some common configuration values for a
// BSDP reply (ACK).
type ReplyConfig struct {
ServerIP net.IP
ServerHostname, BootFileName string
ServerPriority uint16
Images []BootImage
DefaultImage, SelectedImage *BootImage
}
// ParseBootImageListFromAck parses the list of boot images presented in the
// ACK[LIST] packet and returns them as a list of BootImages.
func ParseBootImageListFromAck(ack *dhcpv4.DHCPv4) ([]BootImage, error) {
vendorOpts := GetVendorOptions(ack.Options)
if vendorOpts == nil {
return nil, errors.New("ParseBootImageListFromAck: could not find vendor-specific option")
}
return vendorOpts.BootImageList(), nil
}
func needsReplyPort(replyPort uint16) bool {
return replyPort != 0 && replyPort != dhcpv4.ClientPort
}
// MessageTypeFromPacket extracts the BSDP message type (LIST, SELECT) from the
// vendor-specific options and returns it. If the message type option cannot be
// found, returns false.
func MessageTypeFromPacket(packet *dhcpv4.DHCPv4) MessageType {
vendorOpts := GetVendorOptions(packet.Options)
if vendorOpts == nil {
return MessageTypeNone
}
return vendorOpts.MessageType()
}
// Packet is a BSDP packet wrapper around a DHCPv4 packet in order to print the
// correct vendor-specific BSDP information in String().
type Packet struct {
dhcpv4.DHCPv4
}
// PacketFor returns a wrapped BSDP Packet given a DHCPv4 packet.
func PacketFor(d *dhcpv4.DHCPv4) *Packet {
return &Packet{*d}
}
func (p Packet) v4() *dhcpv4.DHCPv4 {
return &p.DHCPv4
}
func (p Packet) String() string {
return p.DHCPv4.String()
}
// Summary prints the BSDP packet with the correct vendor-specific options.
func (p Packet) Summary() string {
return p.DHCPv4.SummaryWithVendor(&VendorOptions{})
}
// NewInformListForInterface creates a new INFORM packet for interface ifname
// with configuration options specified by config.
func NewInformListForInterface(ifname string, replyPort uint16) (*Packet, error) {
iface, err := net.InterfaceByName(ifname)
if err != nil {
return nil, err
}
// Get currently configured IP.
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
localIPs, err := dhcpv4.GetExternalIPv4Addrs(addrs)
if err != nil {
return nil, fmt.Errorf("could not get local IPv4 addr for %s: %v", iface.Name, err)
}
if len(localIPs) == 0 {
return nil, fmt.Errorf("could not get local IPv4 addr for %s", iface.Name)
}
return NewInformList(iface.HardwareAddr, localIPs[0], replyPort)
}
// NewInformList creates a new INFORM packet for interface with hardware address
// `hwaddr` and IP `localIP`. Packet will be sent out on port `replyPort`.
func NewInformList(hwaddr net.HardwareAddr, localIP net.IP, replyPort uint16, modifiers ...dhcpv4.Modifier) (*Packet, error) {
// Validate replyPort first
if needsReplyPort(replyPort) && replyPort >= 1024 {
return nil, errors.New("replyPort must be a privileged port")
}
vendorClassID, err := MakeVendorClassIdentifier()
if err != nil {
return nil, err
}
// These are vendor-specific options used to pass along BSDP information.
vendorOpts := []dhcpv4.Option{
OptMessageType(MessageTypeList),
OptVersion(Version1_1),
}
if needsReplyPort(replyPort) {
vendorOpts = append(vendorOpts, OptReplyPort(replyPort))
}
d, err := dhcpv4.NewInform(hwaddr, localIP,
dhcpv4.PrependModifiers(modifiers, dhcpv4.WithRequestedOptions(
dhcpv4.OptionVendorSpecificInformation,
dhcpv4.OptionClassIdentifier,
),
dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxDHCPMessageSize)),
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(vendorClassID)),
dhcpv4.WithOption(OptVendorOptions(vendorOpts...)),
)...)
if err != nil {
return nil, err
}
return PacketFor(d), nil
}
// InformSelectForAck constructs an INFORM[SELECT] packet given an ACK to the
// previously-sent INFORM[LIST].
func InformSelectForAck(ack *Packet, replyPort uint16, selectedImage BootImage) (*Packet, error) {
if needsReplyPort(replyPort) && replyPort >= 1024 {
return nil, errors.New("replyPort must be a privileged port")
}
// Data for OptionSelectedBootImageID
vendorOpts := []dhcpv4.Option{
OptMessageType(MessageTypeSelect),
OptVersion(Version1_1),
OptSelectedBootImageID(selectedImage.ID),
}
// Validate replyPort if requested.
if needsReplyPort(replyPort) {
vendorOpts = append(vendorOpts, OptReplyPort(replyPort))
}
// Find server IP address
serverIP := ack.ServerIdentifier()
if serverIP.To4() == nil {
return nil, fmt.Errorf("could not parse server identifier from ACK")
}
vendorOpts = append(vendorOpts, OptServerIdentifier(serverIP))
vendorClassID, err := MakeVendorClassIdentifier()
if err != nil {
return nil, err
}
d, err := dhcpv4.New(dhcpv4.WithReply(ack.v4()),
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(vendorClassID)),
dhcpv4.WithRequestedOptions(
dhcpv4.OptionSubnetMask,
dhcpv4.OptionRouter,
dhcpv4.OptionBootfileName,
dhcpv4.OptionVendorSpecificInformation,
dhcpv4.OptionClassIdentifier,
),
dhcpv4.WithMessageType(dhcpv4.MessageTypeInform),
dhcpv4.WithOption(OptVendorOptions(vendorOpts...)),
)
if err != nil {
return nil, err
}
return PacketFor(d), nil
}
// NewReplyForInformList constructs an ACK for the INFORM[LIST] packet `inform`
// with additional options in `config`.
func NewReplyForInformList(inform *Packet, config ReplyConfig) (*Packet, error) {
if config.DefaultImage == nil {
return nil, errors.New("NewReplyForInformList: no default boot image ID set")
}
if config.Images == nil || len(config.Images) == 0 {
return nil, errors.New("NewReplyForInformList: no boot images provided")
}
reply, err := dhcpv4.NewReplyFromRequest(&inform.DHCPv4)
if err != nil {
return nil, err
}
reply.ClientIPAddr = inform.ClientIPAddr
reply.GatewayIPAddr = inform.GatewayIPAddr
reply.ServerIPAddr = config.ServerIP
reply.ServerHostName = config.ServerHostname
reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
reply.UpdateOption(dhcpv4.OptServerIdentifier(config.ServerIP))
reply.UpdateOption(dhcpv4.OptClassIdentifier(AppleVendorID))
// BSDP opts.
vendorOpts := []dhcpv4.Option{
OptMessageType(MessageTypeList),
OptServerPriority(config.ServerPriority),
OptDefaultBootImageID(config.DefaultImage.ID),
OptBootImageList(config.Images...),
}
if config.SelectedImage != nil {
vendorOpts = append(vendorOpts, OptSelectedBootImageID(config.SelectedImage.ID))
}
reply.UpdateOption(OptVendorOptions(vendorOpts...))
return PacketFor(reply), nil
}
// NewReplyForInformSelect constructs an ACK for the INFORM[Select] packet
// `inform` with additional options in `config`.
func NewReplyForInformSelect(inform *Packet, config ReplyConfig) (*Packet, error) {
if config.SelectedImage == nil {
return nil, errors.New("NewReplyForInformSelect: no selected boot image ID set")
}
if config.Images == nil || len(config.Images) == 0 {
return nil, errors.New("NewReplyForInformSelect: no boot images provided")
}
reply, err := dhcpv4.NewReplyFromRequest(&inform.DHCPv4)
if err != nil {
return nil, err
}
reply.ClientIPAddr = inform.ClientIPAddr
reply.GatewayIPAddr = inform.GatewayIPAddr
reply.ServerIPAddr = config.ServerIP
reply.ServerHostName = config.ServerHostname
reply.BootFileName = config.BootFileName
reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
reply.UpdateOption(dhcpv4.OptServerIdentifier(config.ServerIP))
reply.UpdateOption(dhcpv4.OptClassIdentifier(AppleVendorID))
// BSDP opts.
reply.UpdateOption(OptVendorOptions(
OptMessageType(MessageTypeSelect),
OptSelectedBootImageID(config.SelectedImage.ID),
))
return PacketFor(reply), nil
}
|