diff --git a/cmd/pixiecore/main.go b/cmd/pixiecore/main.go index 134efdf..0d0bde0 100644 --- a/cmd/pixiecore/main.go +++ b/cmd/pixiecore/main.go @@ -20,10 +20,6 @@ import ( "go.universe.tf/netboot/third_party/ipxe" ) -// qemu-system-x86_64 -L . --bios /usr/share/edk2-firmware/ipv6/OVMF.fd -netdev bridge,br=br1,id=net0 -device virtio-net-pci,netdev=net0 -// sudo ./pixiecore bootipv6 --listen-addr=2001:db8:f00f:cafe::4/64 --httpboot-url=http://[2001:db8:f00f:cafe::4]/bootx64.efi --ipxe-url=http://[2001:db8:f00f:cafe::4]/script.ipxe -// sudo ./pixiecore ipv6api --listen-addr=2001:db8:f00f:cafe::4 --api-request-url=http://[2001:db8:f00f:cafe::4]:8888 - func main() { cli.Ipxe[pixiecore.FirmwareX86PC] = ipxe.MustAsset("undionly.kpxe") cli.Ipxe[pixiecore.FirmwareEFI32] = ipxe.MustAsset("ipxe-i386.efi") diff --git a/dhcp6/boot_configuration.go b/dhcp6/boot_configuration.go index 78abcca..e964579 100644 --- a/dhcp6/boot_configuration.go +++ b/dhcp6/boot_configuration.go @@ -7,28 +7,31 @@ import ( "fmt" "net/url" "bytes" + "net" ) type BootConfiguration interface { GetBootUrl(id []byte, clientArchType uint16) ([]byte, error) GetPreference() []byte - GetRecursiveDns() []byte + GetRecursiveDns() []net.IP } type StaticBootConfiguration struct { - HttpBootUrl []byte - IPxeBootUrl []byte - RecursiveDns []byte - Preference []byte - UsePreference bool + HttpBootUrl []byte + IPxeBootUrl []byte + RecursiveDns []net.IP + Preference []byte + UsePreference bool } -func MakeStaticBootConfiguration(httpBootUrl, ipxeBootUrl string, preference uint8, usePreference bool) *StaticBootConfiguration { +func MakeStaticBootConfiguration(httpBootUrl, ipxeBootUrl string, preference uint8, usePreference bool, + dnsServerAddresses []net.IP) *StaticBootConfiguration { ret := &StaticBootConfiguration{HttpBootUrl: []byte(httpBootUrl), IPxeBootUrl: []byte(ipxeBootUrl), UsePreference: usePreference} if usePreference { ret.Preference = make([]byte, 1) ret.Preference[0] = byte(preference) } + ret.RecursiveDns = dnsServerAddresses return ret } @@ -43,19 +46,20 @@ func (bc *StaticBootConfiguration) GetPreference() []byte { return bc.Preference } -func (bc *StaticBootConfiguration) GetRecursiveDns() []byte { +func (bc *StaticBootConfiguration) GetRecursiveDns() []net.IP { return bc.RecursiveDns } type ApiBootConfiguration struct { - client *http.Client - urlPrefix string - RecursiveDns []byte - Preference []byte - UsePreference bool + client *http.Client + urlPrefix string + RecursiveDns []net.IP + Preference []byte + UsePreference bool } -func MakeApiBootConfiguration(url string, timeout time.Duration, preference uint8, usePreference bool) *ApiBootConfiguration { +func MakeApiBootConfiguration(url string, timeout time.Duration, preference uint8, usePreference bool, + dnsServerAddresses []net.IP) *ApiBootConfiguration { if !strings.HasSuffix(url, "/") { url += "/" } @@ -68,6 +72,7 @@ func MakeApiBootConfiguration(url string, timeout time.Duration, preference uint ret.Preference = make([]byte, 1) ret.Preference[0] = byte(preference) } + ret.RecursiveDns = dnsServerAddresses return ret } @@ -110,6 +115,6 @@ func (bc *ApiBootConfiguration) GetPreference() []byte { return bc.Preference } -func (bc *ApiBootConfiguration) GetRecursiveDns() []byte { +func (bc *ApiBootConfiguration) GetRecursiveDns() []net.IP { return bc.RecursiveDns } diff --git a/dhcp6/options.go b/dhcp6/options.go index 1ba7671..d4a4684 100644 --- a/dhcp6/options.go +++ b/dhcp6/options.go @@ -158,6 +158,14 @@ func MakeStatusOption(statusCode uint16, message string) *Option { return MakeOption(OptStatusCode, value) } +func MakeDNSServersOption(addresses []net.IP) *Option { + value := make([]byte, 16*len(addresses)) + for i, dnsAddress := range addresses { + copy(value[i*16:], dnsAddress) + } + return MakeOption(OptRecursiveDns, value) +} + func (o Options) Marshal() ([]byte, error) { buffer := bytes.NewBuffer(make([]byte, 0, 1446)) for _, multipleOptions := range(o) { diff --git a/dhcp6/options_test.go b/dhcp6/options_test.go index 4fec872..0584bc7 100644 --- a/dhcp6/options_test.go +++ b/dhcp6/options_test.go @@ -102,3 +102,22 @@ func TestUnmarshalFailsIfOROLengthIsOdd(t *testing.T) { t.Fatalf("Parsing options should fail: option request for options has odd length.") } } + +func TestMakeDNSServersOption(t *testing.T) { + expectedAddress1 := net.ParseIP("2001:db8:f00f:cafe::99") + expectedAddress2 := net.ParseIP("2001:db8:f00f:cafe::9A") + dnsServersOption := MakeDNSServersOption([]net.IP{expectedAddress1, expectedAddress2}) + + if dnsServersOption.Id != OptRecursiveDns { + t.Fatalf("Expected option id %d, got %d", OptRecursiveDns, dnsServersOption.Id) + } + if dnsServersOption.Length != 32 { + t.Fatalf("Expected length 32 bytes, got %d", dnsServersOption.Length) + } + if string(dnsServersOption.Value[0:16]) != string(expectedAddress1) { + t.Fatalf("Expected dns server address %v, got %v", expectedAddress1, net.IP(dnsServersOption.Value[0:16])) + } + if string(dnsServersOption.Value[16:]) != string(expectedAddress2) { + t.Fatalf("Expected dns server address %v, got %v", expectedAddress2, net.IP(dnsServersOption.Value[16:])) + } +} diff --git a/dhcp6/packet_builder.go b/dhcp6/packet_builder.go index 6e0a700..bc75419 100644 --- a/dhcp6/packet_builder.go +++ b/dhcp6/packet_builder.go @@ -3,6 +3,7 @@ package dhcp6 import ( "hash/fnv" "encoding/binary" + "net" ) type PacketBuilder struct { @@ -27,7 +28,7 @@ func (b *PacketBuilder) BuildResponse(in *Packet, configuration BootConfiguratio return b.MakeMsgAdvertiseWithNoAddrsAvailable(in.TransactionID, in.Options.ClientId(), err), err } return b.MakeMsgAdvertise(in.TransactionID, in.Options.ClientId(), - in.Options.ClientArchType(), associations, bootFileUrl, configuration.GetPreference()), nil + in.Options.ClientArchType(), associations, bootFileUrl, configuration.GetPreference(), configuration.GetRecursiveDns()), nil case MsgRequest: bootFileUrl, err := configuration.GetBootUrl(b.ExtractLLAddressOrId(in.Options.ClientId()), in.Options.ClientArchType()) if err != nil { @@ -35,14 +36,15 @@ func (b *PacketBuilder) BuildResponse(in *Packet, configuration BootConfiguratio } associations, err := addresses.ReserveAddresses(in.Options.ClientId(), in.Options.IaNaIds()) return b.MakeMsgReply(in.TransactionID, in.Options.ClientId(), - in.Options.ClientArchType(), associations, iasWithoutAddesses(associations, in.Options.IaNaIds()), bootFileUrl, err), err + in.Options.ClientArchType(), associations, iasWithoutAddesses(associations, in.Options.IaNaIds()), bootFileUrl, + configuration.GetRecursiveDns(), err), err case MsgInformationRequest: bootFileUrl, err := configuration.GetBootUrl(b.ExtractLLAddressOrId(in.Options.ClientId()), in.Options.ClientArchType()) if err != nil { return nil, err } return b.MakeMsgInformationRequestReply(in.TransactionID, in.Options.ClientId(), - in.Options.ClientArchType(), bootFileUrl), nil + in.Options.ClientArchType(), bootFileUrl, configuration.GetRecursiveDns()), nil case MsgRelease: addresses.ReleaseAddresses(in.Options.ClientId(), in.Options.IaNaIds()) return b.MakeMsgReleaseReply(in.TransactionID, in.Options.ClientId()), nil @@ -52,7 +54,7 @@ func (b *PacketBuilder) BuildResponse(in *Packet, configuration BootConfiguratio } func (b *PacketBuilder) MakeMsgAdvertise(transactionId [3]byte, clientId []byte, clientArchType uint16, - associations []*IdentityAssociation, bootFileUrl, preference []byte) *Packet { + associations []*IdentityAssociation, bootFileUrl, preference []byte, dnsServers []net.IP) *Packet { ret_options := make(Options) ret_options.AddOption(MakeOption(OptClientId, clientId)) for _, association := range(associations) { @@ -65,15 +67,13 @@ func (b *PacketBuilder) MakeMsgAdvertise(transactionId [3]byte, clientId []byte, } ret_options.AddOption(MakeOption(OptBootfileUrl, bootFileUrl)) if preference != nil {ret_options.AddOption(MakeOption(OptPreference, preference))} - - //ret_options.AddOption(OptRecursiveDns, net.ParseIP("2001:db8:f00f:cafe::1")) - //ret_options.AddOption(OptBootfileParam, []byte("http://") + ret_options.AddOption(MakeDNSServersOption(dnsServers)) return &Packet{Type: MsgAdvertise, TransactionID: transactionId, Options: ret_options} } func (b *PacketBuilder) MakeMsgReply(transactionId [3]byte, clientId []byte, clientArchType uint16, - associations []*IdentityAssociation, iasWithoutAddresses [][]byte, bootFileUrl []byte, err error) *Packet { + associations []*IdentityAssociation, iasWithoutAddresses [][]byte, bootFileUrl []byte, dnsServers []net.IP, err error) *Packet { ret_options := make(Options) ret_options.AddOption(MakeOption(OptClientId, clientId)) for _, association := range(associations) { @@ -89,12 +89,13 @@ func (b *PacketBuilder) MakeMsgReply(transactionId [3]byte, clientId []byte, cli 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, bootFileUrl)) + ret_options.AddOption(MakeDNSServersOption(dnsServers)) return &Packet{Type: MsgReply, TransactionID: transactionId, Options: ret_options} } func (b *PacketBuilder) MakeMsgInformationRequestReply(transactionId [3]byte, clientId []byte, clientArchType uint16, - bootFileUrl []byte) *Packet { + bootFileUrl []byte, dnsServers []net.IP) *Packet { ret_options := make(Options) ret_options.AddOption(MakeOption(OptClientId, clientId)) ret_options.AddOption(MakeOption(OptServerId, b.ServerDuid)) @@ -102,6 +103,7 @@ func (b *PacketBuilder) MakeMsgInformationRequestReply(transactionId [3]byte, cl 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, bootFileUrl)) + ret_options.AddOption(MakeDNSServersOption(dnsServers)) return &Packet{Type: MsgReply, TransactionID: transactionId, Options: ret_options} } diff --git a/dhcp6/packet_builder_test.go b/dhcp6/packet_builder_test.go index a99fac5..b78f51f 100644 --- a/dhcp6/packet_builder_test.go +++ b/dhcp6/packet_builder_test.go @@ -14,12 +14,13 @@ func TestMakeMsgAdvertise(t *testing.T) { transactionId := [3]byte{'1', '2', '3'} expectedIp := net.ParseIP("2001:db8:f00f:cafe::1") expectedBootFileUrl := []byte("http://bootfileurl") + expectedDnsServerIp := net.ParseIP("2001:db8:f00f:cafe::99") identityAssociation := &IdentityAssociation{IpAddress: expectedIp, InterfaceId: expectedInterfaceId} builder := MakePacketBuilder(expectedServerId, 90, 100) msg := builder.MakeMsgAdvertise(transactionId, expectedClientId, 0x11, []*IdentityAssociation{identityAssociation}, - expectedBootFileUrl, nil) + expectedBootFileUrl, nil, []net.IP{expectedDnsServerIp}) if msg.Type != MsgAdvertise { t.Fatalf("Expected message type %d, got %d", MsgAdvertise, msg.Type) @@ -70,7 +71,7 @@ func TestShouldSetPreferenceOptionWhenSpecified(t *testing.T) { expectedPreference := []byte{128} msg := builder.MakeMsgAdvertise([3]byte{'t', 'i', 'd'}, []byte("clientid"), 0x11, - []*IdentityAssociation{identityAssociation}, []byte("http://bootfileurl"), expectedPreference) + []*IdentityAssociation{identityAssociation}, []byte("http://bootfileurl"), expectedPreference, []net.IP{}) preferenceOption := msg.Options[OptPreference] if preferenceOption == nil { @@ -92,7 +93,7 @@ func TestMakeMsgAdvertiseWithHttpClientArch(t *testing.T) { builder := MakePacketBuilder(expectedServerId, 90, 100) msg := builder.MakeMsgAdvertise(transactionId, expectedClientId, 0x10, []*IdentityAssociation{identityAssociation}, - expectedBootFileUrl, nil) + expectedBootFileUrl, nil, []net.IP{}) vendorClassOption := msg.Options[OptVendorClass] if vendorClassOption == nil { @@ -158,12 +159,13 @@ func TestMakeMsgReply(t *testing.T) { transactionId := [3]byte{'1', '2', '3'} expectedIp := net.ParseIP("2001:db8:f00f:cafe::1") expectedBootFileUrl := []byte("http://bootfileurl") + expectedDnsServerIp := net.ParseIP("2001:db8:f00f:cafe::99") identityAssociation := &IdentityAssociation{IpAddress: expectedIp, InterfaceId: []byte("id-1")} builder := MakePacketBuilder(expectedServerId, 90, 100) msg := builder.MakeMsgReply(transactionId, expectedClientId, 0x11, []*IdentityAssociation{identityAssociation}, - make([][]byte, 0), expectedBootFileUrl, nil) + make([][]byte, 0), expectedBootFileUrl, []net.IP{expectedDnsServerIp}, nil) if msg.Type != MsgReply { t.Fatalf("Expected message type %d, got %d", MsgAdvertise, msg.Type) @@ -220,7 +222,7 @@ func TestMakeMsgReplyWithHttpClientArch(t *testing.T) { msg := builder.MakeMsgReply(transactionId, expectedClientId, 0x10, []*IdentityAssociation{identityAssociation}, make([][]byte, 0), - expectedBootFileUrl, nil) + expectedBootFileUrl, []net.IP{}, nil) vendorClassOption := msg.Options[OptVendorClass] if vendorClassOption == nil { @@ -248,7 +250,7 @@ func TestMakeMsgReplyWithNoAddrsAvailable(t *testing.T) { builder := MakePacketBuilder(expectedServerId, 90, 100) msg := builder.MakeMsgReply(transactionId, expectedClientId, 0x10, - []*IdentityAssociation{identityAssociation}, [][]byte{[]byte("id-2")}, expectedBootFileUrl, + []*IdentityAssociation{identityAssociation}, [][]byte{[]byte("id-2")}, expectedBootFileUrl, []net.IP{}, fmt.Errorf(expectedErrorMessage)) iaNaOption := msg.Options[OptIaNa] @@ -295,10 +297,12 @@ func TestMakeMsgInformationRequestReply(t *testing.T) { expectedServerId := []byte("serverid") transactionId := [3]byte{'1', '2', '3'} expectedBootFileUrl := []byte("http://bootfileurl") + expectedDnsServerIp := net.ParseIP("2001:db8:f00f:cafe::99") builder := MakePacketBuilder(expectedServerId, 90, 100) - msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x11, expectedBootFileUrl) + msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x11, + expectedBootFileUrl, []net.IP{expectedDnsServerIp}) if msg.Type != MsgReply { t.Fatalf("Expected message type %d, got %d", MsgAdvertise, msg.Type) @@ -346,7 +350,8 @@ func TestMakeMsgInformationRequestReplyWithHttpClientArch(t *testing.T) { builder := MakePacketBuilder(expectedServerId, 90, 100) - msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x10, expectedBootFileUrl) + msg := builder.MakeMsgInformationRequestReply(transactionId, expectedClientId, 0x10, + expectedBootFileUrl, []net.IP{}) vendorClassOption := msg.Options[OptVendorClass] if vendorClassOption == nil { diff --git a/pixiecore/cli/bootipv6cmd.go b/pixiecore/cli/bootipv6cmd.go index 7af2c14..6cc1803 100644 --- a/pixiecore/cli/bootipv6cmd.go +++ b/pixiecore/cli/bootipv6cmd.go @@ -6,8 +6,10 @@ import ( "go.universe.tf/netboot/pixiecorev6" "go.universe.tf/netboot/dhcp6" "net" + "strings" ) +// pixiecore bootipv6 --listen-addr=2001:db8:f00f:cafe::4/64 --httpboot-url=http://[2001:db8:f00f:cafe::4]/bootx64.efi --ipxe-url=http://[2001:db8:f00f:cafe::4]/script.ipxe var bootIPv6Cmd = &cobra.Command{ Use: "bootipv6", Short: "Boot a kernel and optional init ramdisks over IPv6", @@ -50,7 +52,18 @@ var bootIPv6Cmd = &cobra.Command{ if err != nil { fatalf("Error reading flag: %s", err) } - s.BootConfig = dhcp6.MakeStaticBootConfiguration(httpBootUrl, ipxeUrl, preference, cmd.Flags().Changed("preference")) + dnsServers, err := cmd.Flags().GetString("dns-servers") + if err != nil { + fatalf("Error reading flag: %s", err) + } + dnsServerAddresses := make([]net.IP, 0) + if cmd.Flags().Changed("dns-servers") { + for _, dnsServerAddress := range strings.Split(dnsServers, ",") { + dnsServerAddresses = append(dnsServerAddresses, net.ParseIP(dnsServerAddress)) + } + } + s.BootConfig = dhcp6.MakeStaticBootConfiguration(httpBootUrl, ipxeUrl, preference, + cmd.Flags().Changed("preference"), dnsServerAddresses) addressPoolStart, err := cmd.Flags().GetString("address-pool-start") if err != nil { @@ -80,6 +93,7 @@ func serverv6ConfigFlags(cmd *cobra.Command) { cmd.Flags().StringP("address-pool-start", "", "2001:db8:f00f:cafe:ffff::100", "Starting ip of the address pool, e.g. 2001:db8:f00f:cafe:ffff::100") cmd.Flags().Uint64("address-pool-size", 50, "Address pool size") cmd.Flags().Uint32("address-pool-lifetime", 1850, "Address pool ip valid lifetime in seconds") + cmd.Flags().StringP("dns-servers", "", "", "Comma separated list of one or more dns server addresses") } func init() { diff --git a/pixiecore/cli/ipv6apicmd.go b/pixiecore/cli/ipv6apicmd.go index c1aaeae..c0370a6 100644 --- a/pixiecore/cli/ipv6apicmd.go +++ b/pixiecore/cli/ipv6apicmd.go @@ -7,8 +7,11 @@ import ( "go.universe.tf/netboot/dhcp6" "time" "net" + "strings" ) +// pixiecore ipv6api --listen-addr=2001:db8:f00f:cafe::4 --api-request-url=http://[2001:db8:f00f:cafe::4]:8888 + var ipv6ApiCmd = &cobra.Command{ Use: "ipv6api", Short: "Boot a kernel and optional init ramdisks over IPv6 using api", @@ -46,7 +49,18 @@ var ipv6ApiCmd = &cobra.Command{ if err != nil { fatalf("Error reading flag: %s", err) } - s.BootConfig = dhcp6.MakeApiBootConfiguration(apiUrl, apiTimeout, preference, cmd.Flags().Changed("preference")) + dnsServers, err := cmd.Flags().GetString("dns-servers") + if err != nil { + fatalf("Error reading flag: %s", err) + } + dnsServerAddresses := make([]net.IP, 0) + if cmd.Flags().Changed("dns-servers") { + for _, dnsServerAddress := range strings.Split(dnsServers, ",") { + dnsServerAddresses = append(dnsServerAddresses, net.ParseIP(dnsServerAddress)) + } + } + s.BootConfig = dhcp6.MakeApiBootConfiguration(apiUrl, apiTimeout, preference, + cmd.Flags().Changed("preference"), dnsServerAddresses) addressPoolStart, err := cmd.Flags().GetString("address-pool-start") if err != nil { @@ -76,6 +90,7 @@ func serverv6ApiConfigFlags(cmd *cobra.Command) { cmd.Flags().StringP("address-pool-start", "", "2001:db8:f00f:cafe:ffff::100", "Starting ip of the address pool, e.g. 2001:db8:f00f:cafe:ffff::100") cmd.Flags().Uint64("address-pool-size", 50, "Address pool size") cmd.Flags().Uint32("address-pool-lifetime", 1850, "Address pool ip address valid lifetime in seconds") + cmd.Flags().StringP("dns-servers", "", "", "Comma separated list of one or more dns server addresses") } func init() {