From 3a5808cb30a28b79c5d1db20e9af316162c0ef57 Mon Sep 17 00:00:00 2001 From: Dmitri Dolguikh Date: Mon, 5 Jun 2017 13:36:37 +0100 Subject: [PATCH] first stab at dhcpv6 support --- cmd/pixiecore/main.go | 21 +- dhcp6/conn.go | 114 ++++++++++ dhcp6/options.go | 167 +++++++++++++++ dhcp6/options_test.go | 37 ++++ dhcp6/packet.go | 215 +++++++++++++++++++ dhcp6/testdata/dhcp6.pcap | Bin 0 -> 908 bytes dhcp6/testdata/dhcp6.txt | 427 ++++++++++++++++++++++++++++++++++++++ pixiecore/dhcpv6.go | 34 +++ pixiecore/pixicorev6.go | 91 ++++++++ 9 files changed, 1103 insertions(+), 3 deletions(-) create mode 100644 dhcp6/conn.go create mode 100644 dhcp6/options.go create mode 100644 dhcp6/options_test.go create mode 100644 dhcp6/packet.go create mode 100644 dhcp6/testdata/dhcp6.pcap create mode 100644 dhcp6/testdata/dhcp6.txt create mode 100644 pixiecore/dhcpv6.go create mode 100644 pixiecore/pixicorev6.go diff --git a/cmd/pixiecore/main.go b/cmd/pixiecore/main.go index 0d0bde0..1ebeb26 100644 --- a/cmd/pixiecore/main.go +++ b/cmd/pixiecore/main.go @@ -16,15 +16,30 @@ package main import ( "go.universe.tf/netboot/pixiecore" - "go.universe.tf/netboot/pixiecore/cli" - "go.universe.tf/netboot/third_party/ipxe" +// "go.universe.tf/netboot/pixiecore/cli" +// "go.universe.tf/netboot/third_party/ipxe" + "fmt" ) +// 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 func main() { - cli.Ipxe[pixiecore.FirmwareX86PC] = ipxe.MustAsset("undionly.kpxe") + /*cli.Ipxe[pixiecore.FirmwareX86PC] = ipxe.MustAsset("undionly.kpxe") cli.Ipxe[pixiecore.FirmwareEFI32] = ipxe.MustAsset("ipxe-i386.efi") cli.Ipxe[pixiecore.FirmwareEFI64] = ipxe.MustAsset("ipxe-x86_64.efi") cli.Ipxe[pixiecore.FirmwareEFIBC] = ipxe.MustAsset("ipxe-x86_64.efi") cli.Ipxe[pixiecore.FirmwareX86Ipxe] = ipxe.MustAsset("ipxe.pxe") cli.CLI() +*/ + + log := func(subsystem, msg string) { fmt.Printf("[%s] %s", subsystem, msg) } + s := pixiecore.ServerV6{ + Address: "2001:db8:f00f:cafe::4/64", + Log: log, + Debug: log, + } + + err := s.Serve() + if err != nil { + fmt.Printf("Error: %s", err) + } } diff --git a/dhcp6/conn.go b/dhcp6/conn.go new file mode 100644 index 0000000..9251d43 --- /dev/null +++ b/dhcp6/conn.go @@ -0,0 +1,114 @@ +package dhcp6 + +import ( + "io" + "net" + "time" + "golang.org/x/net/ipv6" + "fmt" +) + +type conn interface { + io.Closer + Recv([]byte) (b []byte, addr *net.UDPAddr, ifidx int, err error) + Send(b []byte, addr *net.UDPAddr, ifidx int) error + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error +} + +type Conn struct { + conn *ipv6.PacketConn + group net.IP + ifi *net.Interface + listenAddress string + listenPort string +} + +func NewConn(addr string) (*Conn, error) { + ifi, err := InterfaceIndexByAddress(addr) + if err != nil { + return nil, err + } + + group := net.ParseIP("ff02::1:2") + c, err := net.ListenPacket("udp6", "[::]:547") + if err != nil { + return nil, err + } + pc := ipv6.NewPacketConn(c) + if err := pc.JoinGroup(ifi, &net.UDPAddr{IP: group}); err != nil { + pc.Close() + return nil, err + } + + if err := pc.SetControlMessage(ipv6.FlagSrc | ipv6.FlagDst, true); err != nil { + pc.Close() + return nil, err + } + + return &Conn{ + conn: pc, + group: group, + ifi: ifi, + listenAddress: addr, + listenPort: "547", + }, nil +} + +func (c *Conn) Close() error { + return c.conn.Close() +} + +func InterfaceIndexByAddress(ifAddr string) (*net.Interface, error) { + allIfis, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("Error getting network interface information: %s", err) + } + for _, ifi := range allIfis { + addrs, err := ifi.Addrs() + if err != nil { + return nil, fmt.Errorf("Error getting network interface address information: %s", err) + } + for _, addr := range addrs { + if addr.String() == ifAddr { + return &ifi, nil + } + } + } + return nil, fmt.Errorf("Couldn't find an interface with address %s", ifAddr) +} + +func (c *Conn) RecvDHCP() (*Packet, net.IP, error) { + b := make([]byte, 1500) + for { + packetSize, rcm, _, err := c.conn.ReadFrom(b) + if err != nil { + return nil, nil, err + } + if c.ifi.Index != 0 && rcm.IfIndex != c.ifi.Index { + continue + } + if !rcm.Dst.IsMulticast() || !rcm.Dst.Equal(c.group) { + continue // unknown group, discard + } + pkt := MakePacket(b, packetSize) + + return pkt, rcm.Src, nil + } +} + +func (c *Conn) SendDHCP(dst net.IP, p []byte) error { + dstAddr, err := net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%s", dst.String() + "%en0", "546")) + if err != nil { + return fmt.Errorf("Error resolving ipv6 address %s: %s", dst.String(), err) + } + _, err = c.conn.WriteTo(p, nil, dstAddr) + if err != nil { + return fmt.Errorf("Error sending a reply to %s: %s", dst.String(), err) + } + return nil +} + +func (c *Conn) SourceHardwareAddress() net.HardwareAddr { + return c.ifi.HardwareAddr +} \ No newline at end of file diff --git a/dhcp6/options.go b/dhcp6/options.go new file mode 100644 index 0000000..5e278a3 --- /dev/null +++ b/dhcp6/options.go @@ -0,0 +1,167 @@ +package dhcp6 + +import ( + "encoding/binary" + "fmt" + "bytes" + "net" +) + +const ( + OptClientId uint16 = 1 // IPMask + OptServerId = 2 // int32 + OptIaNa = 3 // IPs + OptIaTa = 4 // IPs + OptIaAddr = 5 // string + OptOro = 6 // uint16 + OptPreference = 7 // string + OptElapsedTime = 8 // IP + OptRelayMessage = 9 // IP + OptAuth = 11 // []byte + OptUnicast = 12 // IP + OptStatusCode = 13 // uint32 + OptRapidCommit = 14 // byte + OptUserClass = 15 // IP + OptVendorClass = 16 // []byte + OptVendorOpts = 17 // string + OptInterfaceId = 18 // uint16 + OptReconfMsg = 19 // uint32 + OptReconfAccept = 20 // uint32 + OptRecursiveDns = 23 // []byte + OptBootfileUrl = 59 + OptBootfileParam = 60 //[][]byte + OptClientArchType = 61 //[][]byte, sent by the client + // 24? Domain search list +) + +type Option struct { + Id uint16 + Length uint16 + Value []byte +} + +type Options map[uint16]*Option + +func MakeOptions(bs []byte) (Options, error) { + to_ret := make(Options) + for len(bs) > 0 { + optionLength := uint16(binary.BigEndian.Uint16(bs[2:4])) + optionId := uint16(binary.BigEndian.Uint16(bs[0:2])) + switch optionId { + // parse client_id + // parse server_id + // parse IaNa # do I need to support IaTa? + //parse ipaddr + case OptOro: + if optionLength% 2 != 0 { + return nil, fmt.Errorf("OptionID request for options (6) length should be even number of bytes: %d", optionLength) + } + default: + if len(bs[4:]) < int(optionLength) { + fmt.Printf("option %d claims to have %d bytes of payload, but only has %d bytes", optionId, optionLength, len(bs[4:])) + return nil, fmt.Errorf("option %d claims to have %d bytes of payload, but only has %d bytes", optionId, optionLength, len(bs[4:])) + } + } + to_ret[optionId] = &Option{ Id: optionId, Length: optionLength, Value: bs[4 : 4+optionLength]} + bs = bs[4+optionLength:] + } + return to_ret, nil +} + +func (o Options) HumanReadable() []string { + to_ret := make([]string, 0, len(o)) + for _, opt := range(o) { + to_ret = append(to_ret, fmt.Sprintf("Option: %d | %d | %d | %s\n", opt.Id, opt.Length, opt.Value, opt.Value)) + } + return to_ret +} + +func (o Options) AddOption(option *Option) { + o[option.Id] = option +} + +func MakeIaNaOption(iaid []byte, t1, t2 uint32, iaAddr *Option) (*Option) { + serializedIaAddr, _ := iaAddr.Marshal() + value := make([]byte, 12 + len(serializedIaAddr)) + copy(value[0:], iaid[0:4]) + binary.BigEndian.PutUint32(value[4:], t1) + binary.BigEndian.PutUint32(value[8:], t2) + copy(value[12:], serializedIaAddr) + return &Option{Id: OptIaNa, Length: uint16(len(value)), Value: value} +} + +func MakeIaAddrOption(addr net.IP, preferredLifetime, validLifetime uint32) (*Option) { + value := make([]byte, 24) + copy(value[0:], addr) + binary.BigEndian.PutUint32(value[16:], preferredLifetime) + binary.BigEndian.PutUint32(value[20:], validLifetime) + return &Option{ Id: OptIaAddr, Length: uint16(len(value)), Value: value} +} + +func (o Options) Marshal() ([]byte, error) { + buffer := bytes.NewBuffer(make([]byte, 0, 1446)) + for _, v := range(o) { + serialized, err := v.Marshal() + if err != nil { + return nil, fmt.Errorf("Error serializing option value: %s", err) + } + if err := binary.Write(buffer, binary.BigEndian, serialized); err != nil { + return nil, fmt.Errorf("Error serializing option value: %s", err) + } + } + return buffer.Bytes(), nil +} + +func (o *Option) Marshal() ([]byte, error) { + buffer := bytes.NewBuffer(make([]byte, 0, o.Length + 2)) + + err := binary.Write(buffer, binary.BigEndian, o.Id) + if err != nil { + return nil, fmt.Errorf("Error serializing option id: %s", err) + } + err = binary.Write(buffer, binary.BigEndian, o.Length) + if err != nil { + return nil, fmt.Errorf("Error serializing option length: %s", err) + } + err = binary.Write(buffer, binary.BigEndian, o.Value) + if err != nil { + return nil, fmt.Errorf("Error serializing option value: %s", err) + } + return buffer.Bytes(), nil +} + +func (o Options) UnmarshalOptionRequestOption() map[uint16]bool { + oro_content := o[OptOro].Value + to_ret := make(map[uint16]bool) + + for i := 0; i < int(o[OptOro].Length)/2; i++ { + to_ret[uint16(binary.BigEndian.Uint16(oro_content[i*2:(i+1)*2]))] = true + } + return to_ret +} + +func (o Options) RequestedBootFileUrlOption() bool { + requested_options := o.UnmarshalOptionRequestOption() + _, present := requested_options[OptBootfileUrl] + return present +} + +func (o Options) HasClientId() bool { + _, present := o[OptClientId] + return present +} + +func (o Options) HasServerId() bool { + _, present := o[OptServerId] + return present +} + +func (o Options) HasIaNa() bool { + _, present := o[OptIaNa] + return present +} + +func (o Options) HasIaTa() bool { + _, present := o[OptIaTa] + return present +} \ No newline at end of file diff --git a/dhcp6/options_test.go b/dhcp6/options_test.go new file mode 100644 index 0000000..297607a --- /dev/null +++ b/dhcp6/options_test.go @@ -0,0 +1,37 @@ +package dhcp6 + +import ( + "testing" +) + + + +func TestUnmarshalFailsIfOROLengthIsOdd(t *testing.T) { + in := []byte{0, 6, 0, 3, 0, 1, 1} + if _, err := MakeOptions(in); err == nil { + t.Fatalf("Parsing options should fail: option request for options has odd length.") + } +} + +func TestUnmarshalORO(t *testing.T) { + options := Options{ + 6: [][]byte{{0, 1, 0, 5, 1, 0}}, + } + parsed_options := options.UnmarshalOptionRequestOption() + + if l := len(parsed_options); l != 3 { + t.Fatalf("Expected 3 options, got: %d", l) + } + + if _, present := parsed_options[1]; !present { + t.Fatalf("Should contain option id 1") + } + + if _, present := parsed_options[5]; !present { + t.Fatalf("Should contain option id 5") + } + + if _, present := parsed_options[256]; !present { + t.Fatalf("Should contain option id 256") + } +} diff --git a/dhcp6/packet.go b/dhcp6/packet.go new file mode 100644 index 0000000..fbedb6e --- /dev/null +++ b/dhcp6/packet.go @@ -0,0 +1,215 @@ +package dhcp6 + +import ( + "fmt" + "net" + "encoding/binary" + "bytes" +) + +type MessageType uint8 + +const ( + MsgSolicit MessageType = iota + 1 + MsgAdvertise + MsgRequest + MsgConfirm + MsgRenew + MsgRebind + MsgReply + MsgRelease + MsgDecline + MsgReconfigure + MsgInformationRequest + MsgRelayForw + MsgRelayRepl +) + +type Packet struct { + Type MessageType + TransactionID [3]byte + Options []byte +} + +func MakePacket(bs []byte, len int) *Packet { + ret := &Packet{Type: MessageType(bs[0]), Options: make([]byte, len - 4)} + copy(ret.TransactionID[:], bs[1:4]) + copy(ret.Options[:], bs[4:len]) + return ret +} + +func (p *Packet) UnmarshalOptions() (Options, error) { + ret, err := MakeOptions(p.Options) + if err != nil { + return nil, fmt.Errorf("packet has malformed options section: %s", err) + } + return ret, nil +} + +func (p *Packet) BuildResponse(serverDuid []byte) ([]byte, error) { + switch p.Type { + case MsgSolicit: + return p.BuildMsgAdvertise(serverDuid) + case MsgRequest: + return p.BuildMsgReply(serverDuid) + case MsgInformationRequest: + return p.BuildMsgInformationRequestReply(serverDuid) + case MsgRelease: + return p.BuildMsgReleaseReply(serverDuid) + default: + return nil, nil + } +} + +func (p *Packet) BuildMsgAdvertise(serverDuid []byte) ([]byte, error) { + in_options, _ := p.UnmarshalOptions() + ret_options := make(Options) + + ret_options.AddOption(&Option{Id: OptClientId, Length: uint16(len(in_options[OptClientId].Value)), Value: in_options[OptClientId].Value}) + ret_options.AddOption(MakeIaNaOption(in_options[OptIaNa].Value[0:4], 0, 0, + MakeIaAddrOption(net.ParseIP("2001:db8:f00f:cafe::99"), 27000, 43200))) + ret_options.AddOption(&Option{Id: OptServerId, Length: uint16(len(serverDuid)), Value: serverDuid}) + + if 0x10 == binary.BigEndian.Uint16(in_options[OptClientArchType].Value) { // HTTPClient + ret_options.AddOption(&Option{Id: OptVendorClass, Length: 16, Value: []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116}}) // HTTPClient + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/bootx64.efi")}) + } else { + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/script.ipxe")}) + } +// ret_options.AddOption(OptRecursiveDns, net.ParseIP("2001:db8:f00f:cafe::1")) + //ret_options.AddOption(OptBootfileParam, []byte("http://") + //ret.Options[OptPreference] = [][]byte("http://") + + marshalled_ret_options, _ := ret_options.Marshal() + + ret := make([]byte, len(marshalled_ret_options) + 4, len(marshalled_ret_options) + 4) + ret[0] = byte(MsgAdvertise) + copy(ret[1:], p.TransactionID[:]) + copy(ret[4:], marshalled_ret_options) + return ret, nil +} + +// TODO: OptClientArchType may not be present + +func (p *Packet) BuildMsgReply(serverDuid []byte) ([]byte, error) { + in_options, _ := p.UnmarshalOptions() + ret_options := make(Options) + + ret_options.AddOption(&Option{Id: OptClientId, Length: uint16(len(in_options[OptClientId].Value)), Value: in_options[OptClientId].Value}) + ret_options.AddOption(MakeIaNaOption(in_options[OptIaNa].Value[0:4], 0, 0, + MakeIaAddrOption(net.ParseIP("2001:db8:f00f:cafe::99"), 27000, 43200))) + ret_options.AddOption(&Option{Id: OptServerId, Length: uint16(len(serverDuid)), Value: serverDuid}) + // ret_options.AddOption(OptRecursiveDns, net.ParseIP("2001:db8:f00f:cafe::1")) + if 0x10 == binary.BigEndian.Uint16(in_options[OptClientArchType].Value) { // HTTPClient + ret_options.AddOption(&Option{Id: OptVendorClass, Length: 16, Value: []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116}}) // HTTPClient + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/bootx64.efi")}) + } else { + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/script.ipxe")}) + } + marshalled_ret_options, _ := ret_options.Marshal() + + ret := make([]byte, len(marshalled_ret_options) + 4, len(marshalled_ret_options) + 4) + ret[0] = byte(MsgReply) + copy(ret[1:], p.TransactionID[:]) + copy(ret[4:], marshalled_ret_options) + return ret, nil +} + +func (p *Packet) BuildMsgInformationRequestReply(serverDuid []byte) ([]byte, error) { + in_options, _ := p.UnmarshalOptions() + ret_options := make(Options) + + ret_options.AddOption(&Option{Id: OptClientId, Length: uint16(len(in_options[OptClientId].Value)), Value: in_options[OptClientId].Value}) + ret_options.AddOption(&Option{Id: OptServerId, Length: uint16(len(serverDuid)), Value: serverDuid}) + // ret_options.AddOption(OptRecursiveDns, net.ParseIP("2001:db8:f00f:cafe::1")) + if 0x10 == binary.BigEndian.Uint16(in_options[OptClientArchType].Value) { // HTTPClient + ret_options.AddOption(&Option{Id: OptVendorClass, Length: 16, Value: []byte {0, 0, 0, 0, 0, 10, 72, 84, 84, 80, 67, 108, 105, 101, 110, 116}}) // HTTPClient + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/bootx64.efi")}) + } else { + ret_options.AddOption(&Option{Id: OptBootfileUrl, Length: 42, Value: []byte("http://[2001:db8:f00f:cafe::4]/script.ipxe")}) + } + marshalled_ret_options, _ := ret_options.Marshal() + + ret := make([]byte, len(marshalled_ret_options) + 4, len(marshalled_ret_options) + 4) + ret[0] = byte(MsgReply) + copy(ret[1:], p.TransactionID[:]) + copy(ret[4:], marshalled_ret_options) + return ret, nil +} + +func (p *Packet) BuildMsgReleaseReply(serverDuid []byte) ([]byte, error){ + in_options, _ := p.UnmarshalOptions() + ret_options := make(Options) + + ret_options.AddOption(&Option{Id: OptClientId, Length: uint16(len(in_options[OptClientId].Value)), Value: in_options[OptClientId].Value}) + ret_options.AddOption(&Option{Id: OptServerId, Length: uint16(len(serverDuid)), Value: serverDuid}) + v := make([]byte, 19, 19) + copy(v[2:], []byte("Release received.")) + ret_options.AddOption(&Option{Id: OptStatusCode, Length: uint16(len(v)), Value: v}) + marshalled_ret_options, _ := ret_options.Marshal() + + ret := make([]byte, len(marshalled_ret_options) + 4, len(marshalled_ret_options) + 4) + ret[0] = byte(MsgReply) + copy(ret[1:], p.TransactionID[:]) + copy(ret[4:], marshalled_ret_options) + //copy(ret.Options, marshalled_ret_options) + return ret, nil +} + +func (p *Packet) ShouldDiscard(serverDuid []byte) error { + switch p.Type { + case MsgSolicit: + return ShouldDiscardSolicit(p) + case MsgRequest: + return ShouldDiscardRequest(p, serverDuid) + case MsgInformationRequest: + return ShouldDiscardInformationRequest(p, serverDuid) + default: + return fmt.Errorf("Unknown packet") + } +} + +func ShouldDiscardSolicit(p *Packet) error { + options, _ := MakeOptions(p.Options) + if !options.RequestedBootFileUrlOption() { + return fmt.Errorf("'Solicit' packet doesn't have file url option") + } + if !options.HasClientId() { + return fmt.Errorf("'Solicit' packet has no client id option") + } + if options.HasServerId() { + return fmt.Errorf("'Solicit' packet has server id option") + } + return nil +} + +func ShouldDiscardRequest(p *Packet, serverDuid []byte) error { + options, _ := MakeOptions(p.Options) + if !options.RequestedBootFileUrlOption() { + return fmt.Errorf("'Request' packet doesn't have file url option") + } + if !options.HasClientId() { + return fmt.Errorf("'Request' packet has no client id option") + } + if !options.HasServerId() { + return fmt.Errorf("'Request' packet has no server id option") + } + if bytes.Compare(options[OptServerId].Value, serverDuid) != 0 { + return fmt.Errorf("'Request' packet's server id option (%d) is different from ours (%d)", options[OptServerId].Value, serverDuid) + } + return nil +} + +func ShouldDiscardInformationRequest(p *Packet, serverDuid []byte) error { + options, _ := MakeOptions(p.Options) + if !options.RequestedBootFileUrlOption() { + return fmt.Errorf("'Information-request' packet doesn't have boot file url option") + } + if options.HasIaNa() || options.HasIaTa() { + return fmt.Errorf("'Information-request' packet has an IA option present") + } + if options.HasServerId() && (bytes.Compare(options[OptServerId].Value, serverDuid) != 0) { + return fmt.Errorf("'Information-request' packet's server id option (%d) is different from ours (%d)", options[OptServerId].Value, serverDuid) + } + return nil +} \ No newline at end of file diff --git a/dhcp6/testdata/dhcp6.pcap b/dhcp6/testdata/dhcp6.pcap new file mode 100644 index 0000000000000000000000000000000000000000..a870bb61acf6991697b6bb1f92c0455acb1c8a1f GIT binary patch literal 908 zcmca|c+)~A1{MYwxWmf8zzF2{1l^1Hslv;!5Xc5$V`HE&lg1^N)@s?dy9qoi%Neo- z8UHl^rGP-|lFR>pK$-tc5I&d$s%KJSQfA1$^^9?9S0_-v5Ccoek|2fs0~-SugE)f(7&9_(Fn~-@XHaD1%PCFHOwrBD&nr#^BAf}G6Ml$`wZ%w(qI zLR8U;~dWfaxgQ42Eqmz1qN0I2?a*p9YBwOLI?zAGH_ZoFsS!}!VH_| zm>Bp#suc>^-a%Xf^rir+8BB~o&+~C)=9Q!t