Bootfile urls can be retrieved via remote api calls now

This commit is contained in:
Dmitri Dolguikh 2017-10-20 16:12:37 -07:00 committed by Dave Anderson
parent 02db3ab76d
commit da6402caf7
9 changed files with 191 additions and 76 deletions

View File

@ -6,26 +6,13 @@ import (
)
type IdentityAssociation struct {
ipAddress net.IP
clientId []byte
interfaceId []byte
createdAt time.Time
IpAddress net.IP
ClientId []byte
InterfaceId []byte
CreatedAt time.Time
}
type AddressPool interface {
ReserveAddress(clientId, interfaceId []byte) *IdentityAssociation
ReleaseAddress(clientId, interfaceId []byte)
}

View File

@ -0,0 +1,79 @@
package dhcp6
import (
"net/http"
"time"
"strings"
"fmt"
"net/url"
"bytes"
)
type BootConfiguration interface {
GetBootUrl(id []byte, clientArchType uint16) (string, error)
}
type StaticBootConfiguration struct {
HttpBootUrl string
IPxeBootUrl string
}
func MakeStaticBootConfiguration(httpBootUrl, ipxeBootUrl string) *StaticBootConfiguration {
return &StaticBootConfiguration{HttpBootUrl: httpBootUrl, IPxeBootUrl: ipxeBootUrl}
}
func (bc *StaticBootConfiguration) GetBootUrl(id []byte, clientArchType uint16) (string, error) {
if 0x10 == clientArchType {
return bc.HttpBootUrl, nil
}
return bc.IPxeBootUrl, nil
}
type ApiBootConfiguration struct {
client *http.Client
urlPrefix string
}
func MakeApiBootConfiguration(url string, timeout time.Duration) *ApiBootConfiguration {
if !strings.HasSuffix(url, "/") {
url += "/"
}
return &ApiBootConfiguration{
client: &http.Client{Timeout: timeout},
urlPrefix: url + "v1",
}
}
func (bc *ApiBootConfiguration) GetBootUrl(id []byte, clientArchType uint16) (string, error) {
reqURL := fmt.Sprintf("%s/boot/%x/%d", bc.urlPrefix, id, clientArchType)
resp, err := bc.client.Get(reqURL)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return "", fmt.Errorf("%s: %s", reqURL, http.StatusText(resp.StatusCode))
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
url, _ := bc.makeURLAbsolute(buf.String())
return url, nil
}
func (bc *ApiBootConfiguration) makeURLAbsolute(urlStr string) (string, error) {
u, err := url.Parse(urlStr)
if err != nil {
return "", fmt.Errorf("%q is not an URL", urlStr)
}
if !u.IsAbs() {
base, err := url.Parse(bc.urlPrefix)
if err != nil {
return "", err
}
u = base.ResolveReference(u)
}
return u.String(), nil
}

View File

@ -3,6 +3,7 @@ package dhcp6
import (
"fmt"
"bytes"
"encoding/binary"
)
type MessageType uint8
@ -30,12 +31,11 @@ type Packet struct {
}
type PacketBuilder struct {
ServerDuid []byte
PreferredLifetime uint32
ValidLifetime uint32
BootFileUrlForHttpBoot string
BootFileUrlForIpxe string
Addresses AddressPool
ServerDuid []byte
PreferredLifetime uint32
ValidLifetime uint32
BootFileUrl BootConfiguration
Addresses AddressPool
}
func MakePacket(bs []byte, packetLength int) (*Packet, error) {
@ -48,10 +48,10 @@ func MakePacket(bs []byte, packetLength int) (*Packet, error) {
return ret, nil
}
func MakePacketBuilder(serverDuid []byte, preferredLifetime, validLifetime uint32, httpBootFileUrl, ipxeBootFileUrl string,
func MakePacketBuilder(serverDuid []byte, preferredLifetime, validLifetime uint32, bootFileUrl BootConfiguration,
addressPool AddressPool) *PacketBuilder {
return &PacketBuilder{ServerDuid: serverDuid, PreferredLifetime: preferredLifetime, ValidLifetime: validLifetime,
BootFileUrlForHttpBoot: httpBootFileUrl, BootFileUrlForIpxe: ipxeBootFileUrl, Addresses: addressPool}
BootFileUrl: bootFileUrl, Addresses: addressPool}
}
func (p *Packet) Marshal() ([]byte, error) {
@ -133,11 +133,11 @@ func (b *PacketBuilder) BuildResponse(in *Packet) *Packet {
case MsgSolicit:
association := b.Addresses.ReserveAddress(in.Options.ClientId(), in.Options.IaNaId())
return b.MakeMsgAdvertise(in.TransactionID, in.Options.ClientId(), in.Options.IaNaId(),
in.Options.ClientArchType(), association.ipAddress)
in.Options.ClientArchType(), association.IpAddress)
case MsgRequest:
association := b.Addresses.ReserveAddress(in.Options.ClientId(), in.Options.IaNaId())
return b.MakeMsgReply(in.TransactionID, in.Options.ClientId(), in.Options.IaNaId(),
in.Options.ClientArchType(), association.ipAddress)
in.Options.ClientArchType(), association.IpAddress)
case MsgInformationRequest:
return b.MakeMsgInformationRequestReply(in.TransactionID, in.Options.ClientId(),
in.Options.ClientArchType())
@ -155,13 +155,16 @@ func (b *PacketBuilder) MakeMsgAdvertise(transactionId [3]byte, clientId, iaId [
ret_options.AddOption(MakeIaNaOption(iaId, b.calculateT1(), b.calculateT2(),
MakeIaAddrOption(ipAddress, b.PreferredLifetime, b.ValidLifetime)))
ret_options.AddOption(MakeOption(OptServerId, b.ServerDuid))
if 0x10 == clientArchType { // HTTPClient
ret_options.AddOption(MakeOption(OptVendorClass, []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116})) // HTTPClient
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForHttpBoot)))
} else {
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForIpxe)))
}
bootfileUrl, err := b.BootFileUrl.GetBootUrl(b.ExtractLLAddressOrId(clientId), clientArchType)
if err != nil {
fmt.Printf("!!!!!!!!!!!!!!!!!!!!!!!! %s", err)
return nil
}
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(bootfileUrl)))
// ret_options.AddOption(OptRecursiveDns, net.ParseIP("2001:db8:f00f:cafe::1"))
//ret_options.AddOption(OptBootfileParam, []byte("http://")
//ret.Options[OptPreference] = [][]byte("http://")
@ -175,13 +178,14 @@ func (b *PacketBuilder) MakeMsgReply(transactionId [3]byte, clientId, iaId []byt
ret_options.AddOption(MakeIaNaOption(iaId, b.calculateT1(), b.calculateT2(),
MakeIaAddrOption(ipAddress, b.PreferredLifetime, b.ValidLifetime)))
ret_options.AddOption(MakeOption(OptServerId, b.ServerDuid))
if 0x10 == clientArchType { // HTTPClient
ret_options.AddOption(MakeOption(OptVendorClass, []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116})) // HTTPClient
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForHttpBoot)))
} else {
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForIpxe)))
}
bootfileUrl, err := b.BootFileUrl.GetBootUrl(b.ExtractLLAddressOrId(clientId), clientArchType)
if err != nil {
return nil
}
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(bootfileUrl)))
return &Packet{Type: MsgReply, TransactionID: transactionId, Options: ret_options}
}
@ -190,13 +194,14 @@ func (b *PacketBuilder) MakeMsgInformationRequestReply(transactionId [3]byte, cl
ret_options := make(Options)
ret_options.AddOption(MakeOption(OptClientId, clientId))
ret_options.AddOption(MakeOption(OptServerId, b.ServerDuid))
if 0x10 == clientArchType { // HTTPClient
ret_options.AddOption(MakeOption(OptVendorClass, []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116})) // HTTPClient
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForHttpBoot)))
} else {
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(b.BootFileUrlForIpxe)))
}
bootfileUrl, err := b.BootFileUrl.GetBootUrl(b.ExtractLLAddressOrId(clientId), clientArchType)
if err != nil {
return nil
}
ret_options.AddOption(MakeOption(OptBootfileUrl, []byte(bootfileUrl)))
return &Packet{Type: MsgReply, TransactionID: transactionId, Options: ret_options}
}
@ -221,3 +226,14 @@ func (b *PacketBuilder) calculateT2() uint32 {
return (b.PreferredLifetime * 4)/5
}
func (b *PacketBuilder) ExtractLLAddressOrId(optClientId []byte) []byte {
idType := binary.BigEndian.Uint16(optClientId[0:2])
switch idType {
case 1:
return optClientId[8:]
case 3:
return optClientId[4:]
default:
return optClientId[2:]
}
}

View File

@ -12,7 +12,8 @@ func TestMakeMsgAdvertise(t *testing.T) {
transactionId := [3]byte{'1', '2', '3'}
expectedIp := net.ParseIP("2001:db8:f00f:cafe::1")
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgAdvertise(transactionId, expectedClientId, []byte("1234"), 0x11, expectedIp)
@ -63,7 +64,8 @@ func TestMakeMsgAdvertiseWithHttpClientArch(t *testing.T) {
transactionId := [3]byte{'1', '2', '3'}
expectedIp := net.ParseIP("2001:db8:f00f:cafe::1")
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgAdvertise(transactionId, expectedClientId, []byte("1234"), 0x10, expectedIp)
@ -85,7 +87,8 @@ func TestMakeMsgReply(t *testing.T) {
transactionId := [3]byte{'1', '2', '3'}
expectedIp := net.ParseIP("2001:db8:f00f:cafe::1")
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgReply(transactionId, expectedClientId, []byte("1234"), 0x11, expectedIp)
@ -136,7 +139,8 @@ func TestMakeMsgReplyWithHttpClientArch(t *testing.T) {
transactionId := [3]byte{'1', '2', '3'}
expectedIp := net.ParseIP("2001:db8:f00f:cafe::1")
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgReply(transactionId, expectedClientId, []byte("1234"), 0x10, expectedIp)
@ -157,7 +161,8 @@ func TestMakeMsgInformationRequestReply(t *testing.T) {
expectedServerId := []byte("serverid")
transactionId := [3]byte{'1', '2', '3'}
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x11)
@ -202,7 +207,8 @@ func TestMakeMsgInformationRequestReplyWithHttpClientArch(t *testing.T) {
expectedServerId := []byte("serverid")
transactionId := [3]byte{'1', '2', '3'}
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x10)
@ -223,7 +229,8 @@ func TestMakeMsgReleaseReply(t *testing.T) {
expectedServerId := []byte("serverid")
transactionId := [3]byte{'1', '2', '3'}
builder := MakePacketBuilder(expectedServerId, 90, 100, "httpbootfileurl", "ipxebootfileurl",
bootConfig := MakeStaticBootConfiguration("httpbootfileurl", "ipxebootfileurl")
builder := MakePacketBuilder(expectedServerId, 90, 100, bootConfig,
NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::1"), net.ParseIP("2001:db8:f00f:cafe::1"), 100))
msg := builder.MakeMsgReleaseReply(transactionId, expectedClientId)
@ -258,6 +265,33 @@ func TestMakeMsgReleaseReply(t *testing.T) {
}
}
func TestExtractLLAddressOrIdWithDUIDLLT(t *testing.T) {
builder := &PacketBuilder{}
expectedLLAddress := []byte{0xac, 0xbc, 0x32, 0xae, 0x86, 0x37}
llAddress := builder.ExtractLLAddressOrId([]byte{0x0, 0x1, 0x0, 0x1, 0x1, 0x2, 0x3, 0x4, 0xac, 0xbc, 0x32, 0xae, 0x86, 0x37})
if string(expectedLLAddress) != string(llAddress) {
t.Fatalf("Expected ll address %x, got: %x", expectedLLAddress, llAddress)
}
}
func TestExtractLLAddressOrIdWithDUIDEN(t *testing.T) {
builder := &PacketBuilder{}
expectedId := []byte{0x0, 0x1, 0x2, 0x3, 0xac, 0xbc, 0x32, 0xae, 0x86, 0x37}
id := builder.ExtractLLAddressOrId([]byte{0x0, 0x2, 0x0, 0x1, 0x2, 0x3, 0xac, 0xbc, 0x32, 0xae, 0x86, 0x37})
if string(expectedId) != string(id) {
t.Fatalf("Expected id %x, got: %x", expectedId, id)
}
}
func TestExtractLLAddressOrIdWithDUIDLL(t *testing.T) {
builder := &PacketBuilder{}
expectedLLAddress := []byte{0xac, 0xbc, 0x32, 0xae, 0x86, 0x37}
llAddress := builder.ExtractLLAddressOrId([]byte{0x0, 0x3, 0x0, 0x1, 0xac, 0xbc, 0x32, 0xae, 0x86, 0x37})
if string(expectedLLAddress) != string(llAddress) {
t.Fatalf("Expected ll address %x, got: %x", expectedLLAddress, llAddress)
}
}
func TestShouldDiscardSolicitWithoutBootfileUrlOption(t *testing.T) {
clientId := []byte("clientid")
options := make(Options)
@ -343,6 +377,7 @@ func TestShouldDiscardRequestWithWrongServerId(t *testing.T) {
t.Fatalf("Should discard request packet with wrong server id option, but didn't")
}
}
func MakeOptionRequestOptions(options []uint16) *Option {
value := make([]byte, len(options)*2)
for i, option := range(options) {

View File

@ -91,10 +91,10 @@ func (p *RandomAddressPool) ReserveAddress(clientId, interfaceId []byte) *Identi
newIp := big.NewInt(0).Add(p.poolStartAddress, big.NewInt(0).SetUint64(hostOffset))
_, exists := p.usedIps[newIp.Uint64()]; if !exists {
timeNow := p.timeNow()
to_ret := &IdentityAssociation{clientId: clientId,
interfaceId: interfaceId,
ipAddress: newIp.Bytes(),
createdAt: timeNow }
to_ret := &IdentityAssociation{ClientId: clientId,
InterfaceId: interfaceId,
IpAddress: newIp.Bytes(),
CreatedAt: timeNow }
p.identityAssociations[clientIdHash] = to_ret
p.usedIps[newIp.Uint64()] = struct{}{}
p.identityAssociationExpirations.Push(&AssociationExpiration{expiresAt: p.calculateAssociationExpiration(timeNow), ia: to_ret})
@ -113,7 +113,7 @@ func (p *RandomAddressPool) ReleaseAddress(clientId, interfaceId []byte) {
p.lock <- 1
return
}
delete(p.usedIps, big.NewInt(0).SetBytes(association.ipAddress).Uint64())
delete(p.usedIps, big.NewInt(0).SetBytes(association.IpAddress).Uint64())
delete(p.identityAssociations, p.calculateIaIdHash(clientId, interfaceId))
p.lock <- 1
}
@ -125,8 +125,8 @@ func (p *RandomAddressPool) ExpireIdentityAssociations() {
expiration := p.identityAssociationExpirations.Peek().(*AssociationExpiration)
if p.timeNow().Before(expiration.expiresAt) { break }
p.identityAssociationExpirations.Shift()
delete(p.identityAssociations, p.calculateIaIdHash(expiration.ia.clientId, expiration.ia.interfaceId))
delete(p.usedIps, big.NewInt(0).SetBytes(expiration.ia.ipAddress).Uint64())
delete(p.identityAssociations, p.calculateIaIdHash(expiration.ia.ClientId, expiration.ia.InterfaceId))
delete(p.usedIps, big.NewInt(0).SetBytes(expiration.ia.IpAddress).Uint64())
}
p.lock <- 1
}

View File

@ -20,20 +20,20 @@ func TestReserveAddress(t *testing.T) {
if ia == nil {
t.Fatalf("Expected a non-nil identity association")
}
if string(ia.ipAddress) != string(expectedIp) {
t.Fatalf("Expected ip: %v, but got: %v", expectedIp, ia.ipAddress)
if string(ia.IpAddress) != string(expectedIp) {
t.Fatalf("Expected ip: %v, but got: %v", expectedIp, ia.IpAddress)
}
if string(ia.clientId) != string(expectedClientId) {
t.Fatalf("Expected client id: %v, but got: %v", expectedClientId, ia.clientId)
if string(ia.ClientId) != string(expectedClientId) {
t.Fatalf("Expected client id: %v, but got: %v", expectedClientId, ia.ClientId)
}
if string(ia.interfaceId) != string(expectedIaId) {
t.Fatalf("Expected interface id: %v, but got: %v", expectedIaId, ia.interfaceId)
if string(ia.InterfaceId) != string(expectedIaId) {
t.Fatalf("Expected interface id: %v, but got: %v", expectedIaId, ia.InterfaceId)
}
if ia.createdAt != expectedTime {
t.Fatalf("Expected creation time: %v, but got: %v", expectedTime, ia.createdAt)
if ia.CreatedAt != expectedTime {
t.Fatalf("Expected creation time: %v, but got: %v", expectedTime, ia.CreatedAt)
}
if ia.createdAt != expectedTime {
t.Fatalf("Expected creation time: %v, but got: %v", expectedTime, ia.createdAt)
if ia.CreatedAt != expectedTime {
t.Fatalf("Expected creation time: %v, but got: %v", expectedTime, ia.CreatedAt)
}
}
@ -53,8 +53,8 @@ func TestReserveAddressUpdatesAddressPool(t *testing.T) {
if !exists {
t.Fatalf("Expected to find identity association at %d but didn't", expectedIdx)
}
if string(a.clientId) != string(expectedClientId) || string(a.interfaceId) != string(expectedIaId) {
t.Fatalf("Expected ia association with client id %x and ia id %x, but got %x %x respectively", expectedClientId, expectedIaId, a.clientId, a.interfaceId)
if string(a.ClientId) != string(expectedClientId) || string(a.InterfaceId) != string(expectedIaId) {
t.Fatalf("Expected ia association with client id %x and ia id %x, but got %x %x respectively", expectedClientId, expectedIaId, a.ClientId, a.InterfaceId)
}
}
@ -104,7 +104,7 @@ func TestReserveAddressReturnsExistingAssociation(t *testing.T) {
firstAssociation := pool.ReserveAddress(expectedClientId, expectedIaId)
secondAssociation := pool.ReserveAddress(expectedClientId, expectedIaId)
if string(firstAssociation.ipAddress) != string(secondAssociation.ipAddress) {
if string(firstAssociation.IpAddress) != string(secondAssociation.IpAddress) {
t.Fatal("Expected return of the same ip address on both invocations")
}
}
@ -122,6 +122,6 @@ func TestReleaseAddress(t *testing.T) {
pool.ReleaseAddress(expectedClientId, expectedIaId)
_, exists := pool.identityAssociations[pool.calculateIaIdHash(expectedClientId, expectedIaId)]; if exists {
t.Fatalf("identity association for %v should've been removed, but is still available", a.ipAddress)
t.Fatalf("identity association for %v should've been removed, but is still available", a.IpAddress)
}
}

View File

@ -2,8 +2,8 @@ package cli
import (
"github.com/spf13/cobra"
"go.universe.tf/netboot/pixiecore"
"fmt"
"go.universe.tf/netboot/pixiecorev6"
)
var bootIPv6Cmd = &cobra.Command{
@ -23,7 +23,7 @@ var bootIPv6Cmd = &cobra.Command{
fatalf("Error reading flag: %s", err)
}
s := pixiecore.NewServerV6()
s := pixiecorev6.NewServerV6()
if addr == "" {
fatalf("Please specify address to listen on")

View File

@ -1,4 +1,4 @@
package pixiecore
package pixiecorev6
import (
"go.universe.tf/netboot/dhcp6"

View File

@ -1,4 +1,4 @@
package pixiecore
package pixiecorev6
import (
"go.universe.tf/netboot/dhcp6"
@ -19,7 +19,6 @@ type ServerV6 struct {
errs chan error
eventsMu sync.Mutex
events map[string][]machineEvent
Log func(subsystem, msg string)
Debug func(subsystem, msg string)
@ -45,7 +44,6 @@ func (s *ServerV6) Serve() error {
s.log("dhcp", "new connection...")
s.events = make(map[string][]machineEvent)
// 5 buffer slots, one for each goroutine, plus one for
// Shutdown(). We only ever pull the first error out, but shutdown
// will likely generate some spurious errors from the other
@ -57,9 +55,9 @@ func (s *ServerV6) Serve() error {
s.SetDUID(dhcp.SourceHardwareAddress())
addressPool := dhcp6.NewRandomAddressPool(net.ParseIP("2001:db8:f00f:cafe::10"), net.ParseIP("2001:db8:f00f:cafe::100"), 1850)
packetBuilder := dhcp6.MakePacketBuilder(s.Duid, 1800, 1850,
"http://[2001:db8:f00f:cafe::4]/bootx64.efi",
"http://[2001:db8:f00f:cafe::4]/script.ipxe", addressPool)
// bootConfiguration := dhcp6.MakeStaticBootConfiguration("http://[2001:db8:f00f:cafe::4]/bootx64.efi", "http://[2001:db8:f00f:cafe::4]/script.ipxe")
bootConfiguration := dhcp6.MakeApiBootConfiguration("http://[2001:db8:f00f:cafe::4]:8888/", 10 *time.Second)
packetBuilder := dhcp6.MakePacketBuilder(s.Duid, 1800, 1850, bootConfiguration, addressPool)
go func() { s.errs <- s.serveDHCP(dhcp, packetBuilder) }()