pax_global_header00006660000000000000000000000064143156035200014511gustar00rootroot0000000000000052 comment=cac85e051c893660a412f94ab9cc98c3e5c784a3 golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/000077500000000000000000000000001431560352000221375ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.github/000077500000000000000000000000001431560352000234775ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.github/workflows/000077500000000000000000000000001431560352000255345ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.github/workflows/lint.yml000066400000000000000000000017571431560352000272370ustar00rootroot00000000000000name: Lint on: push: tags: - v* branches: - master pull_request: jobs: golangci: name: golangci-lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: latest # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true then the action will use pre-installed Go # skip-go-installation: true # checklicenses: # name: checklicenses # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v2 # - name: check license headers # run: | # set -exu # go get -u github.com/u-root/u-root/tools/checklicenses # $(go env GOPATH)/bin/checklicenses -c .ci/checklicenses_config.json golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.github/workflows/tests.yml000066400000000000000000000041611431560352000274230ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: unit-tests: runs-on: ubuntu-latest strategy: matrix: go: ['1.13', '1.14', '1.15', '1.16'] env: GO111MODULE: on steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: stable: false go-version: ${{ matrix.go }} - name: run unit tests run: | go get -v -t ./... echo "" > "${GITHUB_WORKSPACE}"/coverage.txt for d in $(go list ./...); do go test -v -race -coverprofile=profile.out -covermode=atomic "${d}" if [ -f profile.out ]; then cat profile.out >> "${GITHUB_WORKSPACE}"/coverage.txt rm profile.out fi done - name: report coverage to codecov uses: codecov/codecov-action@v1 with: files: coverage.txt flags: unittests fail_ci_if_error: true verbose: true integration-tests: runs-on: ubuntu-latest strategy: matrix: go: ['1.13', '1.14', '1.15', '1.16'] env: GO111MODULE: on steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: stable: false go-version: ${{ matrix.go }} - name: run integ tests run: | go get -v -t -tags=integration ./... echo "" > "${GITHUB_WORKSPACE}"/coverage.txt for d in $(go list -tags=integration ./...); do go test -c -tags=integration -v -race -coverprofile=profile.out -covermode=atomic "${d}" testbin="./$(basename $d).test" # only run it if it was built - i.e. if there are integ tests test -x "${testbin}" && sudo "./${testbin}" if [ -f profile.out ]; then cat profile.out >> "${GITHUB_WORKSPACE}"/coverage.txt rm profile.out fi done - name: report coverage to codecov uses: codecov/codecov-action@v1 with: files: coverage.txt flags: integtests fail_ci_if_error: true verbose: true golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.gitignore000066400000000000000000000001421431560352000241240ustar00rootroot00000000000000.*.swp examples/client6/client6 examples/client4/client4 examples/packetcrafting6/packetcrafting6 golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/.stickler.yml000066400000000000000000000001171431560352000245570ustar00rootroot00000000000000--- linters: golint: min_confidence: 0.85 fixers: enable: true golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/CONTRIBUTORS.md000066400000000000000000000007421431560352000244210ustar00rootroot00000000000000## Contributors * Andrea Barberio (main author) * Pablo Mazzini (tons of fixes and new options) * Sean Karlage (BSDP package, and of tons of improvements to the DHCPv4 package) * Owen Mooney (several option fixes and modifiers) * Mikolaj Walczak (asynchronous DHCPv6 client) * Chris Koch (tons of improvements in DHCPv4 and DHCPv6 internals and interface) * Akshay Navale, Brandon Bennett and Chris Gorham (ZTPv6 and ZTPv4 packages) * Anatole Denis (tons of fixes and new options) golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/LICENSE000066400000000000000000000027531431560352000231530ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2018, Andrea Barberio All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/README.md000066400000000000000000000050161431560352000234200ustar00rootroot00000000000000# dhcp [![Build Status](https://img.shields.io/github/workflow/status/insomniacslk/dhcp/Tests/master)](https://github.com/insomniacslk/dhcp/actions?query=branch%3Amaster) [![GoDoc](https://godoc.org/github.com/insomniacslk/dhcp?status.svg)](https://godoc.org/github.com/insomniacslk/dhcp) [![codecov](https://codecov.io/gh/insomniacslk/dhcp/branch/master/graph/badge.svg)](https://codecov.io/gh/insomniacslk/dhcp) [![Go Report Card](https://goreportcard.com/badge/github.com/insomniacslk/dhcp)](https://goreportcard.com/report/github.com/insomniacslk/dhcp) DHCPv4 and DHCPv6 decoding/encoding library with client and server code, written in Go. # How to get the library The library is split into several parts: * `dhcpv6`: implementation of DHCPv6 packet, client and server * `dhcpv4`: implementation of DHCPv4 packet, client and server * `netboot`: network booting wrappers on top of `dhcpv6` and `dhcpv4` * `iana`: several IANA constants, and helpers used by `dhcpv6` and `dhcpv4` * `rfc1035label`: simple implementation of RFC1035 labels, used by `dhcpv6` and `dhcpv4` * `interfaces`, a thin layer of wrappers around network interfaces You will probably only need `dhcpv6` and/or `dhcpv4` explicitly. The rest is pulled in automatically if necessary. So, to get `dhcpv6` and `dhcpv4` just run: ``` go get -u github.com/insomniacslk/dhcp/dhcpv{4,6} ``` # Examples The sections below will illustrate how to use the `dhcpv6` and `dhcpv4` packages. * [dhcpv6 client](examples/client6/) * [dhcpv6 server](examples/server6/) * [dhcpv6 packet crafting](examples/packetcrafting6) * TODO dhcpv4 client * TODO dhcpv4 server * TODO dhcpv4 packet crafting See more example code at https://github.com/insomniacslk/exdhcp # Public projects that use it * Facebook's DHCP load balancer, `dhcplb`, https://github.com/facebookincubator/dhcplb * Systemboot, a LinuxBoot distribution that runs as system firmware, https://github.com/systemboot/systemboot * Router7, a pure-Go router implementation for fiber7 connections, https://github.com/rtr7/router7 * Beats from ElasticSearch, https://github.com/elastic/beats * Bender from Pinterest, a library for load-testing, https://github.com/pinterest/bender * FBender from Facebook, a tool for load-testing based on Bender, https://github.com/facebookincubator/fbender * CoreDHCP, a fast, multithreaded, modular and extensible DHCP server, https://github.com/coredhcp/coredhcp * u-root, an embeddable root file system, https://github.com/u-root/u-root * Talos: a modern OS for Kubernetes, https://github.com/talos-systems/talos golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/000077500000000000000000000000001431560352000233275ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/async/000077500000000000000000000000001431560352000244445ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/async/client.go000066400000000000000000000117271431560352000262610ustar00rootroot00000000000000package async import ( "context" "fmt" "log" "net" "sync" "time" promise "github.com/fanliao/go-promise" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/client4" ) // Default ports const ( DefaultServerPort = 67 DefaultClientPort = 68 ) // Client implements an asynchronous DHCPv4 client // It doesn't use the broadcast socket! Which means it should be used only when // the network is already established. // https://github.com/insomniacslk/dhcp/issues/143 type Client struct { ReadTimeout time.Duration WriteTimeout time.Duration LocalAddr net.Addr RemoteAddr net.Addr IgnoreErrors bool connection *net.UDPConn cancel context.CancelFunc stopping *sync.WaitGroup receiveQueue chan *dhcpv4.DHCPv4 sendQueue chan *dhcpv4.DHCPv4 packetsLock sync.Mutex packets map[dhcpv4.TransactionID]*promise.Promise errors chan error } // NewClient creates an asynchronous client func NewClient() *Client { return &Client{ ReadTimeout: client4.DefaultReadTimeout, WriteTimeout: client4.DefaultWriteTimeout, } } // Open starts the client. The requests made with Send function call are first // put to the buffered channel and dispatched in FIFO order. BufferSize // indicates the number of packets that can be waiting to be send before // blocking the caller exectution. func (c *Client) Open(bufferSize int) error { var ( addr *net.UDPAddr ok bool err error ) if addr, ok = c.LocalAddr.(*net.UDPAddr); !ok { return fmt.Errorf("Invalid local address: %v not a net.UDPAddr", c.LocalAddr) } // prepare the socket to listen on for replies c.connection, err = net.ListenUDP("udp4", addr) if err != nil { return err } c.stopping = new(sync.WaitGroup) c.sendQueue = make(chan *dhcpv4.DHCPv4, bufferSize) c.receiveQueue = make(chan *dhcpv4.DHCPv4, bufferSize) c.packets = make(map[dhcpv4.TransactionID]*promise.Promise) c.packetsLock = sync.Mutex{} c.errors = make(chan error) var ctx context.Context ctx, c.cancel = context.WithCancel(context.Background()) go c.receiverLoop(ctx) go c.senderLoop(ctx) return nil } // Close stops the client func (c *Client) Close() { // Wait for sender and receiver loops c.stopping.Add(2) c.cancel() c.stopping.Wait() close(c.sendQueue) close(c.receiveQueue) close(c.errors) c.connection.Close() } // Errors returns a channel where runtime errors are posted func (c *Client) Errors() <-chan error { return c.errors } func (c *Client) addError(err error) { if !c.IgnoreErrors { c.errors <- err } } func (c *Client) receiverLoop(ctx context.Context) { defer func() { c.stopping.Done() }() for { select { case <-ctx.Done(): return case packet := <-c.receiveQueue: c.receive(packet) } } } func (c *Client) senderLoop(ctx context.Context) { defer func() { c.stopping.Done() }() for { select { case <-ctx.Done(): return case packet := <-c.sendQueue: c.send(packet) } } } func (c *Client) send(packet *dhcpv4.DHCPv4) { c.packetsLock.Lock() p := c.packets[packet.TransactionID] c.packetsLock.Unlock() raddr, err := c.remoteAddr() if err != nil { _ = p.Reject(err) return } if err := c.connection.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { log.Printf("Warning: cannot set write deadline: %v", err) return } _, err = c.connection.WriteTo(packet.ToBytes(), raddr) if err != nil { _ = p.Reject(err) log.Printf("Warning: cannot write to %s: %v", raddr, err) return } c.receiveQueue <- packet } func (c *Client) receive(_ *dhcpv4.DHCPv4) { var ( oobdata = []byte{} received *dhcpv4.DHCPv4 ) if err := c.connection.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { log.Printf("Warning: cannot set write deadline: %v", err) return } for { buffer := make([]byte, client4.MaxUDPReceivedPacketSize) n, _, _, _, err := c.connection.ReadMsgUDP(buffer, oobdata) if err != nil { if err, ok := err.(net.Error); !ok || !err.Timeout() { c.addError(fmt.Errorf("Error receiving the message: %s", err)) } return } received, err = dhcpv4.FromBytes(buffer[:n]) if err == nil { break } } c.packetsLock.Lock() if p, ok := c.packets[received.TransactionID]; ok { delete(c.packets, received.TransactionID) _ = p.Resolve(received) } c.packetsLock.Unlock() } func (c *Client) remoteAddr() (*net.UDPAddr, error) { if c.RemoteAddr == nil { return &net.UDPAddr{IP: net.IPv4bcast, Port: DefaultServerPort}, nil } if addr, ok := c.RemoteAddr.(*net.UDPAddr); ok { return addr, nil } return nil, fmt.Errorf("Invalid remote address: %v not a net.UDPAddr", c.RemoteAddr) } // Send inserts a message to the queue to be sent asynchronously. // Returns a future which resolves to response and error. func (c *Client) Send(message *dhcpv4.DHCPv4, modifiers ...dhcpv4.Modifier) *promise.Future { for _, mod := range modifiers { mod(message) } p := promise.NewPromise() c.packetsLock.Lock() c.packets[message.TransactionID] = p c.packetsLock.Unlock() c.sendQueue <- message return p.Future } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/async/client_test.go000066400000000000000000000062271431560352000273170ustar00rootroot00000000000000package async import ( "context" "net" "testing" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/client4" "github.com/stretchr/testify/require" ) // server creates a server which responds with a predefined response func serve(ctx context.Context, addr *net.UDPAddr, response *dhcpv4.DHCPv4) error { conn, err := net.ListenUDP("udp4", addr) if err != nil { return err } go func() { defer conn.Close() oobdata := []byte{} buffer := make([]byte, client4.MaxUDPReceivedPacketSize) for { select { case <-ctx.Done(): return default: if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { panic(err) } n, _, _, src, err := conn.ReadMsgUDP(buffer, oobdata) if err != nil { continue } _, err = dhcpv4.FromBytes(buffer[:n]) if err != nil { continue } if err := conn.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil { panic(err) } _, err = conn.WriteTo(response.ToBytes(), src) if err != nil { continue } } } }() return nil } func TestNewClient(t *testing.T) { c := NewClient() require.NotNil(t, c) require.Equal(t, c.ReadTimeout, client4.DefaultReadTimeout) require.Equal(t, c.ReadTimeout, client4.DefaultWriteTimeout) } func TestOpenInvalidAddrFailes(t *testing.T) { c := NewClient() err := c.Open(512) require.Error(t, err) } // This test uses port 15438 so please make sure its not used before running func TestOpenClose(t *testing.T) { c := NewClient() addr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:15438") require.NoError(t, err) c.LocalAddr = addr err = c.Open(512) require.NoError(t, err) defer c.Close() } // This test uses ports 15438 and 15439 so please make sure they are not used // before running func TestSendTimeout(t *testing.T) { c := NewClient() addr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:15438") require.NoError(t, err) remote, err := net.ResolveUDPAddr("udp4", "127.0.0.1:15439") require.NoError(t, err) c.ReadTimeout = 50 * time.Millisecond c.WriteTimeout = 50 * time.Millisecond c.LocalAddr = addr c.RemoteAddr = remote err = c.Open(512) require.NoError(t, err) defer c.Close() m, err := dhcpv4.New() require.NoError(t, err) _, err, timeout := c.Send(m).GetOrTimeout(200) require.NoError(t, err) require.True(t, timeout) } // This test uses ports 15438 and 15439 so please make sure they are not used // before running func TestSend(t *testing.T) { m, err := dhcpv4.New() require.NoError(t, err) require.NotNil(t, m) c := NewClient() addr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:15438") require.NoError(t, err) remote, err := net.ResolveUDPAddr("udp4", "127.0.0.1:15439") require.NoError(t, err) c.LocalAddr = addr c.RemoteAddr = remote ctx, cancel := context.WithCancel(context.Background()) defer cancel() err = serve(ctx, remote, m) require.NoError(t, err) err = c.Open(16) require.NoError(t, err) defer c.Close() f := c.Send(m) response, err, timeout := f.GetOrTimeout(2000) r, ok := response.(*dhcpv4.DHCPv4) require.True(t, ok) require.False(t, timeout) require.NoError(t, err) require.Equal(t, m.TransactionID, r.TransactionID) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bindtointerface.go000066400000000000000000000003641431560352000270210ustar00rootroot00000000000000package dhcpv4 import ( "github.com/insomniacslk/dhcp/interfaces" ) // BindToInterface (deprecated) redirects to interfaces.BindToInterface func BindToInterface(fd int, ifname string) error { return interfaces.BindToInterface(fd, ifname) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/000077500000000000000000000000001431560352000242575ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/boot_image.go000066400000000000000000000067431431560352000267250ustar00rootroot00000000000000package bsdp import ( "fmt" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/uio/uio" ) // BootImageType represents the different BSDP boot image types. type BootImageType byte // Different types of BootImages - e.g. for different flavors of macOS. const ( BootImageTypeMacOS9 BootImageType = 0 BootImageTypeMacOSX BootImageType = 1 BootImageTypeMacOSXServer BootImageType = 2 BootImageTypeHardwareDiagnostics BootImageType = 3 // 4 - 127 are reserved for future use. ) // bootImageTypeToString maps the different BootImageTypes to human-readable // representations. var bootImageTypeToString = map[BootImageType]string{ BootImageTypeMacOS9: "macOS 9", BootImageTypeMacOSX: "macOS", BootImageTypeMacOSXServer: "macOS Server", BootImageTypeHardwareDiagnostics: "Hardware Diagnostic", } // BootImageID describes a boot image ID - whether it's an install image and // what kind of boot image (e.g. OS 9, macOS, hardware diagnostics) type BootImageID struct { IsInstall bool ImageType BootImageType Index uint16 } // ToBytes implements dhcpv4.OptionValue. func (b BootImageID) ToBytes() []byte { return uio.ToBigEndian(b) } // FromBytes reads data into b. func (b *BootImageID) FromBytes(data []byte) error { return uio.FromBigEndian(b, data) } // Marshal writes the binary representation to buf. func (b BootImageID) Marshal(buf *uio.Lexer) { var byte0 byte if b.IsInstall { byte0 |= 0x80 } byte0 |= byte(b.ImageType) buf.Write8(byte0) buf.Write8(byte(0)) buf.Write16(b.Index) } // String converts a BootImageID to a human-readable representation. func (b BootImageID) String() string { s := fmt.Sprintf("[%d]", b.Index) if b.IsInstall { s += " installable" } else { s += " uninstallable" } t, ok := bootImageTypeToString[b.ImageType] if !ok { t = "unknown" } return s + " " + t + " image" } // Unmarshal reads b's binary representation from buf. func (b *BootImageID) Unmarshal(buf *uio.Lexer) error { byte0 := buf.Read8() _ = buf.Read8() b.IsInstall = byte0&0x80 != 0 b.ImageType = BootImageType(byte0 & 0x7f) b.Index = buf.Read16() return buf.Error() } // BootImage describes a boot image - contains the boot image ID and the name. type BootImage struct { ID BootImageID Name string } // Marshal write a BootImage to buf. func (b BootImage) Marshal(buf *uio.Lexer) { b.ID.Marshal(buf) buf.Write8(uint8(len(b.Name))) buf.WriteBytes([]byte(b.Name)) } // String converts a BootImage to a human-readable representation. func (b BootImage) String() string { return fmt.Sprintf("%v %v", b.Name, b.ID.String()) } // Unmarshal reads data from buf into b. func (b *BootImage) Unmarshal(buf *uio.Lexer) error { if err := (&b.ID).Unmarshal(buf); err != nil { return err } nameLength := buf.Read8() b.Name = string(buf.Consume(int(nameLength))) return buf.Error() } func getBootImageID(code dhcpv4.OptionCode, o dhcpv4.Options) *BootImageID { v := o.Get(code) if v == nil { return nil } var b BootImageID if err := uio.FromBigEndian(&b, v); err != nil { return nil } return &b } // OptDefaultBootImageID returns a new default boot image ID option as per // BSDP. func OptDefaultBootImageID(b BootImageID) dhcpv4.Option { return dhcpv4.Option{Code: OptionDefaultBootImageID, Value: b} } // OptSelectedBootImageID returns a new selected boot image ID option as per // BSDP. func OptSelectedBootImageID(b BootImageID) dhcpv4.Option { return dhcpv4.Option{Code: OptionSelectedBootImageID, Value: b} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/boot_image_test.go000066400000000000000000000065141431560352000277600ustar00rootroot00000000000000package bsdp import ( "testing" "github.com/stretchr/testify/require" "github.com/u-root/uio/uio" ) func TestBootImageIDToBytes(t *testing.T) { b := BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, } actual := uio.ToBigEndian(b) expected := []byte{0x81, 0, 0x10, 0} require.Equal(t, expected, actual) b.IsInstall = false actual = uio.ToBigEndian(b) expected = []byte{0x01, 0, 0x10, 0} require.Equal(t, expected, actual) } func TestBootImageIDFromBytes(t *testing.T) { b := BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 0x1000, } var newBootImage BootImageID require.NoError(t, uio.FromBigEndian(&newBootImage, uio.ToBigEndian(b))) require.Equal(t, b, newBootImage) b = BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1011, } require.NoError(t, uio.FromBigEndian(&newBootImage, uio.ToBigEndian(b))) require.Equal(t, b, newBootImage) } func TestBootImageIDFromBytesFail(t *testing.T) { serialized := []byte{0x81, 0, 0x10} // intentionally left short var deserialized BootImageID require.Error(t, uio.FromBigEndian(&deserialized, serialized)) } func TestBootImageIDString(t *testing.T) { b := BootImageID{IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 1001} require.Equal(t, "[1001] uninstallable macOS image", b.String()) } /* * BootImage */ func TestBootImageToBytes(t *testing.T) { b := BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, }, Name: "bsdp-1", } expected := []byte{ 0x81, 0, 0x10, 0, // boot image ID 6, // len(Name) 98, 115, 100, 112, 45, 49, // byte-encoding of Name } actual := uio.ToBigEndian(b) require.Equal(t, expected, actual) b = BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 0x1010, }, Name: "bsdp-21", } expected = []byte{ 0x1, 0, 0x10, 0x10, // boot image ID 7, // len(Name) 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name } actual = uio.ToBigEndian(b) require.Equal(t, expected, actual) } func TestBootImageFromBytes(t *testing.T) { input := []byte{ 0x1, 0, 0x10, 0x10, // boot image ID 7, // len(Name) 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name } var b BootImage require.NoError(t, uio.FromBigEndian(&b, input)) expectedBootImage := BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 0x1010, }, Name: "bsdp-21", } require.Equal(t, expectedBootImage, b) } func TestBootImageFromBytesOnlyBootImageID(t *testing.T) { // Only a BootImageID, nothing else. input := []byte{0x1, 0, 0x10, 0x10} var b BootImage require.Error(t, uio.FromBigEndian(&b, input)) } func TestBootImageFromBytesShortBootImage(t *testing.T) { input := []byte{ 0x1, 0, 0x10, 0x10, // boot image ID 7, // len(Name) 98, 115, 100, 112, 45, 50, // Name bytes (intentionally off-by-one) } var b BootImage require.Error(t, uio.FromBigEndian(&b, input)) } func TestBootImageString(t *testing.T) { b := BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 0x1010, }, Name: "bsdp-21", } require.Equal(t, "bsdp-21 [4112] uninstallable macOS image", b.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp.go000066400000000000000000000201701431560352000255360ustar00rootroot00000000000000package 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 } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_option_boot_image_list.go000066400000000000000000000022731431560352000323520ustar00rootroot00000000000000package bsdp import ( "strings" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/uio/uio" ) // BootImageList contains a list of boot images presented by a netboot server. // // Implements the BSDP option listing the boot images. type BootImageList []BootImage // FromBytes deserializes data into bil. func (bil *BootImageList) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) for buf.Has(5) { var image BootImage if err := image.Unmarshal(buf); err != nil { return err } *bil = append(*bil, image) } return nil } // ToBytes returns a serialized stream of bytes for this option. func (bil BootImageList) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, image := range bil { image.Marshal(buf) } return buf.Data() } // String returns a human-readable string for this option. func (bil BootImageList) String() string { s := make([]string, 0, len(bil)) for _, image := range bil { s = append(s, image.String()) } return strings.Join(s, ", ") } // OptBootImageList returns a new BSDP boot image list. func OptBootImageList(b ...BootImage) dhcpv4.Option { return dhcpv4.Option{ Code: OptionBootImageList, Value: BootImageList(b), } } bsdp_option_boot_image_list_test.go000066400000000000000000000046041431560352000333320ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdppackage bsdp import ( "testing" "github.com/stretchr/testify/require" ) func TestOptBootImageListInterfaceMethods(t *testing.T) { bs := []BootImage{ BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 1001, }, Name: "bsdp-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }, Name: "bsdp-2", }, } o := OptBootImageList(bs...) require.Equal(t, OptionBootImageList, o.Code, "Code") expectedBytes := []byte{ // boot image 1 0x1, 0x0, 0x03, 0xe9, // ID 6, // name length 'b', 's', 'd', 'p', '-', '1', // boot image 1 0x80, 0x0, 0x23, 0x31, // ID 6, // name length 'b', 's', 'd', 'p', '-', '2', } require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") } func TestParseOptBootImageList(t *testing.T) { data := []byte{ // boot image 1 0x1, 0x0, 0x03, 0xe9, // ID 6, // name length 'b', 's', 'd', 'p', '-', '1', // boot image 1 0x80, 0x0, 0x23, 0x31, // ID 6, // name length 'b', 's', 'd', 'p', '-', '2', } var o BootImageList err := o.FromBytes(data) require.NoError(t, err) expectedBootImages := BootImageList{ BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 1001, }, Name: "bsdp-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }, Name: "bsdp-2", }, } require.Equal(t, expectedBootImages, o) // Error parsing boot image (malformed) data = []byte{ // boot image 1 0x1, 0x0, 0x03, 0xe9, // ID 4, // name length 'b', 's', 'd', 'p', '-', '1', // boot image 2 0x80, 0x0, 0x23, 0x31, // ID 6, // name length 'b', 's', 'd', 'p', '-', '2', } err = o.FromBytes(data) require.Error(t, err, "should get error from bad boot image") } func TestOptBootImageListString(t *testing.T) { bs := []BootImage{ BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 1001, }, Name: "bsdp-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }, Name: "bsdp-2", }, } o := OptBootImageList(bs...) expectedString := "BSDP Boot Image List: bsdp-1 [1001] uninstallable macOS image, bsdp-2 [9009] installable macOS 9 image" require.Equal(t, expectedString, o.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_option_message_type.go000066400000000000000000000025541431560352000317010ustar00rootroot00000000000000package bsdp import ( "fmt" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/uio/uio" ) // MessageType represents the different BSDP message types. // // Implements the BSDP option message type. Can be one of LIST, SELECT, or // FAILED. type MessageType byte // BSDP Message types - e.g. LIST, SELECT, FAILED const ( MessageTypeNone MessageType = 0 MessageTypeList MessageType = 1 MessageTypeSelect MessageType = 2 MessageTypeFailed MessageType = 3 ) // ToBytes returns a serialized stream of bytes for this option. func (m MessageType) ToBytes() []byte { return []byte{byte(m)} } // String returns a human-friendly representation of MessageType. func (m MessageType) String() string { if s, ok := messageTypeToString[m]; ok { return s } return fmt.Sprintf("unknown (%d)", m) } // messageTypeToString maps each BSDP message type to a human-readable string. var messageTypeToString = map[MessageType]string{ MessageTypeList: "LIST", MessageTypeSelect: "SELECT", MessageTypeFailed: "FAILED", } // FromBytes reads data into m. func (m *MessageType) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *m = MessageType(buf.Read8()) return buf.FinError() } // OptMessageType returns a new BSDP Message Type option. func OptMessageType(mt MessageType) dhcpv4.Option { return dhcpv4.Option{ Code: OptionMessageType, Value: mt, } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_option_message_type_test.go000066400000000000000000000013301431560352000327270ustar00rootroot00000000000000package bsdp import ( "testing" "github.com/stretchr/testify/require" ) func TestOptMessageTypeInterfaceMethods(t *testing.T) { o := OptMessageType(MessageTypeList) require.Equal(t, OptionMessageType, o.Code, "Code") require.Equal(t, []byte{1}, o.Value.ToBytes(), "ToBytes") } func TestParseOptMessageType(t *testing.T) { var o MessageType data := []byte{1} // DISCOVER err := o.FromBytes(data) require.NoError(t, err) require.Equal(t, MessageTypeList, o) } func TestOptMessageTypeString(t *testing.T) { // known o := OptMessageType(MessageTypeList) require.Equal(t, "BSDP Message Type: LIST", o.String()) // unknown o = OptMessageType(99) require.Equal(t, "BSDP Message Type: unknown (99)", o.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_option_misc.go000066400000000000000000000036141431560352000301450ustar00rootroot00000000000000package bsdp import ( "fmt" "net" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/uio/uio" ) // OptReplyPort returns a new BSDP reply port option. // // Implements the BSDP option reply port. This is used when BSDP responses // should be sent to a reply port other than the DHCP default. The macOS GUI // "Startup Disk Select" sends this option since it's operating in an // unprivileged context. func OptReplyPort(port uint16) dhcpv4.Option { return dhcpv4.Option{Code: OptionReplyPort, Value: dhcpv4.Uint16(port)} } // OptServerPriority returns a new BSDP server priority option. func OptServerPriority(prio uint16) dhcpv4.Option { return dhcpv4.Option{Code: OptionServerPriority, Value: dhcpv4.Uint16(prio)} } // OptMachineName returns a BSDP Machine Name option. func OptMachineName(name string) dhcpv4.Option { return dhcpv4.Option{Code: OptionMachineName, Value: dhcpv4.String(name)} } // Version is the BSDP protocol version. Can be one of 1.0 or 1.1. type Version [2]byte // Specific versions. var ( Version1_0 = Version{1, 0} Version1_1 = Version{1, 1} ) // ToBytes returns a serialized stream of bytes for this option. func (o Version) ToBytes() []byte { return o[:] } // String returns a human-readable string for this option. func (o Version) String() string { return fmt.Sprintf("%d.%d", o[0], o[1]) } // FromBytes constructs a Version struct from a sequence of // bytes and returns it, or an error. func (o *Version) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) buf.ReadBytes(o[:]) return buf.FinError() } // OptVersion returns a new BSDP version option. func OptVersion(version Version) dhcpv4.Option { return dhcpv4.Option{Code: OptionVersion, Value: version} } // OptServerIdentifier returns a new BSDP Server Identifier option. func OptServerIdentifier(ip net.IP) dhcpv4.Option { return dhcpv4.Option{Code: OptionServerIdentifier, Value: dhcpv4.IP(ip)} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_option_misc_test.go000066400000000000000000000062311431560352000312020ustar00rootroot00000000000000package bsdp import ( "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/stretchr/testify/require" ) func TestOptReplyPort(t *testing.T) { o := OptReplyPort(1234) require.Equal(t, OptionReplyPort, o.Code, "Code") require.Equal(t, []byte{4, 210}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "BSDP Reply Port: 1234", o.String()) } func TestGetReplyPort(t *testing.T) { o := VendorOptions{dhcpv4.OptionsFromList(OptReplyPort(1234))} port, err := o.ReplyPort() require.NoError(t, err) require.Equal(t, uint16(1234), port) o = VendorOptions{dhcpv4.Options{}} _, err = o.ReplyPort() require.Error(t, err, "no reply port present") } func TestOptServerPriority(t *testing.T) { o := OptServerPriority(1234) require.Equal(t, OptionServerPriority, o.Code, "Code") require.Equal(t, []byte{4, 210}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "BSDP Server Priority: 1234", o.String()) } func TestGetServerPriority(t *testing.T) { o := VendorOptions{dhcpv4.OptionsFromList(OptServerPriority(1234))} prio, err := o.ServerPriority() require.NoError(t, err) require.Equal(t, uint16(1234), prio) o = VendorOptions{dhcpv4.Options{}} _, err = o.ServerPriority() require.Error(t, err, "no server prio present") } func TestOptMachineName(t *testing.T) { o := OptMachineName("foo") require.Equal(t, OptionMachineName, o.Code, "Code") require.Equal(t, []byte("foo"), o.Value.ToBytes(), "ToBytes") require.Equal(t, "BSDP Machine Name: foo", o.String()) } func TestGetMachineName(t *testing.T) { o := VendorOptions{dhcpv4.OptionsFromList(OptMachineName("foo"))} require.Equal(t, "foo", o.MachineName()) o = VendorOptions{dhcpv4.Options{}} require.Equal(t, "", o.MachineName()) } func TestOptVersion(t *testing.T) { o := OptVersion(Version1_1) require.Equal(t, OptionVersion, o.Code, "Code") require.Equal(t, []byte{1, 1}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "BSDP Version: 1.1", o.String()) } func TestGetVersion(t *testing.T) { o := VendorOptions{dhcpv4.OptionsFromList(OptVersion(Version1_1))} ver, err := o.Version() require.NoError(t, err) require.Equal(t, ver, Version1_1) o = VendorOptions{dhcpv4.Options{}} _, err = o.Version() require.Error(t, err, "no version present") o = VendorOptions{dhcpv4.Options{OptionVersion.Code(): []byte{}}} _, err = o.Version() require.Error(t, err, "empty version field") o = VendorOptions{dhcpv4.Options{OptionVersion.Code(): []byte{1}}} _, err = o.Version() require.Error(t, err, "version option too short") o = VendorOptions{dhcpv4.Options{OptionVersion.Code(): []byte{1, 2, 3}}} _, err = o.Version() require.Error(t, err, "version option too long") } func TestOptServerIdentifier(t *testing.T) { o := OptServerIdentifier(net.IP{1, 1, 1, 1}) require.Equal(t, OptionServerIdentifier, o.Code, "Code") require.Equal(t, []byte{1, 1, 1, 1}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "BSDP Server Identifier: 1.1.1.1", o.String()) } func TestGetServerIdentifier(t *testing.T) { o := VendorOptions{dhcpv4.OptionsFromList(OptServerIdentifier(net.IP{1, 1, 1, 1}))} require.Equal(t, net.IP{1, 1, 1, 1}, o.ServerIdentifier()) o = VendorOptions{dhcpv4.Options{}} require.Nil(t, o.ServerIdentifier()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/bsdp_test.go000066400000000000000000000277711431560352000266130ustar00rootroot00000000000000package bsdp import ( "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func RequireHasOption(t *testing.T, opts dhcpv4.Options, opt dhcpv4.Option) { require.NotNil(t, opts, "must pass list of options") require.NotNil(t, opt, "must pass option") require.True(t, opts.Has(opt.Code)) actual := opts.Get(opt.Code) require.Equal(t, opt.Value.ToBytes(), actual) } func TestParseBootImageListFromAck(t *testing.T) { expectedBootImages := []BootImage{ BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1010, }, Name: "bsdp-1", }, BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOS9, Index: 0x1111, }, Name: "bsdp-2", }, } ack, _ := dhcpv4.New() ack.UpdateOption(OptVendorOptions( OptBootImageList(expectedBootImages...), )) images, err := ParseBootImageListFromAck(ack) require.NoError(t, err) require.NotEmpty(t, images, "should get BootImages") require.Equal(t, expectedBootImages, images, "should get same BootImages") } func TestParseBootImageListFromAckNoVendorOption(t *testing.T) { ack, _ := dhcpv4.New() images, err := ParseBootImageListFromAck(ack) require.Error(t, err) require.Empty(t, images, "no BootImages") } func TestNeedsReplyPort(t *testing.T) { require.True(t, needsReplyPort(123)) require.False(t, needsReplyPort(0)) require.False(t, needsReplyPort(dhcpv4.ClientPort)) } func TestNewInformList_NoReplyPort(t *testing.T) { hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6} localIP := net.IPv4(10, 10, 11, 11) m, err := NewInformList(hwAddr, localIP, 0) require.NoError(t, err) require.True(t, m.Options.Has(dhcpv4.OptionVendorSpecificInformation)) require.True(t, m.Options.Has(dhcpv4.OptionParameterRequestList)) require.True(t, m.Options.Has(dhcpv4.OptionMaximumDHCPMessageSize)) vendorOpts := GetVendorOptions(m.Options) require.NotNil(t, vendorOpts, "vendor opts not present") require.True(t, vendorOpts.Has(OptionMessageType)) require.True(t, vendorOpts.Has(OptionVersion)) mt := vendorOpts.MessageType() require.Equal(t, MessageTypeList, mt) } func TestNewInformList_ReplyPort(t *testing.T) { hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6} localIP := net.IPv4(10, 10, 11, 11) replyPort := uint16(11223) // Bad reply port _, err := NewInformList(hwAddr, localIP, replyPort) require.Error(t, err) // Good reply port replyPort = uint16(999) m, err := NewInformList(hwAddr, localIP, replyPort) require.NoError(t, err) vendorOpts := GetVendorOptions(m.Options) require.True(t, vendorOpts.Options.Has(OptionReplyPort)) port, err := vendorOpts.ReplyPort() require.NoError(t, err) require.Equal(t, replyPort, port) } func newAck(hwAddr net.HardwareAddr, transactionID [4]byte) *dhcpv4.DHCPv4 { ack, _ := dhcpv4.New() ack.OpCode = dhcpv4.OpcodeBootReply ack.TransactionID = transactionID ack.HWType = iana.HWTypeEthernet ack.ClientHWAddr = hwAddr ack.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) return ack } func TestInformSelectForAck_Broadcast(t *testing.T) { hwAddr := net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} tid := [4]byte{0x22, 0, 0, 0} serverID := net.IPv4(1, 2, 3, 4) bootImage := BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, }, Name: "bsdp-1", } ack := newAck(hwAddr, tid) ack.SetBroadcast() ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) m, err := InformSelectForAck(PacketFor(ack), 0, bootImage) require.NoError(t, err) require.Equal(t, dhcpv4.OpcodeBootRequest, m.OpCode) require.Equal(t, ack.HWType, m.HWType) require.Equal(t, ack.ClientHWAddr, m.ClientHWAddr) require.Equal(t, ack.TransactionID, m.TransactionID) require.True(t, m.IsBroadcast()) // Validate options. require.True(t, m.Options.Has(dhcpv4.OptionClassIdentifier)) require.True(t, m.Options.Has(dhcpv4.OptionParameterRequestList)) require.True(t, m.Options.Has(dhcpv4.OptionDHCPMessageType)) mt := m.MessageType() require.Equal(t, dhcpv4.MessageTypeInform, mt) // Validate vendor opts. require.True(t, m.Options.Has(dhcpv4.OptionVendorSpecificInformation)) vendorOpts := GetVendorOptions(m.Options).Options RequireHasOption(t, vendorOpts, OptMessageType(MessageTypeSelect)) require.True(t, vendorOpts.Has(OptionVersion)) RequireHasOption(t, vendorOpts, OptSelectedBootImageID(bootImage.ID)) RequireHasOption(t, vendorOpts, OptServerIdentifier(serverID)) } func TestInformSelectForAck_NoServerID(t *testing.T) { hwAddr := net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} tid := [4]byte{0x22, 0, 0, 0} bootImage := BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, }, Name: "bsdp-1", } ack := newAck(hwAddr, tid) _, err := InformSelectForAck(PacketFor(ack), 0, bootImage) require.Error(t, err, "expect error for no server identifier option") } func TestInformSelectForAck_BadReplyPort(t *testing.T) { hwAddr := net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} tid := [4]byte{0x22, 0, 0, 0} serverID := net.IPv4(1, 2, 3, 4) bootImage := BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, }, Name: "bsdp-1", } ack := newAck(hwAddr, tid) ack.SetBroadcast() ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) _, err := InformSelectForAck(PacketFor(ack), 11223, bootImage) require.Error(t, err, "expect error for > 1024 replyPort") } func TestInformSelectForAck_ReplyPort(t *testing.T) { hwAddr := net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} tid := [4]byte{0x22, 0, 0, 0} serverID := net.IPv4(1, 2, 3, 4) bootImage := BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x1000, }, Name: "bsdp-1", } ack := newAck(hwAddr, tid) ack.SetBroadcast() ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) replyPort := uint16(999) m, err := InformSelectForAck(PacketFor(ack), replyPort, bootImage) require.NoError(t, err) require.True(t, m.Options.Has(dhcpv4.OptionVendorSpecificInformation)) vendorOpts := GetVendorOptions(m.Options).Options RequireHasOption(t, vendorOpts, OptReplyPort(replyPort)) } func TestNewReplyForInformList_NoDefaultImage(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) _, err := NewReplyForInformList(inform, ReplyConfig{}) require.Error(t, err) } func TestNewReplyForInformList_NoImages(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) fakeImage := BootImage{ ID: BootImageID{ImageType: BootImageTypeMacOSX}, } _, err := NewReplyForInformList(inform, ReplyConfig{ Images: []BootImage{}, DefaultImage: &fakeImage, }) require.Error(t, err) _, err = NewReplyForInformList(inform, ReplyConfig{ Images: nil, SelectedImage: &fakeImage, }) require.Error(t, err) } func TestNewReplyForInformList(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) images := []BootImage{ BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x7070, }, Name: "image-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x8080, }, Name: "image-2", }, } config := ReplyConfig{ Images: images, DefaultImage: &images[0], ServerIP: net.IP{9, 9, 9, 9}, ServerHostname: "bsdp.foo.com", ServerPriority: 0x7070, } ack, err := NewReplyForInformList(inform, config) require.NoError(t, err) require.Equal(t, net.IP{1, 2, 3, 4}, ack.ClientIPAddr) require.Equal(t, net.IPv4zero, ack.YourIPAddr) require.Equal(t, "bsdp.foo.com", ack.ServerHostName) // Validate options. RequireHasOption(t, ack.Options, dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) RequireHasOption(t, ack.Options, dhcpv4.OptClassIdentifier(AppleVendorID)) require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) // Vendor-specific options. vendorOpts := GetVendorOptions(ack.Options).Options RequireHasOption(t, vendorOpts, OptMessageType(MessageTypeList)) RequireHasOption(t, vendorOpts, OptDefaultBootImageID(images[0].ID)) RequireHasOption(t, vendorOpts, OptServerPriority(0x7070)) RequireHasOption(t, vendorOpts, OptBootImageList(images...)) // Add in selected boot image, ensure it's in the generated ACK. config.SelectedImage = &images[0] ack, err = NewReplyForInformList(inform, config) require.NoError(t, err) vendorOpts = GetVendorOptions(ack.Options).Options RequireHasOption(t, vendorOpts, OptSelectedBootImageID(images[0].ID)) } func TestNewReplyForInformSelect_NoSelectedImage(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) _, err := NewReplyForInformSelect(inform, ReplyConfig{}) require.Error(t, err) } func TestNewReplyForInformSelect_NoImages(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) fakeImage := BootImage{ ID: BootImageID{ImageType: BootImageTypeMacOSX}, } _, err := NewReplyForInformSelect(inform, ReplyConfig{ Images: []BootImage{}, SelectedImage: &fakeImage, }) require.Error(t, err) _, err = NewReplyForInformSelect(inform, ReplyConfig{ Images: nil, SelectedImage: &fakeImage, }) require.Error(t, err) } func TestNewReplyForInformSelect(t *testing.T) { inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort) images := []BootImage{ BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x7070, }, Name: "image-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 0x8080, }, Name: "image-2", }, } config := ReplyConfig{ Images: images, SelectedImage: &images[0], ServerIP: net.IP{9, 9, 9, 9}, ServerHostname: "bsdp.foo.com", ServerPriority: 0x7070, } ack, err := NewReplyForInformSelect(inform, config) require.NoError(t, err) require.Equal(t, net.IP{1, 2, 3, 4}, ack.ClientIPAddr) require.Equal(t, net.IPv4zero, ack.YourIPAddr) require.Equal(t, "bsdp.foo.com", ack.ServerHostName) // Validate options. RequireHasOption(t, ack.Options, dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) RequireHasOption(t, ack.Options, dhcpv4.OptClassIdentifier(AppleVendorID)) require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) vendorOpts := GetVendorOptions(ack.Options) RequireHasOption(t, vendorOpts.Options, OptMessageType(MessageTypeSelect)) RequireHasOption(t, vendorOpts.Options, OptSelectedBootImageID(images[0].ID)) } func TestMessageTypeForPacket(t *testing.T) { testcases := []struct { tcName string opts []dhcpv4.Option wantMessageType MessageType }{ { tcName: "No options", opts: []dhcpv4.Option{}, }, { tcName: "Some options, no vendor opts", opts: []dhcpv4.Option{ dhcpv4.OptHostName("foobar1234"), }, }, { tcName: "Vendor opts, no message type", opts: []dhcpv4.Option{ dhcpv4.OptHostName("foobar1234"), OptVendorOptions( OptVersion(Version1_1), ), }, }, { tcName: "Vendor opts, with message type", opts: []dhcpv4.Option{ dhcpv4.OptHostName("foobar1234"), OptVendorOptions( OptVersion(Version1_1), OptMessageType(MessageTypeList), ), }, wantMessageType: MessageTypeList, }, } for _, tt := range testcases { t.Run(tt.tcName, func(t *testing.T) { pkt, _ := dhcpv4.New() for _, opt := range tt.opts { pkt.UpdateOption(opt) } gotMessageType := MessageTypeFromPacket(pkt) require.Equal(t, tt.wantMessageType, gotMessageType) }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/client.go000066400000000000000000000040341431560352000260650ustar00rootroot00000000000000package bsdp import ( "errors" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/client4" ) // Client represents a BSDP client that can perform BSDP exchanges via the // broadcast address. type Client struct { client4.Client } // NewClient constructs a new client with default read and write timeouts from // dhcpv4.Client. func NewClient() *Client { return &Client{Client: client4.Client{}} } // Exchange runs a full BSDP exchange (Inform[list], Ack, Inform[select], // Ack). Returns a list of DHCPv4 structures representing the exchange. func (c *Client) Exchange(ifname string) ([]*Packet, error) { conversation := make([]*Packet, 0) // Get our file descriptor for the broadcast socket. sendFd, err := client4.MakeBroadcastSocket(ifname) if err != nil { return conversation, err } recvFd, err := client4.MakeListeningSocket(ifname) if err != nil { return conversation, err } // INFORM[LIST] informList, err := NewInformListForInterface(ifname, dhcpv4.ClientPort) if err != nil { return conversation, err } conversation = append(conversation, informList) // ACK[LIST] ackForList, err := c.Client.SendReceive(sendFd, recvFd, informList.v4(), dhcpv4.MessageTypeAck) if err != nil { return conversation, err } // Rewrite vendor-specific option for pretty printing. conversation = append(conversation, PacketFor(ackForList)) // Parse boot images sent back by server bootImages, err := ParseBootImageListFromAck(ackForList) if err != nil { return conversation, err } if len(bootImages) == 0 { return conversation, errors.New("got no BootImages from server") } // INFORM[SELECT] informSelect, err := InformSelectForAck(PacketFor(ackForList), dhcpv4.ClientPort, bootImages[0]) if err != nil { return conversation, err } conversation = append(conversation, informSelect) // ACK[SELECT] ackForSelect, err := c.Client.SendReceive(sendFd, recvFd, informSelect.v4(), dhcpv4.MessageTypeAck) if err != nil { return conversation, err } return append(conversation, PacketFor(ackForSelect)), nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/doc.go000066400000000000000000000004611431560352000253540ustar00rootroot00000000000000/* The BSDP package implements Apple's Boot Service Discovery Protocol (a pxe-boot-like netboot protocol for booting macOS hardware from network-connected servers). The Canonical implementation is defined here: http://opensource.apple.com/source/bootp/bootp-198.1/Documentation/BSDP.doc */ package bsdp option_vendor_specific_information.go000066400000000000000000000076651431560352000337040ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdppackage bsdp import ( "fmt" "net" "github.com/insomniacslk/dhcp/dhcpv4" ) // VendorOptions is like dhcpv4.Options, but stringifies using BSDP-specific // option codes. type VendorOptions struct { dhcpv4.Options } // String prints the contained options using BSDP-specific option code parsing. func (v VendorOptions) String() string { return v.Options.ToString(bsdpHumanizer) } // FromBytes parses vendor options from func (v *VendorOptions) FromBytes(data []byte) error { v.Options = make(dhcpv4.Options) return v.Options.FromBytes(data) } // DefaultBootImageID returns the default boot image ID in v. func (v VendorOptions) DefaultBootImageID() *BootImageID { return getBootImageID(OptionDefaultBootImageID, v.Options) } // SelectedBootImageID returns the selected boot image ID in v. func (v VendorOptions) SelectedBootImageID() *BootImageID { return getBootImageID(OptionSelectedBootImageID, v.Options) } // BootImageList returns the BSDP boot image list in v. func (v VendorOptions) BootImageList() BootImageList { val := v.Options.Get(OptionBootImageList) if val == nil { return nil } var bil BootImageList if err := bil.FromBytes(val); err != nil { return nil } return bil } // MessageType returns the BSDP Message Type in v. func (v VendorOptions) MessageType() MessageType { val := v.Options.Get(OptionMessageType) if val == nil { return MessageTypeNone } var m MessageType if err := m.FromBytes(val); err != nil { return MessageTypeNone } return m } // GetVersion returns the BSDP version in v if present. func (v VendorOptions) Version() (Version, error) { val := v.Options.Get(OptionVersion) if val == nil { return Version{0, 0}, fmt.Errorf("version not found") } var ver Version if err := ver.FromBytes(val); err != nil { return Version{0, 0}, err } return ver, nil } // GetServerIdentifier returns the BSDP Server Identifier value in v if present. func (v VendorOptions) ServerIdentifier() net.IP { return dhcpv4.GetIP(OptionServerIdentifier, v.Options) } // GetReplyPort returns the BSDP reply port in v if present. func (v VendorOptions) ReplyPort() (uint16, error) { return dhcpv4.GetUint16(OptionReplyPort, v.Options) } // GetServerPriority returns the BSDP server priority in v if present. func (v VendorOptions) ServerPriority() (uint16, error) { return dhcpv4.GetUint16(OptionServerPriority, v.Options) } // GetMachineName finds and parses the BSDP Machine Name option from v. func (v VendorOptions) MachineName() string { return dhcpv4.GetString(OptionMachineName, v.Options) } // OptVendorOptions returns the BSDP Vendor Specific Info in o. func OptVendorOptions(o ...dhcpv4.Option) dhcpv4.Option { return dhcpv4.Option{ Code: dhcpv4.OptionVendorSpecificInformation, Value: VendorOptions{dhcpv4.OptionsFromList(o...)}, } } // GetVendorOptions returns a new BSDP Vendor Specific Info option. func GetVendorOptions(o dhcpv4.Options) *VendorOptions { v := o.Get(dhcpv4.OptionVendorSpecificInformation) if v == nil { return nil } var vo VendorOptions if err := vo.FromBytes(v); err != nil { return nil } return &vo } var bsdpHumanizer = dhcpv4.OptionHumanizer{ ValueHumanizer: parseOption, CodeHumanizer: func(c uint8) dhcpv4.OptionCode { return optionCode(c) }, } // parseOption is similar to dhcpv4.parseOption, except that it interprets // option codes based on the BSDP-specific options. func parseOption(code dhcpv4.OptionCode, data []byte) fmt.Stringer { var d dhcpv4.OptionDecoder switch code { case OptionMachineName: var s dhcpv4.String d = &s case OptionServerIdentifier: d = &dhcpv4.IP{} case OptionServerPriority, OptionReplyPort: var u dhcpv4.Uint16 d = &u case OptionBootImageList: d = &BootImageList{} case OptionDefaultBootImageID, OptionSelectedBootImageID: d = &BootImageID{} case OptionMessageType: var m MessageType d = &m case OptionVersion: d = &Version{} } if d != nil && d.FromBytes(data) == nil { return d } return dhcpv4.OptionGeneric{Data: data} } option_vendor_specific_information_test.go000066400000000000000000000041571431560352000347340ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdppackage bsdp import ( "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/stretchr/testify/require" ) func TestOptVendorSpecificInformationInterfaceMethods(t *testing.T) { o := OptVendorOptions( OptVersion(Version1_1), OptMessageType(MessageTypeList), ) require.Equal(t, dhcpv4.OptionVendorSpecificInformation, o.Code, "Code") expectedBytes := []byte{ 1, 1, 1, // List option 2, 2, 1, 1, // Version option } require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") } func TestOptVendorSpecificInformationString(t *testing.T) { o := OptVendorOptions( OptMessageType(MessageTypeList), OptVersion(Version1_1), ) expectedString := "Vendor Specific Information:\n BSDP Message Type: LIST\n BSDP Version: 1.1\n" require.Equal(t, expectedString, o.String()) // Test more complicated string - sub options of sub options. o = OptVendorOptions( OptMessageType(MessageTypeList), OptBootImageList( BootImage{ ID: BootImageID{ IsInstall: false, ImageType: BootImageTypeMacOSX, Index: 1001, }, Name: "bsdp-1", }, BootImage{ ID: BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }, Name: "bsdp-2", }, ), OptMachineName("foo"), OptServerIdentifier(net.IP{1, 1, 1, 1}), OptServerPriority(1234), OptReplyPort(1235), OptDefaultBootImageID(BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }), OptSelectedBootImageID(BootImageID{ IsInstall: true, ImageType: BootImageTypeMacOS9, Index: 9009, }), ) expectedString = "Vendor Specific Information:\n" + " BSDP Message Type: LIST\n" + " BSDP Server Identifier: 1.1.1.1\n" + " BSDP Server Priority: 1234\n" + " BSDP Reply Port: 1235\n" + " BSDP Default Boot Image ID: [9009] installable macOS 9 image\n" + " BSDP Selected Boot Image ID: [9009] installable macOS 9 image\n" + " BSDP Boot Image List: bsdp-1 [1001] uninstallable macOS image, bsdp-2 [9009] installable macOS 9 image\n" + " BSDP Machine Name: foo\n" require.Equal(t, expectedString, o.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/types.go000066400000000000000000000047141431560352000257600ustar00rootroot00000000000000package bsdp import ( "fmt" ) // DefaultMacOSVendorClassIdentifier is a default vendor class identifier used // on non-darwin hosts where the vendor class identifier cannot be determined. // It should mostly be used for debugging if testing BSDP on a non-darwin // system. const DefaultMacOSVendorClassIdentifier = AppleVendorID + "/i386/MacMini6,1" // optionCode are BSDP option codes. // // optionCode implements the dhcpv4.OptionCode interface. type optionCode uint8 func (o optionCode) Code() uint8 { return uint8(o) } func (o optionCode) String() string { if s, ok := optionCodeToString[o]; ok { return s } return fmt.Sprintf("unknown (%d)", o) } // Options (occur as sub-options of DHCP option 43). const ( OptionMessageType optionCode = 1 OptionVersion optionCode = 2 OptionServerIdentifier optionCode = 3 OptionServerPriority optionCode = 4 OptionReplyPort optionCode = 5 OptionBootImageListPath optionCode = 6 // Not used OptionDefaultBootImageID optionCode = 7 OptionSelectedBootImageID optionCode = 8 OptionBootImageList optionCode = 9 OptionNetboot1_0Firmware optionCode = 10 OptionBootImageAttributesFilterList optionCode = 11 OptionShadowMountPath optionCode = 128 OptionShadowFilePath optionCode = 129 OptionMachineName optionCode = 130 ) // optionCodeToString maps BSDP OptionCodes to human-readable strings // describing what they are. var optionCodeToString = map[optionCode]string{ OptionMessageType: "BSDP Message Type", OptionVersion: "BSDP Version", OptionServerIdentifier: "BSDP Server Identifier", OptionServerPriority: "BSDP Server Priority", OptionReplyPort: "BSDP Reply Port", OptionBootImageListPath: "", // Not used OptionDefaultBootImageID: "BSDP Default Boot Image ID", OptionSelectedBootImageID: "BSDP Selected Boot Image ID", OptionBootImageList: "BSDP Boot Image List", OptionNetboot1_0Firmware: "BSDP Netboot 1.0 Firmware", OptionBootImageAttributesFilterList: "BSDP Boot Image Attributes Filter List", OptionShadowMountPath: "BSDP Shadow Mount Path", OptionShadowFilePath: "BSDP Shadow File Path", OptionMachineName: "BSDP Machine Name", } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdp/vendor_class_identifier.go000066400000000000000000000003601431560352000314710ustar00rootroot00000000000000// +build !darwin package bsdp // MakeVendorClassIdentifier returns a static vendor class identifier for BSDP // use on non-darwin hosts. func MakeVendorClassIdentifier() (string, error) { return DefaultMacOSVendorClassIdentifier, nil } vendor_class_identifier_darwin.go000066400000000000000000000006011431560352000327540ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/bsdppackage bsdp import ( "fmt" "golang.org/x/sys/unix" ) // MakeVendorClassIdentifier calls the sysctl syscall on macOS to get the // platform model. func MakeVendorClassIdentifier() (string, error) { // Fetch hardware model for class ID. hwModel, err := unix.Sysctl("hw.model") if err != nil { return "", err } return fmt.Sprintf("%s/i386/%s", AppleVendorID, hwModel), nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/client4/000077500000000000000000000000001431560352000246715ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/client4/client.go000066400000000000000000000243411431560352000265020ustar00rootroot00000000000000// Package client4 is deprecated. Use "nclient4" instead. package client4 import ( "encoding/binary" "errors" "fmt" "log" "net" "time" "github.com/insomniacslk/dhcp/dhcpv4" "golang.org/x/net/ipv4" "golang.org/x/sys/unix" ) // MaxUDPReceivedPacketSize is the (arbitrary) maximum UDP packet size supported // by this library. Theoretically could be up to 65kb. const ( MaxUDPReceivedPacketSize = 8192 ) var ( // DefaultReadTimeout is the time to wait after listening in which the // exchange is considered failed. DefaultReadTimeout = 3 * time.Second // DefaultWriteTimeout is the time to wait after sending in which the // exchange is considered failed. DefaultWriteTimeout = 3 * time.Second ) // Client is the object that actually performs the DHCP exchange. It currently // only has read and write timeout values, plus (optional) local and remote // addresses. type Client struct { ReadTimeout, WriteTimeout time.Duration RemoteAddr net.Addr LocalAddr net.Addr } // NewClient generates a new client to perform a DHCP exchange with, setting the // read and write timeout fields to defaults. func NewClient() *Client { return &Client{ ReadTimeout: DefaultReadTimeout, WriteTimeout: DefaultWriteTimeout, } } // MakeRawUDPPacket converts a payload (a serialized DHCPv4 packet) into a // raw UDP packet for the specified serverAddr from the specified clientAddr. func MakeRawUDPPacket(payload []byte, serverAddr, clientAddr net.UDPAddr) ([]byte, error) { udp := make([]byte, 8) binary.BigEndian.PutUint16(udp[:2], uint16(clientAddr.Port)) binary.BigEndian.PutUint16(udp[2:4], uint16(serverAddr.Port)) binary.BigEndian.PutUint16(udp[4:6], uint16(8+len(payload))) binary.BigEndian.PutUint16(udp[6:8], 0) // try to offload the checksum h := ipv4.Header{ Version: 4, Len: 20, TotalLen: 20 + len(udp) + len(payload), TTL: 64, Protocol: 17, // UDP Dst: serverAddr.IP, Src: clientAddr.IP, } ret, err := h.Marshal() if err != nil { return nil, err } ret = append(ret, udp...) ret = append(ret, payload...) return ret, nil } // makeRawSocket creates a socket that can be passed to unix.Sendto. func makeRawSocket(ifname string) (int, error) { fd, err := unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_RAW) if err != nil { return fd, err } err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) if err != nil { return fd, err } err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_HDRINCL, 1) if err != nil { return fd, err } err = dhcpv4.BindToInterface(fd, ifname) if err != nil { return fd, err } return fd, nil } // MakeBroadcastSocket creates a socket that can be passed to unix.Sendto // that will send packets out to the broadcast address. func MakeBroadcastSocket(ifname string) (int, error) { fd, err := makeRawSocket(ifname) if err != nil { return fd, err } err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BROADCAST, 1) if err != nil { return fd, err } return fd, nil } // MakeListeningSocket creates a listening socket on 0.0.0.0 for the DHCP client // port and returns it. func MakeListeningSocket(ifname string) (int, error) { return makeListeningSocketWithCustomPort(ifname, dhcpv4.ClientPort) } func htons(v uint16) uint16 { var tmp [2]byte binary.BigEndian.PutUint16(tmp[:], v) return binary.LittleEndian.Uint16(tmp[:]) } func makeListeningSocketWithCustomPort(ifname string, port int) (int, error) { fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM, int(htons(unix.ETH_P_IP))) if err != nil { return fd, err } iface, err := net.InterfaceByName(ifname) if err != nil { return fd, err } llAddr := unix.SockaddrLinklayer{ Ifindex: iface.Index, Protocol: htons(unix.ETH_P_IP), } err = unix.Bind(fd, &llAddr) return fd, err } func toUDPAddr(addr net.Addr, defaultAddr *net.UDPAddr) (*net.UDPAddr, error) { var uaddr *net.UDPAddr if addr == nil { uaddr = defaultAddr } else { if a, ok := addr.(*net.UDPAddr); ok { uaddr = a } else { return nil, fmt.Errorf("could not convert to net.UDPAddr, got %T instead", addr) } } if uaddr.IP.To4() == nil { return nil, fmt.Errorf("'%s' is not a valid IPv4 address", uaddr.IP) } return uaddr, nil } func (c *Client) getLocalUDPAddr() (*net.UDPAddr, error) { defaultLocalAddr := &net.UDPAddr{IP: net.IPv4zero, Port: dhcpv4.ClientPort} laddr, err := toUDPAddr(c.LocalAddr, defaultLocalAddr) if err != nil { return nil, fmt.Errorf("Invalid local address: %s", err) } return laddr, nil } func (c *Client) getRemoteUDPAddr() (*net.UDPAddr, error) { defaultRemoteAddr := &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ServerPort} raddr, err := toUDPAddr(c.RemoteAddr, defaultRemoteAddr) if err != nil { return nil, fmt.Errorf("Invalid remote address: %s", err) } return raddr, nil } // Exchange runs a full DORA transaction: Discover, Offer, Request, Acknowledge, // over UDP. Does not retry in case of failures. Returns a list of DHCPv4 // structures representing the exchange. It can contain up to four elements, // ordered as Discovery, Offer, Request and Acknowledge. In case of errors, an // error is returned, and the list of DHCPv4 objects will be shorted than 4, // containing all the sent and received DHCPv4 messages. func (c *Client) Exchange(ifname string, modifiers ...dhcpv4.Modifier) ([]*dhcpv4.DHCPv4, error) { conversation := make([]*dhcpv4.DHCPv4, 0) raddr, err := c.getRemoteUDPAddr() if err != nil { return nil, err } laddr, err := c.getLocalUDPAddr() if err != nil { return nil, err } // Get our file descriptor for the raw socket we need. var sfd int // If the address is not net.IPV4bcast, use a unicast socket. This should // cover the majority of use cases, but we're essentially ignoring the fact // that the IP could be the broadcast address of a specific subnet. if raddr.IP.Equal(net.IPv4bcast) { sfd, err = MakeBroadcastSocket(ifname) } else { sfd, err = makeRawSocket(ifname) } if err != nil { return conversation, err } rfd, err := makeListeningSocketWithCustomPort(ifname, laddr.Port) if err != nil { return conversation, err } defer func() { // close the sockets if err := unix.Close(sfd); err != nil { log.Printf("unix.Close(sendFd) failed: %v", err) } if sfd != rfd { if err := unix.Close(rfd); err != nil { log.Printf("unix.Close(recvFd) failed: %v", err) } } }() // Discover discover, err := dhcpv4.NewDiscoveryForInterface(ifname, modifiers...) if err != nil { return conversation, err } conversation = append(conversation, discover) // Offer offer, err := c.SendReceive(sfd, rfd, discover, dhcpv4.MessageTypeOffer) if err != nil { return conversation, err } conversation = append(conversation, offer) // Request request, err := dhcpv4.NewRequestFromOffer(offer, modifiers...) if err != nil { return conversation, err } conversation = append(conversation, request) // Ack ack, err := c.SendReceive(sfd, rfd, request, dhcpv4.MessageTypeAck) if err != nil { return conversation, err } conversation = append(conversation, ack) return conversation, nil } // SendReceive sends a packet (with some write timeout) and waits for a // response up to some read timeout value. If the message type is not // MessageTypeNone, it will wait for a specific message type func (c *Client) SendReceive(sendFd, recvFd int, packet *dhcpv4.DHCPv4, messageType dhcpv4.MessageType) (*dhcpv4.DHCPv4, error) { raddr, err := c.getRemoteUDPAddr() if err != nil { return nil, err } laddr, err := c.getLocalUDPAddr() if err != nil { return nil, err } packetBytes, err := MakeRawUDPPacket(packet.ToBytes(), *raddr, *laddr) if err != nil { return nil, err } // Create a goroutine to perform the blocking send, and time it out after // a certain amount of time. var ( destination [net.IPv4len]byte response *dhcpv4.DHCPv4 ) copy(destination[:], raddr.IP.To4()) remoteAddr := unix.SockaddrInet4{Port: laddr.Port, Addr: destination} recvErrors := make(chan error, 1) go func(errs chan<- error) { // set read timeout timeout := unix.NsecToTimeval(c.ReadTimeout.Nanoseconds()) if innerErr := unix.SetsockoptTimeval(recvFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout); innerErr != nil { errs <- innerErr return } for { buf := make([]byte, MaxUDPReceivedPacketSize) n, _, innerErr := unix.Recvfrom(recvFd, buf, 0) if innerErr != nil { errs <- innerErr return } var iph ipv4.Header if err := iph.Parse(buf[:n]); err != nil { // skip non-IP data continue } if iph.Protocol != 17 { // skip non-UDP packets continue } udph := buf[iph.Len:n] // check source and destination ports srcPort := int(binary.BigEndian.Uint16(udph[0:2])) expectedSrcPort := dhcpv4.ServerPort if c.RemoteAddr != nil { expectedSrcPort = c.RemoteAddr.(*net.UDPAddr).Port } if srcPort != expectedSrcPort { continue } dstPort := int(binary.BigEndian.Uint16(udph[2:4])) expectedDstPort := dhcpv4.ClientPort if c.LocalAddr != nil { expectedDstPort = c.LocalAddr.(*net.UDPAddr).Port } if dstPort != expectedDstPort { continue } // UDP checksum is not checked pLen := int(binary.BigEndian.Uint16(udph[4:6])) payload := buf[iph.Len+8 : iph.Len+pLen] response, innerErr = dhcpv4.FromBytes(payload) if innerErr != nil { errs <- innerErr return } // check that this is a response to our message if response.TransactionID != packet.TransactionID { continue } // wait for a response message if response.OpCode != dhcpv4.OpcodeBootReply { continue } // if we are not requested to wait for a specific message type, // return what we have if messageType == dhcpv4.MessageTypeNone { break } // break if it's a reply of the desired type, continue otherwise if response.MessageType() == messageType { break } } recvErrors <- nil }(recvErrors) // send the request while the goroutine waits for replies if err = unix.Sendto(sendFd, packetBytes, 0, &remoteAddr); err != nil { return nil, err } select { case err = <-recvErrors: if err == unix.EAGAIN { return nil, errors.New("timed out while listening for replies") } if err != nil { return nil, err } case <-time.After(c.ReadTimeout): return nil, errors.New("timed out while listening for replies") } return response, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/defaults.go000066400000000000000000000000741431560352000254660ustar00rootroot00000000000000package dhcpv4 const ( ServerPort = 67 ClientPort = 68 ) golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/dhcpv4.go000066400000000000000000000556621431560352000250640ustar00rootroot00000000000000// Package dhcpv4 provides encoding and decoding of DHCPv4 packets and options. // // Example Usage: // // p, err := dhcpv4.New( // dhcpv4.WithClientIP(net.IP{192, 168, 0, 1}), // dhcpv4.WithMessageType(dhcpv4.MessageTypeInform), // ) // p.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 110, 110, 110})) // // // Retrieve the DHCP Message Type option. // m := p.MessageType() // // bytesOnTheWire := p.ToBytes() // longSummary := p.Summary() package dhcpv4 import ( "bytes" "context" "errors" "fmt" "net" "strings" "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/rand" "github.com/u-root/uio/uio" ) const ( // minPacketLen is the minimum DHCP header length. minPacketLen = 236 // MaxHWAddrLen is the maximum hardware address length of the ClientHWAddr // (client hardware address) according to RFC 2131, Section 2. This is the // link-layer destination a server must send responses to. MaxHWAddrLen = 16 // MaxMessageSize is the maximum size in bytes that a DHCPv4 packet can hold. MaxMessageSize = 576 // Per RFC 951, the minimum length of a packet is 300 bytes. bootpMinLen = 300 ) // RandomTimeout is the amount of time to wait until random number generation // is canceled. var RandomTimeout = 2 * time.Minute // magicCookie is the magic 4-byte value at the beginning of the list of options // in a DHCPv4 packet. var magicCookie = [4]byte{99, 130, 83, 99} // DHCPv4 represents a DHCPv4 packet header and options. See the New* functions // to build DHCPv4 packets. type DHCPv4 struct { OpCode OpcodeType HWType iana.HWType HopCount uint8 TransactionID TransactionID NumSeconds uint16 Flags uint16 ClientIPAddr net.IP YourIPAddr net.IP ServerIPAddr net.IP GatewayIPAddr net.IP ClientHWAddr net.HardwareAddr ServerHostName string BootFileName string Options Options } // Modifier defines the signature for functions that can modify DHCPv4 // structures. This is used to simplify packet manipulation type Modifier func(d *DHCPv4) // IPv4AddrsForInterface obtains the currently-configured, non-loopback IPv4 // addresses for iface. func IPv4AddrsForInterface(iface *net.Interface) ([]net.IP, error) { if iface == nil { return nil, errors.New("IPv4AddrsForInterface: iface cannot be nil") } addrs, err := iface.Addrs() if err != nil { return nil, err } return GetExternalIPv4Addrs(addrs) } // GetExternalIPv4Addrs obtains the currently-configured, non-loopback IPv4 // addresses from `addrs` coming from a particular interface (e.g. // net.Interface.Addrs). func GetExternalIPv4Addrs(addrs []net.Addr) ([]net.IP, error) { var v4addrs []net.IP for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPAddr: ip = v.IP case *net.IPNet: ip = v.IP } if ip == nil || ip.IsLoopback() { continue } ip = ip.To4() if ip == nil { continue } v4addrs = append(v4addrs, ip) } return v4addrs, nil } // GenerateTransactionID generates a random 32-bits number suitable for use as // TransactionID func GenerateTransactionID() (TransactionID, error) { var xid TransactionID ctx, cancel := context.WithTimeout(context.Background(), RandomTimeout) defer cancel() n, err := rand.ReadContext(ctx, xid[:]) if err != nil { return xid, fmt.Errorf("could not get random number: %v", err) } if n != 4 { return xid, errors.New("invalid random sequence for transaction ID: smaller than 32 bits") } return xid, err } // New creates a new DHCPv4 structure and fill it up with default values. It // won't be a valid DHCPv4 message so you will need to adjust its fields. // See also NewDiscovery, NewRequest, NewAcknowledge, NewInform and NewRelease. func New(modifiers ...Modifier) (*DHCPv4, error) { xid, err := GenerateTransactionID() if err != nil { return nil, err } d := DHCPv4{ OpCode: OpcodeBootRequest, HWType: iana.HWTypeEthernet, ClientHWAddr: make(net.HardwareAddr, 6), HopCount: 0, TransactionID: xid, NumSeconds: 0, Flags: 0, ClientIPAddr: net.IPv4zero, YourIPAddr: net.IPv4zero, ServerIPAddr: net.IPv4zero, GatewayIPAddr: net.IPv4zero, Options: make(Options), } for _, mod := range modifiers { mod(&d) } return &d, nil } // NewDiscoveryForInterface builds a new DHCPv4 Discovery message, with a default // Ethernet HW type and the hardware address obtained from the specified // interface. func NewDiscoveryForInterface(ifname string, modifiers ...Modifier) (*DHCPv4, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } return NewDiscovery(iface.HardwareAddr, modifiers...) } // NewDiscovery builds a new DHCPv4 Discovery message, with a default Ethernet // HW type and specified hardware address. func NewDiscovery(hwaddr net.HardwareAddr, modifiers ...Modifier) (*DHCPv4, error) { return New(PrependModifiers(modifiers, WithHwAddr(hwaddr), WithRequestedOptions( OptionSubnetMask, OptionRouter, OptionDomainName, OptionDomainNameServer, ), WithMessageType(MessageTypeDiscover), )...) } // NewInformForInterface builds a new DHCPv4 Informational message with default // Ethernet HW type and the hardware address obtained from the specified // interface. func NewInformForInterface(ifname string, needsBroadcast bool) (*DHCPv4, error) { // get hw addr iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } // Set Client IP as iface's currently-configured IP. localIPs, err := IPv4AddrsForInterface(iface) if err != nil || len(localIPs) == 0 { return nil, fmt.Errorf("could not get local IPs for iface %s", ifname) } pkt, err := NewInform(iface.HardwareAddr, localIPs[0]) if err != nil { return nil, err } if needsBroadcast { pkt.SetBroadcast() } else { pkt.SetUnicast() } return pkt, nil } // PrependModifiers prepends other to m. func PrependModifiers(m []Modifier, other ...Modifier) []Modifier { return append(other, m...) } // NewInform builds a new DHCPv4 Informational message with the specified // hardware address. func NewInform(hwaddr net.HardwareAddr, localIP net.IP, modifiers ...Modifier) (*DHCPv4, error) { return New(PrependModifiers( modifiers, WithHwAddr(hwaddr), WithMessageType(MessageTypeInform), WithClientIP(localIP), )...) } // NewRequestFromOffer builds a DHCPv4 request from an offer. // It assumes the SELECTING state by default, see Section 4.3.2 in RFC 2131 for more details. func NewRequestFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) { return New(PrependModifiers(modifiers, WithReply(offer), WithMessageType(MessageTypeRequest), WithClientIP(offer.ClientIPAddr), WithOption(OptRequestedIPAddress(offer.YourIPAddr)), // This is usually the server IP. WithOptionCopied(offer, OptionServerIdentifier), WithRequestedOptions( OptionSubnetMask, OptionRouter, OptionDomainName, OptionDomainNameServer, ), )...) } // NewRenewFromOffer builds a DHCPv4 RENEW-style request from an offer. RENEW requests have minor // changes to their options compared to SELECT requests as specified by RFC 2131, section 4.3.2. func NewRenewFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) { return NewRequestFromOffer(offer, PrependModifiers(modifiers, // The server identifier option must not be filled in WithoutOption(OptionServerIdentifier), // The requested IP address must not be filled in WithoutOption(OptionRequestedIPAddress), // The client IP must be filled in with the IP offered to the client WithClientIP(offer.YourIPAddr), // The renewal request must use unicast WithBroadcast(false), )...) } // NewReplyFromRequest builds a DHCPv4 reply from a request. func NewReplyFromRequest(request *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) { return New(PrependModifiers(modifiers, WithReply(request), WithGatewayIP(request.GatewayIPAddr), WithOptionCopied(request, OptionRelayAgentInformation), // RFC 6842 states the Client Identifier option must be copied // from the request if a client specified it. WithOptionCopied(request, OptionClientIdentifier), )...) } // NewReleaseFromACK creates a DHCPv4 Release message from ACK. // default Release message without any Modifer is created as following: // - option Message Type is Release // - ClientIP is set to ack.YourIPAddr // - ClientHWAddr is set to ack.ClientHWAddr // - Unicast // - option Server Identifier is set to ack's ServerIdentifier func NewReleaseFromACK(ack *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) { return New(PrependModifiers(modifiers, WithMessageType(MessageTypeRelease), WithClientIP(ack.YourIPAddr), WithHwAddr(ack.ClientHWAddr), WithBroadcast(false), WithOptionCopied(ack, OptionServerIdentifier), )...) } // FromBytes decodes a DHCPv4 packet from a sequence of bytes, and returns an // error if the packet is not valid. func FromBytes(q []byte) (*DHCPv4, error) { var p DHCPv4 buf := uio.NewBigEndianBuffer(q) p.OpCode = OpcodeType(buf.Read8()) p.HWType = iana.HWType(buf.Read8()) hwAddrLen := buf.Read8() p.HopCount = buf.Read8() buf.ReadBytes(p.TransactionID[:]) p.NumSeconds = buf.Read16() p.Flags = buf.Read16() p.ClientIPAddr = net.IP(buf.CopyN(net.IPv4len)) p.YourIPAddr = net.IP(buf.CopyN(net.IPv4len)) p.ServerIPAddr = net.IP(buf.CopyN(net.IPv4len)) p.GatewayIPAddr = net.IP(buf.CopyN(net.IPv4len)) if hwAddrLen > 16 { hwAddrLen = 16 } // Always read 16 bytes, but only use hwaddrlen of them. p.ClientHWAddr = make(net.HardwareAddr, 16) buf.ReadBytes(p.ClientHWAddr) p.ClientHWAddr = p.ClientHWAddr[:hwAddrLen] var sname [64]byte buf.ReadBytes(sname[:]) length := strings.Index(string(sname[:]), "\x00") if length == -1 { length = 64 } p.ServerHostName = string(sname[:length]) var file [128]byte buf.ReadBytes(file[:]) length = strings.Index(string(file[:]), "\x00") if length == -1 { length = 128 } p.BootFileName = string(file[:length]) var cookie [4]byte buf.ReadBytes(cookie[:]) if err := buf.Error(); err != nil { return nil, err } if cookie != magicCookie { return nil, fmt.Errorf("malformed DHCP packet: got magic cookie %v, want %v", cookie[:], magicCookie[:]) } p.Options = make(Options) if err := p.Options.fromBytesCheckEnd(buf.Data(), true); err != nil { return nil, err } return &p, nil } // FlagsToString returns a human-readable representation of the flags field. func (d *DHCPv4) FlagsToString() string { flags := "" if d.IsBroadcast() { flags += "Broadcast" } else { flags += "Unicast" } if d.Flags&0xfe != 0 { flags += " (reserved bits not zeroed)" } return flags } // IsBroadcast indicates whether the packet is a broadcast packet. func (d *DHCPv4) IsBroadcast() bool { return d.Flags&0x8000 == 0x8000 } // SetBroadcast sets the packet to be a broadcast packet. func (d *DHCPv4) SetBroadcast() { d.Flags |= 0x8000 } // IsUnicast indicates whether the packet is a unicast packet. func (d *DHCPv4) IsUnicast() bool { return d.Flags&0x8000 == 0 } // SetUnicast sets the packet to be a unicast packet. func (d *DHCPv4) SetUnicast() { d.Flags &= ^uint16(0x8000) } // GetOneOption returns the option that matches the given option code. // // According to RFC 3396, options that are specified more than once are // concatenated, and hence this should always just return one option. func (d *DHCPv4) GetOneOption(code OptionCode) []byte { return d.Options.Get(code) } // DeleteOption deletes an existing option with the given option code. func (d *DHCPv4) DeleteOption(code OptionCode) { if d.Options != nil { d.Options.Del(code) } } // UpdateOption replaces an existing option with the same option code with the // given one, adding it if not already present. func (d *DHCPv4) UpdateOption(opt Option) { if d.Options == nil { d.Options = make(Options) } d.Options.Update(opt) } // String implements fmt.Stringer. func (d *DHCPv4) String() string { return fmt.Sprintf("DHCPv4(xid=%s hwaddr=%s msg_type=%s, your_ip=%s, server_ip=%s)", d.TransactionID, d.ClientHWAddr, d.MessageType(), d.YourIPAddr, d.ServerIPAddr) } // SummaryWithVendor prints a summary of the packet, interpreting the // vendor-specific info option using the given parser (can be nil). func (d *DHCPv4) SummaryWithVendor(vendorDecoder OptionDecoder) string { ret := fmt.Sprintf( "DHCPv4 Message\n"+ " opcode: %s\n"+ " hwtype: %s\n"+ " hopcount: %v\n"+ " transaction ID: %s\n"+ " num seconds: %v\n"+ " flags: %v (0x%02x)\n"+ " client IP: %s\n"+ " your IP: %s\n"+ " server IP: %s\n"+ " gateway IP: %s\n"+ " client MAC: %s\n"+ " server hostname: %s\n"+ " bootfile name: %s\n", d.OpCode, d.HWType, d.HopCount, d.TransactionID, d.NumSeconds, d.FlagsToString(), d.Flags, d.ClientIPAddr, d.YourIPAddr, d.ServerIPAddr, d.GatewayIPAddr, d.ClientHWAddr, d.ServerHostName, d.BootFileName, ) ret += " options:\n" ret += d.Options.Summary(vendorDecoder) return ret } // Summary prints detailed information about the packet. func (d *DHCPv4) Summary() string { return d.SummaryWithVendor(nil) } // IsOptionRequested returns true if that option is within the requested // options of the DHCPv4 message. func (d *DHCPv4) IsOptionRequested(requested OptionCode) bool { rq := d.ParameterRequestList() if rq == nil { // RFC2131§3.5 // Not all clients require initialization of all parameters [...] // Two techniques are used to reduce the number of parameters transmitted from // the server to the client. [...] Second, in its initial DHCPDISCOVER or // DHCPREQUEST message, a client may provide the server with a list of specific // parameters the client is interested in. // We interpret this to say that all available parameters should be sent if // the parameter request list is not sent at all. return true } for _, o := range rq { if o == requested { return true } } return false } // In case somebody forgets to set an IP, just write 0s as default values. func writeIP(b *uio.Lexer, ip net.IP) { var zeros [net.IPv4len]byte if ip == nil { b.WriteBytes(zeros[:]) } else { // Converting IP to 4 byte format ip = ip.To4() b.WriteBytes(ip[:net.IPv4len]) } } // ToBytes writes the packet to binary. func (d *DHCPv4) ToBytes() []byte { buf := uio.NewBigEndianBuffer(make([]byte, 0, minPacketLen)) buf.Write8(uint8(d.OpCode)) buf.Write8(uint8(d.HWType)) // HwAddrLen hlen := uint8(len(d.ClientHWAddr)) buf.Write8(hlen) buf.Write8(d.HopCount) buf.WriteBytes(d.TransactionID[:]) buf.Write16(d.NumSeconds) buf.Write16(d.Flags) writeIP(buf, d.ClientIPAddr) writeIP(buf, d.YourIPAddr) writeIP(buf, d.ServerIPAddr) writeIP(buf, d.GatewayIPAddr) copy(buf.WriteN(16), d.ClientHWAddr) var sname [64]byte copy(sname[:63], []byte(d.ServerHostName)) buf.WriteBytes(sname[:]) var file [128]byte copy(file[:127], []byte(d.BootFileName)) buf.WriteBytes(file[:]) // The magic cookie. buf.WriteBytes(magicCookie[:]) // Write all options. d.Options.Marshal(buf) // Finish the options. buf.Write8(OptionEnd.Code()) // DHCP is based on BOOTP, and BOOTP messages have a minimum length of // 300 bytes per RFC 951. This not stated explicitly, but if you sum up // all the bytes in the message layout, you'll get 300 bytes. // // Some DHCP servers and relay agents care about this BOOTP legacy B.S. // and "conveniently" drop messages that are less than 300 bytes long. if buf.Len() < bootpMinLen { buf.WriteBytes(bytes.Repeat([]byte{OptionPad.Code()}, bootpMinLen-buf.Len())) } return buf.Data() } // GetBroadcastAddress returns the DHCPv4 Broadcast Address value in d. // // The broadcast address option is described in RFC 2132, Section 5.3. func (d *DHCPv4) BroadcastAddress() net.IP { return GetIP(OptionBroadcastAddress, d.Options) } // RequestedIPAddress returns the DHCPv4 Requested IP Address value in d. // // The requested IP address option is described by RFC 2132, Section 9.1. func (d *DHCPv4) RequestedIPAddress() net.IP { return GetIP(OptionRequestedIPAddress, d.Options) } // ServerIdentifier returns the DHCPv4 Server Identifier value in d. // // The server identifier option is described by RFC 2132, Section 9.7. func (d *DHCPv4) ServerIdentifier() net.IP { return GetIP(OptionServerIdentifier, d.Options) } // Router parses the DHCPv4 Router option if present. // // The Router option is described by RFC 2132, Section 3.5. func (d *DHCPv4) Router() []net.IP { return GetIPs(OptionRouter, d.Options) } // ClasslessStaticRoute parses the DHCPv4 Classless Static Route option if present. // // The Classless Static Route option is described by RFC 3442. func (d *DHCPv4) ClasslessStaticRoute() []*Route { v := d.Options.Get(OptionClasslessStaticRoute) if v == nil { return nil } var routes Routes if err := routes.FromBytes(v); err != nil { return nil } return routes } // NTPServers parses the DHCPv4 NTP Servers option if present. // // The NTP servers option is described by RFC 2132, Section 8.3. func (d *DHCPv4) NTPServers() []net.IP { return GetIPs(OptionNTPServers, d.Options) } // DNS parses the DHCPv4 Domain Name Server option if present. // // The DNS server option is described by RFC 2132, Section 3.8. func (d *DHCPv4) DNS() []net.IP { return GetIPs(OptionDomainNameServer, d.Options) } // DomainName parses the DHCPv4 Domain Name option if present. // // The Domain Name option is described by RFC 2132, Section 3.17. func (d *DHCPv4) DomainName() string { return GetString(OptionDomainName, d.Options) } // HostName parses the DHCPv4 Host Name option if present. // // The Host Name option is described by RFC 2132, Section 3.14. func (d *DHCPv4) HostName() string { name := GetString(OptionHostName, d.Options) return strings.TrimRight(name, "\x00") } // RootPath parses the DHCPv4 Root Path option if present. // // The Root Path option is described by RFC 2132, Section 3.19. func (d *DHCPv4) RootPath() string { return GetString(OptionRootPath, d.Options) } // BootFileNameOption parses the DHCPv4 Bootfile Name option if present. // // The Bootfile Name option is described by RFC 2132, Section 9.5. func (d *DHCPv4) BootFileNameOption() string { name := GetString(OptionBootfileName, d.Options) return strings.TrimRight(name, "\x00") } // TFTPServerName parses the DHCPv4 TFTP Server Name option if present. // // The TFTP Server Name option is described by RFC 2132, Section 9.4. func (d *DHCPv4) TFTPServerName() string { name := GetString(OptionTFTPServerName, d.Options) return strings.TrimRight(name, "\x00") } // ClassIdentifier parses the DHCPv4 Class Identifier option if present. // // The Vendor Class Identifier option is described by RFC 2132, Section 9.13. func (d *DHCPv4) ClassIdentifier() string { return GetString(OptionClassIdentifier, d.Options) } // ClientArch returns the Client System Architecture Type option. func (d *DHCPv4) ClientArch() []iana.Arch { v := d.Options.Get(OptionClientSystemArchitectureType) if v == nil { return nil } var archs iana.Archs if err := archs.FromBytes(v); err != nil { return nil } return archs } // DomainSearch returns the domain search list if present. // // The domain search option is described by RFC 3397, Section 2. func (d *DHCPv4) DomainSearch() *rfc1035label.Labels { v := d.Options.Get(OptionDNSDomainSearchList) if v == nil { return nil } labels, err := rfc1035label.FromBytes(v) if err != nil { return nil } return labels } // IPAddressLeaseTime returns the IP address lease time or the given // default duration if not present. // // The IP address lease time option is described by RFC 2132, Section 9.2. func (d *DHCPv4) IPAddressLeaseTime(def time.Duration) time.Duration { v := d.Options.Get(OptionIPAddressLeaseTime) if v == nil { return def } var dur Duration if err := dur.FromBytes(v); err != nil { return def } return time.Duration(dur) } // IPAddressRenewalTime returns the IP address renewal time or the given // default duration if not present. // // The IP address renewal time option is described by RFC 2132, Section 9.11. func (d *DHCPv4) IPAddressRenewalTime(def time.Duration) time.Duration { v := d.Options.Get(OptionRenewTimeValue) if v == nil { return def } var dur Duration if err := dur.FromBytes(v); err != nil { return def } return time.Duration(dur) } // IPAddressRebindingTime returns the IP address rebinding time or the given // default duration if not present. // // The IP address rebinding time option is described by RFC 2132, Section 9.12. func (d *DHCPv4) IPAddressRebindingTime(def time.Duration) time.Duration { v := d.Options.Get(OptionRebindingTimeValue) if v == nil { return def } var dur Duration if err := dur.FromBytes(v); err != nil { return def } return time.Duration(dur) } // MaxMessageSize returns the DHCP Maximum Message Size if present. // // The Maximum DHCP Message Size option is described by RFC 2132, Section 9.10. func (d *DHCPv4) MaxMessageSize() (uint16, error) { return GetUint16(OptionMaximumDHCPMessageSize, d.Options) } // MessageType returns the DHCPv4 Message Type option. func (d *DHCPv4) MessageType() MessageType { v := d.Options.Get(OptionDHCPMessageType) if v == nil { return MessageTypeNone } var m MessageType if err := m.FromBytes(v); err != nil { return MessageTypeNone } return m } // Message returns the DHCPv4 (Error) Message option. // // The message options is described in RFC 2132, Section 9.9. func (d *DHCPv4) Message() string { return GetString(OptionMessage, d.Options) } // ParameterRequestList returns the DHCPv4 Parameter Request List. // // The parameter request list option is described by RFC 2132, Section 9.8. func (d *DHCPv4) ParameterRequestList() OptionCodeList { v := d.Options.Get(OptionParameterRequestList) if v == nil { return nil } var codes OptionCodeList if err := codes.FromBytes(v); err != nil { return nil } return codes } // RelayAgentInfo returns options embedded by the relay agent. // // The relay agent info option is described by RFC 3046. func (d *DHCPv4) RelayAgentInfo() *RelayOptions { v := d.Options.Get(OptionRelayAgentInformation) if v == nil { return nil } var relayOptions RelayOptions if err := relayOptions.FromBytes(v); err != nil { return nil } return &relayOptions } // SubnetMask returns a subnet mask option contained if present. // // The subnet mask option is described by RFC 2132, Section 3.3. func (d *DHCPv4) SubnetMask() net.IPMask { v := d.Options.Get(OptionSubnetMask) if v == nil { return nil } var im IPMask if err := im.FromBytes(v); err != nil { return nil } return net.IPMask(im) } // UserClass returns the user class if present. // // The user class information option is defined by RFC 3004. func (d *DHCPv4) UserClass() []string { v := d.Options.Get(OptionUserClassInformation) if v == nil { return nil } var uc Strings if err := uc.FromBytes(v); err != nil { return []string{GetString(OptionUserClassInformation, d.Options)} } return uc } // VIVC returns the vendor-identifying vendor class option if present. func (d *DHCPv4) VIVC() VIVCIdentifiers { v := d.Options.Get(OptionVendorIdentifyingVendorClass) if v == nil { return nil } var ids VIVCIdentifiers if err := ids.FromBytes(v); err != nil { return nil } return ids } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/dhcpv4_test.go000066400000000000000000000306211431560352000261070ustar00rootroot00000000000000package dhcpv4 import ( "bytes" "net" "strconv" "strings" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" "github.com/u-root/uio/uio" ) func TestGetExternalIPv4Addrs(t *testing.T) { addrs4and6 := []net.Addr{ &net.IPAddr{IP: net.IP{1, 2, 3, 4}}, &net.IPAddr{IP: net.IP{4, 3, 2, 1}}, &net.IPNet{IP: net.IP{4, 3, 2, 0}}, &net.IPAddr{IP: net.IP{1, 2, 3, 4, 1, 1, 1, 1}}, &net.IPAddr{IP: net.IP{4, 3, 2, 1, 1, 1, 1, 1}}, &net.IPAddr{}, // nil IP &net.IPAddr{IP: net.IP{127, 0, 0, 1}}, // loopback IP } expected := []net.IP{ net.IP{1, 2, 3, 4}, net.IP{4, 3, 2, 1}, net.IP{4, 3, 2, 0}, } actual, err := GetExternalIPv4Addrs(addrs4and6) require.NoError(t, err) require.Equal(t, expected, actual) } func TestFromBytes(t *testing.T) { data := []byte{ 1, // dhcp request 1, // ethernet hw type 6, // hw addr length 3, // hop count 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network) 0, 3, // number of seconds 0, 1, // broadcast 0, 0, 0, 0, // client IP address 0, 0, 0, 0, // your IP address 0, 0, 0, 0, // server IP address 0, 0, 0, 0, // gateway IP address 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding } // server host name expectedHostname := []byte{} for i := 0; i < 64; i++ { expectedHostname = append(expectedHostname, 0) } data = append(data, expectedHostname...) // boot file name expectedBootfilename := []byte{} for i := 0; i < 128; i++ { expectedBootfilename = append(expectedBootfilename, 0) } data = append(data, expectedBootfilename...) // magic cookie, then no options data = append(data, magicCookie[:]...) d, err := FromBytes(data) require.NoError(t, err) require.Equal(t, d.OpCode, OpcodeBootRequest) require.Equal(t, d.HWType, iana.HWTypeEthernet) require.Equal(t, d.HopCount, byte(3)) require.Equal(t, d.TransactionID, TransactionID{0xaa, 0xbb, 0xcc, 0xdd}) require.Equal(t, d.NumSeconds, uint16(3)) require.Equal(t, d.Flags, uint16(1)) require.True(t, d.ClientIPAddr.Equal(net.IPv4zero)) require.True(t, d.YourIPAddr.Equal(net.IPv4zero)) require.True(t, d.GatewayIPAddr.Equal(net.IPv4zero)) require.Equal(t, d.ClientHWAddr, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) require.Equal(t, d.ServerHostName, "") require.Equal(t, d.BootFileName, "") // no need to check Magic Cookie as it is already validated in FromBytes // above } func TestFromBytesGenericInvalid(t *testing.T) { data := [][]byte{ {}, {1, 1, 6, 0}, } t.Parallel() for i, packet := range data { t.Run(strconv.Itoa(i), func(t *testing.T) { _, err := FromBytes(packet) require.Error(t, err) }) } } func TestFromBytesInvalidOptions(t *testing.T) { data := []byte{ 1, // dhcp request 1, // ethernet hw type 6, // hw addr length 0, // hop count 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID 3, 0, // number of seconds 1, 0, // broadcast 0, 0, 0, 0, // client IP address 0, 0, 0, 0, // your IP address 0, 0, 0, 0, // server IP address 0, 0, 0, 0, // gateway IP address 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding } // server host name for i := 0; i < 64; i++ { data = append(data, 0) } // boot file name for i := 0; i < 128; i++ { data = append(data, 0) } // invalid magic cookie, forcing option parsing to fail data = append(data, []byte{99, 130, 83, 98}...) _, err := FromBytes(data) require.Error(t, err) } func TestToStringMethods(t *testing.T) { d, err := New() if err != nil { t.Fatal(err) } // FlagsToString d.SetUnicast() require.Equal(t, "Unicast", d.FlagsToString()) d.SetBroadcast() require.Equal(t, "Broadcast", d.FlagsToString()) d.Flags = 0xffff require.Equal(t, "Broadcast (reserved bits not zeroed)", d.FlagsToString()) } func TestNewToBytes(t *testing.T) { // the following bytes match what dhcpv4.New would create. Keep them in // sync! expected := []byte{ 1, // Opcode BootRequest 1, // HwType Ethernet 6, // HwAddrLen 0, // HopCount 0x11, 0x22, 0x33, 0x44, // TransactionID 0, 0, // NumSeconds 0, 0, // Flags 0, 0, 0, 0, // ClientIPAddr 0, 0, 0, 0, // YourIPAddr 0, 0, 0, 0, // ServerIPAddr 0, 0, 0, 0, // GatewayIPAddr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ClientHwAddr } // ServerHostName expected = append(expected, bytes.Repeat([]byte{0}, 64)...) // BootFileName expected = append(expected, bytes.Repeat([]byte{0}, 128)...) // Magic Cookie expected = append(expected, magicCookie[:]...) // End expected = append(expected, 0xff) // Minimum message length padding. // // 236 + 4 byte cookie + 1 byte end + 59 bytes padding. expected = append(expected, bytes.Repeat([]byte{0}, 59)...) d, err := New() require.NoError(t, err) // fix TransactionID to match the expected one, since it's randomly // generated in New() d.TransactionID = TransactionID{0x11, 0x22, 0x33, 0x44} got := d.ToBytes() require.Equal(t, expected, got) } func TestToBytesStringTooLong(t *testing.T) { d, err := New() if err != nil { t.Fatal(err) } d.ServerHostName = strings.Repeat("a", 256) d.BootFileName = strings.Repeat("a", 256) require.NotPanics(t, func() { _ = d.ToBytes() }) } func TestGetOption(t *testing.T) { d, err := New() if err != nil { t.Fatal(err) } hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar")) bootFileOpt := OptBootFileName("boot.img") d.UpdateOption(hostnameOpt) d.UpdateOption(bootFileOpt) require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar")) require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot.img")) require.Equal(t, d.GetOneOption(OptionRouter), []byte(nil)) } func TestUpdateOption(t *testing.T) { d, err := New() require.NoError(t, err) hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar")) bootFileOpt1 := OptBootFileName("boot.img") bootFileOpt2 := OptBootFileName("boot2.img") d.UpdateOption(hostnameOpt) d.UpdateOption(bootFileOpt1) d.UpdateOption(bootFileOpt2) options := d.Options require.Equal(t, len(options), 2) require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar")) require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot2.img")) } func TestDHCPv4NewRequestFromOffer(t *testing.T) { offer, err := New() require.NoError(t, err) offer.SetBroadcast() offer.UpdateOption(OptMessageType(MessageTypeOffer)) offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1))) // Broadcast request var req *DHCPv4 req, err = NewRequestFromOffer(offer) require.NoError(t, err) require.Equal(t, MessageTypeRequest, req.MessageType()) require.False(t, req.IsUnicast()) require.True(t, req.IsBroadcast()) // Following options are standard in other dhcp clients (isc-dhclient/udhcp) and required // for final ACK to have all info for a proper lease setup (like used in u-root/pkgs/dhclient). require.True(t, req.IsOptionRequested(OptionRouter)) require.True(t, req.IsOptionRequested(OptionSubnetMask)) require.True(t, req.IsOptionRequested(OptionDomainName)) require.True(t, req.IsOptionRequested(OptionDomainNameServer)) // Unicast request offer.SetUnicast() req, err = NewRequestFromOffer(offer) require.NoError(t, err) require.True(t, req.IsUnicast()) require.False(t, req.IsBroadcast()) } func TestDHCPv4NewRequestFromOfferWithModifier(t *testing.T) { offer, err := New() require.NoError(t, err) offer.UpdateOption(OptMessageType(MessageTypeOffer)) offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1))) userClass := WithUserClass("linuxboot", false) req, err := NewRequestFromOffer(offer, userClass) require.NoError(t, err) require.Equal(t, MessageTypeRequest, req.MessageType()) } func TestDHCPv4NewRenewFromOffer(t *testing.T) { offer, err := New() require.NoError(t, err) offer.SetBroadcast() offer.UpdateOption(OptMessageType(MessageTypeOffer)) offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1))) offer.UpdateOption(OptRequestedIPAddress(net.IPv4(192, 168, 0, 1))) offer.YourIPAddr = net.IPv4(192, 168, 0, 1) // RFC 2131: RENEW-style requests will be unicast var req *DHCPv4 req, err = NewRenewFromOffer(offer) require.NoError(t, err) require.Equal(t, MessageTypeRequest, req.MessageType()) require.Nil(t, req.GetOneOption(OptionServerIdentifier)) require.Nil(t, req.GetOneOption(OptionRequestedIPAddress)) require.Equal(t, offer.YourIPAddr, req.ClientIPAddr) require.True(t, req.IsUnicast()) require.False(t, req.IsBroadcast()) // Renewals should behave identically to initial requests regarding requested options require.True(t, req.IsOptionRequested(OptionRouter)) require.True(t, req.IsOptionRequested(OptionSubnetMask)) require.True(t, req.IsOptionRequested(OptionDomainName)) require.True(t, req.IsOptionRequested(OptionDomainNameServer)) } func TestDHCPv4NewRenewFromOfferWithModifier(t *testing.T) { offer, err := New() require.NoError(t, err) offer.UpdateOption(OptMessageType(MessageTypeOffer)) userClass := WithUserClass("linuxboot", false) req, err := NewRenewFromOffer(offer, userClass) require.NoError(t, err) require.Equal(t, MessageTypeRequest, req.MessageType()) require.Contains(t, req.UserClass(), "linuxboot") } func TestNewReplyFromRequest(t *testing.T) { discover, err := New() require.NoError(t, err) discover.GatewayIPAddr = net.IPv4(192, 168, 0, 1) reply, err := NewReplyFromRequest(discover) require.NoError(t, err) require.Equal(t, discover.TransactionID, reply.TransactionID) require.Equal(t, discover.GatewayIPAddr, reply.GatewayIPAddr) } func TestNewReplyFromRequestWithModifier(t *testing.T) { discover, err := New() require.NoError(t, err) discover.GatewayIPAddr = net.IPv4(192, 168, 0, 1) userClass := WithUserClass("linuxboot", false) reply, err := NewReplyFromRequest(discover, userClass) require.NoError(t, err) require.Equal(t, discover.TransactionID, reply.TransactionID) } func TestDHCPv4MessageTypeNil(t *testing.T) { m, err := New() require.NoError(t, err) require.Equal(t, MessageTypeNone, m.MessageType()) } func TestNewDiscovery(t *testing.T) { hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6} m, err := NewDiscovery(hwAddr) require.NoError(t, err) require.Equal(t, MessageTypeDiscover, m.MessageType()) // Validate fields of DISCOVER packet. require.Equal(t, OpcodeBootRequest, m.OpCode) require.Equal(t, iana.HWTypeEthernet, m.HWType) require.Equal(t, hwAddr, m.ClientHWAddr) require.True(t, m.Options.Has(OptionParameterRequestList)) } func TestNewInform(t *testing.T) { hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6} localIP := net.IPv4(10, 10, 11, 11) m, err := NewInform(hwAddr, localIP) require.NoError(t, err) require.Equal(t, OpcodeBootRequest, m.OpCode) require.Equal(t, iana.HWTypeEthernet, m.HWType) require.Equal(t, hwAddr, m.ClientHWAddr) require.Equal(t, MessageTypeInform, m.MessageType()) require.True(t, m.ClientIPAddr.Equal(localIP)) } func TestIsOptionRequested(t *testing.T) { pkt, err := New() require.NoError(t, err) require.True(t, pkt.IsOptionRequested(OptionDomainNameServer)) optprl := OptParameterRequestList(OptionRouter, OptionDomainName) pkt.UpdateOption(optprl) require.False(t, pkt.IsOptionRequested(OptionDomainNameServer)) optprl = OptParameterRequestList(OptionDomainNameServer) pkt.UpdateOption(optprl) require.True(t, pkt.IsOptionRequested(OptionDomainNameServer)) } // TODO // test Summary() and String() func TestSummary(t *testing.T) { packet, err := New(WithMessageType(MessageTypeInform)) packet.TransactionID = [4]byte{1, 1, 1, 1} require.NoError(t, err) want := "DHCPv4 Message\n" + " opcode: BootRequest\n" + " hwtype: Ethernet\n" + " hopcount: 0\n" + " transaction ID: 0x01010101\n" + " num seconds: 0\n" + " flags: Unicast (0x00)\n" + " client IP: 0.0.0.0\n" + " your IP: 0.0.0.0\n" + " server IP: 0.0.0.0\n" + " gateway IP: 0.0.0.0\n" + " client MAC: 00:00:00:00:00:00\n" + " server hostname: \n" + " bootfile name: \n" + " options:\n" + " DHCP Message Type: INFORM\n" require.Equal(t, want, packet.Summary()) } func Test_withIP(t *testing.T) { buff := uio.NewBigEndianBuffer(make([]byte, 0, 20)) // Converting a string into IP, ip1 will be in 16 byte format ip1 := net.ParseIP("10.0.0.1") writeIP(buff, ip1) b := buff.Buffer require.Equal(t, b.Len(), 4, "Testing no of bytes written by writeIP func") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/fuzz.go000066400000000000000000000020421431560352000246520ustar00rootroot00000000000000// +build gofuzz package dhcpv4 import ( "fmt" "reflect" ) // Fuzz is the entrypoint for go-fuzz func Fuzz(data []byte) int { msg, err := FromBytes(data) if err != nil { return 0 } serialized := msg.ToBytes() // Compared to dhcpv6, dhcpv4 has padding and fixed-size fields containing // variable-length data; We can't expect the library to output byte-for-byte // identical packets after a round-trip. // Instead, we check that after a round-trip, the packet reserializes to the // same internal representation rtMsg, err := FromBytes(serialized) if err != nil || !reflect.DeepEqual(msg, rtMsg) { fmt.Printf("Input: %x\n", data) fmt.Printf("Round-trip: %x\n", serialized) fmt.Println("Message: ", msg.Summary()) fmt.Printf("Go repr: %#v\n", msg) fmt.Println("Reserialized: ", rtMsg.Summary()) fmt.Printf("Go repr: %#v\n", rtMsg) if err != nil { fmt.Printf("Got error while reserializing: %v\n", err) panic("round-trip error: " + err.Error()) } panic("round-trip different: " + msg.Summary()) } return 1 } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/modifiers.go000066400000000000000000000103231431560352000256360ustar00rootroot00000000000000package dhcpv4 import ( "net" "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" ) // WithTransactionID sets the Transaction ID for the DHCPv4 packet func WithTransactionID(xid TransactionID) Modifier { return func(d *DHCPv4) { d.TransactionID = xid } } // WithClientIP sets the Client IP for a DHCPv4 packet. func WithClientIP(ip net.IP) Modifier { return func(d *DHCPv4) { d.ClientIPAddr = ip } } // WithYourIP sets the Your IP for a DHCPv4 packet. func WithYourIP(ip net.IP) Modifier { return func(d *DHCPv4) { d.YourIPAddr = ip } } // WithServerIP sets the Server IP for a DHCPv4 packet. func WithServerIP(ip net.IP) Modifier { return func(d *DHCPv4) { d.ServerIPAddr = ip } } // WithGatewayIP sets the Gateway IP for the DHCPv4 packet. func WithGatewayIP(ip net.IP) Modifier { return func(d *DHCPv4) { d.GatewayIPAddr = ip } } // WithOptionCopied copies the value of option opt from request. func WithOptionCopied(request *DHCPv4, opt OptionCode) Modifier { return func(d *DHCPv4) { if val := request.Options.Get(opt); val != nil { d.UpdateOption(OptGeneric(opt, val)) } } } // WithReply fills in opcode, hwtype, xid, clienthwaddr, and flags from the given packet. func WithReply(request *DHCPv4) Modifier { return func(d *DHCPv4) { if request.OpCode == OpcodeBootRequest { d.OpCode = OpcodeBootReply } else { d.OpCode = OpcodeBootRequest } d.HWType = request.HWType d.TransactionID = request.TransactionID d.ClientHWAddr = request.ClientHWAddr d.Flags = request.Flags } } // WithHWType sets the Hardware Type for a DHCPv4 packet. func WithHWType(hwt iana.HWType) Modifier { return func(d *DHCPv4) { d.HWType = hwt } } // WithBroadcast sets the packet to be broadcast or unicast func WithBroadcast(broadcast bool) Modifier { return func(d *DHCPv4) { if broadcast { d.SetBroadcast() } else { d.SetUnicast() } } } // WithHwAddr sets the hardware address for a packet func WithHwAddr(hwaddr net.HardwareAddr) Modifier { return func(d *DHCPv4) { d.ClientHWAddr = hwaddr } } // WithOption appends a DHCPv4 option provided by the user func WithOption(opt Option) Modifier { return func(d *DHCPv4) { d.UpdateOption(opt) } } // WithoutOption removes the DHCPv4 option with the given code func WithoutOption(code OptionCode) Modifier { return func(d *DHCPv4) { d.DeleteOption(code) } } // WithUserClass adds a user class option to the packet. // The rfc parameter allows you to specify if the userclass should be // rfc compliant or not. More details in issue #113 func WithUserClass(uc string, rfc bool) Modifier { // TODO let the user specify multiple user classes return func(d *DHCPv4) { if rfc { d.UpdateOption(OptRFC3004UserClass([]string{uc})) } else { d.UpdateOption(OptUserClass(uc)) } } } // WithNetboot adds bootfile URL and bootfile param options to a DHCPv4 packet. func WithNetboot(d *DHCPv4) { WithRequestedOptions(OptionTFTPServerName, OptionBootfileName)(d) } // WithMessageType adds the DHCPv4 message type m to a packet. func WithMessageType(m MessageType) Modifier { return WithOption(OptMessageType(m)) } // WithRequestedOptions adds requested options to the packet. func WithRequestedOptions(optionCodes ...OptionCode) Modifier { return func(d *DHCPv4) { cl := d.ParameterRequestList() cl.Add(optionCodes...) d.UpdateOption(OptParameterRequestList(cl...)) } } // WithRelay adds parameters required for DHCPv4 to be relayed by the relay // server with given ip func WithRelay(ip net.IP) Modifier { return func(d *DHCPv4) { d.SetUnicast() d.GatewayIPAddr = ip d.HopCount++ } } // WithNetmask adds or updates an OptSubnetMask func WithNetmask(mask net.IPMask) Modifier { return WithOption(OptSubnetMask(mask)) } // WithLeaseTime adds or updates an OptIPAddressLeaseTime func WithLeaseTime(leaseTime uint32) Modifier { return WithOption(OptIPAddressLeaseTime(time.Duration(leaseTime) * time.Second)) } // WithDomainSearchList adds or updates an OptionDomainSearch func WithDomainSearchList(searchList ...string) Modifier { return WithOption(OptDomainSearch(&rfc1035label.Labels{ Labels: searchList, })) } func WithGeneric(code OptionCode, value []byte) Modifier { return WithOption(OptGeneric(code, value)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/modifiers_test.go000066400000000000000000000122541431560352000267020ustar00rootroot00000000000000package dhcpv4 import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestTransactionIDModifier(t *testing.T) { d, err := New() require.NoError(t, err) WithTransactionID(TransactionID{0xdd, 0xcc, 0xbb, 0xaa})(d) require.Equal(t, TransactionID{0xdd, 0xcc, 0xbb, 0xaa}, d.TransactionID) } func TestBroadcastModifier(t *testing.T) { d, err := New() require.NoError(t, err) // set and test broadcast WithBroadcast(true)(d) require.Equal(t, true, d.IsBroadcast()) // set and test unicast WithBroadcast(false)(d) require.Equal(t, true, d.IsUnicast()) } func TestHwAddrModifier(t *testing.T) { hwaddr := net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf} d, err := New(WithHwAddr(hwaddr)) require.NoError(t, err) require.Equal(t, hwaddr, d.ClientHWAddr) } func TestWithOptionModifier(t *testing.T) { d, err := New(WithOption(OptDomainName("slackware.it"))) require.NoError(t, err) dnOpt := d.DomainName() require.NotNil(t, dnOpt) require.Equal(t, "slackware.it", dnOpt) } func TestWithoutOptionModifier(t *testing.T) { d, err := New( WithOption(OptDomainName("slackware.it")), WithoutOption(OptionDomainName), ) require.NoError(t, err) require.False(t, d.Options.Has(OptionDomainName)) require.Equal(t, "", d.DomainName()) } func TestUserClassModifier(t *testing.T) { d, err := New(WithUserClass("linuxboot", false)) require.NoError(t, err) expected := []byte{ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } require.Equal(t, expected, d.GetOneOption(OptionUserClassInformation)) } func TestUserClassModifierRFC(t *testing.T) { d, err := New(WithUserClass("linuxboot", true)) require.NoError(t, err) expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } require.Equal(t, expected, d.GetOneOption(OptionUserClassInformation)) } func TestWithNetboot(t *testing.T) { d, err := New(WithNetboot) require.NoError(t, err) require.Equal(t, "TFTP Server Name, Bootfile Name", d.ParameterRequestList().String()) } func TestWithNetbootExistingTFTP(t *testing.T) { d, _ := New() d.UpdateOption(OptParameterRequestList(OptionTFTPServerName)) WithNetboot(d) require.Equal(t, "TFTP Server Name, Bootfile Name", d.ParameterRequestList().String()) } func TestWithNetbootExistingBootfileName(t *testing.T) { d, _ := New() d.UpdateOption(OptParameterRequestList(OptionBootfileName)) WithNetboot(d) require.Equal(t, "TFTP Server Name, Bootfile Name", d.ParameterRequestList().String()) } func TestWithNetbootExistingBoth(t *testing.T) { d, _ := New() d.UpdateOption(OptParameterRequestList(OptionBootfileName, OptionTFTPServerName)) WithNetboot(d) require.Equal(t, "TFTP Server Name, Bootfile Name", d.ParameterRequestList().String()) } func TestWithRequestedOptions(t *testing.T) { // Check if OptionParameterRequestList is created when not present d, err := New(WithRequestedOptions(OptionFQDN)) require.NoError(t, err) require.NotNil(t, d) opts := d.ParameterRequestList() require.NotNil(t, opts) require.ElementsMatch(t, opts, []OptionCode{OptionFQDN}) // Check if already set options are preserved WithRequestedOptions(OptionHostName)(d) require.NotNil(t, d) opts = d.ParameterRequestList() require.NotNil(t, opts) require.ElementsMatch(t, opts, []OptionCode{OptionFQDN, OptionHostName}) } func TestWithRelay(t *testing.T) { ip := net.IP{10, 0, 0, 1} d, err := New(WithRelay(ip)) require.NoError(t, err) require.True(t, d.IsUnicast(), "expected unicast") require.Equal(t, ip, d.GatewayIPAddr) require.Equal(t, uint8(1), d.HopCount) } func TestWithNetmask(t *testing.T) { d, err := New(WithNetmask(net.IPv4Mask(255, 255, 255, 0))) require.NoError(t, err) require.Equal(t, net.IPv4Mask(255, 255, 255, 0), d.SubnetMask()) } func TestWithLeaseTime(t *testing.T) { d, err := New(WithLeaseTime(uint32(3600))) require.NoError(t, err) require.True(t, d.Options.Has(OptionIPAddressLeaseTime)) require.Equal(t, 3600*time.Second, d.IPAddressLeaseTime(10*time.Second)) } func TestWithDNS(t *testing.T) { d, err := New(WithDNS(net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2"))) require.NoError(t, err) dns := d.DNS() require.Equal(t, net.ParseIP("10.0.0.1").To4(), dns[0]) require.Equal(t, net.ParseIP("10.0.0.2").To4(), dns[1]) } func TestWithDomainSearchList(t *testing.T) { d, err := New(WithDomainSearchList("slackware.it", "dhcp.slackware.it")) require.NoError(t, err) osl := d.DomainSearch() require.NotNil(t, osl) require.Equal(t, 2, len(osl.Labels)) require.Equal(t, "slackware.it", osl.Labels[0]) require.Equal(t, "dhcp.slackware.it", osl.Labels[1]) } func TestWithRouter(t *testing.T) { rtr := net.ParseIP("10.0.0.254").To4() d, err := New(WithRouter(rtr)) require.NoError(t, err) ortr := d.Router() require.Equal(t, rtr, ortr[0]) } func TestWithRelayAgentInfo(t *testing.T) { req, _ := New(WithGeneric(OptionRelayAgentInformation, []byte{ 1, 5, 'l', 'i', 'n', 'u', 'x', 2, 4, 'b', 'o', 'o', 't', })) req.OpCode = OpcodeBootRequest resp, _ := NewReplyFromRequest(req) opt := resp.RelayAgentInfo() require.NotNil(t, opt) require.Equal(t, len(opt.Options), 2) circuit := opt.Get(GenericOptionCode(1)) remote := opt.Get(GenericOptionCode(2)) require.Equal(t, circuit, []byte("linux")) require.Equal(t, remote, []byte("boot")) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/000077500000000000000000000000001431560352000250475ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/client.go000066400000000000000000000435731431560352000266700ustar00rootroot00000000000000// Copyright 2018 the u-root Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build go1.12 // Package nclient4 is a small, minimum-functionality client for DHCPv4. // // It only supports the 4-way DHCPv4 Discover-Offer-Request-Ack handshake as // well as the Request-Ack renewal process. package nclient4 import ( "bytes" "context" "errors" "fmt" "log" "net" "os" "sync" "sync/atomic" "time" "github.com/insomniacslk/dhcp/dhcpv4" ) const ( defaultBufferCap = 5 // DefaultTimeout is the default value for read-timeout if option WithTimeout is not set DefaultTimeout = 5 * time.Second // DefaultRetries is amount of retries will be done if no answer was received within read-timeout amount of time DefaultRetries = 3 // MaxMessageSize is the value to be used for DHCP option "MaxMessageSize". MaxMessageSize = 1500 // ClientPort is the port that DHCP clients listen on. ClientPort = 68 // ServerPort is the port that DHCP servers and relay agents listen on. ServerPort = 67 ) var ( // DefaultServers is the address of all link-local DHCP servers and // relay agents. DefaultServers = &net.UDPAddr{ IP: net.IPv4bcast, Port: ServerPort, } ) var ( // ErrNoResponse is returned when no response packet is received. ErrNoResponse = errors.New("no matching response packet received") // ErrNoConn is returned when NewWithConn is called with nil-value as conn. ErrNoConn = errors.New("conn is nil") // ErrNoIfaceHWAddr is returned when NewWithConn is called with nil-value as ifaceHWAddr ErrNoIfaceHWAddr = errors.New("ifaceHWAddr is nil") ) // pendingCh is a channel associated with a pending TransactionID. type pendingCh struct { // SendAndRead closes done to indicate that it wishes for no more // messages for this particular XID. done <-chan struct{} // ch is used by the receive loop to distribute DHCP messages. ch chan<- *dhcpv4.DHCPv4 } // Logger is a handler which will be used to output logging messages type Logger interface { // PrintMessage print _all_ DHCP messages PrintMessage(prefix string, message *dhcpv4.DHCPv4) // Printf is use to print the rest debugging information Printf(format string, v ...interface{}) } // EmptyLogger prints nothing type EmptyLogger struct{} // Printf is just a dummy function that does nothing func (e EmptyLogger) Printf(format string, v ...interface{}) {} // PrintMessage is just a dummy function that does nothing func (e EmptyLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {} // Printfer is used for actual output of the logger. For example *log.Logger is a Printfer. type Printfer interface { // Printf is the function for logging output. Arguments are handled in the manner of fmt.Printf. Printf(format string, v ...interface{}) } // ShortSummaryLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the short format. type ShortSummaryLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (s ShortSummaryLogger) Printf(format string, v ...interface{}) { s.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the short format via predefined Printfer func (s ShortSummaryLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { s.Printf("%s: %s", prefix, message) } // DebugLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the long format. type DebugLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (d DebugLogger) Printf(format string, v ...interface{}) { d.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the long format via predefined Printfer func (d DebugLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { d.Printf("%s: %s", prefix, message.Summary()) } // Client is an IPv4 DHCP client. type Client struct { ifaceHWAddr net.HardwareAddr conn net.PacketConn timeout time.Duration retry int logger Logger // bufferCap is the channel capacity for each TransactionID. bufferCap int // serverAddr is the UDP address to send all packets to. // // This may be an actual broadcast address, or a unicast address. serverAddr *net.UDPAddr // closed is an atomic bool set to 1 when done is closed. closed uint32 // done is closed to unblock the receive loop. done chan struct{} // wg protects any spawned goroutines, namely the receiveLoop. wg sync.WaitGroup pendingMu sync.Mutex // pending stores the distribution channels for each pending // TransactionID. receiveLoop uses this map to determine which channel // to send a new DHCP message to. pending map[dhcpv4.TransactionID]*pendingCh } // New returns a client usable with an unconfigured interface. func New(iface string, opts ...ClientOpt) (*Client, error) { return new(iface, nil, nil, opts...) } // NewWithConn creates a new DHCP client that sends and receives packets on the // given interface. func NewWithConn(conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { return new(``, conn, ifaceHWAddr, opts...) } func new(iface string, conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { c := &Client{ ifaceHWAddr: ifaceHWAddr, timeout: DefaultTimeout, retry: DefaultRetries, serverAddr: DefaultServers, bufferCap: defaultBufferCap, conn: conn, logger: EmptyLogger{}, done: make(chan struct{}), pending: make(map[dhcpv4.TransactionID]*pendingCh), } for _, opt := range opts { err := opt(c) if err != nil { return nil, fmt.Errorf("unable to apply option: %w", err) } } if c.ifaceHWAddr == nil { if iface == `` { return nil, ErrNoIfaceHWAddr } i, err := net.InterfaceByName(iface) if err != nil { return nil, fmt.Errorf("unable to get interface information: %w", err) } c.ifaceHWAddr = i.HardwareAddr } if c.conn == nil { var err error if iface == `` { return nil, ErrNoConn } c.conn, err = NewRawUDPConn(iface, ClientPort) // broadcast if err != nil { return nil, fmt.Errorf("unable to open a broadcasting socket: %w", err) } } c.wg.Add(1) go c.receiveLoop() return c, nil } // Close closes the underlying connection. func (c *Client) Close() error { // Make sure not to close done twice. if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) { return nil } err := c.conn.Close() // Closing c.done sets off a chain reaction: // // Any SendAndRead unblocks trying to receive more messages, which // means rem() gets called. // // rem() should be unblocking receiveLoop if it is blocked. // // receiveLoop should then exit gracefully. close(c.done) // Wait for receiveLoop to stop. c.wg.Wait() return err } func (c *Client) isClosed() bool { return atomic.LoadUint32(&c.closed) != 0 } func (c *Client) receiveLoop() { defer c.wg.Done() for { // TODO: Clients can send a "max packet size" option in their // packets, IIRC. Choose a reasonable size and set it. b := make([]byte, MaxMessageSize) n, _, err := c.conn.ReadFrom(b) if err != nil { if !c.isClosed() { c.logger.Printf("error reading from UDP connection: %v", err) } return } msg, err := dhcpv4.FromBytes(b[:n]) if err != nil { // Not a valid DHCP packet; keep listening. continue } if msg.OpCode != dhcpv4.OpcodeBootReply { // Not a response message. continue } // This is a somewhat non-standard check, by the looks // of RFC 2131. It should work as long as the DHCP // server is spec-compliant for the HWAddr field. if c.ifaceHWAddr != nil && !bytes.Equal(c.ifaceHWAddr, msg.ClientHWAddr) { // Not for us. continue } c.pendingMu.Lock() p, ok := c.pending[msg.TransactionID] if ok { select { case <-p.done: close(p.ch) delete(c.pending, msg.TransactionID) // This send may block. case p.ch <- msg: } } c.pendingMu.Unlock() } } // ClientOpt is a function that configures the Client. type ClientOpt func(c *Client) error // WithTimeout configures the retransmission timeout. // // Default is 5 seconds. func WithTimeout(d time.Duration) ClientOpt { return func(c *Client) (err error) { c.timeout = d return } } // WithSummaryLogger logs one-line DHCPv4 message summaries when sent & received. func WithSummaryLogger() ClientOpt { return func(c *Client) (err error) { c.logger = ShortSummaryLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } return } } // WithDebugLogger logs multi-line full DHCPv4 messages when sent & received. func WithDebugLogger() ClientOpt { return func(c *Client) (err error) { c.logger = DebugLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } return } } // WithLogger set the logger (see interface Logger). func WithLogger(newLogger Logger) ClientOpt { return func(c *Client) (err error) { c.logger = newLogger return } } // WithUnicast forces client to send messages as unicast frames. // By default client sends messages as broadcast frames even if server address is defined. // // srcAddr is both: // * The source address of outgoing frames. // * The address to be listened for incoming frames. func WithUnicast(srcAddr *net.UDPAddr) ClientOpt { return func(c *Client) (err error) { if srcAddr == nil { srcAddr = &net.UDPAddr{Port: ClientPort} } c.conn, err = net.ListenUDP("udp4", srcAddr) if err != nil { err = fmt.Errorf("unable to start listening UDP port: %w", err) } return } } // WithHWAddr tells to the Client to receive messages destinated to selected // hardware address func WithHWAddr(hwAddr net.HardwareAddr) ClientOpt { return func(c *Client) (err error) { c.ifaceHWAddr = hwAddr return } } func withBufferCap(n int) ClientOpt { return func(c *Client) (err error) { c.bufferCap = n return } } // WithRetry configures the number of retransmissions to attempt. // // Default is 3. func WithRetry(r int) ClientOpt { return func(c *Client) (err error) { c.retry = r return } } // WithServerAddr configures the address to send messages to. func WithServerAddr(n *net.UDPAddr) ClientOpt { return func(c *Client) (err error) { c.serverAddr = n return } } // Matcher matches DHCP packets. type Matcher func(*dhcpv4.DHCPv4) bool // IsMessageType returns a matcher that checks for the message types. func IsMessageType(t dhcpv4.MessageType, tt ...dhcpv4.MessageType) Matcher { return func(p *dhcpv4.DHCPv4) bool { if p.MessageType() == t { return true } for _, mt := range tt { if p.MessageType() == mt { return true } } return false } } // IsCorrectServer returns a matcher that checks for the correct ServerAddress. func IsCorrectServer(s net.IP) Matcher { return func(p *dhcpv4.DHCPv4) bool { return p.ServerIdentifier().Equal(s) } } // IsAll returns a matcher that checks for all given matchers to be true. func IsAll(ms ...Matcher) Matcher { return func(p *dhcpv4.DHCPv4) bool { for _, m := range ms { if !m(p) { return false } } return true } } // RemoteAddr is the default DHCP server address this client sends messages to. func (c *Client) RemoteAddr() *net.UDPAddr { // Make a copy so the caller cannot modify the address once the client // is running. cop := *c.serverAddr return &cop } // InterfaceAddr returns the MAC address of the client's interface. func (c *Client) InterfaceAddr() net.HardwareAddr { b := make(net.HardwareAddr, len(c.ifaceHWAddr)) copy(b, c.ifaceHWAddr) return b } // DiscoverOffer sends a DHCPDiscover message and returns the first valid offer // received. func (c *Client) DiscoverOffer(ctx context.Context, modifiers ...dhcpv4.Modifier) (offer *dhcpv4.DHCPv4, err error) { // RFC 2131, Section 4.4.1, Table 5 details what a DISCOVER packet should // contain. discover, err := dhcpv4.NewDiscovery(c.ifaceHWAddr, dhcpv4.PrependModifiers(modifiers, dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...) if err != nil { return nil, fmt.Errorf("unable to create a discovery request: %w", err) } offer, err = c.SendAndRead(ctx, c.serverAddr, discover, IsMessageType(dhcpv4.MessageTypeOffer)) if err != nil { return nil, fmt.Errorf("got an error while the discovery request: %w", err) } return offer, nil } // Request completes the 4-way Discover-Offer-Request-Ack handshake. // // Note that modifiers will be applied *both* to Discover and Request packets. func (c *Client) Request(ctx context.Context, modifiers ...dhcpv4.Modifier) (lease *Lease, err error) { offer, err := c.DiscoverOffer(ctx, modifiers...) if err != nil { err = fmt.Errorf("unable to receive an offer: %w", err) return } return c.RequestFromOffer(ctx, offer, modifiers...) } // ErrNak is returned if a DHCP server rejected our Request. type ErrNak struct { Offer *dhcpv4.DHCPv4 Nak *dhcpv4.DHCPv4 } // Error implements error.Error. func (e *ErrNak) Error() string { if msg := e.Nak.Message(); len(msg) > 0 { return fmt.Sprintf("server rejected request with Nak (msg: %s)", msg) } return "server rejected request with Nak" } // RequestFromOffer sends a Request message and waits for an response. // It assumes the SELECTING state by default, see Section 4.3.2 in RFC 2131 for more details. func (c *Client) RequestFromOffer(ctx context.Context, offer *dhcpv4.DHCPv4, modifiers ...dhcpv4.Modifier) (*Lease, error) { // TODO(chrisko): should this be unicast to the server? request, err := dhcpv4.NewRequestFromOffer(offer, dhcpv4.PrependModifiers(modifiers, dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...) if err != nil { return nil, fmt.Errorf("unable to create a request: %w", err) } // Servers are supposed to only respond to Requests containing their server identifier, // but sometimes non-compliant servers respond anyway. // Clients are not required to validate this field, but servers are required to // include the server identifier in their Offer per RFC 2131 Section 4.3.1 Table 3. response, err := c.SendAndRead(ctx, c.serverAddr, request, IsAll( IsCorrectServer(offer.ServerIdentifier()), IsMessageType(dhcpv4.MessageTypeAck, dhcpv4.MessageTypeNak))) if err != nil { return nil, fmt.Errorf("got an error while processing the request: %w", err) } if response.MessageType() == dhcpv4.MessageTypeNak { return nil, &ErrNak{ Offer: offer, Nak: response, } } lease := &Lease{} lease.ACK = response lease.Offer = offer lease.CreationTime = time.Now() return lease, nil } // ErrTransactionIDInUse is returned if there were an attempt to send a message // with the same TransactionID as we are already waiting an answer for. type ErrTransactionIDInUse struct { // TransactionID is the transaction ID of the message which the error is related to. TransactionID dhcpv4.TransactionID } // Error is just the method to comply interface "error". func (err *ErrTransactionIDInUse) Error() string { return fmt.Sprintf("transaction ID %s already in use", err.TransactionID) } // send sends p to destination and returns a response channel. // // Responses will be matched by transaction ID and ClientHWAddr. // // The returned lambda function must be called after all desired responses have // been received in order to return the Transaction ID to the usable pool. func (c *Client) send(dest *net.UDPAddr, msg *dhcpv4.DHCPv4) (resp <-chan *dhcpv4.DHCPv4, cancel func(), err error) { c.pendingMu.Lock() if _, ok := c.pending[msg.TransactionID]; ok { c.pendingMu.Unlock() return nil, nil, &ErrTransactionIDInUse{msg.TransactionID} } ch := make(chan *dhcpv4.DHCPv4, c.bufferCap) done := make(chan struct{}) c.pending[msg.TransactionID] = &pendingCh{done: done, ch: ch} c.pendingMu.Unlock() cancel = func() { // Why can't we just close ch here? // // Because receiveLoop may potentially be blocked trying to // send on ch. We gotta unblock it first, and then we can take // the lock and remove the XID from the pending transaction // map. close(done) c.pendingMu.Lock() if p, ok := c.pending[msg.TransactionID]; ok { close(p.ch) delete(c.pending, msg.TransactionID) } c.pendingMu.Unlock() } if _, err := c.conn.WriteTo(msg.ToBytes(), dest); err != nil { cancel() return nil, nil, fmt.Errorf("error writing packet to connection: %w", err) } return ch, cancel, nil } // This error should never be visible to users. // It is used only to increase the timeout in retryFn. var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded") // SendAndRead sends a packet p to a destination dest and waits for the first // response matching `match` as well as its Transaction ID and ClientHWAddr. // // If match is nil, the first packet matching the Transaction ID and // ClientHWAddr is returned. func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcpv4.DHCPv4, match Matcher) (*dhcpv4.DHCPv4, error) { var response *dhcpv4.DHCPv4 err := c.retryFn(func(timeout time.Duration) error { ch, rem, err := c.send(dest, p) if err != nil { return err } c.logger.PrintMessage("sent message", p) defer rem() for { select { case <-c.done: return ErrNoResponse case <-time.After(timeout): return errDeadlineExceeded case <-ctx.Done(): return ctx.Err() case packet := <-ch: if match == nil || match(packet) { c.logger.PrintMessage("received message", packet) response = packet return nil } } } }) if err == errDeadlineExceeded { return nil, ErrNoResponse } if err != nil { return nil, err } return response, nil } func (c *Client) retryFn(fn func(timeout time.Duration) error) error { timeout := c.timeout // Each retry takes the amount of timeout at worst. for i := 0; i < c.retry || c.retry < 0; i++ { // TODO: why is this called "retry" if this is "tries" ("retries"+1)? switch err := fn(timeout); err { case nil: // Got it! return nil case errDeadlineExceeded: // Double timeout, then retry. timeout *= 2 default: return err } } return errDeadlineExceeded } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/client_test.go000066400000000000000000000225531431560352000277220ustar00rootroot00000000000000// Copyright 2018 the u-root Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build go1.12 package nclient4 import ( "bytes" "context" "fmt" "net" "sync" "testing" "time" "github.com/hugelgupf/socketpair" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" ) type handler struct { mu sync.Mutex received []*dhcpv4.DHCPv4 // Each received packet can have more than one response (in theory, // from different servers sending different Advertise, for example). responses [][]*dhcpv4.DHCPv4 } func (h *handler) handle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { h.mu.Lock() defer h.mu.Unlock() h.received = append(h.received, m) if len(h.responses) > 0 { for _, resp := range h.responses[0] { _, _ = conn.WriteTo(resp.ToBytes(), peer) } h.responses = h.responses[1:] } } func serveAndClient(ctx context.Context, responses [][]*dhcpv4.DHCPv4, opts ...ClientOpt) (*Client, net.PacketConn) { // Fake PacketConn connection. clientRawConn, serverRawConn, err := socketpair.PacketSocketPair() if err != nil { panic(err) } clientConn := NewBroadcastUDPConn(clientRawConn, &net.UDPAddr{Port: ClientPort}) serverConn := NewBroadcastUDPConn(serverRawConn, &net.UDPAddr{Port: ServerPort}) o := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)} o = append(o, opts...) mc, err := NewWithConn(clientConn, net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, o...) if err != nil { panic(err) } h := &handler{responses: responses} s, err := server4.NewServer("", nil, h.handle, server4.WithConn(serverConn)) if err != nil { panic(err) } go func() { _ = s.Serve() }() return mc, serverConn } func ComparePacket(got *dhcpv4.DHCPv4, want *dhcpv4.DHCPv4) error { if got == nil && got == want { return nil } if (want == nil || got == nil) && (got != want) { return fmt.Errorf("packet got %v, want %v", got, want) } if !bytes.Equal(got.ToBytes(), want.ToBytes()) { return fmt.Errorf("packet got %v, want %v", got, want) } return nil } func pktsExpected(got []*dhcpv4.DHCPv4, want []*dhcpv4.DHCPv4) error { if len(got) != len(want) { return fmt.Errorf("got %d packets, want %d packets", len(got), len(want)) } for i := range got { if err := ComparePacket(got[i], want[i]); err != nil { return err } } return nil } func newPacketWeirdHWAddr(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 { p, err := dhcpv4.New() if err != nil { panic(fmt.Sprintf("newpacket: %v", err)) } p.OpCode = op p.TransactionID = xid p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 1, 2, 3, 4, 5, 6} return p } func newPacket(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 { p, err := dhcpv4.New() if err != nil { panic(fmt.Sprintf("newpacket: %v", err)) } p.OpCode = op p.TransactionID = xid p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf} return p } func TestSendAndRead(t *testing.T) { for _, tt := range []struct { desc string send *dhcpv4.DHCPv4 server []*dhcpv4.DHCPv4 // If want is nil, we assume server[0] contains what is wanted. want *dhcpv4.DHCPv4 wantErr error }{ { desc: "two response packets", send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), server: []*dhcpv4.DHCPv4{ newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, { desc: "one response packet", send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), server: []*dhcpv4.DHCPv4{ newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, { desc: "one response packet, one invalid XID, one invalid opcode, one invalid hwaddr", send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), server: []*dhcpv4.DHCPv4{ newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x77, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacketWeirdHWAddr(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, { desc: "discard wrong XID", send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), server: []*dhcpv4.DHCPv4{ newPacket(dhcpv4.OpcodeBootReply, [4]byte{0, 0, 0, 0}), }, want: nil, // Explicitly empty. wantErr: ErrNoResponse, }, { desc: "no response, timeout", send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), wantErr: ErrNoResponse, }, } { t.Run(tt.desc, func(t *testing.T) { // Both server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{tt.server}, // Use an unbuffered channel to make sure we // have no deadlocks. withBufferCap(0)) defer mc.Close() rcvd, err := mc.SendAndRead(context.Background(), DefaultServers, tt.send, nil) if err != tt.wantErr { t.Error(err) } if err := ComparePacket(rcvd, tt.want); err != nil { t.Errorf("got unexpected packets: %v", err) } }) } } func TestParallelSendAndRead(t *testing.T) { pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) // Both the server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{}, WithTimeout(10*time.Second), // Use an unbuffered channel to make sure nothing blocks. withBufferCap(0)) defer mc.Close() var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) } }() wg.Add(1) go func() { defer wg.Done() time.Sleep(4 * time.Second) if err := mc.Close(); err != nil { t.Errorf("closing failed: %v", err) } }() wg.Wait() } func TestReuseXID(t *testing.T) { pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) // Both the server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{}) defer mc.Close() if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) } if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) } } func TestSimpleSendAndReadDiscardGarbage(t *testing.T) { pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) responses := newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}) // Both the server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, udpConn := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{{responses}}) defer mc.Close() // Too short for valid DHCPv4 packet. _, _ = udpConn.WriteTo([]byte{0x01}, nil) _, _ = udpConn.WriteTo([]byte{0x01, 0x2}, nil) rcvd, err := mc.SendAndRead(ctx, DefaultServers, pkt, nil) if err != nil { t.Errorf("SendAndRead(%v) = %v, want nil", pkt, err) } if err := ComparePacket(rcvd, responses); err != nil { t.Errorf("got unexpected packets: %v", err) } } func TestMultipleSendAndRead(t *testing.T) { for _, tt := range []struct { desc string send []*dhcpv4.DHCPv4 server [][]*dhcpv4.DHCPv4 wantErr []error }{ { desc: "two requests, two responses", send: []*dhcpv4.DHCPv4{ newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x44, 0x44, 0x44, 0x44}), }, server: [][]*dhcpv4.DHCPv4{ []*dhcpv4.DHCPv4{ // Response for first packet. newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, []*dhcpv4.DHCPv4{ // Response for second packet. newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x44, 0x44, 0x44, 0x44}), }, }, wantErr: []error{ nil, nil, }, }, } { // Both server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, tt.server) defer mc.Close() for i, send := range tt.send { ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) defer cancel() rcvd, err := mc.SendAndRead(ctx, DefaultServers, send, nil) if wantErr := tt.wantErr[i]; err != wantErr { t.Errorf("SendAndReadOne(%v): got %v, want %v", send, err, wantErr) } if err := pktsExpected([]*dhcpv4.DHCPv4{rcvd}, tt.server[i]); err != nil { t.Errorf("got unexpected packets: %v", err) } } } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/conn_unix.go000066400000000000000000000104651431560352000274040ustar00rootroot00000000000000// Copyright 2018 the u-root Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.12 && (darwin || freebsd || linux || netbsd || openbsd) // +build go1.12 // +build darwin freebsd linux netbsd openbsd package nclient4 import ( "errors" "io" "net" "github.com/mdlayher/ethernet" "github.com/mdlayher/raw" "github.com/u-root/uio/uio" ) var ( // BroadcastMac is the broadcast MAC address. // // Any UDP packet sent to this address is broadcast on the subnet. BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) ) var ( // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") ) // NewRawUDPConn returns a UDP connection bound to the interface and port // given based on a raw packet socket. All packets are broadcasted. // // The interface can be completely unconfigured. func NewRawUDPConn(iface string, port int) (net.PacketConn, error) { ifc, err := net.InterfaceByName(iface) if err != nil { return nil, err } rawConn, err := raw.ListenPacket(ifc, uint16(ethernet.EtherTypeIPv4), &raw.Config{LinuxSockDGRAM: true}) if err != nil { return nil, err } return NewBroadcastUDPConn(rawConn, &net.UDPAddr{Port: port}), nil } // BroadcastRawUDPConn uses a raw socket to send UDP packets to the broadcast // MAC address. type BroadcastRawUDPConn struct { // PacketConn is a raw DGRAM socket. net.PacketConn // boundAddr is the address this RawUDPConn is "bound" to. // // Calls to ReadFrom will only return packets destined to this address. boundAddr *net.UDPAddr } // NewBroadcastUDPConn returns a PacketConn that marshals and unmarshals UDP // packets, sending them to the broadcast MAC at on rawPacketConn. // // Calls to ReadFrom will only return packets destined to boundAddr. func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr) net.PacketConn { return &BroadcastRawUDPConn{ PacketConn: rawPacketConn, boundAddr: boundAddr, } } func udpMatch(addr *net.UDPAddr, bound *net.UDPAddr) bool { if bound == nil { return true } if bound.IP != nil && !bound.IP.Equal(addr.IP) { return false } return bound.Port == addr.Port } // ReadFrom implements net.PacketConn.ReadFrom. // // ReadFrom reads raw IP packets and will try to match them against // upc.boundAddr. Any matching packets are returned via the given buffer. func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { ipHdrMaxLen := ipv4MaximumHeaderSize udpHdrLen := udpMinimumSize for { pkt := make([]byte, ipHdrMaxLen+udpHdrLen+len(b)) n, _, err := upc.PacketConn.ReadFrom(pkt) if err != nil { return 0, nil, err } if n == 0 { return 0, nil, io.EOF } pkt = pkt[:n] buf := uio.NewBigEndianBuffer(pkt) // To read the header length, access data directly. if !buf.Has(ipv4MinimumSize) { continue } ipHdr := ipv4(buf.Data()) if !buf.Has(int(ipHdr.headerLength())) { continue } ipHdr = ipv4(buf.Consume(int(ipHdr.headerLength()))) if ipHdr.transportProtocol() != udpProtocolNumber { continue } if !buf.Has(udpHdrLen) { continue } udpHdr := udp(buf.Consume(udpHdrLen)) addr := &net.UDPAddr{ IP: ipHdr.destinationAddress(), Port: int(udpHdr.destinationPort()), } if !udpMatch(addr, upc.boundAddr) { continue } srcAddr := &net.UDPAddr{ IP: ipHdr.sourceAddress(), Port: int(udpHdr.sourcePort()), } // Extra padding after end of IP packet should be ignored, // if not dhcp option parsing will fail. dhcpLen := int(ipHdr.payloadLength()) - udpHdrLen return copy(b, buf.Consume(dhcpLen)), srcAddr, nil } } // WriteTo implements net.PacketConn.WriteTo and broadcasts all packets at the // raw socket level. // // WriteTo wraps the given packet in the appropriate UDP and IP header before // sending it on the packet conn. func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { udpAddr, ok := addr.(*net.UDPAddr) if !ok { return 0, ErrUDPAddrIsRequired } // Using the boundAddr is not quite right here, but it works. packet := udp4pkt(b, udpAddr, upc.boundAddr) // Broadcasting is not always right, but hell, what the ARP do I know. return upc.PacketConn.WriteTo(packet, &raw.Addr{HardwareAddr: BroadcastMac}) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/ipv4.go000066400000000000000000000226761431560352000262750ustar00rootroot00000000000000// Copyright 2018 Google LLC // // 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. // // This file contains code taken from gVisor. // +build go1.12 package nclient4 import ( "encoding/binary" "net" "github.com/u-root/uio/uio" ) const ( versIHL = 0 tos = 1 totalLen = 2 id = 4 flagsFO = 6 ttl = 8 protocol = 9 checksumOff = 10 srcAddr = 12 dstAddr = 16 ) // transportProtocolNumber is the number of a transport protocol. type transportProtocolNumber uint32 // ipv4Fields contains the fields of an IPv4 packet. It is used to describe the // fields of a packet that needs to be encoded. type ipv4Fields struct { // IHL is the "internet header length" field of an IPv4 packet. IHL uint8 // TOS is the "type of service" field of an IPv4 packet. TOS uint8 // TotalLength is the "total length" field of an IPv4 packet. TotalLength uint16 // ID is the "identification" field of an IPv4 packet. ID uint16 // Flags is the "flags" field of an IPv4 packet. Flags uint8 // FragmentOffset is the "fragment offset" field of an IPv4 packet. FragmentOffset uint16 // TTL is the "time to live" field of an IPv4 packet. TTL uint8 // Protocol is the "protocol" field of an IPv4 packet. Protocol uint8 // checksum is the "checksum" field of an IPv4 packet. checksum uint16 // SrcAddr is the "source ip address" of an IPv4 packet. SrcAddr net.IP // DstAddr is the "destination ip address" of an IPv4 packet. DstAddr net.IP } // ipv4 represents an ipv4 header stored in a byte array. // Most of the methods of IPv4 access to the underlying slice without // checking the boundaries and could panic because of 'index out of range'. // Always call IsValid() to validate an instance of IPv4 before using other methods. type ipv4 []byte const ( // ipv4MinimumSize is the minimum size of a valid IPv4 packet. ipv4MinimumSize = 20 // ipv4MaximumHeaderSize is the maximum size of an IPv4 header. Given // that there are only 4 bits to represents the header length in 32-bit // units, the header cannot exceed 15*4 = 60 bytes. ipv4MaximumHeaderSize = 60 // ipv4AddressSize is the size, in bytes, of an IPv4 address. ipv4AddressSize = 4 ) // headerLength returns the value of the "header length" field of the ipv4 // header. func (b ipv4) headerLength() uint8 { return (b[versIHL] & 0xf) * 4 } // protocol returns the value of the protocol field of the ipv4 header. func (b ipv4) protocol() uint8 { return b[protocol] } // sourceAddress returns the "source address" field of the ipv4 header. func (b ipv4) sourceAddress() net.IP { return net.IP(b[srcAddr : srcAddr+ipv4AddressSize]) } // destinationAddress returns the "destination address" field of the ipv4 // header. func (b ipv4) destinationAddress() net.IP { return net.IP(b[dstAddr : dstAddr+ipv4AddressSize]) } // transportProtocol implements Network.transportProtocol. func (b ipv4) transportProtocol() transportProtocolNumber { return transportProtocolNumber(b.protocol()) } // payloadLength returns the length of the payload portion of the ipv4 packet. func (b ipv4) payloadLength() uint16 { return b.totalLength() - uint16(b.headerLength()) } // totalLength returns the "total length" field of the ipv4 header. func (b ipv4) totalLength() uint16 { return binary.BigEndian.Uint16(b[totalLen:]) } // setTotalLength sets the "total length" field of the ipv4 header. func (b ipv4) setTotalLength(totalLength uint16) { binary.BigEndian.PutUint16(b[totalLen:], totalLength) } // setChecksum sets the checksum field of the ipv4 header. func (b ipv4) setChecksum(v uint16) { binary.BigEndian.PutUint16(b[checksumOff:], v) } // setFlagsFragmentOffset sets the "flags" and "fragment offset" fields of the // ipv4 header. func (b ipv4) setFlagsFragmentOffset(flags uint8, offset uint16) { v := (uint16(flags) << 13) | (offset >> 3) binary.BigEndian.PutUint16(b[flagsFO:], v) } // calculateChecksum calculates the checksum of the ipv4 header. func (b ipv4) calculateChecksum() uint16 { return checksum(b[:b.headerLength()], 0) } // encode encodes all the fields of the ipv4 header. func (b ipv4) encode(i *ipv4Fields) { b[versIHL] = (4 << 4) | ((i.IHL / 4) & 0xf) b[tos] = i.TOS b.setTotalLength(i.TotalLength) binary.BigEndian.PutUint16(b[id:], i.ID) b.setFlagsFragmentOffset(i.Flags, i.FragmentOffset) b[ttl] = i.TTL b[protocol] = i.Protocol b.setChecksum(i.checksum) copy(b[srcAddr:srcAddr+ipv4AddressSize], i.SrcAddr) copy(b[dstAddr:dstAddr+ipv4AddressSize], i.DstAddr) } const ( udpSrcPort = 0 udpDstPort = 2 udpLength = 4 udpchecksum = 6 ) // udpFields contains the fields of a udp packet. It is used to describe the // fields of a packet that needs to be encoded. type udpFields struct { // SrcPort is the "source port" field of a udp packet. SrcPort uint16 // DstPort is the "destination port" field of a UDP packet. DstPort uint16 // Length is the "length" field of a UDP packet. Length uint16 // checksum is the "checksum" field of a UDP packet. checksum uint16 } // udp represents a udp header stored in a byte array. type udp []byte const ( // udpMinimumSize is the minimum size of a valid udp packet. udpMinimumSize = 8 // udpProtocolNumber is udp's transport protocol number. udpProtocolNumber transportProtocolNumber = 17 ) // sourcePort returns the "source port" field of the udp header. func (b udp) sourcePort() uint16 { return binary.BigEndian.Uint16(b[udpSrcPort:]) } // DestinationPort returns the "destination port" field of the udp header. func (b udp) destinationPort() uint16 { return binary.BigEndian.Uint16(b[udpDstPort:]) } // Length returns the "length" field of the udp header. func (b udp) length() uint16 { return binary.BigEndian.Uint16(b[udpLength:]) } // setChecksum sets the "checksum" field of the udp header. func (b udp) setChecksum(checksum uint16) { binary.BigEndian.PutUint16(b[udpchecksum:], checksum) } // calculateChecksum calculates the checksum of the udp packet, given the total // length of the packet and the checksum of the network-layer pseudo-header // (excluding the total length) and the checksum of the payload. func (b udp) calculateChecksum(partialchecksum uint16, totalLen uint16) uint16 { // Add the length portion of the checksum to the pseudo-checksum. tmp := make([]byte, 2) binary.BigEndian.PutUint16(tmp, totalLen) xsum := checksum(tmp, partialchecksum) // Calculate the rest of the checksum. return checksum(b[:udpMinimumSize], xsum) } // encode encodes all the fields of the udp header. func (b udp) encode(u *udpFields) { binary.BigEndian.PutUint16(b[udpSrcPort:], u.SrcPort) binary.BigEndian.PutUint16(b[udpDstPort:], u.DstPort) binary.BigEndian.PutUint16(b[udpLength:], u.Length) binary.BigEndian.PutUint16(b[udpchecksum:], u.checksum) } func calculateChecksum(buf []byte, initial uint32) uint16 { v := initial l := len(buf) if l&1 != 0 { l-- v += uint32(buf[l]) << 8 } for i := 0; i < l; i += 2 { v += (uint32(buf[i]) << 8) + uint32(buf[i+1]) } return checksumCombine(uint16(v), uint16(v>>16)) } // checksum calculates the checksum (as defined in RFC 1071) of the bytes in the // given byte array. // // The initial checksum must have been computed on an even number of bytes. func checksum(buf []byte, initial uint16) uint16 { return calculateChecksum(buf, uint32(initial)) } // checksumCombine combines the two uint16 to form their checksum. This is done // by adding them and the carry. // // Note that checksum a must have been computed on an even number of bytes. func checksumCombine(a, b uint16) uint16 { v := uint32(a) + uint32(b) return uint16(v + v>>16) } // pseudoHeaderchecksum calculates the pseudo-header checksum for the // given destination protocol and network address, ignoring the length // field. pseudo-headers are needed by transport layers when calculating // their own checksum. func pseudoHeaderchecksum(protocol transportProtocolNumber, srcAddr net.IP, dstAddr net.IP) uint16 { xsum := checksum([]byte(srcAddr), 0) xsum = checksum([]byte(dstAddr), xsum) return checksum([]byte{0, uint8(protocol)}, xsum) } func udp4pkt(packet []byte, dest *net.UDPAddr, src *net.UDPAddr) []byte { ipLen := ipv4MinimumSize udpLen := udpMinimumSize h := make([]byte, 0, ipLen+udpLen+len(packet)) hdr := uio.NewBigEndianBuffer(h) ipv4fields := &ipv4Fields{ IHL: ipv4MinimumSize, TotalLength: uint16(ipLen + udpLen + len(packet)), TTL: 64, // Per RFC 1700's recommendation for IP time to live Protocol: uint8(udpProtocolNumber), SrcAddr: src.IP.To4(), DstAddr: dest.IP.To4(), } ipv4hdr := ipv4(hdr.WriteN(ipLen)) ipv4hdr.encode(ipv4fields) ipv4hdr.setChecksum(^ipv4hdr.calculateChecksum()) udphdr := udp(hdr.WriteN(udpLen)) udphdr.encode(&udpFields{ SrcPort: uint16(src.Port), DstPort: uint16(dest.Port), Length: uint16(udpLen + len(packet)), }) xsum := checksum(packet, pseudoHeaderchecksum( ipv4hdr.transportProtocol(), ipv4fields.SrcAddr, ipv4fields.DstAddr)) udphdr.setChecksum(^udphdr.calculateChecksum(xsum, udphdr.length())) hdr.WriteBytes(packet) return hdr.Data() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/lease.go000066400000000000000000000052071431560352000264730ustar00rootroot00000000000000// This is lease support for nclient4 package nclient4 import ( "context" "fmt" "net" "time" "github.com/insomniacslk/dhcp/dhcpv4" ) // Lease contains a DHCPv4 lease after DORA. // note: Lease doesn't include binding interface name type Lease struct { Offer *dhcpv4.DHCPv4 ACK *dhcpv4.DHCPv4 CreationTime time.Time } // Release send DHCPv4 release messsage to server, based on specified lease. // release is sent as unicast per RFC2131, section 4.4.4. // Note: some DHCP server requries of using assigned IP address as source IP, // use nclient4.WithUnicast to create client for such case. func (c *Client) Release(lease *Lease, modifiers ...dhcpv4.Modifier) error { if lease == nil { return fmt.Errorf("lease is nil") } req, err := dhcpv4.NewReleaseFromACK(lease.ACK, modifiers...) if err != nil { return fmt.Errorf("fail to create release request,%w", err) } _, err = c.conn.WriteTo(req.ToBytes(), &net.UDPAddr{IP: lease.ACK.Options.Get(dhcpv4.OptionServerIdentifier), Port: ServerPort}) if err == nil { c.logger.PrintMessage("sent message:", req) } return err } // Renew sends a DHCPv4 request to the server to renew the given lease. The renewal information is // sourced from the initial offer in the lease, and the ACK of the lease is updated to the ACK of // the latest renewal. This avoids issues with DHCP servers that omit information needed to build a // completely new lease from their renewal ACK (such as the Windows DHCP Server). func (c *Client) Renew(ctx context.Context, lease *Lease, modifiers ...dhcpv4.Modifier) error { if lease == nil { return fmt.Errorf("lease is nil") } request, err := dhcpv4.NewRenewFromOffer(lease.Offer, dhcpv4.PrependModifiers(modifiers, dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...) if err != nil { return fmt.Errorf("unable to create a request: %w", err) } // Servers are supposed to only respond to Requests containing their server identifier, // but sometimes non-compliant servers respond anyway. // Clients are not required to validate this field, but servers are required to // include the server identifier in their Offer per RFC 2131 Section 4.3.1 Table 3. response, err := c.SendAndRead(ctx, c.serverAddr, request, IsAll( IsCorrectServer(lease.Offer.ServerIdentifier()), IsMessageType(dhcpv4.MessageTypeAck, dhcpv4.MessageTypeNak))) if err != nil { return fmt.Errorf("got an error while processing the request: %w", err) } if response.MessageType() == dhcpv4.MessageTypeNak { return &ErrNak{ Offer: lease.Offer, Nak: response, } } // Update the ACK of the lease with the ACK of the latest renewal lease.ACK = response return nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/nclient4/lease_test.go000066400000000000000000000211071431560352000275270ustar00rootroot00000000000000// this tests nclient4 with lease, renew and release package nclient4 import ( "context" "fmt" "log" "net" "sync" "testing" "time" "github.com/hugelgupf/socketpair" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" ) type testLeaseKey struct { mac net.HardwareAddr } func (lk testLeaseKey) compare(b testLeaseKey) bool { for i := 0; i < 6; i++ { if lk.mac[i] != b.mac[i] { return false } } return true } //this represents one test case type testServerLease struct { key *testLeaseKey assignedAddr net.IP ShouldFail bool //expected result } type testServerLeaseList struct { list []*testServerLease lastTestSvrErr error lastTestSvrErrLock *sync.RWMutex } func newtestServerLeaseList(l dhcpv4.OptionCodeList) *testServerLeaseList { r := &testServerLeaseList{} r.lastTestSvrErrLock = &sync.RWMutex{} return r } func (sll testServerLeaseList) get(k *testLeaseKey) *testServerLease { for i := range sll.list { if sll.list[i].key.compare(*k) { return sll.list[i] } } return nil } func (sll *testServerLeaseList) getKey(m *dhcpv4.DHCPv4) *testLeaseKey { key := &testLeaseKey{} key.mac = m.ClientHWAddr return key } //use following setting to handle DORA //server-id: 1.2.3.4 //subnet-mask: /24 //return address from sll.list func (sll *testServerLeaseList) testLeaseDORAHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { reply, err := dhcpv4.NewReplyFromRequest(m) if err != nil { return fmt.Errorf("NewReplyFromRequest failed: %v", err) } svrIP := net.ParseIP("1.2.3.4") reply.UpdateOption(dhcpv4.OptServerIdentifier(svrIP.To4())) reply.UpdateOption(dhcpv4.OptSubnetMask(net.IPv4Mask(255, 255, 255, 0))) //build lease key key := sll.getKey(m) clease := sll.get(key) if clease == nil { return fmt.Errorf("unable to find the lease") } reply.YourIPAddr = clease.assignedAddr switch mt := m.MessageType(); mt { case dhcpv4.MessageTypeDiscover: reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) case dhcpv4.MessageTypeRequest: reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) default: return fmt.Errorf("Unhandled message type: %v", mt) } if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil { return fmt.Errorf("Cannot reply to client: %v", err) } return nil } //return check list for options must and may in the release msg according to RFC2131,section 4.4.1 func (sll *testServerLeaseList) getCheckList() (mustHaveOpts, mayHaveOpts map[uint8]bool) { mustHaveOpts = make(map[uint8]bool) mayHaveOpts = make(map[uint8]bool) mustHaveOpts[dhcpv4.OptionDHCPMessageType.Code()] = false mustHaveOpts[dhcpv4.OptionServerIdentifier.Code()] = false mayHaveOpts[dhcpv4.OptionClassIdentifier.Code()] = false mayHaveOpts[dhcpv4.OptionMessage.Code()] = false return } //check request message according to RFC2131, section 4.4.1 func (sll *testServerLeaseList) testLeaseReleaseHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { if m.HopCount != 0 { return fmt.Errorf("hop count is %v, should be 0", m.HopCount) } if m.NumSeconds != 0 { return fmt.Errorf("seconds is %v, should be 0", m.NumSeconds) } if m.Flags != 0 { return fmt.Errorf("flags is %v, should be 0", m.Flags) } key := sll.getKey(m) clease := sll.get(key) if clease == nil { return fmt.Errorf("can't find the lease") } if !m.ClientIPAddr.Equal(clease.assignedAddr) { return fmt.Errorf("client IP is %v, expecting %v", m.ClientIPAddr, clease.assignedAddr) } if !m.YourIPAddr.Equal(net.ParseIP("0.0.0.0")) { return fmt.Errorf("your IP is %v, expect 0", m.YourIPAddr) } if !m.GatewayIPAddr.Equal(net.ParseIP("0.0.0.0")) { return fmt.Errorf("gateway IP is %v, expect 0", m.GatewayIPAddr) } mustlist, maylist := sll.getCheckList() for o := range m.Options { foundInMust := false foundInMay := false if _, ok := mustlist[o]; ok { mustlist[o] = true foundInMust = true } if _, ok := maylist[o]; ok { foundInMay = true } if !foundInMay && !foundInMust { return fmt.Errorf("option %v is not allowed in DHCP release msg", o) } } for o, got := range mustlist { if !got { return fmt.Errorf("option %v is missing in DHCP release msg", o) } } if !net.IP(m.Options.Get(dhcpv4.OptionServerIdentifier)).Equal(net.ParseIP("1.2.3.4")) { return fmt.Errorf("release misses servier id option=1.2.3.4") } return nil } func (sll *testServerLeaseList) handle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { if m == nil { log.Fatal("Packet is nil!") } if m.OpCode != dhcpv4.OpcodeBootRequest { log.Fatal("Not a BootRequest!") } sll.lastTestSvrErrLock.Lock() defer sll.lastTestSvrErrLock.Unlock() switch m.MessageType() { case dhcpv4.MessageTypeDiscover, dhcpv4.MessageTypeRequest: sll.lastTestSvrErr = sll.testLeaseDORAHandle(conn, peer, m) if sll.lastTestSvrErr != nil { log.Printf("svr failed to handle DORA,%v", sll.lastTestSvrErr) } case dhcpv4.MessageTypeRelease: sll.lastTestSvrErr = sll.testLeaseReleaseHandle(conn, peer, m) if sll.lastTestSvrErr != nil { log.Printf("svr failed to handle release,%v", sll.lastTestSvrErr) } default: sll.lastTestSvrErr = fmt.Errorf("svr got unexpeceted message type %v", m.MessageType()) log.Print(sll.lastTestSvrErr) } } func (sll *testServerLeaseList) runTest(t *testing.T) { for _, l := range sll.list { sll.lastTestSvrErr = nil t.Logf("running lease test case for mac %v", l.key.mac) // Fake PacketConn connection. //note can't reuse conn between different clients, because there is currently //no way to stop a client's reciev loop clientRawConn, serverRawConn, err := socketpair.PacketSocketPair() if err != nil { panic(err) } clientConn := NewBroadcastUDPConn(clientRawConn, &net.UDPAddr{Port: ClientPort}) serverConn := NewBroadcastUDPConn(serverRawConn, &net.UDPAddr{Port: ServerPort}) s, err := server4.NewServer("", nil, sll.handle, server4.WithConn(serverConn)) if err != nil { t.Fatal(err) } go func() { _ = s.Serve() }() clnt, err := testCreateClientWithServerLease(clientConn, l) if err != nil { t.Fatal(err) } // modList := []dhcpv4.Modifier{} // for op, val := range l.key.idOptions { // modList = append(modList, dhcpv4.WithOption(dhcpv4.OptGeneric(dhcpv4.GenericOptionCode(op), val))) // } chkerr := func(err, lastsvrerr error, shouldfail bool, t *testing.T) bool { if err != nil || lastsvrerr != nil { if !shouldfail { t.Fatalf("case failed,%v,svr err:%v ", err, lastsvrerr) } else { t.Logf("case failed as expected,%v,svr err: %v", err, lastsvrerr) return false } } return true } lease, err := clnt.Request(context.Background()) sll.lastTestSvrErrLock.RLock() keepgoing := chkerr(err, sll.lastTestSvrErr, l.ShouldFail, t) sll.lastTestSvrErrLock.RUnlock() if keepgoing { err = clnt.Renew(context.Background(), lease) sll.lastTestSvrErrLock.RLock() keepgoing = chkerr(err, sll.lastTestSvrErr, l.ShouldFail, t) sll.lastTestSvrErrLock.RUnlock() } if keepgoing { err = clnt.Release(lease) //this sleep is to make sure release is handled by server time.Sleep(time.Second) sll.lastTestSvrErrLock.RLock() chkerr(err, sll.lastTestSvrErr, l.ShouldFail, t) sll.lastTestSvrErrLock.RUnlock() } } } func testCreateClientWithServerLease(conn net.PacketConn, sl *testServerLease) (*Client, error) { clntModList := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)} clntModList = append(clntModList, WithHWAddr(sl.key.mac)) clnt, err := NewWithConn(conn, sl.key.mac, clntModList...) if err != nil { return nil, fmt.Errorf("failed to create dhcpv4 client,%v", err) } return clnt, nil } func TestLease(t *testing.T) { //test data set 1 var idoptlist dhcpv4.OptionCodeList idoptlist.Add(dhcpv4.OptionClientIdentifier) sll := newtestServerLeaseList(idoptlist) sll.list = []*testServerLease{ &testServerLease{ assignedAddr: net.ParseIP("192.168.1.1"), key: &testLeaseKey{ mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 1}, }, }, &testServerLease{ assignedAddr: net.ParseIP("192.168.1.2"), key: &testLeaseKey{ mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 2}, }, }, } sll.runTest(t) //test data set 2 idoptlist = dhcpv4.OptionCodeList{} sll = newtestServerLeaseList(idoptlist) sll.list = []*testServerLease{ &testServerLease{ assignedAddr: net.ParseIP("192.168.2.1"), key: &testLeaseKey{ mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 2, 1}, }, }, &testServerLease{ assignedAddr: net.ParseIP("192.168.2.2"), key: &testLeaseKey{ mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 2, 2}, }, }, } sll.runTest(t) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_generic.go000066400000000000000000000012631431560352000266640ustar00rootroot00000000000000package dhcpv4 import ( "fmt" ) // OptionGeneric is an option that only contains the option code and associated // data. Every option that does not have a specific implementation will fall // back to this option. type OptionGeneric struct { Data []byte } // ToBytes returns a serialized generic option as a slice of bytes. func (o OptionGeneric) ToBytes() []byte { return o.Data } // String returns a human-readable representation of a generic option. func (o OptionGeneric) String() string { return fmt.Sprintf("%v", o.Data) } // OptGeneric returns a generic option. func OptGeneric(code OptionCode, value []byte) Option { return Option{Code: code, Value: OptionGeneric{value}} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_generic_test.go000066400000000000000000000010201431560352000277120ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptionGenericCode(t *testing.T) { o := OptGeneric(OptionDHCPMessageType, []byte{byte(MessageTypeDiscover)}) require.Equal(t, OptionDHCPMessageType, o.Code) require.Equal(t, []byte{1}, o.Value.ToBytes()) require.Equal(t, "DHCP Message Type: [1]", o.String()) } func TestOptionGenericStringUnknown(t *testing.T) { o := OptGeneric(optionCode(102), []byte{byte(MessageTypeDiscover)}) require.Equal(t, "unknown (102): [1]", o.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ip.go000066400000000000000000000031401431560352000256540ustar00rootroot00000000000000package dhcpv4 import ( "net" "github.com/u-root/uio/uio" ) // IP implements DHCPv4 IP option marshaling and unmarshaling as described by // RFC 2132, Sections 5.3, 9.1, 9.7, and others. type IP net.IP // FromBytes parses an IP from data in binary form. func (i *IP) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *i = IP(buf.CopyN(net.IPv4len)) return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. func (i IP) ToBytes() []byte { return []byte(net.IP(i).To4()) } // String returns a human-readable IP. func (i IP) String() string { return net.IP(i).String() } // GetIP returns code out of o parsed as an IP. func GetIP(code OptionCode, o Options) net.IP { v := o.Get(code) if v == nil { return nil } var ip IP if err := ip.FromBytes(v); err != nil { return nil } return net.IP(ip) } // OptBroadcastAddress returns a new DHCPv4 Broadcast Address option. // // The broadcast address option is described in RFC 2132, Section 5.3. func OptBroadcastAddress(ip net.IP) Option { return Option{Code: OptionBroadcastAddress, Value: IP(ip)} } // OptRequestedIPAddress returns a new DHCPv4 Requested IP Address option. // // The requested IP address option is described by RFC 2132, Section 9.1. func OptRequestedIPAddress(ip net.IP) Option { return Option{Code: OptionRequestedIPAddress, Value: IP(ip)} } // OptServerIdentifier returns a new DHCPv4 Server Identifier option. // // The server identifier option is described by RFC 2132, Section 9.7. func OptServerIdentifier(ip net.IP) Option { return Option{Code: OptionServerIdentifier, Value: IP(ip)} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ip_address_lease_time.go000066400000000000000000000022351431560352000315540ustar00rootroot00000000000000package dhcpv4 import ( "math" "time" "github.com/u-root/uio/uio" ) // MaxLeaseTime is the maximum lease time that can be encoded. var MaxLeaseTime = math.MaxUint32 * time.Second // Duration implements the IP address lease time option described by RFC 2132, // Section 9.2. type Duration time.Duration // FromBytes parses a duration from a byte stream according to RFC 2132, Section 9.2. func (d *Duration) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *d = Duration(time.Duration(buf.Read32()) * time.Second) return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. func (d Duration) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write32(uint32(time.Duration(d) / time.Second)) return buf.Data() } // String returns a human-readable string for this option. func (d Duration) String() string { return time.Duration(d).String() } // OptIPAddressLeaseTime returns a new IP address lease time option. // // The IP address lease time option is described by RFC 2132, Section 9.2. func OptIPAddressLeaseTime(d time.Duration) Option { return Option{Code: OptionIPAddressLeaseTime, Value: Duration(d)} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ip_address_lease_time_test.go000066400000000000000000000020141431560352000326060ustar00rootroot00000000000000package dhcpv4 import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIPAddressLeaseTime(t *testing.T) { o := OptIPAddressLeaseTime(43200 * time.Second) require.Equal(t, OptionIPAddressLeaseTime, o.Code, "Code") require.Equal(t, []byte{0, 0, 168, 192}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "IP Addresses Lease Time: 12h0m0s", o.String(), "String") } func TestGetIPAddressLeaseTime(t *testing.T) { m, _ := New(WithGeneric(OptionIPAddressLeaseTime, []byte{0, 0, 168, 192})) leaseTime := m.IPAddressLeaseTime(0) require.Equal(t, 43200*time.Second, leaseTime) // Too short. m, _ = New(WithGeneric(OptionIPAddressLeaseTime, []byte{168, 192})) leaseTime = m.IPAddressLeaseTime(0) require.Equal(t, time.Duration(0), leaseTime) // Too long. m, _ = New(WithGeneric(OptionIPAddressLeaseTime, []byte{1, 1, 1, 1, 1})) leaseTime = m.IPAddressLeaseTime(0) require.Equal(t, time.Duration(0), leaseTime) // Empty. m, _ = New() require.Equal(t, time.Duration(10), m.IPAddressLeaseTime(10)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ip_test.go000066400000000000000000000034301431560352000267150ustar00rootroot00000000000000package dhcpv4 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestOptBroadcastAddress(t *testing.T) { ip := net.IP{192, 168, 0, 1} o := OptBroadcastAddress(ip) require.Equal(t, OptionBroadcastAddress, o.Code, "Code") require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") require.Equal(t, "Broadcast Address: 192.168.0.1", o.String(), "String") } func TestGetIPs(t *testing.T) { o := Options{102: []byte{}} i := GetIPs(optionCode(102), o) require.Nil(t, i) o = Options{102: []byte{192, 168, 0}} i = GetIPs(optionCode(102), o) require.Nil(t, i) o = Options{102: []byte{192, 168, 0, 1}} i = GetIPs(optionCode(102), o) require.Equal(t, i, []net.IP{{192, 168, 0, 1}}) o = Options{102: []byte{192, 168, 0, 1, 192, 168, 0, 2}} i = GetIPs(optionCode(102), o) require.Equal(t, i, []net.IP{{192, 168, 0, 1}, {192, 168, 0, 2}}) } func TestParseIP(t *testing.T) { var ip IP err := ip.FromBytes([]byte{}) require.Error(t, err, "empty byte stream") err = ip.FromBytes([]byte{192, 168, 0}) require.Error(t, err, "wrong IP length") err = ip.FromBytes([]byte{192, 168, 0, 1}) require.NoError(t, err) require.Equal(t, net.IP{192, 168, 0, 1}, net.IP(ip)) } func TestOptRequestedIPAddress(t *testing.T) { ip := net.IP{192, 168, 0, 1} o := OptRequestedIPAddress(ip) require.Equal(t, OptionRequestedIPAddress, o.Code, "Code") require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") require.Equal(t, "Requested IP Address: 192.168.0.1", o.String(), "String") } func TestOptServerIdentifier(t *testing.T) { ip := net.IP{192, 168, 0, 1} o := OptServerIdentifier(ip) require.Equal(t, OptionServerIdentifier, o.Code, "Code") require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") require.Equal(t, "Server Identifier: 192.168.0.1", o.String(), "String") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ips.go000066400000000000000000000047421431560352000260500ustar00rootroot00000000000000package dhcpv4 import ( "fmt" "net" "strings" "github.com/u-root/uio/uio" ) // IPs are IPv4 addresses from a DHCP packet as used and specified by options // in RFC 2132, Sections 3.5 through 3.13, 8.2, 8.3, 8.5, 8.6, 8.9, and 8.10. // // IPs implements the OptionValue type. type IPs []net.IP // FromBytes parses an IPv4 address from a DHCP packet as used and specified by // options in RFC 2132, Sections 3.5 through 3.13, 8.2, 8.3, 8.5, 8.6, 8.9, and // 8.10. func (i *IPs) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) if buf.Len() == 0 { return fmt.Errorf("IP DHCP options must always list at least one IP") } *i = make(IPs, 0, buf.Len()/net.IPv4len) for buf.Has(net.IPv4len) { *i = append(*i, net.IP(buf.CopyN(net.IPv4len))) } return buf.FinError() } // ToBytes marshals IPv4 addresses to a DHCP packet as specified by RFC 2132, // Section 3.5 et al. func (i IPs) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, ip := range i { buf.WriteBytes(ip.To4()) } return buf.Data() } // String returns a human-readable representation of a list of IPs. func (i IPs) String() string { s := make([]string, 0, len(i)) for _, ip := range i { s = append(s, ip.String()) } return strings.Join(s, ", ") } // GetIPs parses a list of IPs from code in o. func GetIPs(code OptionCode, o Options) []net.IP { v := o.Get(code) if v == nil { return nil } var ips IPs if err := ips.FromBytes(v); err != nil { return nil } return []net.IP(ips) } // OptRouter returns a new DHCPv4 Router option. // // The Router option is described by RFC 2132, Section 3.5. func OptRouter(routers ...net.IP) Option { return Option{ Code: OptionRouter, Value: IPs(routers), } } // WithRouter updates a packet with the DHCPv4 Router option. func WithRouter(routers ...net.IP) Modifier { return WithOption(OptRouter(routers...)) } // OptNTPServers returns a new DHCPv4 NTP Server option. // // The NTP servers option is described by RFC 2132, Section 8.3. func OptNTPServers(ntpServers ...net.IP) Option { return Option{ Code: OptionNTPServers, Value: IPs(ntpServers), } } // OptDNS returns a new DHCPv4 Domain Name Server option. // // The DNS server option is described by RFC 2132, Section 3.8. func OptDNS(servers ...net.IP) Option { return Option{ Code: OptionDomainNameServer, Value: IPs(servers), } } // WithDNS modifies a packet with the DHCPv4 Domain Name Server option. func WithDNS(servers ...net.IP) Modifier { return WithOption(OptDNS(servers...)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_ips_test.go000066400000000000000000000043411431560352000271020ustar00rootroot00000000000000package dhcpv4 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestParseIPs(t *testing.T) { var i IPs data := []byte{ 192, 168, 0, 10, // DNS #1 192, 168, 0, 20, // DNS #2 } err := i.FromBytes(data) require.NoError(t, err) servers := []net.IP{ net.IP{192, 168, 0, 10}, net.IP{192, 168, 0, 20}, } require.Equal(t, servers, []net.IP(i)) // Bad length data = []byte{1, 1, 1} err = i.FromBytes(data) require.Error(t, err, "should get error from bad length") // RFC2132 requires that at least one IP is specified for each IP field. err = i.FromBytes([]byte{}) require.Error(t, err) } func TestOptDomainNameServer(t *testing.T) { o := OptDNS(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) require.Equal(t, OptionDomainNameServer, o.Code) require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) require.Equal(t, "Domain Name Server: 192.168.0.1, 192.168.0.10", o.String()) } func TestGetDomainNameServer(t *testing.T) { ips := []net.IP{ net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 10}, } m, _ := New(WithOption(OptDNS(ips...))) require.Equal(t, ips, m.DNS()) m, _ = New() require.Nil(t, m.DNS()) } func TestOptNTPServers(t *testing.T) { o := OptNTPServers(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) require.Equal(t, OptionNTPServers, o.Code) require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) require.Equal(t, "NTP Servers: 192.168.0.1, 192.168.0.10", o.String()) } func TestGetNTPServers(t *testing.T) { ips := []net.IP{ net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 10}, } m, _ := New(WithOption(OptNTPServers(ips...))) require.Equal(t, ips, m.NTPServers()) m, _ = New() require.Nil(t, m.NTPServers()) } func TestOptRouter(t *testing.T) { o := OptRouter(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) require.Equal(t, OptionRouter, o.Code) require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) require.Equal(t, "Router: 192.168.0.1, 192.168.0.10", o.String()) } func TestGetRouter(t *testing.T) { ips := []net.IP{ net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 10}, } m, _ := New(WithOption(OptRouter(ips...))) require.Equal(t, ips, m.Router()) m, _ = New() require.Nil(t, m.Router()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_maximum_dhcp_message_size.go000066400000000000000000000023571431560352000324660ustar00rootroot00000000000000package dhcpv4 import ( "fmt" "github.com/u-root/uio/uio" ) // Uint16 implements encoding and decoding functions for a uint16 as used in // RFC 2132, Section 9.10. type Uint16 uint16 // ToBytes returns a serialized stream of bytes for this option. func (o Uint16) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(o)) return buf.Data() } // String returns a human-readable string for this option. func (o Uint16) String() string { return fmt.Sprintf("%d", uint16(o)) } // FromBytes decodes data into o as per RFC 2132, Section 9.10. func (o *Uint16) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *o = Uint16(buf.Read16()) return buf.FinError() } // GetUint16 parses a uint16 from code in o. func GetUint16(code OptionCode, o Options) (uint16, error) { v := o.Get(code) if v == nil { return 0, fmt.Errorf("option not present") } var u Uint16 if err := u.FromBytes(v); err != nil { return 0, err } return uint16(u), nil } // OptMaxMessageSize returns a new DHCP Maximum Message Size option. // // The Maximum DHCP Message Size option is described by RFC 2132, Section 9.10. func OptMaxMessageSize(size uint16) Option { return Option{Code: OptionMaximumDHCPMessageSize, Value: Uint16(size)} } option_maximum_dhcp_message_size_test.go000066400000000000000000000016431431560352000334430ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptMaximumDHCPMessageSize(t *testing.T) { o := OptMaxMessageSize(1500) require.Equal(t, OptionMaximumDHCPMessageSize, o.Code, "Code") require.Equal(t, []byte{5, 220}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Maximum DHCP Message Size: 1500", o.String()) } func TestGetMaximumDHCPMessageSize(t *testing.T) { m, _ := New(WithGeneric(OptionMaximumDHCPMessageSize, []byte{5, 220})) o, err := m.MaxMessageSize() require.NoError(t, err) require.Equal(t, uint16(1500), o) // Short byte stream m, _ = New(WithGeneric(OptionMaximumDHCPMessageSize, []byte{2})) _, err = m.MaxMessageSize() require.Error(t, err, "should get error from short byte stream") // Bad length m, _ = New(WithGeneric(OptionMaximumDHCPMessageSize, []byte{2, 2, 2})) _, err = m.MaxMessageSize() require.Error(t, err, "should get error from bad length") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_message_type.go000066400000000000000000000002601431560352000277310ustar00rootroot00000000000000package dhcpv4 // OptMessageType returns a new DHCPv4 Message Type option. func OptMessageType(m MessageType) Option { return Option{Code: OptionDHCPMessageType, Value: m} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_message_type_test.go000066400000000000000000000017131431560352000307740ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptMessageType(t *testing.T) { o := OptMessageType(MessageTypeDiscover) require.Equal(t, OptionDHCPMessageType, o.Code, "Code") require.Equal(t, []byte{1}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "DHCP Message Type: DISCOVER", o.String()) // unknown o = OptMessageType(99) require.Equal(t, "DHCP Message Type: unknown (99)", o.String()) } func TestParseOptMessageType(t *testing.T) { var m MessageType data := []byte{1} // DISCOVER err := m.FromBytes(data) require.NoError(t, err) require.Equal(t, MessageTypeDiscover, m) // Bad length data = []byte{1, 2} err = m.FromBytes(data) require.Error(t, err, "should get error from bad length") } func TestGetMessageType(t *testing.T) { m, _ := New(WithMessageType(MessageTypeDiscover)) require.Equal(t, MessageTypeDiscover, m.MessageType()) m, _ = New() require.Equal(t, MessageTypeNone, m.MessageType()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_misc.go000066400000000000000000000013261431560352000262030ustar00rootroot00000000000000package dhcpv4 import ( "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" ) // OptDomainSearch returns a new domain search option. // // The domain search option is described by RFC 3397, Section 2. func OptDomainSearch(labels *rfc1035label.Labels) Option { return Option{Code: OptionDNSDomainSearchList, Value: labels} } // OptClientArch returns a new Client System Architecture Type option. func OptClientArch(archs ...iana.Arch) Option { return Option{Code: OptionClientSystemArchitectureType, Value: iana.Archs(archs)} } // OptClientIdentifier returns a new Client Identifier option. func OptClientIdentifier(ident []byte) Option { return OptGeneric(OptionClientIdentifier, ident) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_misc_test.go000066400000000000000000000043661431560352000272510ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/stretchr/testify/require" ) func TestGetDomainSearch(t *testing.T) { data := []byte{ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0, } m, _ := New(WithGeneric(OptionDNSDomainSearchList, data)) labels := m.DomainSearch() require.NotNil(t, labels) require.Equal(t, 2, len(labels.Labels)) require.Equal(t, data, labels.ToBytes()) require.Equal(t, labels.Labels[0], "example.com") require.Equal(t, labels.Labels[1], "subnet.example.org") } func TestOptDomainSearchToBytes(t *testing.T) { expected := []byte{ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0, } opt := OptDomainSearch(&rfc1035label.Labels{ Labels: []string{ "example.com", "subnet.example.org", }, }) require.Equal(t, opt.Value.ToBytes(), expected) } func TestParseOptClientArchType(t *testing.T) { m, _ := New(WithGeneric(OptionClientSystemArchitectureType, []byte{ 0, 6, // EFI_IA32 })) archs := m.ClientArch() require.NotNil(t, archs) require.Equal(t, archs[0], iana.EFI_IA32) } func TestParseOptClientArchTypeMultiple(t *testing.T) { m, _ := New(WithGeneric(OptionClientSystemArchitectureType, []byte{ 0, 6, // EFI_IA32 0, 2, // EFI_ITANIUM })) archs := m.ClientArch() require.NotNil(t, archs) require.Equal(t, archs[0], iana.EFI_IA32) require.Equal(t, archs[1], iana.EFI_ITANIUM) } func TestParseOptClientArchTypeInvalid(t *testing.T) { m, _ := New(WithGeneric(OptionClientSystemArchitectureType, []byte{42})) archs := m.ClientArch() require.Nil(t, archs) } func TestGetClientArchEmpty(t *testing.T) { m, _ := New() require.Nil(t, m.ClientArch()) } func TestOptClientArchTypeParseAndToBytesMultiple(t *testing.T) { data := []byte{ 0, 6, // EFI_IA32 0, 8, // EFI_XSCALE } opt := OptClientArch(iana.EFI_IA32, iana.EFI_XSCALE) require.Equal(t, opt.Value.ToBytes(), data) require.Equal(t, opt.Code, OptionClientSystemArchitectureType) require.Equal(t, opt.String(), "Client System Architecture Type: EFI IA32, EFI Xscale") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_parameter_request_list.go000066400000000000000000000032771431560352000320420ustar00rootroot00000000000000package dhcpv4 import ( "sort" "strings" "github.com/u-root/uio/uio" ) // OptionCodeList is a list of DHCP option codes. type OptionCodeList []OptionCode // Has returns whether c is in the list. func (ol OptionCodeList) Has(c OptionCode) bool { for _, code := range ol { if code == c { return true } } return false } // Add adds option codes in cs to ol. func (ol *OptionCodeList) Add(cs ...OptionCode) { for _, c := range cs { if !ol.Has(c) { *ol = append(*ol, c) } } } func (ol OptionCodeList) sort() { sort.Slice(ol, func(i, j int) bool { return ol[i].Code() < ol[j].Code() }) } // String returns a human-readable string for the option names. func (ol OptionCodeList) String() string { var names []string ol.sort() for _, code := range ol { names = append(names, code.String()) } return strings.Join(names, ", ") } // ToBytes returns a serialized stream of bytes for this option as defined by // RFC 2132, Section 9.8. func (ol OptionCodeList) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, req := range ol { buf.Write8(req.Code()) } return buf.Data() } // FromBytes parses a byte stream for this option as described by RFC 2132, // Section 9.8. func (ol *OptionCodeList) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *ol = make(OptionCodeList, 0, buf.Len()) for buf.Has(1) { *ol = append(*ol, optionCode(buf.Read8())) } return buf.FinError() } // OptParameterRequestList returns a new DHCPv4 Parameter Request List. // // The parameter request list option is described by RFC 2132, Section 9.8. func OptParameterRequestList(codes ...OptionCode) Option { return Option{Code: OptionParameterRequestList, Value: OptionCodeList(codes)} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_parameter_request_list_test.go000066400000000000000000000014111431560352000330650ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptParameterRequestListInterfaceMethods(t *testing.T) { opts := []OptionCode{OptionBootfileName, OptionNameServer} o := OptParameterRequestList(opts...) require.Equal(t, OptionParameterRequestList, o.Code, "Code") expectedBytes := []byte{67, 5} require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") expectedString := "Parameter Request List: Name Server, Bootfile Name" require.Equal(t, expectedString, o.String(), "String") } func TestParseOptParameterRequestList(t *testing.T) { var o OptionCodeList err := o.FromBytes([]byte{67, 5}) require.NoError(t, err) expectedOpts := OptionCodeList{OptionBootfileName, OptionNameServer} require.Equal(t, expectedOpts, o) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_relay_agent_information.go000066400000000000000000000066071431560352000321560ustar00rootroot00000000000000package dhcpv4 import ( "fmt" ) // RelayOptions is like Options, but stringifies using the Relay Agent Specific // option space. type RelayOptions struct { Options } var relayHumanizer = OptionHumanizer{ ValueHumanizer: func(code OptionCode, data []byte) fmt.Stringer { return raiSubOptionValue{data} }, CodeHumanizer: func(c uint8) OptionCode { return raiSubOptionCode(c) }, } // String prints the contained options using Relay Agent-specific option code parsing. func (r RelayOptions) String() string { return "\n" + r.Options.ToString(relayHumanizer) } // FromBytes parses relay agent options from data. func (r *RelayOptions) FromBytes(data []byte) error { r.Options = make(Options) return r.Options.FromBytes(data) } // OptRelayAgentInfo returns a new DHCP Relay Agent Info option. // // The relay agent info option is described by RFC 3046. func OptRelayAgentInfo(o ...Option) Option { return Option{Code: OptionRelayAgentInformation, Value: RelayOptions{OptionsFromList(o...)}} } type raiSubOptionValue struct { val []byte } func (rv raiSubOptionValue) String() string { return fmt.Sprintf("%s (%v)", string(rv.val), rv.val) } type raiSubOptionCode uint8 func (o raiSubOptionCode) Code() uint8 { return uint8(o) } func (o raiSubOptionCode) String() string { if s, ok := raiSubOptionCodeToString[o]; ok { return s } return fmt.Sprintf("unknown (%d)", o) } // Option 82 Relay Agention Information Sub Options const ( AgentCircuitIDSubOption raiSubOptionCode = 1 // RFC 3046 AgentRemoteIDSubOption raiSubOptionCode = 2 // RFC 3046 DOCSISDeviceClassSubOption raiSubOptionCode = 4 // RFC 3256 LinkSelectionSubOption raiSubOptionCode = 5 // RFC 3527 SubscriberIDSubOption raiSubOptionCode = 6 // RFC 3993 RADIUSAttributesSubOption raiSubOptionCode = 7 // RFC 4014 AuthenticationSubOption raiSubOptionCode = 8 // RFC 4030 VendorSpecificInformationSubOption raiSubOptionCode = 9 // RFC 4243 RelayAgentFlagsSubOption raiSubOptionCode = 10 // RFC 5010 ServerIdentifierOverrideSubOption raiSubOptionCode = 11 // RFC 5107 RelaySourcePortSubOption raiSubOptionCode = 19 // RFC 8357 VirtualSubnetSelectionSubOption raiSubOptionCode = 151 // RFC 6607 VirtualSubnetSelectionControlSubOption raiSubOptionCode = 152 // RFC 6607 ) var raiSubOptionCodeToString = map[raiSubOptionCode]string{ AgentCircuitIDSubOption: "Agent Circuit ID Sub-option", AgentRemoteIDSubOption: "Agent Remote ID Sub-option", DOCSISDeviceClassSubOption: "DOCSIS Device Class Sub-option", LinkSelectionSubOption: "Link Selection Sub-option", SubscriberIDSubOption: "Subscriber ID Sub-option", RADIUSAttributesSubOption: "RADIUS Attributes Sub-option", AuthenticationSubOption: "Authentication Sub-option", VendorSpecificInformationSubOption: "Vendor Specific Sub-option", RelayAgentFlagsSubOption: "Relay Agent Flags Sub-option", ServerIdentifierOverrideSubOption: "Server Identifier Override Sub-option", RelaySourcePortSubOption: "Relay Source Port Sub-option", VirtualSubnetSelectionSubOption: "Virtual Subnet Selection Sub-option", VirtualSubnetSelectionControlSubOption: "Virtual Subnet Selection Control Sub-option", } option_relay_agent_information_test.go000066400000000000000000000024551431560352000331330ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestGetRelayAgentInformation(t *testing.T) { m, _ := New(WithGeneric(OptionRelayAgentInformation, []byte{ 1, 5, 'l', 'i', 'n', 'u', 'x', 2, 4, 'b', 'o', 'o', 't', })) opt := m.RelayAgentInfo() require.NotNil(t, opt) require.Equal(t, len(opt.Options), 2) circuit := opt.Get(GenericOptionCode(1)) remote := opt.Get(GenericOptionCode(2)) require.Equal(t, circuit, []byte("linux")) require.Equal(t, remote, []byte("boot")) // Empty. m, _ = New() require.Nil(t, m.RelayAgentInfo()) // Invalid contents. m, _ = New(WithGeneric(OptionRelayAgentInformation, []byte{ 1, 7, 'l', 'i', 'n', 'u', 'x', })) require.Nil(t, m.RelayAgentInfo()) } func TestOptRelayAgentInfo(t *testing.T) { opt := OptRelayAgentInfo( OptGeneric(GenericOptionCode(1), []byte("linux")), OptGeneric(GenericOptionCode(2), []byte("boot")), ) wantBytes := []byte{ 1, 5, 'l', 'i', 'n', 'u', 'x', 2, 4, 'b', 'o', 'o', 't', } wantString := "Relay Agent Information:\n\n Agent Circuit ID Sub-option: linux ([108 105 110 117 120])\n Agent Remote ID Sub-option: boot ([98 111 111 116])\n" require.Equal(t, wantBytes, opt.Value.ToBytes()) require.Equal(t, OptionRelayAgentInformation, opt.Code) require.Equal(t, wantString, opt.String()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_routes.go000066400000000000000000000045121431560352000265710ustar00rootroot00000000000000package dhcpv4 import ( "fmt" "net" "strings" "github.com/u-root/uio/uio" ) // Route is a classless static route as per RFC 3442. type Route struct { // Dest is the destination network. Dest *net.IPNet // Router is the router to use for the given destination network. Router net.IP } // Marshal implements uio.Marshaler. // // Format described in RFC 3442: // // // // func (r Route) Marshal(buf *uio.Lexer) { ones, _ := r.Dest.Mask.Size() buf.Write8(uint8(ones)) // Only write the non-zero octets. dstLen := (ones + 7) / 8 buf.WriteBytes(r.Dest.IP.To4()[:dstLen]) buf.WriteBytes(r.Router.To4()) } // Unmarshal implements uio.Unmarshaler. func (r *Route) Unmarshal(buf *uio.Lexer) error { maskSize := buf.Read8() if maskSize > 32 { return fmt.Errorf("invalid mask length %d in route option", maskSize) } r.Dest = &net.IPNet{ IP: make([]byte, net.IPv4len), Mask: net.CIDRMask(int(maskSize), 32), } dstLen := (maskSize + 7) / 8 buf.ReadBytes(r.Dest.IP[:dstLen]) r.Router = buf.CopyN(net.IPv4len) return buf.Error() } // String prints the destination network and router IP. func (r *Route) String() string { return fmt.Sprintf("route to %s via %s", r.Dest, r.Router) } // Routes is a collection of network routes. type Routes []*Route // FromBytes parses routes from a set of bytes as described by RFC 3442. func (r *Routes) FromBytes(p []byte) error { buf := uio.NewBigEndianBuffer(p) for buf.Has(1) { var route Route if err := route.Unmarshal(buf); err != nil { return err } *r = append(*r, &route) } return buf.FinError() } // ToBytes marshals a set of routes as described by RFC 3442. func (r Routes) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, route := range r { route.Marshal(buf) } return buf.Data() } // String prints all routes. func (r Routes) String() string { s := make([]string, 0, len(r)) for _, route := range r { s = append(s, route.String()) } return strings.Join(s, "; ") } // OptClasslessStaticRoute returns a new DHCPv4 Classless Static Route // option. // // The Classless Static Route option is described by RFC 3442. func OptClasslessStaticRoute(routes ...*Route) Option { return Option{ Code: OptionClasslessStaticRoute, Value: Routes(routes), } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_routes_test.go000066400000000000000000000023531431560352000276310ustar00rootroot00000000000000package dhcpv4 import ( "net" "reflect" "testing" ) func mustParseIPNet(s string) *net.IPNet { _, ipnet, err := net.ParseCIDR(s) if err != nil { panic(err) } return ipnet } func TestParseRoutes(t *testing.T) { for _, tt := range []struct { p []byte want Routes wantErr bool }{ { p: []byte{32, 10, 2, 3, 4, 0, 0, 0, 0}, want: Routes{ &Route{ Dest: mustParseIPNet("10.2.3.4/32"), Router: net.IP{0, 0, 0, 0}, }, }, }, { p: []byte{0, 0, 0, 0, 0}, want: Routes{ &Route{ Dest: mustParseIPNet("0.0.0.0/0"), Router: net.IP{0, 0, 0, 0}, }, }, }, { p: []byte{32, 10, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0}, want: Routes{ &Route{ Dest: mustParseIPNet("10.2.3.4/32"), Router: net.IP{0, 0, 0, 0}, }, &Route{ Dest: mustParseIPNet("0.0.0.0/0"), Router: net.IP{0, 0, 0, 0}, }, }, }, { p: []byte{64, 10, 2, 3, 4}, wantErr: true, // Mask length 64 > 32 }, } { var r Routes if err := r.FromBytes(tt.p); (err != nil) != tt.wantErr { t.Errorf("FromBytes(%v) Unexpected error state: %v", tt.p, err) } if !reflect.DeepEqual(r, tt.want) { t.Errorf("FromBytes(%v) = %v, want %v", tt.p, r, tt.want) } } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_string.go000066400000000000000000000047311431560352000265610ustar00rootroot00000000000000package dhcpv4 // String represents an option encapsulating a string in IPv4 DHCP. // // This representation is shared by multiple options specified by RFC 2132, // Sections 3.14, 3.16, 3.17, 3.19, and 3.20. type String string // ToBytes returns a serialized stream of bytes for this option. func (o String) ToBytes() []byte { return []byte(o) } // String returns a human-readable string. func (o String) String() string { return string(o) } // FromBytes parses a serialized stream of bytes into o. func (o *String) FromBytes(data []byte) error { *o = String(string(data)) return nil } // GetString parses an RFC 2132 string from o[code]. func GetString(code OptionCode, o Options) string { v := o.Get(code) if v == nil { return "" } return string(v) } // OptDomainName returns a new DHCPv4 Domain Name option. // // The Domain Name option is described by RFC 2132, Section 3.17. func OptDomainName(name string) Option { return Option{Code: OptionDomainName, Value: String(name)} } // OptHostName returns a new DHCPv4 Host Name option. // // The Host Name option is described by RFC 2132, Section 3.14. func OptHostName(name string) Option { return Option{Code: OptionHostName, Value: String(name)} } // OptRootPath returns a new DHCPv4 Root Path option. // // The Root Path option is described by RFC 2132, Section 3.19. func OptRootPath(name string) Option { return Option{Code: OptionRootPath, Value: String(name)} } // OptBootFileName returns a new DHCPv4 Boot File Name option. // // The Bootfile Name option is described by RFC 2132, Section 9.5. func OptBootFileName(name string) Option { return Option{Code: OptionBootfileName, Value: String(name)} } // OptTFTPServerName returns a new DHCPv4 TFTP Server Name option. // // The TFTP Server Name option is described by RFC 2132, Section 9.4. func OptTFTPServerName(name string) Option { return Option{Code: OptionTFTPServerName, Value: String(name)} } // OptClassIdentifier returns a new DHCPv4 Class Identifier option. // // The Vendor Class Identifier option is described by RFC 2132, Section 9.13. func OptClassIdentifier(name string) Option { return Option{Code: OptionClassIdentifier, Value: String(name)} } // OptUserClass returns a new DHCPv4 User Class option. func OptUserClass(name string) Option { return Option{Code: OptionUserClassInformation, Value: String(name)} } // OptMessage returns a new DHCPv4 (Error) Message option. func OptMessage(msg string) Option { return Option{Code: OptionMessage, Value: String(msg)} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_string_test.go000066400000000000000000000073111431560352000276150ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptDomainName(t *testing.T) { o := OptDomainName("foo") require.Equal(t, OptionDomainName, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Domain Name: foo", o.String()) } func TestParseOptDomainName(t *testing.T) { m, _ := New(WithGeneric(OptionDomainName, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.DomainName()) m, _ = New() require.Equal(t, "", m.DomainName()) } func TestOptHostName(t *testing.T) { o := OptHostName("foo") require.Equal(t, OptionHostName, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Host Name: foo", o.String()) } func TestParseOptHostName(t *testing.T) { m, _ := New(WithGeneric(OptionHostName, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.HostName()) m, _ = New() require.Equal(t, "", m.HostName()) m, _ = New(WithGeneric(OptionHostName, []byte{'t', 'e', 's', 't', 0})) require.Equal(t, "test", m.HostName()) } func TestOptRootPath(t *testing.T) { o := OptRootPath("foo") require.Equal(t, OptionRootPath, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Root Path: foo", o.String()) } func TestParseOptRootPath(t *testing.T) { m, _ := New(WithGeneric(OptionRootPath, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.RootPath()) m, _ = New() require.Equal(t, "", m.RootPath()) } func TestOptBootFileName(t *testing.T) { o := OptBootFileName("foo") require.Equal(t, OptionBootfileName, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Bootfile Name: foo", o.String()) } func TestParseOptBootFileName(t *testing.T) { m, _ := New(WithGeneric(OptionBootfileName, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.BootFileNameOption()) m, _ = New() require.Equal(t, "", m.BootFileNameOption()) m, _ = New(WithGeneric(OptionBootfileName, []byte{'t', 'e', 's', 't', 0})) require.Equal(t, "test", m.BootFileNameOption()) } func TestOptTFTPServerName(t *testing.T) { o := OptTFTPServerName("foo") require.Equal(t, OptionTFTPServerName, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "TFTP Server Name: foo", o.String()) } func TestParseOptTFTPServerName(t *testing.T) { m, _ := New(WithGeneric(OptionTFTPServerName, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.TFTPServerName()) m, _ = New() require.Equal(t, "", m.TFTPServerName()) m, _ = New(WithGeneric(OptionTFTPServerName, []byte{'t', 'e', 's', 't', 0})) require.Equal(t, "test", m.TFTPServerName()) } func TestOptClassIdentifier(t *testing.T) { o := OptClassIdentifier("foo") require.Equal(t, OptionClassIdentifier, o.Code, "Code") require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") require.Equal(t, "Class Identifier: foo", o.String()) } func TestParseOptClassIdentifier(t *testing.T) { m, _ := New(WithGeneric(OptionClassIdentifier, []byte{'t', 'e', 's', 't'})) require.Equal(t, "test", m.ClassIdentifier()) m, _ = New() require.Equal(t, "", m.ClassIdentifier()) } func TestOptUserClass(t *testing.T) { o := OptUserClass("linuxboot") require.Equal(t, OptionUserClassInformation, o.Code, "Code") expected := []byte{ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } require.Equal(t, expected, o.Value.ToBytes(), "ToBytes") require.Equal(t, "User Class Information: linuxboot", o.String()) } func TestParseOptUserClass(t *testing.T) { m, _ := New(WithUserClass("linuxboot", false)) require.Equal(t, []string{"linuxboot"}, m.UserClass()) m, _ = New() require.Equal(t, 0, len(m.UserClass())) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_strings.go000066400000000000000000000024751431560352000267470ustar00rootroot00000000000000package dhcpv4 import ( "fmt" "strings" "github.com/u-root/uio/uio" ) // Strings represents an option encapsulating a list of strings in IPv4 DHCP as // specified in RFC 3004 // // Strings implements the OptionValue type. type Strings []string // FromBytes parses Strings from a DHCP packet as specified by RFC 3004. func (o *Strings) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) if buf.Len() == 0 { return fmt.Errorf("Strings DHCP option must always list at least one String") } *o = make(Strings, 0) for buf.Has(1) { ucLen := buf.Read8() if ucLen == 0 { return fmt.Errorf("DHCP Strings must have length greater than 0") } *o = append(*o, string(buf.CopyN(int(ucLen)))) } return buf.FinError() } // ToBytes marshals Strings to a DHCP packet as specified by RFC 3004. func (o Strings) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, uc := range o { buf.Write8(uint8(len(uc))) buf.WriteBytes([]byte(uc)) } return buf.Data() } // String returns a human-readable representation of a list of Strings. func (o Strings) String() string { return strings.Join(o, ", ") } // OptRFC3004UserClass returns a new user class option according to RFC 3004. func OptRFC3004UserClass(v []string) Option { return Option{ Code: OptionUserClassInformation, Value: Strings(v), } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_strings_test.go000066400000000000000000000026741431560352000300070ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) func TestParseStringsMultiple(t *testing.T) { var opt Strings expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 4, 't', 'e', 's', 't', } err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, len(opt), 2) require.Equal(t, "linuxboot", opt[0]) require.Equal(t, "test", opt[1]) } func TestParseStringsNone(t *testing.T) { var opt Strings expected := []byte{} err := opt.FromBytes(expected) require.Error(t, err) } func TestParseStrings(t *testing.T) { var opt Strings expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(opt)) require.Equal(t, "linuxboot", opt[0]) } func TestParseStringsZeroLength(t *testing.T) { var opt Strings err := opt.FromBytes([]byte{0, 0}) require.Error(t, err) } func TestOptRFC3004UserClass(t *testing.T) { opt := OptRFC3004UserClass(Strings([]string{"linuxboot"})) data := opt.Value.ToBytes() expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } require.Equal(t, expected, data) } func TestOptRFC3004UserClassMultiple(t *testing.T) { opt := OptRFC3004UserClass( []string{ "linuxboot", "test", }, ) data := opt.Value.ToBytes() expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 4, 't', 'e', 's', 't', } require.Equal(t, expected, data) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_subnet_mask.go000066400000000000000000000016161431560352000275650ustar00rootroot00000000000000package dhcpv4 import ( "net" "github.com/u-root/uio/uio" ) // IPMask represents an option encapsulating the subnet mask. // // This option implements the subnet mask option in RFC 2132, Section 3.3. type IPMask net.IPMask // ToBytes returns a serialized stream of bytes for this option. func (im IPMask) ToBytes() []byte { if len(im) > net.IPv4len { return im[:net.IPv4len] } return im } // String returns a human-readable string. func (im IPMask) String() string { return net.IPMask(im).String() } // FromBytes parses im from data per RFC 2132. func (im *IPMask) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *im = IPMask(buf.CopyN(net.IPv4len)) return buf.FinError() } // OptSubnetMask returns a new DHCPv4 SubnetMask option per RFC 2132, Section 3.3. func OptSubnetMask(mask net.IPMask) Option { return Option{ Code: OptionSubnetMask, Value: IPMask(mask), } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_subnet_mask_test.go000066400000000000000000000014411431560352000306200ustar00rootroot00000000000000package dhcpv4 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestOptSubnetMask(t *testing.T) { o := OptSubnetMask(net.IPMask{255, 255, 255, 0}) require.Equal(t, o.Code, OptionSubnetMask, "Code") require.Equal(t, "Subnet Mask: ffffff00", o.String(), "String") require.Equal(t, []byte{255, 255, 255, 0}, o.Value.ToBytes(), "ToBytes") } func TestGetSubnetMask(t *testing.T) { m, _ := New(WithOption(OptSubnetMask(net.IPMask{}))) mask := m.SubnetMask() require.Nil(t, mask, "empty byte stream") m, _ = New(WithOption(OptSubnetMask(net.IPMask{255}))) mask = m.SubnetMask() require.Nil(t, mask, "short byte stream") m, _ = New(WithOption(OptSubnetMask(net.IPMask{255, 255, 255, 0}))) mask = m.SubnetMask() require.Equal(t, net.IPMask{255, 255, 255, 0}, mask) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_vivc.go000066400000000000000000000031411431560352000262140ustar00rootroot00000000000000package dhcpv4 import ( "bytes" "fmt" "github.com/insomniacslk/dhcp/iana" "github.com/u-root/uio/uio" ) // VIVCIdentifier implements the vendor-identifying vendor class option // described by RFC 3925. type VIVCIdentifier struct { // EntID is the enterprise ID. EntID iana.EnterpriseID Data []byte } // OptVIVC returns a new vendor-identifying vendor class option. // // The option is described by RFC 3925. func OptVIVC(identifiers ...VIVCIdentifier) Option { return Option{ Code: OptionVendorIdentifyingVendorClass, Value: VIVCIdentifiers(identifiers), } } // VIVCIdentifiers implements encoding and decoding methods for a DHCP option // described in RFC 3925. type VIVCIdentifiers []VIVCIdentifier // FromBytes parses data into ids per RFC 3925. func (ids *VIVCIdentifiers) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) for buf.Has(5) { entID := iana.EnterpriseID(buf.Read32()) idLen := int(buf.Read8()) *ids = append(*ids, VIVCIdentifier{EntID: entID, Data: buf.CopyN(idLen)}) } return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. func (ids VIVCIdentifiers) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, id := range ids { buf.Write32(uint32(id.EntID)) buf.Write8(uint8(len(id.Data))) buf.WriteBytes(id.Data) } return buf.Data() } // String returns a human-readable string for this option. func (ids VIVCIdentifiers) String() string { if len(ids) == 0 { return "" } buf := bytes.Buffer{} for _, id := range ids { fmt.Fprintf(&buf, " %d:'%s',", id.EntID, id.Data) } return buf.String()[1 : buf.Len()-1] } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/option_vivc_test.go000066400000000000000000000030511431560352000272530ustar00rootroot00000000000000package dhcpv4 import ( "testing" "github.com/stretchr/testify/require" ) var ( sampleVIVCOpt = VIVCIdentifiers{ VIVCIdentifier{EntID: 9, Data: []byte("CiscoIdentifier")}, VIVCIdentifier{EntID: 18, Data: []byte("WellfleetIdentifier")}, } sampleVIVCOptRaw = []byte{ 0x0, 0x0, 0x0, 0x9, // enterprise id 9 0xf, // length 'C', 'i', 's', 'c', 'o', 'I', 'd', 'e', 'n', 't', 'i', 'f', 'i', 'e', 'r', 0x0, 0x0, 0x0, 0x12, // enterprise id 18 0x13, // length 'W', 'e', 'l', 'l', 'f', 'l', 'e', 'e', 't', 'I', 'd', 'e', 'n', 't', 'i', 'f', 'i', 'e', 'r', } ) func TestOptVIVCInterfaceMethods(t *testing.T) { opt := OptVIVC(sampleVIVCOpt...) require.Equal(t, OptionVendorIdentifyingVendorClass, opt.Code, "Code") require.Equal(t, sampleVIVCOptRaw, opt.Value.ToBytes(), "ToBytes") require.Equal(t, "Vendor-Identifying Vendor Class: 9:'CiscoIdentifier', 18:'WellfleetIdentifier'", opt.String()) } func TestParseOptVICO(t *testing.T) { m, _ := New(WithGeneric(OptionVendorIdentifyingVendorClass, sampleVIVCOptRaw)) o := m.VIVC() require.Equal(t, sampleVIVCOpt, o) // Identifier len too long data := make([]byte, len(sampleVIVCOptRaw)) copy(data, sampleVIVCOptRaw) data[4] = 40 m, _ = New(WithGeneric(OptionVendorIdentifyingVendorClass, data)) o = m.VIVC() require.Nil(t, o, "should get error from bad length") // Longer than length data[4] = 5 m, _ = New(WithGeneric(OptionVendorIdentifyingVendorClass, data[:10])) o = m.VIVC() require.Equal(t, o[0].Data, []byte("Cisco")) m, _ = New() require.Equal(t, VIVCIdentifiers(nil), m.VIVC()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/options.go000066400000000000000000000221551431560352000253560ustar00rootroot00000000000000package dhcpv4 import ( "errors" "fmt" "io" "math" "sort" "strings" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/uio" ) var ( // ErrShortByteStream is an error that is thrown any time a short byte stream is // detected during option parsing. ErrShortByteStream = errors.New("short byte stream") // ErrZeroLengthByteStream is an error that is thrown any time a zero-length // byte stream is encountered. ErrZeroLengthByteStream = errors.New("zero-length byte stream") // ErrInvalidOptions is returned when invalid options data is // encountered during parsing. The data could report an incorrect // length or have trailing bytes which are not part of the option. ErrInvalidOptions = errors.New("invalid options data") ) // OptionValue is an interface that all DHCP v4 options adhere to. type OptionValue interface { ToBytes() []byte String() string } // Option is a DHCPv4 option and consists of a 1-byte option code and a value // stream of bytes. // // The value is to be interpreted based on the option code. type Option struct { Code OptionCode Value OptionValue } // String returns a human-readable version of this option. func (o Option) String() string { v := o.Value.String() if strings.Contains(v, "\n") { return fmt.Sprintf("%s:\n%s", o.Code, v) } return fmt.Sprintf("%s: %s", o.Code, v) } // Options is a collection of options. type Options map[uint8][]byte // OptionsFromList adds all given options to an options map. func OptionsFromList(o ...Option) Options { opts := make(Options) for _, opt := range o { opts.Update(opt) } return opts } // Get will attempt to get all options that match a DHCPv4 option // from its OptionCode. If the option was not found it will return an // empty list. // // According to RFC 3396, options that are specified more than once are // concatenated, and hence this should always just return one option. This // currently returns a list to be API compatible. func (o Options) Get(code OptionCode) []byte { return o[code.Code()] } // Has checks whether o has the given opcode. func (o Options) Has(opcode OptionCode) bool { _, ok := o[opcode.Code()] return ok } // Del deletes the option matching the option code. func (o Options) Del(opcode OptionCode) { delete(o, opcode.Code()) } // Update updates the existing options with the passed option, adding it // at the end if not present already func (o Options) Update(option Option) { o[option.Code.Code()] = option.Value.ToBytes() } // ToBytes makes Options usable as an OptionValue as well. // // Used in the case of vendor-specific and relay agent options. func (o Options) ToBytes() []byte { return uio.ToBigEndian(o) } // FromBytes parses a sequence of bytes until the end and builds a list of // options from it. // // The sequence should not contain the DHCP magic cookie. // // Returns an error if any invalid option or length is found. func (o Options) FromBytes(data []byte) error { return o.fromBytesCheckEnd(data, false) } const ( optPad = 0 optEnd = 255 ) // FromBytesCheckEnd parses Options from byte sequences using the // parsing function that is passed in as a paremeter func (o Options) fromBytesCheckEnd(data []byte, checkEndOption bool) error { if len(data) == 0 { return nil } buf := uio.NewBigEndianBuffer(data) var end bool for buf.Len() >= 1 { // 1 byte: option code // 1 byte: option length n // n bytes: data code := buf.Read8() if code == optPad { continue } else if code == optEnd { end = true break } length := int(buf.Read8()) // N bytes: option data data := buf.Consume(length) if data == nil { return fmt.Errorf("error collecting options: %v", buf.Error()) } data = data[:length:length] // RFC 2131, Section 4.1 "Options may appear only once, [...]. // The client concatenates the values of multiple instances of // the same option into a single parameter list for // configuration." // // See also RFC 3396 for concatenation order and options longer // than 255 bytes. o[code] = append(o[code], data...) } // If we never read the End option, the sender of this packet screwed // up. if !end && checkEndOption { return io.ErrUnexpectedEOF } // Any bytes left must be padding. var pad uint8 for buf.Len() >= 1 { pad = buf.Read8() if pad != optPad && pad != optEnd { return ErrInvalidOptions } } return nil } // sortedKeys returns an ordered slice of option keys from the Options map, for // use in serializing options to binary. func (o Options) sortedKeys() []int { // Send all values for a given key var codes []int for k := range o { codes = append(codes, int(k)) } sort.Ints(codes) return codes } // Marshal writes options binary representations to b. func (o Options) Marshal(b *uio.Lexer) { for _, c := range o.sortedKeys() { code := uint8(c) // Even if the End option is in there, don't marshal it until // the end. // Don't write padding either, since the options are sorted // it would always be written first which isn't useful if code == optEnd || code == optPad { continue } data := o[code] // Ensure even 0-length options are written out if len(data) == 0 { b.Write8(code) b.Write8(0) continue } // RFC 3396: If more than 256 bytes of data are given, the // option is simply listed multiple times. for len(data) > 0 { // 1 byte: option code b.Write8(code) n := len(data) if n > math.MaxUint8 { n = math.MaxUint8 } // 1 byte: option length b.Write8(uint8(n)) // N bytes: option data b.WriteBytes(data[:n]) data = data[n:] } } } // String prints options using DHCP-specified option codes. func (o Options) String() string { return o.ToString(dhcpHumanizer) } // Summary prints options in human-readable values. // // Summary uses vendorParser to interpret the OptionVendorSpecificInformation option. func (o Options) Summary(vendorDecoder OptionDecoder) string { return o.ToString(OptionHumanizer{ ValueHumanizer: parserFor(vendorDecoder), CodeHumanizer: func(c uint8) OptionCode { return optionCode(c) }, }) } // OptionParser gives a human-legible interpretation of data for the given option code. type OptionParser func(code OptionCode, data []byte) fmt.Stringer // OptionHumanizer is used to interpret a set of Options for their option code // name and values. // // There should be separate OptionHumanizers for each Option "space": DHCP, // BSDP, Relay Agent Info, and others. type OptionHumanizer struct { ValueHumanizer OptionParser CodeHumanizer func(code uint8) OptionCode } // Stringify returns a human-readable interpretation of the option code and its // associated data. func (oh OptionHumanizer) Stringify(code uint8, data []byte) string { c := oh.CodeHumanizer(code) val := oh.ValueHumanizer(c, data) return fmt.Sprintf("%s: %s", c, val) } // dhcpHumanizer humanizes the set of DHCP option codes. var dhcpHumanizer = OptionHumanizer{ ValueHumanizer: parseOption, CodeHumanizer: func(c uint8) OptionCode { return optionCode(c) }, } // ToString uses parse to parse options into human-readable values. func (o Options) ToString(humanizer OptionHumanizer) string { var ret string for _, c := range o.sortedKeys() { code := uint8(c) v := o[code] optString := humanizer.Stringify(code, v) // If this option has sub structures, offset them accordingly. if strings.Contains(optString, "\n") { optString = strings.Replace(optString, "\n ", "\n ", -1) } ret += fmt.Sprintf(" %v\n", optString) } return ret } func parseOption(code OptionCode, data []byte) fmt.Stringer { return parserFor(nil)(code, data) } func parserFor(vendorParser OptionDecoder) OptionParser { return func(code OptionCode, data []byte) fmt.Stringer { return getOption(code, data, vendorParser) } } // OptionDecoder can decode a byte stream into a human-readable option. type OptionDecoder interface { fmt.Stringer FromBytes([]byte) error } func getOption(code OptionCode, data []byte, vendorDecoder OptionDecoder) fmt.Stringer { var d OptionDecoder switch code { case OptionRouter, OptionDomainNameServer, OptionNTPServers, OptionServerIdentifier: d = &IPs{} case OptionBroadcastAddress, OptionRequestedIPAddress: d = &IP{} case OptionClientSystemArchitectureType: d = &iana.Archs{} case OptionSubnetMask: d = &IPMask{} case OptionDHCPMessageType: var mt MessageType d = &mt case OptionParameterRequestList: d = &OptionCodeList{} case OptionHostName, OptionDomainName, OptionRootPath, OptionClassIdentifier, OptionTFTPServerName, OptionBootfileName: var s String d = &s case OptionRelayAgentInformation: d = &RelayOptions{} case OptionDNSDomainSearchList: d = &rfc1035label.Labels{} case OptionIPAddressLeaseTime: var dur Duration d = &dur case OptionMaximumDHCPMessageSize: var u Uint16 d = &u case OptionUserClassInformation: var s Strings d = &s if s.FromBytes(data) != nil { var s String d = &s } case OptionVendorIdentifyingVendorClass: d = &VIVCIdentifiers{} case OptionVendorSpecificInformation: d = vendorDecoder case OptionClasslessStaticRoute: d = &Routes{} } if d != nil && d.FromBytes(data) == nil { return d } return OptionGeneric{data} } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/options_test.go000066400000000000000000000142661431560352000264210ustar00rootroot00000000000000package dhcpv4 import ( "bytes" "fmt" "math" "testing" "github.com/stretchr/testify/require" "github.com/u-root/uio/uio" ) func TestParseOption(t *testing.T) { for _, tt := range []struct { code OptionCode value []byte want string }{ { code: OptionNameServer, value: []byte{192, 168, 1, 254}, want: "[192 168 1 254]", }, { code: OptionSubnetMask, value: []byte{255, 255, 255, 0}, want: "ffffff00", }, { code: OptionRouter, value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, want: "192.168.1.1, 192.168.2.1", }, { code: OptionDomainNameServer, value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, want: "192.168.1.1, 192.168.2.1", }, { code: OptionNTPServers, value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, want: "192.168.1.1, 192.168.2.1", }, { code: OptionServerIdentifier, value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, want: "192.168.1.1, 192.168.2.1", }, { code: OptionHostName, value: []byte("test"), want: "test", }, { code: OptionDomainName, value: []byte("test"), want: "test", }, { code: OptionRootPath, value: []byte("test"), want: "test", }, { code: OptionClassIdentifier, value: []byte("test"), want: "test", }, { code: OptionTFTPServerName, value: []byte("test"), want: "test", }, { code: OptionBootfileName, value: []byte("test"), want: "test", }, { code: OptionBroadcastAddress, value: []byte{192, 168, 1, 1}, want: "192.168.1.1", }, { code: OptionRequestedIPAddress, value: []byte{192, 168, 1, 1}, want: "192.168.1.1", }, { code: OptionIPAddressLeaseTime, value: []byte{0, 0, 0, 12}, want: "12s", }, { code: OptionDHCPMessageType, value: []byte{1}, want: "DISCOVER", }, { code: OptionParameterRequestList, value: []byte{3, 4, 5}, want: "Router, Time Server, Name Server", }, { code: OptionMaximumDHCPMessageSize, value: []byte{1, 2}, want: "258", }, { code: OptionUserClassInformation, value: []byte{4, 't', 'e', 's', 't', 3, 'f', 'o', 'o'}, want: "test, foo", }, { code: OptionRelayAgentInformation, value: []byte{1, 12, 99, 105, 114, 99, 117, 105, 116, 45, 105, 100, 45, 49}, want: "\n Agent Circuit ID Sub-option: circuit-id-1 ([99 105 114 99 117 105 116 45 105 100 45 49])\n", }, { code: OptionClientSystemArchitectureType, value: []byte{0, 0}, want: "Intel x86PC", }, } { s := parseOption(tt.code, tt.value) if got := s.String(); got != tt.want { t.Errorf("parseOption(%s, %v) = %s, want %s", tt.code, tt.value, got, tt.want) } } } func TestOptionToBytes(t *testing.T) { o := Option{ Code: OptionDHCPMessageType, Value: &OptionGeneric{[]byte{byte(MessageTypeDiscover)}}, } serialized := o.Value.ToBytes() expected := []byte{1} require.Equal(t, expected, serialized) } func TestOptionString(t *testing.T) { o := Option{ Code: OptionDHCPMessageType, Value: MessageTypeDiscover, } require.Equal(t, "DHCP Message Type: DISCOVER", o.String()) } func TestOptionStringUnknown(t *testing.T) { o := Option{ Code: GenericOptionCode(102), // Returend option code. Value: &OptionGeneric{[]byte{byte(MessageTypeDiscover)}}, } require.Equal(t, "unknown (102): [1]", o.String()) } func TestOptionsMarshal(t *testing.T) { for i, tt := range []struct { opts Options want []byte }{ { opts: nil, want: nil, }, { opts: Options{ 5: []byte{1, 2, 3, 4}, }, want: []byte{ 5 /* key */, 4 /* length */, 1, 2, 3, 4, }, }, { // Test sorted key order. opts: Options{ 5: []byte{1, 2, 3}, 100: []byte{101, 102, 103}, 255: []byte{}, }, want: []byte{ 5, 3, 1, 2, 3, 100, 3, 101, 102, 103, }, }, { // Test RFC 3396. opts: Options{ 5: bytes.Repeat([]byte{10}, math.MaxUint8+1), }, want: append(append( []byte{5, math.MaxUint8}, bytes.Repeat([]byte{10}, math.MaxUint8)...), 5, 1, 10, ), }, { // Test 0-length options opts: Options{ 80: []byte{}, }, want: []byte{80, 0}, }, { // Test special options, handled by the message marshalling code // and ignored by the options marshalling code opts: Options{ 0: []byte{}, // Padding 255: []byte{}, // End of options }, want: nil, // not written out }, } { t.Run(fmt.Sprintf("Test %02d", i), func(t *testing.T) { require.Equal(t, tt.want, uio.ToBigEndian(tt.opts)) }) } } func TestOptionsUnmarshal(t *testing.T) { for i, tt := range []struct { input []byte want Options wantError bool }{ { // Buffer missing data. input: []byte{ 3 /* key */, 3 /* length */, 1, }, wantError: true, }, { input: []byte{ // This may look too long, but 0 is padding. // The issue here is the missing OptionEnd. 3, 3, 0, 0, 0, 0, 0, 0, 0, }, wantError: true, }, { // Only OptionPad and OptionEnd can stand on their own // without a length field. So this is too short. input: []byte{ 3, }, wantError: true, }, { // Option present after the End is a nono. input: []byte{byte(OptionEnd), 3}, wantError: true, }, { input: []byte{byte(OptionEnd)}, want: Options{}, }, { input: []byte{ 3, 2, 5, 6, byte(OptionEnd), }, want: Options{ 3: []byte{5, 6}, }, }, { // Test RFC 3396. input: append( append([]byte{3, math.MaxUint8}, bytes.Repeat([]byte{10}, math.MaxUint8)...), 3, 5, 10, 10, 10, 10, 10, byte(OptionEnd), ), want: Options{ 3: bytes.Repeat([]byte{10}, math.MaxUint8+5), }, }, { input: []byte{ 10, 2, 255, 254, 11, 3, 5, 5, 5, byte(OptionEnd), }, want: Options{ 10: []byte{255, 254}, 11: []byte{5, 5, 5}, }, }, { input: append( append([]byte{10, 2, 255, 254}, bytes.Repeat([]byte{byte(OptionPad)}, 255)...), byte(OptionEnd), ), want: Options{ 10: []byte{255, 254}, }, }, } { t.Run(fmt.Sprintf("Test %02d", i), func(t *testing.T) { opt := make(Options) err := opt.fromBytesCheckEnd(tt.input, true) if tt.wantError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, opt, tt.want) } }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/000077500000000000000000000000001431560352000247215ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/conn_unix.go000066400000000000000000000041521431560352000272520ustar00rootroot00000000000000// +build !windows package server4 import ( "errors" "fmt" "net" "os" "github.com/insomniacslk/dhcp/dhcpv4" "golang.org/x/sys/unix" ) // NewIPv4UDPConn returns a UDP connection bound to both the interface and port // given based on a IPv4 DGRAM socket. The UDP connection allows broadcasting. // // The interface must already be configured. func NewIPv4UDPConn(iface string, addr *net.UDPAddr) (*net.UDPConn, error) { fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_UDP) if err != nil { return nil, fmt.Errorf("cannot get a UDP socket: %v", err) } f := os.NewFile(uintptr(fd), "") // net.FilePacketConn dups the FD, so we have to close this in any case. defer f.Close() // Allow broadcasting. if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BROADCAST, 1); err != nil { return nil, fmt.Errorf("cannot set broadcasting on socket: %v", err) } // Allow reusing the addr to aid debugging. if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { return nil, fmt.Errorf("cannot set reuseaddr on socket: %v", err) } // Allow reusing the port to aid debugging and testing. if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { return nil, fmt.Errorf("cannot set reuseport on socket: %v", err) } if len(iface) != 0 { // Bind directly to the interface. if err := dhcpv4.BindToInterface(fd, iface); err != nil { return nil, fmt.Errorf("cannot bind to interface %s: %v", iface, err) } } if addr == nil { addr = &net.UDPAddr{Port: dhcpv4.ServerPort} } // Bind to the port. saddr := unix.SockaddrInet4{Port: addr.Port} if addr.IP != nil && addr.IP.To4() == nil { return nil, fmt.Errorf("wrong address family (expected v4) for %s", addr.IP) } copy(saddr.Addr[:], addr.IP.To4()) if err := unix.Bind(fd, &saddr); err != nil { return nil, fmt.Errorf("cannot bind to port %d: %v", addr.Port, err) } conn, err := net.FilePacketConn(f) if err != nil { return nil, err } udpconn, ok := conn.(*net.UDPConn) if !ok { return nil, errors.New("BUG(dhcp4): incorrect socket type, expected UDP") } return udpconn, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/conn_windows.go000066400000000000000000000003761431560352000277650ustar00rootroot00000000000000package server4 import ( "errors" "net" ) // NewIPv4UDPConn fails on Windows. Use WithConn() to pass the connection. func NewIPv4UDPConn(iface string, addr *net.UDPAddr) (*net.UDPConn, error) { return nil, errors.New("not implemented on Windows") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/logger.go000066400000000000000000000040741431560352000265340ustar00rootroot00000000000000package server4 import ( "github.com/insomniacslk/dhcp/dhcpv4" ) // Logger is a handler which will be used to output logging messages type Logger interface { // PrintMessage print _all_ DHCP messages PrintMessage(prefix string, message *dhcpv4.DHCPv4) // Printf is use to print the rest debugging information Printf(format string, v ...interface{}) } // EmptyLogger prints nothing type EmptyLogger struct{} // Printf is just a dummy function that does nothing func (e EmptyLogger) Printf(format string, v ...interface{}) {} // PrintMessage is just a dummy function that does nothing func (e EmptyLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {} // Printfer is used for actual output of the logger. For example *log.Logger is a Printfer. type Printfer interface { // Printf is the function for logging output. Arguments are handled in the manner of fmt.Printf. Printf(format string, v ...interface{}) } // ShortSummaryLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the short format. type ShortSummaryLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (s ShortSummaryLogger) Printf(format string, v ...interface{}) { s.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the short format via predefined Printfer func (s ShortSummaryLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { s.Printf("%s: %s", prefix, message) } // DebugLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the long format. type DebugLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (d DebugLogger) Printf(format string, v ...interface{}) { d.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the long format via predefined Printfer func (d DebugLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { d.Printf("%s: %s", prefix, message.Summary()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/logger_test.go000066400000000000000000000014201431560352000275630ustar00rootroot00000000000000package server4 import( "log" "os" "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/stretchr/testify/require" ) func TestEmptyLogger(t *testing.T) { l := EmptyLogger{} msg, err := dhcpv4.New() require.Nil(t, err) l.Printf("test") l.PrintMessage("prefix", msg) } func TestShortSummaryLogger(t *testing.T) { l := ShortSummaryLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } msg, err := dhcpv4.New() require.Nil(t, err) require.NotNil(t, msg) l.Printf("test") l.PrintMessage("prefix", msg) } func TestDebugLogger(t *testing.T) { l := DebugLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } msg, err := dhcpv4.New() require.Nil(t, err) require.NotNil(t, msg) l.Printf("test") l.PrintMessage("prefix", msg) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/server.go000066400000000000000000000105071431560352000265610ustar00rootroot00000000000000// Package server4 is a basic, extensible DHCPv4 server. // // To use the DHCPv4 server code you have to call NewServer with two arguments: // - an interface to listen on, // - an address to listen on, and // - a handler function, that will be called every time a valid DHCPv4 packet is // received. // // The address to listen on is used to know IP address, port and optionally the // scope to create and UDP socket to listen on for DHCPv4 traffic. // // The handler is a function that takes as input a packet connection, that can // be used to reply to the client; a peer address, that identifies the client // sending the request, and the DHCPv4 packet itself. Just implement your // custom logic in the handler. // // Optionally, NewServer can receive options that will modify the server // object. Some options already exist, for example WithConn. If this option is // passed with a valid connection, the listening address argument is ignored. // // Example program: // // package main // // import ( // "log" // "net" // // "github.com/insomniacslk/dhcp/dhcpv4" // "github.com/insomniacslk/dhcp/dhcpv4/server4" // ) // // func handler(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { // // this function will just print the received DHCPv4 message, without replying // log.Print(m.Summary()) // } // // func main() { // laddr := net.UDPAddr{ // IP: net.ParseIP("127.0.0.1"), // Port: 67, // } // server, err := server4.NewServer("eth0", &laddr, handler) // if err != nil { // log.Fatal(err) // } // // // This never returns. If you want to do other stuff, dump it into a // // goroutine. // server.Serve() // } // package server4 import ( "log" "net" "os" "github.com/insomniacslk/dhcp/dhcpv4" ) // Handler is a type that defines the handler function to be called every time a // valid DHCPv4 message is received type Handler func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) // Server represents a DHCPv4 server object type Server struct { conn net.PacketConn Handler Handler logger Logger } // Serve serves requests. func (s *Server) Serve() error { s.logger.Printf("Server listening on %s", s.conn.LocalAddr()) s.logger.Printf("Ready to handle requests") defer s.Close() for { rbuf := make([]byte, 4096) // FIXME this is bad n, peer, err := s.conn.ReadFrom(rbuf) if err != nil { s.logger.Printf("Error reading from packet conn: %v", err) return err } s.logger.Printf("Handling request from %v", peer) m, err := dhcpv4.FromBytes(rbuf[:n]) if err != nil { s.logger.Printf("Error parsing DHCPv4 request: %v", err) continue } upeer, ok := peer.(*net.UDPAddr) if !ok { s.logger.Printf("Not a UDP connection? Peer is %s", peer) continue } // Set peer to broadcast if the client did not have an IP. if upeer.IP == nil || upeer.IP.To4().Equal(net.IPv4zero) { upeer = &net.UDPAddr{ IP: net.IPv4bcast, Port: upeer.Port, } } go s.Handler(s.conn, upeer, m) } } // Close sends a termination request to the server, and closes the UDP listener. func (s *Server) Close() error { return s.conn.Close() } // ServerOpt adds optional configuration to a server. type ServerOpt func(s *Server) // WithConn configures the server with the given connection. func WithConn(c net.PacketConn) ServerOpt { return func(s *Server) { s.conn = c } } // NewServer initializes and returns a new Server object func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerOpt) (*Server, error) { s := &Server{ Handler: handler, logger: EmptyLogger{}, } for _, o := range opt { o(s) } if s.conn == nil { var err error conn, err := NewIPv4UDPConn(ifname, addr) if err != nil { return nil, err } s.conn = conn } return s, nil } // WithSummaryLogger logs one-line DHCPv4 message summaries when sent & received. func WithSummaryLogger() ServerOpt { return func(s *Server) { s.logger = ShortSummaryLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } } } // WithDebugLogger logs multi-line full DHCPv4 messages when sent & received. func WithDebugLogger() ServerOpt { return func(s *Server) { s.logger = DebugLogger{ Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), } } } // WithLogger set the logger (see interface Logger). func WithLogger(newLogger Logger) ServerOpt { return func(s *Server) { s.logger = newLogger } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/server4/server_test.go000066400000000000000000000070331431560352000276200ustar00rootroot00000000000000// +build go1.12 package server4 import ( "context" "log" "math/rand" "net" "testing" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/interfaces" "github.com/stretchr/testify/require" ) func init() { // initialize seed. This is generally bad, but "good enough" // to generate random ports for these tests rand.Seed(time.Now().UTC().UnixNano()) } // DORAHandler is a server handler suitable for DORA transactions func DORAHandler(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { if m == nil { log.Printf("Packet is nil!") return } if m.OpCode != dhcpv4.OpcodeBootRequest { log.Printf("Not a BootRequest!") return } reply, err := dhcpv4.NewReplyFromRequest(m) if err != nil { log.Printf("NewReplyFromRequest failed: %v", err) return } reply.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{1, 2, 3, 4})) switch mt := m.MessageType(); mt { case dhcpv4.MessageTypeDiscover: reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) case dhcpv4.MessageTypeRequest: reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) default: log.Printf("Unhandled message type: %v", mt) return } if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil { log.Printf("Cannot reply to client: %v", err) } } // utility function to set up a client and a server instance and run it in // background. The caller needs to call Server.Close() once finished. func setUpClientAndServer(t *testing.T, iface net.Interface, handler Handler) (*nclient4.Client, *Server) { // strong assumption, I know loAddr := net.ParseIP("127.0.0.1") saddr := &net.UDPAddr{ IP: loAddr, Port: 0, } caddr := net.UDPAddr{ IP: loAddr, Port: 0, } s, err := NewServer("", saddr, handler) if err != nil { t.Fatal(err) } go func() { _ = s.Serve() }() saddr = s.conn.LocalAddr().(*net.UDPAddr) clientConn, err := NewIPv4UDPConn("", &caddr) if err != nil { t.Fatal(err) } c, err := nclient4.NewWithConn(clientConn, iface.HardwareAddr, nclient4.WithServerAddr(saddr)) if err != nil { t.Fatal(err) } return c, s } func TestServer(t *testing.T) { ifaces, err := interfaces.GetLoopbackInterfaces() require.NoError(t, err) require.NotEqual(t, 0, len(ifaces)) // lo has a HardwareAddr of "nil". The client will drop all packets // that don't match the HWAddr of the client interface. hwaddr := net.HardwareAddr{1, 2, 3, 4, 5, 6} ifaces[0].HardwareAddr = hwaddr c, s := setUpClientAndServer(t, ifaces[0], DORAHandler) defer func() { require.Nil(t, s.Close()) }() xid := dhcpv4.TransactionID{0xaa, 0xbb, 0xcc, 0xdd} modifiers := []dhcpv4.Modifier{ dhcpv4.WithTransactionID(xid), dhcpv4.WithHwAddr(ifaces[0].HardwareAddr), } lease, err := c.Request(context.Background(), modifiers...) require.NoError(t, err) require.NotNil(t, lease.Offer, lease.ACK) for _, p := range []*dhcpv4.DHCPv4{lease.Offer, lease.ACK} { require.Equal(t, xid, p.TransactionID) require.Equal(t, ifaces[0].HardwareAddr, p.ClientHWAddr) } err = c.Renew(context.Background(), lease, modifiers...) require.NoError(t, err) require.NotNil(t, lease.Offer, lease.ACK) for _, p := range []*dhcpv4.DHCPv4{lease.Offer, lease.ACK} { require.Equal(t, xid, p.TransactionID) require.Equal(t, ifaces[0].HardwareAddr, p.ClientHWAddr) } } func TestBadAddrFamily(t *testing.T) { saddr := &net.UDPAddr{ IP: net.IPv6loopback, Port: 0, } _, err := NewServer("", saddr, DORAHandler) if err == nil { t.Fatal("Expected server4.NewServer to fail with an IPv6 address") } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/types.go000066400000000000000000000564021431560352000250310ustar00rootroot00000000000000package dhcpv4 import ( "fmt" "github.com/u-root/uio/uio" ) // values from http://www.networksorcery.com/enp/protocol/dhcp.htm and // http://www.networksorcery.com/enp/protocol/bootp/options.htm // TransactionID represents a 4-byte DHCP transaction ID as defined in RFC 951, // Section 3. // // The TransactionID is used to match DHCP replies to their original request. type TransactionID [4]byte // String prints a hex transaction ID. func (xid TransactionID) String() string { return fmt.Sprintf("0x%x", xid[:]) } // MessageType represents the possible DHCP message types - DISCOVER, OFFER, etc type MessageType byte // DHCP message types const ( // MessageTypeNone is not a real message type, it is used by certain // functions to signal that no explicit message type is requested MessageTypeNone MessageType = 0 MessageTypeDiscover MessageType = 1 MessageTypeOffer MessageType = 2 MessageTypeRequest MessageType = 3 MessageTypeDecline MessageType = 4 MessageTypeAck MessageType = 5 MessageTypeNak MessageType = 6 MessageTypeRelease MessageType = 7 MessageTypeInform MessageType = 8 ) // ToBytes returns the serialized version of this option described by RFC 2132, // Section 9.6. func (m MessageType) ToBytes() []byte { return []byte{byte(m)} } // String prints a human-readable message type name. func (m MessageType) String() string { if s, ok := messageTypeToString[m]; ok { return s } return fmt.Sprintf("unknown (%d)", byte(m)) } // FromBytes reads a message type from data as described by RFC 2132, Section // 9.6. func (m *MessageType) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) *m = MessageType(buf.Read8()) return buf.FinError() } var messageTypeToString = map[MessageType]string{ MessageTypeDiscover: "DISCOVER", MessageTypeOffer: "OFFER", MessageTypeRequest: "REQUEST", MessageTypeDecline: "DECLINE", MessageTypeAck: "ACK", MessageTypeNak: "NAK", MessageTypeRelease: "RELEASE", MessageTypeInform: "INFORM", } // OpcodeType represents a DHCPv4 opcode. type OpcodeType uint8 // constants that represent valid values for OpcodeType const ( OpcodeBootRequest OpcodeType = 1 OpcodeBootReply OpcodeType = 2 ) func (o OpcodeType) String() string { if s, ok := opcodeToString[o]; ok { return s } return fmt.Sprintf("unknown (%d)", uint8(o)) } var opcodeToString = map[OpcodeType]string{ OpcodeBootRequest: "BootRequest", OpcodeBootReply: "BootReply", } // OptionCode is a single byte representing the code for a given Option. // // OptionCode is an interface purely to support different stringers on options // with the same Code value, as vendor-specific options use option codes that // have the same value, but mean a different thing. type OptionCode interface { // Code is the 1 byte option code for the wire. Code() uint8 // String returns the option's name. String() string } // optionCode is a DHCP option code. type optionCode uint8 // Code implements OptionCode.Code. func (o optionCode) Code() uint8 { return uint8(o) } // String returns an option name. func (o optionCode) String() string { if s, ok := optionCodeToString[o]; ok { return s } return fmt.Sprintf("unknown (%d)", uint8(o)) } // GenericOptionCode is an unnamed option code. type GenericOptionCode uint8 // Code implements OptionCode.Code. func (o GenericOptionCode) Code() uint8 { return uint8(o) } // String returns the option's name. func (o GenericOptionCode) String() string { return fmt.Sprintf("unknown (%d)", uint8(o)) } // DHCPv4 Options const ( OptionPad optionCode = 0 OptionSubnetMask optionCode = 1 OptionTimeOffset optionCode = 2 OptionRouter optionCode = 3 OptionTimeServer optionCode = 4 OptionNameServer optionCode = 5 OptionDomainNameServer optionCode = 6 OptionLogServer optionCode = 7 OptionQuoteServer optionCode = 8 OptionLPRServer optionCode = 9 OptionImpressServer optionCode = 10 OptionResourceLocationServer optionCode = 11 OptionHostName optionCode = 12 OptionBootFileSize optionCode = 13 OptionMeritDumpFile optionCode = 14 OptionDomainName optionCode = 15 OptionSwapServer optionCode = 16 OptionRootPath optionCode = 17 OptionExtensionsPath optionCode = 18 OptionIPForwarding optionCode = 19 OptionNonLocalSourceRouting optionCode = 20 OptionPolicyFilter optionCode = 21 OptionMaximumDatagramAssemblySize optionCode = 22 OptionDefaultIPTTL optionCode = 23 OptionPathMTUAgingTimeout optionCode = 24 OptionPathMTUPlateauTable optionCode = 25 OptionInterfaceMTU optionCode = 26 OptionAllSubnetsAreLocal optionCode = 27 OptionBroadcastAddress optionCode = 28 OptionPerformMaskDiscovery optionCode = 29 OptionMaskSupplier optionCode = 30 OptionPerformRouterDiscovery optionCode = 31 OptionRouterSolicitationAddress optionCode = 32 OptionStaticRoutingTable optionCode = 33 OptionTrailerEncapsulation optionCode = 34 OptionArpCacheTimeout optionCode = 35 OptionEthernetEncapsulation optionCode = 36 OptionDefaulTCPTTL optionCode = 37 OptionTCPKeepaliveInterval optionCode = 38 OptionTCPKeepaliveGarbage optionCode = 39 OptionNetworkInformationServiceDomain optionCode = 40 OptionNetworkInformationServers optionCode = 41 OptionNTPServers optionCode = 42 OptionVendorSpecificInformation optionCode = 43 OptionNetBIOSOverTCPIPNameServer optionCode = 44 OptionNetBIOSOverTCPIPDatagramDistributionServer optionCode = 45 OptionNetBIOSOverTCPIPNodeType optionCode = 46 OptionNetBIOSOverTCPIPScope optionCode = 47 OptionXWindowSystemFontServer optionCode = 48 OptionXWindowSystemDisplayManger optionCode = 49 OptionRequestedIPAddress optionCode = 50 OptionIPAddressLeaseTime optionCode = 51 OptionOptionOverload optionCode = 52 OptionDHCPMessageType optionCode = 53 OptionServerIdentifier optionCode = 54 OptionParameterRequestList optionCode = 55 OptionMessage optionCode = 56 OptionMaximumDHCPMessageSize optionCode = 57 OptionRenewTimeValue optionCode = 58 OptionRebindingTimeValue optionCode = 59 OptionClassIdentifier optionCode = 60 OptionClientIdentifier optionCode = 61 OptionNetWareIPDomainName optionCode = 62 OptionNetWareIPInformation optionCode = 63 OptionNetworkInformationServicePlusDomain optionCode = 64 OptionNetworkInformationServicePlusServers optionCode = 65 OptionTFTPServerName optionCode = 66 OptionBootfileName optionCode = 67 OptionMobileIPHomeAgent optionCode = 68 OptionSimpleMailTransportProtocolServer optionCode = 69 OptionPostOfficeProtocolServer optionCode = 70 OptionNetworkNewsTransportProtocolServer optionCode = 71 OptionDefaultWorldWideWebServer optionCode = 72 OptionDefaultFingerServer optionCode = 73 OptionDefaultInternetRelayChatServer optionCode = 74 OptionStreetTalkServer optionCode = 75 OptionStreetTalkDirectoryAssistanceServer optionCode = 76 OptionUserClassInformation optionCode = 77 OptionSLPDirectoryAgent optionCode = 78 OptionSLPServiceScope optionCode = 79 OptionRapidCommit optionCode = 80 OptionFQDN optionCode = 81 OptionRelayAgentInformation optionCode = 82 OptionInternetStorageNameService optionCode = 83 // Option 84 returned in RFC 3679 OptionNDSServers optionCode = 85 OptionNDSTreeName optionCode = 86 OptionNDSContext optionCode = 87 OptionBCMCSControllerDomainNameList optionCode = 88 OptionBCMCSControllerIPv4AddressList optionCode = 89 OptionAuthentication optionCode = 90 OptionClientLastTransactionTime optionCode = 91 OptionAssociatedIP optionCode = 92 OptionClientSystemArchitectureType optionCode = 93 OptionClientNetworkInterfaceIdentifier optionCode = 94 OptionLDAP optionCode = 95 // Option 96 returned in RFC 3679 OptionClientMachineIdentifier optionCode = 97 OptionOpenGroupUserAuthentication optionCode = 98 OptionGeoConfCivic optionCode = 99 OptionIEEE10031TZString optionCode = 100 OptionReferenceToTZDatabase optionCode = 101 // Options 102-111 returned in RFC 3679 OptionNetInfoParentServerAddress optionCode = 112 OptionNetInfoParentServerTag optionCode = 113 OptionURL optionCode = 114 // Option 115 returned in RFC 3679 OptionAutoConfigure optionCode = 116 OptionNameServiceSearch optionCode = 117 OptionSubnetSelection optionCode = 118 OptionDNSDomainSearchList optionCode = 119 OptionSIPServers optionCode = 120 OptionClasslessStaticRoute optionCode = 121 OptionCCC optionCode = 122 OptionGeoConf optionCode = 123 OptionVendorIdentifyingVendorClass optionCode = 124 OptionVendorIdentifyingVendorSpecific optionCode = 125 // Options 126-127 returned in RFC 3679 OptionTFTPServerIPAddress optionCode = 128 OptionCallServerIPAddress optionCode = 129 OptionDiscriminationString optionCode = 130 OptionRemoteStatisticsServerIPAddress optionCode = 131 Option8021PVLANID optionCode = 132 Option8021QL2Priority optionCode = 133 OptionDiffservCodePoint optionCode = 134 OptionHTTPProxyForPhoneSpecificApplications optionCode = 135 OptionPANAAuthenticationAgent optionCode = 136 OptionLoSTServer optionCode = 137 OptionCAPWAPAccessControllerAddresses optionCode = 138 OptionOPTIONIPv4AddressMoS optionCode = 139 OptionOPTIONIPv4FQDNMoS optionCode = 140 OptionSIPUAConfigurationServiceDomains optionCode = 141 OptionOPTIONIPv4AddressANDSF optionCode = 142 OptionOPTIONIPv6AddressANDSF optionCode = 143 // Options 144-149 returned in RFC 3679 OptionTFTPServerAddress optionCode = 150 OptionStatusCode optionCode = 151 OptionBaseTime optionCode = 152 OptionStartTimeOfState optionCode = 153 OptionQueryStartTime optionCode = 154 OptionQueryEndTime optionCode = 155 OptionDHCPState optionCode = 156 OptionDataSource optionCode = 157 // Options 158-174 returned in RFC 3679 OptionEtherboot optionCode = 175 OptionIPTelephone optionCode = 176 OptionEtherbootPacketCableAndCableHome optionCode = 177 // Options 178-207 returned in RFC 3679 OptionPXELinuxMagicString optionCode = 208 OptionPXELinuxConfigFile optionCode = 209 OptionPXELinuxPathPrefix optionCode = 210 OptionPXELinuxRebootTime optionCode = 211 OptionOPTION6RD optionCode = 212 OptionOPTIONv4AccessDomain optionCode = 213 // Options 214-219 returned in RFC 3679 OptionSubnetAllocation optionCode = 220 OptionVirtualSubnetAllocation optionCode = 221 // Options 222-223 returned in RFC 3679 // Options 224-254 are reserved for private use OptionEnd optionCode = 255 ) var optionCodeToString = map[OptionCode]string{ OptionPad: "Pad", OptionSubnetMask: "Subnet Mask", OptionTimeOffset: "Time Offset", OptionRouter: "Router", OptionTimeServer: "Time Server", OptionNameServer: "Name Server", OptionDomainNameServer: "Domain Name Server", OptionLogServer: "Log Server", OptionQuoteServer: "Quote Server", OptionLPRServer: "LPR Server", OptionImpressServer: "Impress Server", OptionResourceLocationServer: "Resource Location Server", OptionHostName: "Host Name", OptionBootFileSize: "Boot File Size", OptionMeritDumpFile: "Merit Dump File", OptionDomainName: "Domain Name", OptionSwapServer: "Swap Server", OptionRootPath: "Root Path", OptionExtensionsPath: "Extensions Path", OptionIPForwarding: "IP Forwarding enable/disable", OptionNonLocalSourceRouting: "Non-local Source Routing enable/disable", OptionPolicyFilter: "Policy Filter", OptionMaximumDatagramAssemblySize: "Maximum Datagram Reassembly Size", OptionDefaultIPTTL: "Default IP Time-to-live", OptionPathMTUAgingTimeout: "Path MTU Aging Timeout", OptionPathMTUPlateauTable: "Path MTU Plateau Table", OptionInterfaceMTU: "Interface MTU", OptionAllSubnetsAreLocal: "All Subnets Are Local", OptionBroadcastAddress: "Broadcast Address", OptionPerformMaskDiscovery: "Perform Mask Discovery", OptionMaskSupplier: "Mask Supplier", OptionPerformRouterDiscovery: "Perform Router Discovery", OptionRouterSolicitationAddress: "Router Solicitation Address", OptionStaticRoutingTable: "Static Routing Table", OptionTrailerEncapsulation: "Trailer Encapsulation", OptionArpCacheTimeout: "ARP Cache Timeout", OptionEthernetEncapsulation: "Ethernet Encapsulation", OptionDefaulTCPTTL: "Default TCP TTL", OptionTCPKeepaliveInterval: "TCP Keepalive Interval", OptionTCPKeepaliveGarbage: "TCP Keepalive Garbage", OptionNetworkInformationServiceDomain: "Network Information Service Domain", OptionNetworkInformationServers: "Network Information Servers", OptionNTPServers: "NTP Servers", OptionVendorSpecificInformation: "Vendor Specific Information", OptionNetBIOSOverTCPIPNameServer: "NetBIOS over TCP/IP Name Server", OptionNetBIOSOverTCPIPDatagramDistributionServer: "NetBIOS over TCP/IP Datagram Distribution Server", OptionNetBIOSOverTCPIPNodeType: "NetBIOS over TCP/IP Node Type", OptionNetBIOSOverTCPIPScope: "NetBIOS over TCP/IP Scope", OptionXWindowSystemFontServer: "X Window System Font Server", OptionXWindowSystemDisplayManger: "X Window System Display Manager", OptionRequestedIPAddress: "Requested IP Address", OptionIPAddressLeaseTime: "IP Addresses Lease Time", OptionOptionOverload: "Option Overload", OptionDHCPMessageType: "DHCP Message Type", OptionServerIdentifier: "Server Identifier", OptionParameterRequestList: "Parameter Request List", OptionMessage: "Message", OptionMaximumDHCPMessageSize: "Maximum DHCP Message Size", OptionRenewTimeValue: "Renew Time Value", OptionRebindingTimeValue: "Rebinding Time Value", OptionClassIdentifier: "Class Identifier", OptionClientIdentifier: "Client identifier", OptionNetWareIPDomainName: "NetWare/IP Domain Name", OptionNetWareIPInformation: "NetWare/IP Information", OptionNetworkInformationServicePlusDomain: "Network Information Service+ Domain", OptionNetworkInformationServicePlusServers: "Network Information Service+ Servers", OptionTFTPServerName: "TFTP Server Name", OptionBootfileName: "Bootfile Name", OptionMobileIPHomeAgent: "Mobile IP Home Agent", OptionSimpleMailTransportProtocolServer: "SMTP Server", OptionPostOfficeProtocolServer: "POP Server", OptionNetworkNewsTransportProtocolServer: "NNTP Server", OptionDefaultWorldWideWebServer: "Default WWW Server", OptionDefaultFingerServer: "Default Finger Server", OptionDefaultInternetRelayChatServer: "Default IRC Server", OptionStreetTalkServer: "StreetTalk Server", OptionStreetTalkDirectoryAssistanceServer: "StreetTalk Directory Assistance Server", OptionUserClassInformation: "User Class Information", OptionSLPDirectoryAgent: "SLP DIrectory Agent", OptionSLPServiceScope: "SLP Service Scope", OptionRapidCommit: "Rapid Commit", OptionFQDN: "FQDN", OptionRelayAgentInformation: "Relay Agent Information", OptionInternetStorageNameService: "Internet Storage Name Service", // Option 84 returned in RFC 3679 OptionNDSServers: "NDS Servers", OptionNDSTreeName: "NDS Tree Name", OptionNDSContext: "NDS Context", OptionBCMCSControllerDomainNameList: "BCMCS Controller Domain Name List", OptionBCMCSControllerIPv4AddressList: "BCMCS Controller IPv4 Address List", OptionAuthentication: "Authentication", OptionClientLastTransactionTime: "Client Last Transaction Time", OptionAssociatedIP: "Associated IP", OptionClientSystemArchitectureType: "Client System Architecture Type", OptionClientNetworkInterfaceIdentifier: "Client Network Interface Identifier", OptionLDAP: "LDAP", // Option 96 returned in RFC 3679 OptionClientMachineIdentifier: "Client Machine Identifier", OptionOpenGroupUserAuthentication: "OpenGroup's User Authentication", OptionGeoConfCivic: "GEOCONF_CIVIC", OptionIEEE10031TZString: "IEEE 1003.1 TZ String", OptionReferenceToTZDatabase: "Reference to the TZ Database", // Options 102-111 returned in RFC 3679 OptionNetInfoParentServerAddress: "NetInfo Parent Server Address", OptionNetInfoParentServerTag: "NetInfo Parent Server Tag", OptionURL: "URL", // Option 115 returned in RFC 3679 OptionAutoConfigure: "Auto-Configure", OptionNameServiceSearch: "Name Service Search", OptionSubnetSelection: "Subnet Selection", OptionDNSDomainSearchList: "DNS Domain Search List", OptionSIPServers: "SIP Servers", OptionClasslessStaticRoute: "Classless Static Route", OptionCCC: "CCC, CableLabs Client Configuration", OptionGeoConf: "GeoConf", OptionVendorIdentifyingVendorClass: "Vendor-Identifying Vendor Class", OptionVendorIdentifyingVendorSpecific: "Vendor-Identifying Vendor-Specific", // Options 126-127 returned in RFC 3679 OptionTFTPServerIPAddress: "TFTP Server IP Address", OptionCallServerIPAddress: "Call Server IP Address", OptionDiscriminationString: "Discrimination String", OptionRemoteStatisticsServerIPAddress: "RemoteStatistics Server IP Address", Option8021PVLANID: "802.1P VLAN ID", Option8021QL2Priority: "802.1Q L2 Priority", OptionDiffservCodePoint: "Diffserv Code Point", OptionHTTPProxyForPhoneSpecificApplications: "HTTP Proxy for phone-specific applications", OptionPANAAuthenticationAgent: "PANA Authentication Agent", OptionLoSTServer: "LoST Server", OptionCAPWAPAccessControllerAddresses: "CAPWAP Access Controller Addresses", OptionOPTIONIPv4AddressMoS: "OPTION-IPv4_Address-MoS", OptionOPTIONIPv4FQDNMoS: "OPTION-IPv4_FQDN-MoS", OptionSIPUAConfigurationServiceDomains: "SIP UA Configuration Service Domains", OptionOPTIONIPv4AddressANDSF: "OPTION-IPv4_Address-ANDSF", OptionOPTIONIPv6AddressANDSF: "OPTION-IPv6_Address-ANDSF", // Options 144-149 returned in RFC 3679 OptionTFTPServerAddress: "TFTP Server Address", OptionStatusCode: "Status Code", OptionBaseTime: "Base Time", OptionStartTimeOfState: "Start Time of State", OptionQueryStartTime: "Query Start Time", OptionQueryEndTime: "Query End Time", OptionDHCPState: "DHCP Staet", OptionDataSource: "Data Source", // Options 158-174 returned in RFC 3679 OptionEtherboot: "Etherboot", OptionIPTelephone: "IP Telephone", OptionEtherbootPacketCableAndCableHome: "Etherboot / PacketCable and CableHome", // Options 178-207 returned in RFC 3679 OptionPXELinuxMagicString: "PXELinux Magic String", OptionPXELinuxConfigFile: "PXELinux Config File", OptionPXELinuxPathPrefix: "PXELinux Path Prefix", OptionPXELinuxRebootTime: "PXELinux Reboot Time", OptionOPTION6RD: "OPTION_6RD", OptionOPTIONv4AccessDomain: "OPTION_V4_ACCESS_DOMAIN", // Options 214-219 returned in RFC 3679 OptionSubnetAllocation: "Subnet Allocation", OptionVirtualSubnetAllocation: "Virtual Subnet Selection", // Options 222-223 returned in RFC 3679 // Options 224-254 are reserved for private use OptionEnd: "End", } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/ztpv4/000077500000000000000000000000001431560352000244165ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/ztpv4/parse_circuitid.go000066400000000000000000000060701431560352000301210ustar00rootroot00000000000000package ztpv4 import ( "fmt" "regexp" "github.com/insomniacslk/dhcp/dhcpv4" ) // CircuitID represents the structure of network vendor interface formats type CircuitID struct { Slot string Module string Port string SubPort string Vlan string } var circuitRegexs = []*regexp.Regexp{ // Juniper QFX et-0/0/0:0.0 and xe-0/0/0:0.0 regexp.MustCompile("^(et|xe)-(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+):(?P[0-9]+).*$"), // Juniper PTX et-0/0/0.0 regexp.MustCompile("^et-(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+).(?P[0-9]+)$"), // Juniper EX ge-0/0/0.0 regexp.MustCompile("^ge-(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+).(?P[0-9]+).*"), // Arista Ethernet3/17/1 // Sometimes Arista prepend circuit id type(1 byte) and length(1 byte) not using ^ regexp.MustCompile("Ethernet(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)$"), // Juniper QFX et-1/0/61 regexp.MustCompile("^et-(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)$"), // Arista Ethernet14:Vlan2001 // Arista Ethernet10:2020 regexp.MustCompile("Ethernet(?P[0-9]+):(?P.*)$"), // Cisco Gi1/10:2020 regexp.MustCompile("^Gi(?P[0-9]+)/(?P[0-9]+):(?P.*)$"), // Nexus Ethernet1/3 regexp.MustCompile("^Ethernet(?P[0-9]+)/(?P[0-9]+)$"), // Juniper bundle interface ae52.0 regexp.MustCompile("^ae(?P[0-9]+).(?P[0-9])$"), // Ciena interface format regexp.MustCompile(`\.OSC(-[0-9]+)?-(?P[0-9]+)-(?P[0-9]+)$`), } // ParseCircuitID will parse dhcpv4 packet and return CircuitID info func ParseCircuitID(packet *dhcpv4.DHCPv4) (*CircuitID, error) { // CircuitId info is stored as sub-option field in RelayAgentInformationOption relayOptions := packet.RelayAgentInfo() if relayOptions == nil { return nil, fmt.Errorf("No relay agent information option found in the dhcpv4 pkt") } // As per RFC 3046 sub-Option 1 is circuit-id. Look at 2.0 section in that RFC // https://tools.ietf.org/html/rfc3046 circuitIdStr := string(relayOptions.Options.Get(dhcpv4.AgentCircuitIDSubOption)) if circuitIdStr == "" { return nil, fmt.Errorf("no circuit-id suboption found in dhcpv4 packet") } circuitId, err := matchCircuitID(circuitIdStr) if err != nil { return nil, err } return circuitId, nil } func matchCircuitID(circuitIdStr string) (*CircuitID, error) { for _, re := range circuitRegexs { match := re.FindStringSubmatch(circuitIdStr) if len(match) == 0 { continue } c := CircuitID{} for i, k := range re.SubexpNames() { switch k { case "slot": c.Slot = match[i] case "mod": c.Module = match[i] case "port": c.Port = match[i] case "subport": c.SubPort = match[i] case "vlan": c.Vlan = match[i] } } return &c, nil } return nil, fmt.Errorf("Unable to match circuit id : %s with listed regexes of interface types", circuitIdStr) } // FormatCircuitID is the CircuitID format we send in our Bootfile URL for ZTP devices func (c *CircuitID) FormatCircuitID() string { return fmt.Sprintf("%v,%v,%v,%v,%v", c.Slot, c.Module, c.Port, c.SubPort, c.Vlan) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/ztpv4/parse_circuitid_test.go000066400000000000000000000133171431560352000311620ustar00rootroot00000000000000package ztpv4 import ( "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/stretchr/testify/require" ) func TestMatchCircuitID(t *testing.T) { tt := []struct { name string circuit string want *CircuitID fail bool }{ {name: "Bogus string", circuit: "bogus_interface", fail: true, want: nil}, {name: "juniperQFX pattern et", circuit: "et-0/0/0:0.0", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, {name: "juniperQFX pattern xe", circuit: "xe-0/0/14:2", want: &CircuitID{Slot: "0", Module: "0", Port: "14", SubPort: "2"}}, {name: "juniperPTX pattern", circuit: "et-0/0/0.0", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, {name: "Arista pattern", circuit: "Ethernet3/17/1", want: &CircuitID{Slot: "3", Module: "17", Port: "1"}}, {name: "Juniper QFX pattern", circuit: "et-1/0/61", want: &CircuitID{Slot: "1", Module: "0", Port: "61"}}, {name: "Arista Vlan pattern 1", circuit: "Ethernet14:Vlan2001", want: &CircuitID{Port: "14", Vlan: "Vlan2001"}}, {name: "Arista Vlan pattern 2", circuit: "Ethernet10:2020", want: &CircuitID{Port: "10", Vlan: "2020"}}, {name: "Cisco pattern", circuit: "Gi1/10:2020", want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}}, {name: "Cisco Nexus pattern", circuit: "Ethernet1/3", want: &CircuitID{Slot: "1", Port: "3"}}, {name: "Juniper Bundle Pattern", circuit: "ae52.0", want: &CircuitID{Port: "52", SubPort: "0"}}, {name: "Juniper EX device pattern", circuit: "ge-0/0/0.0:RANDOMCHAR", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { circuit, err := matchCircuitID(tc.circuit) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if circuit != nil { require.Equal(t, tc.want, circuit, "comparing CircuitID") } else { require.Equal(t, tc.want, circuit, "comparing CircuitID") } }) } } func TestFormatCircuitID(t *testing.T) { tt := []struct { name string circuit *CircuitID want string }{ {name: "empty", circuit: &CircuitID{}, want: ",,,,"}, {name: "juniperQFX pattern et", circuit: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}, want: "0,0,0,0,"}, {name: "juniperQFX pattern xe", circuit: &CircuitID{Slot: "0", Module: "0", Port: "14", SubPort: "2"}, want: "0,0,14,2,"}, {name: "juniperPTX pattern", circuit: &CircuitID{Slot: "0", Module: "0", Port: "0"}, want: "0,0,0,,"}, {name: "Arista pattern", circuit: &CircuitID{Slot: "3", Module: "17", Port: "1"}, want: "3,17,1,,"}, {name: "Juniper QFX pattern", circuit: &CircuitID{Slot: "1", Module: "0", Port: "61"}, want: "1,0,61,,"}, {name: "Arista Vlan pattern 1", circuit: &CircuitID{Port: "14", Vlan: "Vlan2001"}, want: ",,14,,Vlan2001"}, {name: "Arista Vlan pattern 2", circuit: &CircuitID{Port: "10", Vlan: "2020"}, want: ",,10,,2020"}, {name: "Cisco Nexus pattern", circuit: &CircuitID{Slot: "1", Port: "3"}, want: "1,,3,,"}, {name: "Juniper Bundle Pattern", circuit: &CircuitID{Port: "52", SubPort: "0"}, want: ",,52,0,"}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { circuit := tc.circuit.FormatCircuitID() require.Equal(t, tc.want, circuit, "FormatCircuitID data") }) } } func TestParseCircuitID(t *testing.T) { tt := []struct { name string circuit []byte want *CircuitID fail bool }{ {name: "Bogus test", circuit: []byte("bogusInterface"), fail: true, want: nil}, {name: "juniperQFX pattern et", circuit: []byte("et-0/0/0:0.0"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, {name: "juniperQFX pattern xe", circuit: []byte("xe-0/0/14:2"), want: &CircuitID{Slot: "0", Module: "0", Port: "14", SubPort: "2"}}, {name: "juniperPTX pattern", circuit: []byte("et-0/0/0.0"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, {name: "Arista pattern", circuit: []byte("Ethernet3/17/1"), want: &CircuitID{Slot: "3", Module: "17", Port: "1"}}, {name: "Juniper QFX pattern", circuit: []byte("et-1/0/61"), want: &CircuitID{Slot: "1", Module: "0", Port: "61"}}, {name: "Arista Vlan pattern 1", circuit: []byte("Ethernet14:Vlan2001"), want: &CircuitID{Port: "14", Vlan: "Vlan2001"}}, {name: "Arista Vlan pattern 2", circuit: []byte("Ethernet10:2020"), want: &CircuitID{Port: "10", Vlan: "2020"}}, {name: "Cisco pattern", circuit: []byte("Gi1/10:2020"), want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}}, {name: "Cisco Nexus pattern", circuit: []byte("Ethernet1/3"), want: &CircuitID{Slot: "1", Port: "3"}}, {name: "Juniper Bundle Pattern", circuit: []byte("ae52.0"), want: &CircuitID{Port: "52", SubPort: "0"}}, {name: "Arista Vlan pattern 1 with circuitid type and length", circuit: []byte("\x00\x0fEthernet14:2001"), want: &CircuitID{Port: "14", Vlan: "2001"}}, {name: "juniperEX pattern", circuit: []byte("ge-0/0/0.0:RANDOMCHAR"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, {name: "Ciena pattern 1", circuit: []byte("tt-tt-tttt-6-7.OSC-1-2"), want: &CircuitID{Slot: "1", Port: "2"}}, {name: "Ciena pattern 2", circuit: []byte("tt-tt-tttt-6-7.OSC-1-2-3"), want: &CircuitID{Slot: "2", Port: "3"}}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { addOption := dhcpv4.WithOption(dhcpv4.OptRelayAgentInfo(dhcpv4.OptGeneric(dhcpv4.GenericOptionCode(1), tc.circuit))) packet, err := dhcpv4.New(addOption) if err != nil { t.Errorf("Unable to create dhcpv4 packet with circuiti") } c, err := ParseCircuitID(packet) if err != nil && !tc.fail { t.Errorf("Testcase Failed %v", err) } if c != nil { require.Equal(t, *tc.want, *c, "Comparing DHCPv4 Relay Agent Info CircuitId") } else { require.Equal(t, tc.want, c, "Comparing DHCPv4 Relay Agent Info CircuitId") } }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/ztpv4/ztp.go000066400000000000000000000100421431560352000255570ustar00rootroot00000000000000package ztpv4 import ( "bytes" "errors" "fmt" "strconv" "strings" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" ) // VendorData is optional data a particular vendor may or may not include // in the Vendor Class options. type VendorData struct { VendorName, Model, Serial string } var errVendorOptionMalformed = errors.New("malformed vendor option") var errClientIDOptionMissing = errors.New("client identifier option is missing") func parseClassIdentifier(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd := &VendorData{} switch vc := packet.ClassIdentifier(); { // Arista;DCS-7050S-64;01.23;JPE12221671 case strings.HasPrefix(vc, "Arista;"): p := strings.Split(vc, ";") if len(p) < 4 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[3] return vd, nil // ZPESystems:NSC:002251623 case strings.HasPrefix(vc, "ZPESystems:"): p := strings.Split(vc, ":") if len(p) < 3 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[2] return vd, nil // Juniper option 60 parsing is a bit more nuanced. The following are all // "valid" identifying strings for Juniper: // Juniper-ptx1000-DD576 --- (serial in hostname option) // Juniper-qfx10002-361-DN817 -- (model has a dash in it!) case strings.HasPrefix(vc, "Juniper-"): p := strings.Split(vc, "-") if len(p) < 3 { vd.Model = p[1] vd.Serial = packet.HostName() if len(vd.Serial) == 0 { return nil, errors.New("host name option is missing") } } else { vd.Model = strings.Join(p[1:len(p)-1], "-") vd.Serial = p[len(p)-1] } vd.VendorName = p[0] return vd, nil // For Ciena the class identifier (opt 60) is written in the following format: // {vendor iana code}-{product}-{type} // For Ciena the iana code is 1271 // The product type is a number that maps to a Ciena product // The type is used to identified different subtype of the product. // An example can be ‘1271-23422Z11-123’. case strings.HasPrefix(vc, strconv.Itoa(int(iana.EnterpriseIDCienaCorporation))): v := strings.Split(vc, "-") if len(v) != 3 { return nil, fmt.Errorf("%w got '%s'", errVendorOptionMalformed, vc) } vd.VendorName = iana.EnterpriseIDCienaCorporation.String() vd.Model = v[1] + "-" + v[2] vd.Serial = dhcpv4.GetString(dhcpv4.OptionClientIdentifier, packet.Options) if len(vd.Serial) == 0 { return nil, errClientIDOptionMissing } return vd, nil // Cisco Firepower FPR4100/9300 models use Opt 60 for model info // and Opt 61 contains the serial number case vc == "FPR4100" || vc == "FPR9300": vd.VendorName = iana.EnterpriseIDCiscoSystems.String() vd.Model = vc vd.Serial = dhcpv4.GetString(dhcpv4.OptionClientIdentifier, packet.Options) if len(vd.Serial) == 0 { return nil, errClientIDOptionMissing } return vd, nil } return nil, nil } func parseVIVC(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd := &VendorData{} for _, id := range packet.VIVC() { if id.EntID == iana.EnterpriseIDCiscoSystems { vd.VendorName = iana.EnterpriseIDCiscoSystems.String() //SN:0;PID:R-IOSXRV9000-CC for _, f := range bytes.Split(id.Data, []byte(";")) { p := bytes.Split(f, []byte(":")) if len(p) != 2 { return nil, errVendorOptionMalformed } switch string(p[0]) { case "SN": vd.Serial = string(p[1]) case "PID": vd.Model = string(p[1]) } } return vd, nil } } return nil, nil } // ParseVendorData will try to parse dhcp4 options looking for more // specific vendor data (like model, serial number, etc). func ParseVendorData(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd, err := parseClassIdentifier(packet) if err != nil { return nil, err } // If VendorData is set, return early if vd != nil { return vd, nil } vd, err = parseVIVC(packet) if err != nil { return nil, err } if vd != nil { return vd, nil } return nil, errors.New("no known ZTP vendor found") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv4/ztpv4/ztp_test.go000066400000000000000000000067251431560352000266330ustar00rootroot00000000000000package ztpv4 import ( "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseClassIdentifier(t *testing.T) { tt := []struct { name string vc, hostname string ci []byte // Client Identifier want *VendorData fail bool }{ {name: "empty", fail: true}, {name: "unknownVendor", vc: "VendorX;BFR10K;XX12345", fail: true}, {name: "truncatedVendor", vc: "Arista;1234", fail: true}, { name: "arista", vc: "Arista;DCS-7050S-64;01.23;JPE12345678", want: &VendorData{VendorName: "Arista", Model: "DCS-7050S-64", Serial: "JPE12345678"}, }, { name: "juniper", vc: "Juniper-ptx1000-DD123", want: &VendorData{VendorName: "Juniper", Model: "ptx1000", Serial: "DD123"}, }, { name: "juniperModelDash", vc: "Juniper-qfx10002-36q-DN817", want: &VendorData{VendorName: "Juniper", Model: "qfx10002-36q", Serial: "DN817"}, }, { name: "juniperHostnameSerial", vc: "Juniper-qfx10008", hostname: "DE123", want: &VendorData{VendorName: "Juniper", Model: "qfx10008", Serial: "DE123"}, }, {name: "juniperNoSerial", vc: "Juniper-qfx10008", fail: true}, { name: "zpe", vc: "ZPESystems:NSC:001234567", want: &VendorData{VendorName: "ZPESystems", Model: "NSC", Serial: "001234567"}, }, { name: "ciena", vc: "1271-00011E00-032", ci: []byte("JUSTASN"), want: &VendorData{VendorName: "Ciena Corporation", Model: "00011E00-032", Serial: "JUSTASN"}, }, {name: "cienaInvalidVendorClass", vc: "127100011E00032", fail: true}, {name: "cienaNoSerial", vc: "1271-00011E00-032", fail: true}, { name: "cisco", vc: "FPR4100", ci: []byte("JMX2525X0BW"), want: &VendorData{VendorName: "Cisco Systems", Model: "FPR4100", Serial: "JMX2525X0BW"}, }, {name: "ciscoNoSerial", vc: "FPR4100", fail: true}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { packet, err := dhcpv4.New() if err != nil { t.Fatalf("failed to creat dhcpv4 packet object: %v", err) } if tc.vc != "" { packet.UpdateOption(dhcpv4.OptClassIdentifier(tc.vc)) } if tc.hostname != "" { packet.UpdateOption(dhcpv4.OptHostName(tc.hostname)) } if tc.ci != nil { packet.UpdateOption(dhcpv4.OptClientIdentifier(tc.ci)) } vd, err := ParseVendorData(packet) if tc.fail { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.want, vd) } }) } } func TestParseVIVC(t *testing.T) { tt := []struct { name string vivc string entID iana.EnterpriseID want *VendorData fail bool }{ { name: "cisco", entID: iana.EnterpriseIDCiscoSystems, vivc: "SN:0;PID:R-IOSXRV9000-CC", want: &VendorData{VendorName: "Cisco Systems", Model: "R-IOSXRV9000-CC", Serial: "0"}, }, { name: "ciscoMultipleColonDelimiters", entID: iana.EnterpriseIDCiscoSystems, vivc: "SN:0:123;PID:R-IOSXRV9000-CC:456", fail: true, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { packet, err := dhcpv4.New() if err != nil { t.Fatalf("failed to creat dhcpv4 packet object: %v", err) } if tc.vivc != "" { vivc := dhcpv4.VIVCIdentifier{EntID: tc.entID, Data: []byte(tc.vivc)} packet.UpdateOption(dhcpv4.OptVIVC(vivc)) } vd, err := ParseVendorData(packet) if tc.fail { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.want, vd) } }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/000077500000000000000000000000001431560352000233315ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/async/000077500000000000000000000000001431560352000244465ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/async/client.go000066400000000000000000000127441431560352000262630ustar00rootroot00000000000000package async import ( "context" "fmt" "log" "net" "sync" "time" promise "github.com/fanliao/go-promise" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" ) // Client implements an asynchronous DHCPv6 client type Client struct { ReadTimeout time.Duration WriteTimeout time.Duration LocalAddr net.Addr RemoteAddr net.Addr IgnoreErrors bool connection *net.UDPConn cancel context.CancelFunc stopping *sync.WaitGroup receiveQueue chan dhcpv6.DHCPv6 sendQueue chan dhcpv6.DHCPv6 packetsLock sync.Mutex packets map[dhcpv6.TransactionID]*promise.Promise errors chan error } // NewClient creates an asynchronous client func NewClient() *Client { return &Client{ ReadTimeout: client6.DefaultReadTimeout, WriteTimeout: client6.DefaultWriteTimeout, } } // OpenForInterface starts the client on the specified interface, replacing // client LocalAddr with a link-local address of the given interface and // standard DHCP port (546). func (c *Client) OpenForInterface(ifname string, bufferSize int) error { addr, err := dhcpv6.GetLinkLocalAddr(ifname) if err != nil { return err } c.LocalAddr = &net.UDPAddr{IP: addr, Port: dhcpv6.DefaultClientPort, Zone: ifname} return c.Open(bufferSize) } // Open starts the client func (c *Client) Open(bufferSize int) error { var ( addr *net.UDPAddr ok bool err error ) if addr, ok = c.LocalAddr.(*net.UDPAddr); !ok { return fmt.Errorf("Invalid local address: %v not a net.UDPAddr", c.LocalAddr) } // prepare the socket to listen on for replies c.connection, err = net.ListenUDP("udp6", addr) if err != nil { return err } c.stopping = new(sync.WaitGroup) c.sendQueue = make(chan dhcpv6.DHCPv6, bufferSize) c.receiveQueue = make(chan dhcpv6.DHCPv6, bufferSize) c.packets = make(map[dhcpv6.TransactionID]*promise.Promise) c.packetsLock = sync.Mutex{} c.errors = make(chan error) var ctx context.Context ctx, c.cancel = context.WithCancel(context.Background()) go c.receiverLoop(ctx) go c.senderLoop(ctx) return nil } // Close stops the client func (c *Client) Close() { // Wait for sender and receiver loops c.stopping.Add(2) c.cancel() c.stopping.Wait() close(c.sendQueue) close(c.receiveQueue) close(c.errors) c.connection.Close() } // Errors returns a channel where runtime errors are posted func (c *Client) Errors() <-chan error { return c.errors } func (c *Client) addError(err error) { if !c.IgnoreErrors { c.errors <- err } } func (c *Client) receiverLoop(ctx context.Context) { defer func() { c.stopping.Done() }() for { select { case <-ctx.Done(): return case packet := <-c.receiveQueue: c.receive(packet) } } } func (c *Client) senderLoop(ctx context.Context) { defer func() { c.stopping.Done() }() for { select { case <-ctx.Done(): return case packet := <-c.sendQueue: c.send(packet) } } } func (c *Client) send(packet dhcpv6.DHCPv6) { transactionID, err := dhcpv6.GetTransactionID(packet) if err != nil { c.addError(fmt.Errorf("Warning: This should never happen, there is no transaction ID on %s", packet)) return } c.packetsLock.Lock() p := c.packets[transactionID] c.packetsLock.Unlock() raddr, err := c.remoteAddr() if err != nil { _ = p.Reject(err) log.Printf("Warning: cannot get remote address :%v", err) return } if err := c.connection.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { _ = p.Reject(err) log.Printf("Warning: cannot set write deadline :%v", err) return } _, err = c.connection.WriteTo(packet.ToBytes(), raddr) if err != nil { _ = p.Reject(err) log.Printf("Warning: cannot write to %s :%v", raddr, err) return } c.receiveQueue <- packet } func (c *Client) receive(_ dhcpv6.DHCPv6) { var ( oobdata = []byte{} received dhcpv6.DHCPv6 ) if err := c.connection.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { log.Printf("Warning: cannot set read deadline :%v", err) } for { buffer := make([]byte, client6.MaxUDPReceivedPacketSize) n, _, _, _, err := c.connection.ReadMsgUDP(buffer, oobdata) if err != nil { if err, ok := err.(net.Error); !ok || !err.Timeout() { c.addError(fmt.Errorf("Error receiving the message: %s", err)) } return } received, err = dhcpv6.FromBytes(buffer[:n]) if err != nil { // skip non-DHCP packets continue } break } transactionID, err := dhcpv6.GetTransactionID(received) if err != nil { c.addError(fmt.Errorf("Unable to get a transactionID for %s: %s", received, err)) return } c.packetsLock.Lock() if p, ok := c.packets[transactionID]; ok { delete(c.packets, transactionID) _ = p.Resolve(received) } c.packetsLock.Unlock() } func (c *Client) remoteAddr() (*net.UDPAddr, error) { if c.RemoteAddr == nil { return &net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort}, nil } if addr, ok := c.RemoteAddr.(*net.UDPAddr); ok { return addr, nil } return nil, fmt.Errorf("Invalid remote address: %v not a net.UDPAddr", c.RemoteAddr) } // Send inserts a message to the queue to be sent asynchronously. // Returns a future which resolves to response and error. func (c *Client) Send(message dhcpv6.DHCPv6, modifiers ...dhcpv6.Modifier) *promise.Future { for _, mod := range modifiers { mod(message) } transactionID, err := dhcpv6.GetTransactionID(message) if err != nil { return promise.Wrap(err) } p := promise.NewPromise() c.packetsLock.Lock() c.packets[transactionID] = p c.packetsLock.Unlock() c.sendQueue <- message return p.Future } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/async/client_test.go000066400000000000000000000070041431560352000273130ustar00rootroot00000000000000package async import ( "context" "net" "testing" "time" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" "github.com/stretchr/testify/require" ) const retries = 5 // solicit creates new solicit based on the mac address func solicit(input string) (*dhcpv6.Message, error) { mac, err := net.ParseMAC(input) if err != nil { return nil, err } return dhcpv6.NewSolicit(mac) } // server creates a server which responds with a predefined response func serve(ctx context.Context, addr *net.UDPAddr, response dhcpv6.DHCPv6) error { conn, err := net.ListenUDP("udp6", addr) if err != nil { return err } go func() { defer conn.Close() oobdata := []byte{} buffer := make([]byte, client6.MaxUDPReceivedPacketSize) for { select { case <-ctx.Done(): return default: if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { panic(err) } n, _, _, src, err := conn.ReadMsgUDP(buffer, oobdata) if err != nil { continue } _, err = dhcpv6.FromBytes(buffer[:n]) if err != nil { continue } if err := conn.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil { panic(err) } _, err = conn.WriteTo(response.ToBytes(), src) if err != nil { continue } } } }() return nil } func TestNewClient(t *testing.T) { c := NewClient() require.NotNil(t, c) require.Equal(t, c.ReadTimeout, client6.DefaultReadTimeout) require.Equal(t, c.ReadTimeout, client6.DefaultWriteTimeout) } func TestOpenInvalidAddrFailes(t *testing.T) { c := NewClient() err := c.Open(512) require.Error(t, err) } // This test uses port 15438 so please make sure its not used before running func TestOpenClose(t *testing.T) { c := NewClient() addr, err := net.ResolveUDPAddr("udp6", ":15438") require.NoError(t, err) c.LocalAddr = addr err = c.Open(512) require.NoError(t, err) defer c.Close() } // This test uses ports 15438 and 15439 so please make sure they are not used // before running func TestSendTimeout(t *testing.T) { c := NewClient() addr, err := net.ResolveUDPAddr("udp6", ":15438") require.NoError(t, err) remote, err := net.ResolveUDPAddr("udp6", ":15439") require.NoError(t, err) c.ReadTimeout = 50 * time.Millisecond c.WriteTimeout = 50 * time.Millisecond c.LocalAddr = addr c.RemoteAddr = remote err = c.Open(512) require.NoError(t, err) defer c.Close() m, err := dhcpv6.NewMessage() require.NoError(t, err) _, err, timeout := c.Send(m).GetOrTimeout(200) require.NoError(t, err) require.True(t, timeout) } // This test uses ports 15438 and 15439 so please make sure they are not used // before running func TestSend(t *testing.T) { s, err := solicit("c8:6c:2c:47:96:fd") require.NoError(t, err) require.NotNil(t, s) a, err := dhcpv6.NewAdvertiseFromSolicit(s) require.NoError(t, err) require.NotNil(t, a) c := NewClient() addr, err := net.ResolveUDPAddr("udp6", ":15438") require.NoError(t, err) remote, err := net.ResolveUDPAddr("udp6", ":15439") require.NoError(t, err) c.LocalAddr = addr c.RemoteAddr = remote ctx, cancel := context.WithCancel(context.Background()) defer cancel() err = serve(ctx, remote, a) require.NoError(t, err) err = c.Open(16) require.NoError(t, err) defer c.Close() f := c.Send(s) var passed bool for i := 0; i < retries; i++ { response, err, timeout := f.GetOrTimeout(1000) if timeout { continue } passed = true require.NoError(t, err) require.Equal(t, a, response) } require.True(t, passed, "All attempts to TestSend timed out") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/client6/000077500000000000000000000000001431560352000246755ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/client6/client.go000066400000000000000000000160511431560352000265050ustar00rootroot00000000000000package client6 import ( "errors" "fmt" "net" "time" "github.com/insomniacslk/dhcp/dhcpv6" ) // Client constants const ( DefaultWriteTimeout = 3 * time.Second // time to wait for write calls DefaultReadTimeout = 3 * time.Second // time to wait for read calls DefaultInterfaceUpTimeout = 3 * time.Second // time to wait before a network interface goes up MaxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb ) // Client implements a DHCPv6 client type Client struct { ReadTimeout time.Duration WriteTimeout time.Duration LocalAddr net.Addr RemoteAddr net.Addr SimulateRelay bool RelayOptions dhcpv6.Options // These options will be added to relay message if SimulateRelay is true } // NewClient returns a Client with default settings func NewClient() *Client { return &Client{ ReadTimeout: DefaultReadTimeout, WriteTimeout: DefaultWriteTimeout, } } // Exchange executes a 4-way DHCPv6 request (Solicit, Advertise, Request, // Reply). The modifiers will be applied to the Solicit and Request packets. // A common use is to make sure that the Solicit packet has the right options, // see modifiers.go func (c *Client) Exchange(ifname string, modifiers ...dhcpv6.Modifier) ([]dhcpv6.DHCPv6, error) { conversation := make([]dhcpv6.DHCPv6, 0) var err error // Solicit solicit, advertise, err := c.Solicit(ifname, modifiers...) if solicit != nil { conversation = append(conversation, solicit) } if err != nil { return conversation, err } conversation = append(conversation, advertise) // Decapsulate advertise if it's relayed before passing it to Request if advertise.IsRelay() { advertiseRelay := advertise.(*dhcpv6.RelayMessage) advertise, err = advertiseRelay.GetInnerMessage() if err != nil { return conversation, err } } request, reply, err := c.Request(ifname, advertise.(*dhcpv6.Message), modifiers...) if request != nil { conversation = append(conversation, request) } if err != nil { return conversation, err } conversation = append(conversation, reply) return conversation, nil } func (c *Client) sendReceive(ifname string, packet dhcpv6.DHCPv6, expectedType dhcpv6.MessageType) (dhcpv6.DHCPv6, error) { if packet == nil { return nil, fmt.Errorf("Packet to send cannot be nil") } // if no LocalAddr is specified, get the interface's link-local address var laddr net.UDPAddr if c.LocalAddr == nil { llAddr, err := dhcpv6.GetLinkLocalAddr(ifname) if err != nil { return nil, err } laddr = net.UDPAddr{IP: llAddr, Port: dhcpv6.DefaultClientPort, Zone: ifname} } else { if addr, ok := c.LocalAddr.(*net.UDPAddr); ok { laddr = *addr } else { return nil, fmt.Errorf("Invalid local address: not a net.UDPAddr: %v", c.LocalAddr) } } if c.SimulateRelay { var err error packet, err = dhcpv6.EncapsulateRelay(packet, dhcpv6.MessageTypeRelayForward, net.IPv6zero, laddr.IP) if err != nil { return nil, err } // Add Relay Options to ecapsulated Packet for _, opt := range c.RelayOptions { packet.UpdateOption(opt) } } if expectedType == dhcpv6.MessageTypeNone { // infer the expected type from the packet being sent if packet.Type() == dhcpv6.MessageTypeSolicit { expectedType = dhcpv6.MessageTypeAdvertise } else if packet.Type() == dhcpv6.MessageTypeRequest { expectedType = dhcpv6.MessageTypeReply } else if packet.Type() == dhcpv6.MessageTypeRelayForward { expectedType = dhcpv6.MessageTypeRelayReply } else if packet.Type() == dhcpv6.MessageTypeLeaseQuery { expectedType = dhcpv6.MessageTypeLeaseQueryReply } // and probably more } // if no RemoteAddr is specified, use AllDHCPRelayAgentsAndServers var raddr net.UDPAddr if c.RemoteAddr == nil { raddr = net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort} } else { if addr, ok := c.RemoteAddr.(*net.UDPAddr); ok { raddr = *addr } else { return nil, fmt.Errorf("Invalid remote address: not a net.UDPAddr: %v", c.RemoteAddr) } } // prepare the socket to listen on for replies conn, err := net.ListenUDP("udp6", &laddr) if err != nil { return nil, err } defer conn.Close() // wait for the listener to be ready, fail if it takes too much time deadline := time.Now().Add(time.Second) for { if now := time.Now(); now.After(deadline) { return nil, errors.New("Timed out waiting for listener to be ready") } if conn.LocalAddr() != nil { break } time.Sleep(10 * time.Millisecond) } // send the packet out if err := conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { return nil, err } _, err = conn.WriteTo(packet.ToBytes(), &raddr) if err != nil { return nil, err } // wait for a reply oobdata := []byte{} // ignoring oob data if err := conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return nil, err } var ( adv dhcpv6.DHCPv6 isMessage bool ) defer conn.Close() msg, ok := packet.(*dhcpv6.Message) if ok { isMessage = true } for { buf := make([]byte, MaxUDPReceivedPacketSize) n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata) if err != nil { return nil, err } adv, err = dhcpv6.FromBytes(buf[:n]) if err != nil { // skip non-DHCP packets // // TODO: It also skips DHCP packets with any errors (for example // if bootfile params are encoded incorrectly). We need to // log such cases instead of silently skip them. continue } if recvMsg, ok := adv.(*dhcpv6.Message); ok && isMessage { // if a regular message, check the transaction ID first // XXX should this unpack relay messages and check the XID of the // inner packet too? if msg.TransactionID != recvMsg.TransactionID { // different XID, we don't want this packet for sure continue } } if expectedType == dhcpv6.MessageTypeNone { // just take whatever arrived break } else if adv.Type() == expectedType { break } } return adv, nil } // Solicit sends a Solicit, returns the Solicit, an Advertise (if not nil), and // an error if any. The modifiers will be applied to the Solicit before sending // it, see modifiers.go func (c *Client) Solicit(ifname string, modifiers ...dhcpv6.Modifier) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, nil, err } solicit, err := dhcpv6.NewSolicit(iface.HardwareAddr) if err != nil { return nil, nil, err } for _, mod := range modifiers { mod(solicit) } advertise, err := c.sendReceive(ifname, solicit, dhcpv6.MessageTypeNone) return solicit, advertise, err } // Request sends a Request built from an Advertise. It returns the Request, a // Reply (if not nil), and an error if any. The modifiers will be applied to // the Request before sending it, see modifiers.go func (c *Client) Request(ifname string, advertise *dhcpv6.Message, modifiers ...dhcpv6.Modifier) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { request, err := dhcpv6.NewRequestFromAdvertise(advertise) if err != nil { return nil, nil, err } for _, mod := range modifiers { mod(request) } reply, err := c.sendReceive(ifname, request, dhcpv6.MessageTypeNone) return request, reply, err } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/client6/client_test.go000066400000000000000000000004111431560352000275350ustar00rootroot00000000000000package client6 import ( "testing" "github.com/stretchr/testify/require" ) func TestNewClient(t *testing.T) { c := NewClient() require.NotNil(t, c) require.Equal(t, DefaultReadTimeout, c.ReadTimeout) require.Equal(t, DefaultWriteTimeout, c.WriteTimeout) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/defaults.go000066400000000000000000000004021431560352000254630ustar00rootroot00000000000000package dhcpv6 import "net" // Default ports const ( DefaultClientPort = 546 DefaultServerPort = 547 ) // Default multicast groups var ( AllDHCPRelayAgentsAndServers = net.ParseIP("ff02::1:2") AllDHCPServers = net.ParseIP("ff05::1:3") ) golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6.go000066400000000000000000000117761431560352000250660ustar00rootroot00000000000000// Package dhcpv6 provides encoding and decoding of DHCPv6 messages and // options. package dhcpv6 import ( "fmt" "net" "github.com/u-root/uio/uio" ) type DHCPv6 interface { Type() MessageType ToBytes() []byte String() string Summary() string IsRelay() bool // GetInnerMessage returns the innermost encapsulated DHCPv6 message. // // If it is already a message, it will be returned. If it is a relay // message, the encapsulated message will be recursively extracted. GetInnerMessage() (*Message, error) GetOption(code OptionCode) []Option GetOneOption(code OptionCode) Option AddOption(Option) UpdateOption(Option) } // Modifier defines the signature for functions that can modify DHCPv6 // structures. This is used to simplify packet manipulation type Modifier func(d DHCPv6) // MessageFromBytes parses a DHCPv6 message from a byte stream. func MessageFromBytes(data []byte) (*Message, error) { buf := uio.NewBigEndianBuffer(data) messageType := MessageType(buf.Read8()) if messageType == MessageTypeRelayForward || messageType == MessageTypeRelayReply { return nil, fmt.Errorf("wrong message type") } d := &Message{ MessageType: messageType, } buf.ReadBytes(d.TransactionID[:]) if buf.Error() != nil { return nil, fmt.Errorf("Error parsing DHCPv6 header: %v", buf.Error()) } if err := d.Options.FromBytes(buf.Data()); err != nil { return nil, err } return d, nil } // RelayMessageFromBytes parses a relay message from a byte stream. func RelayMessageFromBytes(data []byte) (*RelayMessage, error) { buf := uio.NewBigEndianBuffer(data) messageType := MessageType(buf.Read8()) if messageType != MessageTypeRelayForward && messageType != MessageTypeRelayReply { return nil, fmt.Errorf("wrong message type") } d := &RelayMessage{ MessageType: messageType, HopCount: buf.Read8(), } d.LinkAddr = net.IP(buf.CopyN(net.IPv6len)) d.PeerAddr = net.IP(buf.CopyN(net.IPv6len)) if buf.Error() != nil { return nil, fmt.Errorf("Error parsing RelayMessage header: %v", buf.Error()) } // TODO: fail if no OptRelayMessage is present. if err := d.Options.FromBytes(buf.Data()); err != nil { return nil, err } return d, nil } // FromBytes reads a DHCPv6 message from a byte stream. func FromBytes(data []byte) (DHCPv6, error) { buf := uio.NewBigEndianBuffer(data) messageType := MessageType(buf.Read8()) if buf.Error() != nil { return nil, buf.Error() } if messageType == MessageTypeRelayForward || messageType == MessageTypeRelayReply { return RelayMessageFromBytes(data) } else { return MessageFromBytes(data) } } // NewMessage creates a new DHCPv6 message with default options func NewMessage(modifiers ...Modifier) (*Message, error) { tid, err := GenerateTransactionID() if err != nil { return nil, err } msg := &Message{ MessageType: MessageTypeSolicit, TransactionID: tid, } // apply modifiers for _, mod := range modifiers { mod(msg) } return msg, nil } // DecapsulateRelay extracts the content of a relay message. It does not recurse // if there are nested relay messages. Returns the original packet if is not not // a relay message func DecapsulateRelay(l DHCPv6) (DHCPv6, error) { if !l.IsRelay() { return l, nil } if rm := l.(*RelayMessage).Options.RelayMessage(); rm != nil { return rm, nil } return nil, fmt.Errorf("malformed Relay message: no embedded message found") } // DecapsulateRelayIndex extracts the content of a relay message. It takes an // integer as index (e.g. if 0 return the outermost relay, 1 returns the // second, etc, and -1 returns the last). Returns the original packet if // it is not not a relay message. func DecapsulateRelayIndex(l DHCPv6, index int) (DHCPv6, error) { if !l.IsRelay() { return l, nil } if index < -1 { return nil, fmt.Errorf("Invalid index: %d", index) } else if index == -1 { for { d, err := DecapsulateRelay(l) if err != nil { return nil, err } if !d.IsRelay() { return l, nil } l = d } } for i := 0; i <= index; i++ { d, err := DecapsulateRelay(l) if err != nil { return nil, err } l = d } return l, nil } // EncapsulateRelay creates a RelayMessage message containing the passed DHCPv6 // message as payload. The passed message type must be either RELAY_FORW or // RELAY_REPL func EncapsulateRelay(d DHCPv6, mType MessageType, linkAddr, peerAddr net.IP) (*RelayMessage, error) { if mType != MessageTypeRelayForward && mType != MessageTypeRelayReply { return nil, fmt.Errorf("Message type must be either RELAY_FORW or RELAY_REPL") } outer := RelayMessage{ MessageType: mType, LinkAddr: linkAddr, PeerAddr: peerAddr, } if d.IsRelay() { relay := d.(*RelayMessage) outer.HopCount = relay.HopCount + 1 } else { outer.HopCount = 0 } outer.AddOption(OptRelayMessage(d)) return &outer, nil } // GetTransactionID returns a transactionID of a message or its inner message // in case of relay func GetTransactionID(packet DHCPv6) (TransactionID, error) { m, err := packet.GetInnerMessage() if err != nil { return TransactionID{0, 0, 0}, err } return m.TransactionID, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6_test.go000066400000000000000000000172761431560352000261260ustar00rootroot00000000000000package dhcpv6 import ( "encoding/binary" "errors" "net" "strconv" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/u-root/uio/rand" ) func randomReadMock(value []byte, n int, err error) func([]byte) (int, error) { return func(b []byte) (int, error) { copy(b, value) return n, err } } type GenerateTransactionIDTestSuite struct { suite.Suite random []byte } func (s *GenerateTransactionIDTestSuite) SetupTest() { s.random = make([]byte, 16) } func (s *GenerateTransactionIDTestSuite) TearDown() { randomRead = rand.Read } func (s *GenerateTransactionIDTestSuite) TestErrors() { // Error is returned from random number generator e := errors.New("mocked error") randomRead = randomReadMock(s.random, 0, e) _, err := GenerateTransactionID() s.Assert().Equal(e, err) // Less than 4 bytes are generated randomRead = randomReadMock(s.random, 2, nil) _, err = GenerateTransactionID() s.Assert().EqualError(err, "invalid random sequence: shorter than 3 bytes") } func (s *GenerateTransactionIDTestSuite) TestSuccess() { binary.BigEndian.PutUint32(s.random, 0x01020300) randomRead = randomReadMock(s.random, 3, nil) tid, err := GenerateTransactionID() s.Require().NoError(err) s.Assert().Equal(TransactionID{0x1, 0x2, 0x3}, tid) } func TestGenerateTransactionIDTestSuite(t *testing.T) { suite.Run(t, new(GenerateTransactionIDTestSuite)) } func TestNewMessage(t *testing.T) { d, err := NewMessage() require.NoError(t, err) require.NotNil(t, d) require.Equal(t, MessageTypeSolicit, d.Type()) require.NotEqual(t, 0, d.TransactionID) require.Empty(t, d.Options) } func TestDecapsulateRelayIndex(t *testing.T) { m := Message{} r1, err := EncapsulateRelay(&m, MessageTypeRelayForward, net.IPv6linklocalallnodes, net.IPv6interfacelocalallnodes) require.NoError(t, err) r2, err := EncapsulateRelay(r1, MessageTypeRelayForward, net.IPv6loopback, net.IPv6linklocalallnodes) require.NoError(t, err) r3, err := EncapsulateRelay(r2, MessageTypeRelayForward, net.IPv6unspecified, net.IPv6linklocalallrouters) require.NoError(t, err) first, err := DecapsulateRelayIndex(r3, 0) require.NoError(t, err) relay, ok := first.(*RelayMessage) require.True(t, ok) require.Equal(t, relay.HopCount, uint8(1)) require.Equal(t, relay.LinkAddr, net.IPv6loopback) require.Equal(t, relay.PeerAddr, net.IPv6linklocalallnodes) second, err := DecapsulateRelayIndex(r3, 1) require.NoError(t, err) relay, ok = second.(*RelayMessage) require.True(t, ok) require.Equal(t, relay.HopCount, uint8(0)) require.Equal(t, relay.LinkAddr, net.IPv6linklocalallnodes) require.Equal(t, relay.PeerAddr, net.IPv6interfacelocalallnodes) third, err := DecapsulateRelayIndex(r3, 2) require.NoError(t, err) _, ok = third.(*Message) require.True(t, ok) rfirst, err := DecapsulateRelayIndex(r3, -1) require.NoError(t, err) relay, ok = rfirst.(*RelayMessage) require.True(t, ok) require.Equal(t, relay.HopCount, uint8(0)) require.Equal(t, relay.LinkAddr, net.IPv6linklocalallnodes) require.Equal(t, relay.PeerAddr, net.IPv6interfacelocalallnodes) _, err = DecapsulateRelayIndex(r3, -2) require.Error(t, err) } func TestAddOption(t *testing.T) { d := Message{} require.Empty(t, d.Options) opt := OptionGeneric{OptionCode: 0, OptionData: []byte{}} d.AddOption(&opt) require.Equal(t, Options{&opt}, d.Options.Options) } func TestToBytes(t *testing.T) { d := Message{ MessageType: MessageTypeSolicit, TransactionID: TransactionID{0xa, 0xb, 0xc}, } d.AddOption(&OptionGeneric{OptionCode: 0, OptionData: []byte{}}) bytes := d.ToBytes() expected := []byte{01, 0xa, 0xb, 0xc, 0x00, 0x00, 0x00, 0x00} require.Equal(t, expected, bytes) } func TestFromAndToBytes(t *testing.T) { expected := [][]byte{ {01, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00}, []byte("0000\x00\x01\x00\x0e\x00\x01000000000000"), } t.Parallel() for i, packet := range expected { t.Run(strconv.Itoa(i), func(t *testing.T) { d, err := FromBytes(packet) require.NoError(t, err) toBytes := d.ToBytes() require.Equal(t, packet, toBytes) }) } } func TestFromBytesInvalid(t *testing.T) { expected := [][]byte{ {}, {30}, {12}, } t.Parallel() for i, packet := range expected { t.Run(strconv.Itoa(i), func(t *testing.T) { _, err := FromBytes(packet) require.Error(t, err) }) } } func TestNewAdvertiseFromSolicit(t *testing.T) { s := Message{ MessageType: MessageTypeSolicit, TransactionID: TransactionID{0xa, 0xb, 0xc}, } s.AddOption(OptClientID(Duid{})) a, err := NewAdvertiseFromSolicit(&s, WithServerID(Duid{})) require.NoError(t, err) require.Equal(t, a.TransactionID, s.TransactionID) require.Equal(t, a.Type(), MessageTypeAdvertise) } func TestNewReplyFromMessage(t *testing.T) { msg := Message{ TransactionID: TransactionID{0xa, 0xb, 0xc}, MessageType: MessageTypeConfirm, } var duid Duid msg.AddOption(OptClientID(duid)) msg.AddOption(OptServerID(duid)) rep, err := NewReplyFromMessage(&msg, WithServerID(duid)) require.NoError(t, err) require.Equal(t, rep.TransactionID, msg.TransactionID) require.Equal(t, rep.Type(), MessageTypeReply) msg.MessageType = MessageTypeRenew rep, err = NewReplyFromMessage(&msg, WithServerID(duid)) require.NoError(t, err) require.Equal(t, rep.TransactionID, msg.TransactionID) require.Equal(t, rep.Type(), MessageTypeReply) msg.MessageType = MessageTypeRebind rep, err = NewReplyFromMessage(&msg, WithServerID(duid)) require.NoError(t, err) require.Equal(t, rep.TransactionID, msg.TransactionID) require.Equal(t, rep.Type(), MessageTypeReply) msg.MessageType = MessageTypeRelease rep, err = NewReplyFromMessage(&msg, WithServerID(duid)) require.NoError(t, err) require.Equal(t, rep.TransactionID, msg.TransactionID) require.Equal(t, rep.Type(), MessageTypeReply) msg.MessageType = MessageTypeInformationRequest rep, err = NewReplyFromMessage(&msg, WithServerID(duid)) require.NoError(t, err) require.Equal(t, rep.TransactionID, msg.TransactionID) require.Equal(t, rep.Type(), MessageTypeReply) msg.MessageType = MessageTypeSolicit _, err = NewReplyFromMessage(&msg) require.Error(t, err) msg.MessageType = MessageTypeSolicit msg.AddOption(&OptionGeneric{OptionCode: OptionRapidCommit}) _, err = NewReplyFromMessage(&msg) require.NoError(t, err) msg.Options.Del(OptionRapidCommit) } func TestNewMessageTypeSolicit(t *testing.T) { hwAddr, err := net.ParseMAC("24:0A:9E:9F:EB:2B") require.NoError(t, err) duid := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: hwAddr, } s, err := NewSolicit(hwAddr, WithClientID(duid)) require.NoError(t, err) require.Equal(t, s.Type(), MessageTypeSolicit) // Check CID cduid := s.Options.ClientID() require.NotNil(t, cduid) require.Equal(t, cduid, &duid) // Check ORO oro := s.Options.RequestedOptions() require.Contains(t, oro, OptionDNSRecursiveNameServer) require.Contains(t, oro, OptionDomainSearchList) require.Equal(t, len(oro), 2) // Check IA_NA iaid := [4]byte{hwAddr[2], hwAddr[3], hwAddr[4], hwAddr[5]} iana := s.Options.OneIANA() require.NotNil(t, iana) require.Equal(t, iaid, iana.IaId) } func TestGetTransactionIDMessage(t *testing.T) { message, err := NewMessage() require.NoError(t, err) transactionID, err := GetTransactionID(message) require.NoError(t, err) require.Equal(t, transactionID, message.TransactionID) } func TestGetTransactionIDRelay(t *testing.T) { message, err := NewMessage() require.NoError(t, err) relay, err := EncapsulateRelay(message, MessageTypeRelayForward, nil, nil) require.NoError(t, err) transactionID, err := GetTransactionID(relay) require.NoError(t, err) require.Equal(t, transactionID, message.TransactionID) } // TODO test NewMessageTypeSolicit // test String and Summary golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6message.go000066400000000000000000000362671431560352000264350ustar00rootroot00000000000000package dhcpv6 import ( "errors" "fmt" "net" "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/rand" "github.com/u-root/uio/uio" ) const MessageHeaderSize = 4 // MessageOptions are the options that may appear in a normal DHCPv6 message. // // RFC 3315 Appendix B lists the valid options that can be used. type MessageOptions struct { Options } // ArchTypes returns the architecture type option. func (mo MessageOptions) ArchTypes() iana.Archs { opt := mo.GetOne(OptionClientArchType) if opt == nil { return nil } return opt.(*optClientArchType).Archs } // ClientID returns the client identifier option. func (mo MessageOptions) ClientID() *Duid { opt := mo.GetOne(OptionClientID) if opt == nil { return nil } return &opt.(*optClientID).Duid } // ServerID returns the server identifier option. func (mo MessageOptions) ServerID() *Duid { opt := mo.GetOne(OptionServerID) if opt == nil { return nil } return &opt.(*optServerID).Duid } // IANA returns all Identity Association for Non-temporary Address options. func (mo MessageOptions) IANA() []*OptIANA { opts := mo.Get(OptionIANA) var ianas []*OptIANA for _, o := range opts { ianas = append(ianas, o.(*OptIANA)) } return ianas } // OneIANA returns the first IANA option. func (mo MessageOptions) OneIANA() *OptIANA { ianas := mo.IANA() if len(ianas) == 0 { return nil } return ianas[0] } // IATA returns all Identity Association for Temporary Address options. func (mo MessageOptions) IATA() []*OptIATA { opts := mo.Get(OptionIATA) var iatas []*OptIATA for _, o := range opts { iatas = append(iatas, o.(*OptIATA)) } return iatas } // OneIATA returns the first IATA option. func (mo MessageOptions) OneIATA() *OptIATA { iatas := mo.IATA() if len(iatas) == 0 { return nil } return iatas[0] } // IAPD returns all Identity Association for Prefix Delegation options. func (mo MessageOptions) IAPD() []*OptIAPD { opts := mo.Get(OptionIAPD) var ianas []*OptIAPD for _, o := range opts { ianas = append(ianas, o.(*OptIAPD)) } return ianas } // OneIAPD returns the first IAPD option. func (mo MessageOptions) OneIAPD() *OptIAPD { iapds := mo.IAPD() if len(iapds) == 0 { return nil } return iapds[0] } // Status returns the status code associated with this option. func (mo MessageOptions) Status() *OptStatusCode { opt := mo.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // RequestedOptions returns the Options Requested Option. func (mo MessageOptions) RequestedOptions() OptionCodes { // Technically, RFC 8415 states that ORO may only appear once in the // area of a DHCP message. However, some proprietary clients have been // observed sending more than one OptionORO. // // So we merge them. opt := mo.Options.Get(OptionORO) if len(opt) == 0 { return nil } var oc OptionCodes for _, o := range opt { if oro, ok := o.(*optRequestedOption); ok { oc = append(oc, oro.OptionCodes...) } } return oc } // DNS returns the DNS Recursive Name Server option as defined by RFC 3646. func (mo MessageOptions) DNS() []net.IP { opt := mo.Options.GetOne(OptionDNSRecursiveNameServer) if opt == nil { return nil } if dns, ok := opt.(*optDNS); ok { return dns.NameServers } return nil } // DomainSearchList returns the Domain List option as defined by RFC 3646. func (mo MessageOptions) DomainSearchList() *rfc1035label.Labels { opt := mo.Options.GetOne(OptionDomainSearchList) if opt == nil { return nil } if dsl, ok := opt.(*optDomainSearchList); ok { return dsl.DomainSearchList } return nil } // BootFileURL returns the Boot File URL option as defined by RFC 5970. func (mo MessageOptions) BootFileURL() string { opt := mo.Options.GetOne(OptionBootfileURL) if opt == nil { return "" } if u, ok := opt.(optBootFileURL); ok { return string(u) } return "" } // BootFileParam returns the Boot File Param option as defined by RFC 5970. func (mo MessageOptions) BootFileParam() []string { opt := mo.Options.GetOne(OptionBootfileParam) if opt == nil { return nil } if u, ok := opt.(optBootFileParam); ok { return []string(u) } return nil } // UserClasses returns a list of user classes. func (mo MessageOptions) UserClasses() [][]byte { opt := mo.Options.GetOne(OptionUserClass) if opt == nil { return nil } if t, ok := opt.(*OptUserClass); ok { return t.UserClasses } return nil } // VendorOpts returns the all vendor-specific options. // // RFC 8415 Section 21.17: // // Multiple instances of the Vendor-specific Information option may appear in // a DHCP message. func (mo MessageOptions) VendorOpts() []*OptVendorOpts { opt := mo.Options.Get(OptionVendorOpts) if opt == nil { return nil } var vo []*OptVendorOpts for _, o := range opt { if t, ok := o.(*OptVendorOpts); ok { vo = append(vo, t) } } return vo } // VendorOpt returns the vendor options matching the given enterprise number. // // RFC 8415 Section 21.17: // // Servers and clients MUST NOT send more than one instance of the // Vendor-specific Information option with the same Enterprise Number. func (mo MessageOptions) VendorOpt(enterpriseNumber uint32) Options { vo := mo.VendorOpts() for _, v := range vo { if v.EnterpriseNumber == enterpriseNumber { return v.VendorOpts } } return nil } // ElapsedTime returns the Elapsed Time option as defined by RFC 3315 Section 22.9. // // ElapsedTime returns a duration of 0 if the option is not present. func (mo MessageOptions) ElapsedTime() time.Duration { opt := mo.Options.GetOne(OptionElapsedTime) if opt == nil { return 0 } if t, ok := opt.(*optElapsedTime); ok { return t.ElapsedTime } return 0 } // InformationRefreshTime returns the Information Refresh Time option // as defined by RFC 815 Section 21.23. // // InformationRefreshTime returns the provided default if no option is present. func (mo MessageOptions) InformationRefreshTime(def time.Duration) time.Duration { opt := mo.Options.GetOne(OptionInformationRefreshTime) if opt == nil { return def } if t, ok := opt.(*optInformationRefreshTime); ok { return t.InformationRefreshtime } return def } // FQDN returns the FQDN option as defined by RFC 4704. func (mo MessageOptions) FQDN() *OptFQDN { opt := mo.Options.GetOne(OptionFQDN) if opt == nil { return nil } if fqdn, ok := opt.(*OptFQDN); ok { return fqdn } return nil } // DHCP4oDHCP6Server returns the DHCP 4o6 Server Address option as // defined by RFC 7341. func (mo MessageOptions) DHCP4oDHCP6Server() *OptDHCP4oDHCP6Server { opt := mo.Options.GetOne(OptionDHCP4oDHCP6Server) if opt == nil { return nil } if server, ok := opt.(*OptDHCP4oDHCP6Server); ok { return server } return nil } // NTPServers returns the NTP server addresses contained in the // NTP_SUBOPTION_SRV_ADDR of an OPTION_NTP_SERVER. // If multiple NTP server options exist, the function will return all the NTP // server addresses it finds, as defined by RFC 5908. func (mo MessageOptions) NTPServers() []net.IP { opts := mo.Options.Get(OptionNTPServer) if opts == nil { return nil } addrs := make([]net.IP, 0) for _, opt := range opts { ntp, ok := opt.(*OptNTPServer) if !ok { continue } for _, subopt := range ntp.Suboptions { so, ok := subopt.(*NTPSuboptionSrvAddr) if !ok { continue } addrs = append(addrs, net.IP(*so)) } } return addrs } // Message represents a DHCPv6 Message as defined by RFC 3315 Section 6. type Message struct { MessageType MessageType TransactionID TransactionID Options MessageOptions } var randomRead = rand.Read // GenerateTransactionID generates a random 3-byte transaction ID. func GenerateTransactionID() (TransactionID, error) { var tid TransactionID n, err := randomRead(tid[:]) if err != nil { return tid, err } if n != len(tid) { return tid, fmt.Errorf("invalid random sequence: shorter than 3 bytes") } return tid, nil } // GetTime returns a time integer suitable for DUID-LLT, i.e. the current time counted // in seconds since January 1st, 2000, midnight UTC, modulo 2^32 func GetTime() uint32 { now := time.Since(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) return uint32((now.Nanoseconds() / 1000000000) % 0xffffffff) } // NewSolicit creates a new SOLICIT message, using the given hardware address to // derive the IAID in the IA_NA option. func NewSolicit(hwaddr net.HardwareAddr, modifiers ...Modifier) (*Message, error) { duid := Duid{ Type: DUID_LLT, HwType: iana.HWTypeEthernet, Time: GetTime(), LinkLayerAddr: hwaddr, } m, err := NewMessage() if err != nil { return nil, err } m.MessageType = MessageTypeSolicit m.AddOption(OptClientID(duid)) m.AddOption(OptRequestedOption( OptionDNSRecursiveNameServer, OptionDomainSearchList, )) m.AddOption(OptElapsedTime(0)) if len(hwaddr) < 4 { return nil, errors.New("short hardware addrss: less than 4 bytes") } l := len(hwaddr) var iaid [4]byte copy(iaid[:], hwaddr[l-4:l]) modifiers = append([]Modifier{WithIAID(iaid)}, modifiers...) // Apply modifiers for _, mod := range modifiers { mod(m) } return m, nil } // NewAdvertiseFromSolicit creates a new ADVERTISE packet based on an SOLICIT packet. func NewAdvertiseFromSolicit(sol *Message, modifiers ...Modifier) (*Message, error) { if sol == nil { return nil, errors.New("SOLICIT cannot be nil") } if sol.Type() != MessageTypeSolicit { return nil, errors.New("The passed SOLICIT must have SOLICIT type set") } // build ADVERTISE from SOLICIT adv := &Message{ MessageType: MessageTypeAdvertise, TransactionID: sol.TransactionID, } // add Client ID cid := sol.GetOneOption(OptionClientID) if cid == nil { return nil, errors.New("Client ID cannot be nil in SOLICIT when building ADVERTISE") } adv.AddOption(cid) // apply modifiers for _, mod := range modifiers { mod(adv) } return adv, nil } // NewRequestFromAdvertise creates a new REQUEST packet based on an ADVERTISE // packet options. func NewRequestFromAdvertise(adv *Message, modifiers ...Modifier) (*Message, error) { if adv == nil { return nil, errors.New("ADVERTISE cannot be nil") } if adv.MessageType != MessageTypeAdvertise { return nil, fmt.Errorf("The passed ADVERTISE must have ADVERTISE type set") } // build REQUEST from ADVERTISE req, err := NewMessage() if err != nil { return nil, err } req.MessageType = MessageTypeRequest // add Client ID cid := adv.GetOneOption(OptionClientID) if cid == nil { return nil, fmt.Errorf("Client ID cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(cid) // add Server ID sid := adv.GetOneOption(OptionServerID) if sid == nil { return nil, fmt.Errorf("Server ID cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(sid) // add Elapsed Time req.AddOption(OptElapsedTime(0)) // add IA_NA iana := adv.Options.OneIANA() if iana == nil { return nil, fmt.Errorf("IA_NA cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(iana) // add IA_PD if iaPd := adv.GetOneOption(OptionIAPD); iaPd != nil { req.AddOption(iaPd) } req.AddOption(OptRequestedOption( OptionDNSRecursiveNameServer, OptionDomainSearchList, )) // add OPTION_VENDOR_CLASS, only if present in the original request // TODO implement OptionVendorClass vClass := adv.GetOneOption(OptionVendorClass) if vClass != nil { req.AddOption(vClass) } // apply modifiers for _, mod := range modifiers { mod(req) } return req, nil } // NewReplyFromMessage creates a new REPLY packet based on a // Message. The function is to be used when generating a reply to a SOLICIT with // rapid-commit, REQUEST, CONFIRM, RENEW, REBIND, RELEASE and INFORMATION-REQUEST // packets. func NewReplyFromMessage(msg *Message, modifiers ...Modifier) (*Message, error) { if msg == nil { return nil, errors.New("message cannot be nil") } switch msg.Type() { case MessageTypeSolicit: if msg.GetOneOption(OptionRapidCommit) == nil { return nil, errors.New("cannot create REPLY from a SOLICIT without rapid-commit option") } modifiers = append([]Modifier{WithRapidCommit}, modifiers...) case MessageTypeRequest, MessageTypeConfirm, MessageTypeRenew, MessageTypeRebind, MessageTypeRelease, MessageTypeInformationRequest: default: return nil, errors.New("cannot create REPLY from the passed message type set") } // build REPLY from MESSAGE rep := &Message{ MessageType: MessageTypeReply, TransactionID: msg.TransactionID, } // add Client ID cid := msg.GetOneOption(OptionClientID) if cid == nil { return nil, errors.New("Client ID cannot be nil when building REPLY") } rep.AddOption(cid) // apply modifiers for _, mod := range modifiers { mod(rep) } return rep, nil } // Type returns this message's message type. func (m Message) Type() MessageType { return m.MessageType } // GetInnerMessage returns the message itself. func (m *Message) GetInnerMessage() (*Message, error) { return m, nil } // AddOption adds an option to this message. func (m *Message) AddOption(option Option) { m.Options.Add(option) } // UpdateOption updates the existing options with the passed option, adding it // at the end if not present already func (m *Message) UpdateOption(option Option) { m.Options.Update(option) } // IsNetboot returns true if the machine is trying to netboot. It checks if // "boot file" is one of the requested options, which is useful for // SOLICIT/REQUEST packet types, it also checks if the "boot file" option is // included in the packet, which is useful for ADVERTISE/REPLY packet. func (m *Message) IsNetboot() bool { if m.IsOptionRequested(OptionBootfileURL) { return true } if optbf := m.GetOneOption(OptionBootfileURL); optbf != nil { return true } return false } // IsOptionRequested takes an OptionCode and returns true if that option is // within the requested options of the DHCPv6 message. func (m *Message) IsOptionRequested(requested OptionCode) bool { return m.Options.RequestedOptions().Contains(requested) } // String returns a short human-readable string for this message. func (m *Message) String() string { return fmt.Sprintf("Message(messageType=%s transactionID=%s, %d options)", m.MessageType, m.TransactionID, len(m.Options.Options)) } // Summary prints all options associated with this message. func (m *Message) Summary() string { ret := fmt.Sprintf( "Message\n"+ " messageType=%s\n"+ " transactionid=%s\n", m.MessageType, m.TransactionID, ) ret += " options=[" if len(m.Options.Options) > 0 { ret += "\n" } for _, opt := range m.Options.Options { ret += fmt.Sprintf(" %v\n", opt.String()) } ret += " ]\n" return ret } // ToBytes returns the serialized version of this message as defined by RFC // 3315, Section 5. func (m *Message) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write8(uint8(m.MessageType)) buf.WriteBytes(m.TransactionID[:]) buf.WriteBytes(m.Options.ToBytes()) return buf.Data() } // GetOption returns the options associated with the code. func (m *Message) GetOption(code OptionCode) []Option { return m.Options.Get(code) } // GetOneOption returns the first associated option with the code from this // message. func (m *Message) GetOneOption(code OptionCode) Option { return m.Options.GetOne(code) } // IsRelay returns whether this is a relay message or not. func (m *Message) IsRelay() bool { return false } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6message_test.go000066400000000000000000000012521431560352000274560ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestIsNetboot(t *testing.T) { msg1 := Message{} require.False(t, msg1.IsNetboot()) msg2 := Message{} msg2.AddOption(OptRequestedOption(OptionBootfileURL)) require.True(t, msg2.IsNetboot()) msg3 := Message{} optbf := OptBootFileURL("") msg3.AddOption(optbf) require.True(t, msg3.IsNetboot()) } func TestIsOptionRequested(t *testing.T) { msg1 := Message{} require.False(t, msg1.IsOptionRequested(OptionDNSRecursiveNameServer)) msg2 := Message{} msg2.AddOption(OptRequestedOption(OptionDNSRecursiveNameServer)) require.True(t, msg2.IsOptionRequested(OptionDNSRecursiveNameServer)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6relay.go000066400000000000000000000131041431560352000261060ustar00rootroot00000000000000package dhcpv6 import ( "errors" "fmt" "net" "github.com/insomniacslk/dhcp/iana" "github.com/u-root/uio/uio" ) const RelayHeaderSize = 34 // RelayOptions are the options valid for RelayForw and RelayRepl messages. // // RFC 3315 Appendix B defines them to be InterfaceID and RelayMsg options; RFC // 4649 also adds the RemoteID option. type RelayOptions struct { Options } // RelayMessage returns the message embedded. func (ro RelayOptions) RelayMessage() DHCPv6 { opt := ro.Options.GetOne(OptionRelayMsg) if opt == nil { return nil } if relayOpt, ok := opt.(*optRelayMsg); ok { return relayOpt.Msg } return nil } // InterfaceID returns the interface ID of this relay message. func (ro RelayOptions) InterfaceID() []byte { opt := ro.Options.GetOne(OptionInterfaceID) if opt == nil { return nil } if iid, ok := opt.(*optInterfaceID); ok { return iid.ID } return nil } // RemoteID returns the remote ID in this relay message. func (ro RelayOptions) RemoteID() *OptRemoteID { opt := ro.Options.GetOne(OptionRemoteID) if opt == nil { return nil } if rid, ok := opt.(*OptRemoteID); ok { return rid } return nil } // ClientLinkLayerAddress returns the Hardware Type and // Link Layer Address of the requesting client in this relay message. func (ro RelayOptions) ClientLinkLayerAddress() (iana.HWType, net.HardwareAddr) { opt := ro.Options.GetOne(OptionClientLinkLayerAddr) if opt == nil { return 0, nil } if lla, ok := opt.(*optClientLinkLayerAddress); ok { return lla.LinkLayerType, lla.LinkLayerAddress } return 0, nil } // RelayMessage is a DHCPv6 relay agent message as defined by RFC 3315 Section // 7. type RelayMessage struct { MessageType MessageType HopCount uint8 LinkAddr net.IP PeerAddr net.IP Options RelayOptions } func write16(b *uio.Lexer, ip net.IP) { if ip == nil || ip.To16() == nil { var zeros [net.IPv6len]byte b.WriteBytes(zeros[:]) } else { b.WriteBytes(ip.To16()) } } // Type is this relay message's types. func (r *RelayMessage) Type() MessageType { return r.MessageType } // String prints a short human-readable relay message. func (r *RelayMessage) String() string { ret := fmt.Sprintf( "RelayMessage(messageType=%s hopcount=%d, linkaddr=%s, peeraddr=%s, %d options)", r.Type(), r.HopCount, r.LinkAddr, r.PeerAddr, len(r.Options.Options), ) return ret } // Summary prints all options associated with this relay message. func (r *RelayMessage) Summary() string { ret := fmt.Sprintf( "RelayMessage\n"+ " messageType=%v\n"+ " hopcount=%v\n"+ " linkaddr=%v\n"+ " peeraddr=%v\n"+ " options=%v\n", r.Type(), r.HopCount, r.LinkAddr, r.PeerAddr, r.Options, ) return ret } // ToBytes returns the serialized version of this relay message as defined by // RFC 3315, Section 7. func (r *RelayMessage) ToBytes() []byte { buf := uio.NewBigEndianBuffer(make([]byte, 0, RelayHeaderSize)) buf.Write8(byte(r.MessageType)) buf.Write8(r.HopCount) write16(buf, r.LinkAddr) write16(buf, r.PeerAddr) buf.WriteBytes(r.Options.ToBytes()) return buf.Data() } // GetOption returns the options associated with the code. func (r *RelayMessage) GetOption(code OptionCode) []Option { return r.Options.Get(code) } // GetOneOption returns the first associated option with the code from this // message. func (r *RelayMessage) GetOneOption(code OptionCode) Option { return r.Options.GetOne(code) } // AddOption adds an option to this message. func (r *RelayMessage) AddOption(option Option) { r.Options.Add(option) } // UpdateOption replaces the first option of the same type as the specified one. func (r *RelayMessage) UpdateOption(option Option) { r.Options.Update(option) } // IsRelay returns whether this is a relay message or not. func (r *RelayMessage) IsRelay() bool { return true } // GetInnerMessage recurses into a relay message and extract and return the // inner Message. Return nil if none found (e.g. not a relay message). func (r *RelayMessage) GetInnerMessage() (*Message, error) { var ( p DHCPv6 err error ) p = r for { p, err = DecapsulateRelay(p) if err != nil { return nil, err } if m, ok := p.(*Message); ok { return m, nil } } } // NewRelayReplFromRelayForw creates a MessageTypeRelayReply based on a // MessageTypeRelayForward and replaces the inner message with the passed // DHCPv6 message. It copies the OptionInterfaceID and OptionRemoteID if the // options are present in the Relay packet. func NewRelayReplFromRelayForw(relay *RelayMessage, msg *Message) (DHCPv6, error) { var ( err error linkAddr, peerAddr []net.IP optiid []Option optrid []Option ) if relay == nil { return nil, errors.New("Relay message cannot be nil") } if relay.Type() != MessageTypeRelayForward { return nil, errors.New("The passed packet is not of type MessageTypeRelayForward") } if msg == nil { return nil, errors.New("The passed message cannot be nil") } for { linkAddr = append(linkAddr, relay.LinkAddr) peerAddr = append(peerAddr, relay.PeerAddr) optiid = append(optiid, relay.GetOneOption(OptionInterfaceID)) optrid = append(optrid, relay.GetOneOption(OptionRemoteID)) decap, err := DecapsulateRelay(relay) if err != nil { return nil, err } if decap.IsRelay() { relay = decap.(*RelayMessage) } else { break } } m := DHCPv6(msg) for i := len(linkAddr) - 1; i >= 0; i-- { m, err = EncapsulateRelay(m, MessageTypeRelayReply, linkAddr[i], peerAddr[i]) if err != nil { return nil, err } if opt := optiid[i]; opt != nil { m.AddOption(opt) } if opt := optrid[i]; opt != nil { m.AddOption(opt) } } return m, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/dhcpv6relay_test.go000066400000000000000000000074531431560352000271570ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "net" "testing" "github.com/stretchr/testify/require" ) func TestRelayMessage(t *testing.T) { ll := net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0xcc, 0xff, 0xfe, 0xdd, 0xee, 0xff} ma := net.IP{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} r := RelayMessage{ MessageType: MessageTypeRelayForward, HopCount: 10, LinkAddr: ll, PeerAddr: ma, // options is left empty here for testing purposes, even if it's // mandatory to have at least a relay message option } if mt := r.Type(); mt != MessageTypeRelayForward { t.Fatalf("Invalid message type. Expected %v, got %v", MessageTypeRelayForward, mt) } if hc := r.HopCount; hc != 10 { t.Fatalf("Invalid hop count. Expected 10, got %v", hc) } if la := r.LinkAddr; !la.Equal(ll) { t.Fatalf("Invalid link address. Expected %v, got %v", ll, la) } if pa := r.PeerAddr; !pa.Equal(ma) { t.Fatalf("Invalid peer address. Expected %v, got %v", ma, pa) } if opts := r.Options.Options; len(opts) != 0 { t.Fatalf("Invalid options. Expected none, got %v", opts) } } func TestRelayMessageToBytesDefault(t *testing.T) { want := []byte{ 12, // MessageTypeRelayForward 0, // hop count 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // link addr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // peer addr } r := RelayMessage{MessageType: MessageTypeRelayForward} require.Equal(t, r.ToBytes(), want) } func TestRelayMessageToBytes(t *testing.T) { expected := []byte{ 12, // MessageTypeRelayForward 1, // hop count 0xff, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, // link addr 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x02, // peer addr // option relay message 0, 9, // relay msg 0, 10, // option length // inner dhcp solicit 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID // inner option - elapsed time 0, 8, // elapsed time 0, 2, // length 0, 0, } r := RelayMessage{ MessageType: MessageTypeRelayForward, HopCount: 1, LinkAddr: net.IPv6interfacelocalallnodes, PeerAddr: net.IPv6linklocalallrouters, } opt := OptRelayMessage(&Message{ MessageType: MessageTypeSolicit, TransactionID: TransactionID{0xaa, 0xbb, 0xcc}, Options: MessageOptions{[]Option{ OptElapsedTime(0), }}, }) r.AddOption(opt) relayBytes := r.ToBytes() if !bytes.Equal(expected, relayBytes) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, relayBytes) } } func TestNewRelayRepFromRelayForw(t *testing.T) { // create a new relay forward rf := RelayMessage{} rf.MessageType = MessageTypeRelayForward rf.PeerAddr = net.IPv6linklocalallrouters rf.LinkAddr = net.IPv6interfacelocalallnodes rf.AddOption(OptInterfaceID(nil)) rf.AddOption(&OptRemoteID{}) // create the inner message s, err := NewMessage() require.NoError(t, err) s.AddOption(OptClientID(Duid{})) rf.AddOption(OptRelayMessage(s)) a, err := NewAdvertiseFromSolicit(s) require.NoError(t, err) rr, err := NewRelayReplFromRelayForw(&rf, a) require.NoError(t, err) relay := rr.(*RelayMessage) require.Equal(t, rr.Type(), MessageTypeRelayReply) require.Equal(t, relay.HopCount, rf.HopCount) require.Equal(t, relay.PeerAddr, rf.PeerAddr) require.Equal(t, relay.LinkAddr, rf.LinkAddr) require.NotNil(t, rr.GetOneOption(OptionInterfaceID)) require.NotNil(t, rr.GetOneOption(OptionRemoteID)) m, err := relay.GetInnerMessage() require.NoError(t, err) require.Equal(t, m, a) _, err = NewRelayReplFromRelayForw(nil, a) require.Error(t, err) _, err = NewRelayReplFromRelayForw(&rf, nil) require.Error(t, err) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/duid.go000066400000000000000000000105541431560352000246120ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "encoding/binary" "fmt" "net" "github.com/insomniacslk/dhcp/iana" ) // DuidType is the DUID type as defined in rfc3315. type DuidType uint16 // DUID types const ( DUID_LLT DuidType = 1 DUID_EN DuidType = 2 DUID_LL DuidType = 3 DUID_UUID DuidType = 4 ) // DuidTypeToString maps a DuidType to a name. var DuidTypeToString = map[DuidType]string{ DUID_LL: "DUID-LL", DUID_LLT: "DUID-LLT", DUID_EN: "DUID-EN", DUID_UUID: "DUID-UUID", } func (d DuidType) String() string { if dtype, ok := DuidTypeToString[d]; ok { return dtype } return "Unknown" } // Duid is a DHCP Unique Identifier. type Duid struct { Type DuidType HwType iana.HWType // for DUID-LLT and DUID-LL. Ignored otherwise. RFC 826 Time uint32 // for DUID-LLT. Ignored otherwise LinkLayerAddr net.HardwareAddr EnterpriseNumber uint32 // for DUID-EN. Ignored otherwise EnterpriseIdentifier []byte // for DUID-EN. Ignored otherwise Uuid []byte // for DUID-UUID. Ignored otherwise Opaque []byte // for unknown DUIDs } // Length returns the DUID length in bytes. func (d *Duid) Length() int { if d.Type == DUID_LLT { return 8 + len(d.LinkLayerAddr) } else if d.Type == DUID_LL { return 4 + len(d.LinkLayerAddr) } else if d.Type == DUID_EN { return 6 + len(d.EnterpriseIdentifier) } else if d.Type == DUID_UUID { return 18 } else { return 2 + len(d.Opaque) } } // Equal compares two Duid objects. func (d Duid) Equal(o Duid) bool { if d.Type != o.Type || d.HwType != o.HwType || d.Time != o.Time || !bytes.Equal(d.LinkLayerAddr, o.LinkLayerAddr) || d.EnterpriseNumber != o.EnterpriseNumber || !bytes.Equal(d.EnterpriseIdentifier, o.EnterpriseIdentifier) || !bytes.Equal(d.Uuid, o.Uuid) || !bytes.Equal(d.Opaque, o.Opaque) { return false } return true } // ToBytes serializes a Duid object. func (d *Duid) ToBytes() []byte { if d.Type == DUID_LLT { buf := make([]byte, 8) binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) binary.BigEndian.PutUint16(buf[2:4], uint16(d.HwType)) binary.BigEndian.PutUint32(buf[4:8], d.Time) return append(buf, d.LinkLayerAddr...) } else if d.Type == DUID_LL { buf := make([]byte, 4) binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) binary.BigEndian.PutUint16(buf[2:4], uint16(d.HwType)) return append(buf, d.LinkLayerAddr...) } else if d.Type == DUID_EN { buf := make([]byte, 6) binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) binary.BigEndian.PutUint32(buf[2:6], d.EnterpriseNumber) return append(buf, d.EnterpriseIdentifier...) } else if d.Type == DUID_UUID { buf := make([]byte, 2) binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) return append(buf, d.Uuid...) } else { buf := make([]byte, 2) binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) return append(buf, d.Opaque...) } } func (d *Duid) String() string { var hwaddr string if d.HwType == iana.HWTypeEthernet { for _, b := range d.LinkLayerAddr { hwaddr += fmt.Sprintf("%02x:", b) } if len(hwaddr) > 0 && hwaddr[len(hwaddr)-1] == ':' { hwaddr = hwaddr[:len(hwaddr)-1] } } return fmt.Sprintf("DUID{type=%v hwtype=%v hwaddr=%v}", d.Type.String(), d.HwType.String(), hwaddr) } // DuidFromBytes parses a Duid from a byte slice. func DuidFromBytes(data []byte) (*Duid, error) { if len(data) < 2 { return nil, fmt.Errorf("Invalid DUID: shorter than 2 bytes") } d := Duid{} d.Type = DuidType(binary.BigEndian.Uint16(data[0:2])) if d.Type == DUID_LLT { if len(data) < 8 { return nil, fmt.Errorf("Invalid DUID-LLT: shorter than 8 bytes") } d.HwType = iana.HWType(binary.BigEndian.Uint16(data[2:4])) d.Time = binary.BigEndian.Uint32(data[4:8]) d.LinkLayerAddr = data[8:] } else if d.Type == DUID_LL { if len(data) < 4 { return nil, fmt.Errorf("Invalid DUID-LL: shorter than 4 bytes") } d.HwType = iana.HWType(binary.BigEndian.Uint16(data[2:4])) d.LinkLayerAddr = data[4:] } else if d.Type == DUID_EN { if len(data) < 6 { return nil, fmt.Errorf("Invalid DUID-EN: shorter than 6 bytes") } d.EnterpriseNumber = binary.BigEndian.Uint32(data[2:6]) d.EnterpriseIdentifier = data[6:] } else if d.Type == DUID_UUID { if len(data) != 18 { return nil, fmt.Errorf("Invalid DUID-UUID length. Expected 18, got %v", len(data)) } d.Uuid = data[2:18] } else { d.Opaque = data[2:] } return &d, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/duid_test.go000066400000000000000000000102541431560352000256460ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "net" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestDuidInvalidTooShort(t *testing.T) { // too short DUID at all (must be at least 2 bytes) _, err := DuidFromBytes([]byte{0}) require.Error(t, err) // too short DUID_LL (must be at least 4 bytes) _, err = DuidFromBytes([]byte{0, 3, 0xa}) require.Error(t, err) // too short DUID_EN (must be at least 6 bytes) _, err = DuidFromBytes([]byte{0, 2, 0xa, 0xb, 0xc}) require.Error(t, err) // too short DUID_LLT (must be at least 8 bytes) _, err = DuidFromBytes([]byte{0, 1, 0xa, 0xb, 0xc, 0xd, 0xe}) require.Error(t, err) // too short DUID_UUID (must be at least 18 bytes) _, err = DuidFromBytes([]byte{0, 4, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}) require.Error(t, err) } func TestDuidLLTFromBytes(t *testing.T) { buf := []byte{ 0, 1, // DUID_LLT 0, 1, // HwTypeEthernet 0x01, 0x02, 0x03, 0x04, // time 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr } duid, err := DuidFromBytes(buf) require.NoError(t, err) require.Equal(t, 14, duid.Length()) require.Equal(t, DUID_LLT, duid.Type) require.Equal(t, uint32(0x01020304), duid.Time) require.Equal(t, iana.HWTypeEthernet, duid.HwType) require.Equal(t, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, duid.LinkLayerAddr) } func TestDuidLLFromBytes(t *testing.T) { buf := []byte{ 0, 3, // DUID_LL 0, 1, // HwTypeEthernet 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr } duid, err := DuidFromBytes(buf) require.NoError(t, err) require.Equal(t, 10, duid.Length()) require.Equal(t, DUID_LL, duid.Type) require.Equal(t, iana.HWTypeEthernet, duid.HwType) require.Equal(t, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, duid.LinkLayerAddr) } func TestDuidUuidFromBytes(t *testing.T) { buf := []byte{ 0x00, 0x04, // DUID_UUID } uuid := []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08} buf = append(buf, uuid...) duid, err := DuidFromBytes(buf) require.NoError(t, err) require.Equal(t, 18, duid.Length()) require.Equal(t, DUID_UUID, duid.Type) require.Equal(t, uuid, duid.Uuid) } func TestDuidLLTToBytes(t *testing.T) { expected := []byte{ 0, 1, // DUID_LLT 0, 1, // HwTypeEthernet 0x01, 0x02, 0x03, 0x04, // time 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr } duid := Duid{ Type: DUID_LLT, HwType: iana.HWTypeEthernet, Time: uint32(0x01020304), LinkLayerAddr: []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, } toBytes := duid.ToBytes() require.Equal(t, expected, toBytes) } func TestDuidUuidToBytes(t *testing.T) { uuid := []byte{0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09} expected := []byte{00, 04} expected = append(expected, uuid...) duid := Duid{ Type: DUID_UUID, Uuid: uuid, } toBytes := duid.ToBytes() if !bytes.Equal(toBytes, expected) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes) } } func TestOpaqueDuid(t *testing.T) { duid := []byte("\x00\x0a\x00\x03\x00\x01\x4c\x5e\x0c\x43\xbf\x39") d, err := DuidFromBytes(duid) if err != nil { t.Fatalf("DuidFromBytes: unexpected error: %v", err) } if got, want := d.Length(), len(duid); got != want { t.Errorf("Length: unexpected result: got %d, want %d", got, want) } if got, want := d.ToBytes(), duid; !bytes.Equal(got, want) { t.Fatalf("ToBytes: unexpected result: got %x, want %x", got, want) } } func TestDuidEqual(t *testing.T) { d := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, } o := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, } require.True(t, d.Equal(o)) } func TestDuidEqualNotEqual(t *testing.T) { d := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, } o := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x00}, } require.False(t, d.Equal(o)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/fuzz.go000066400000000000000000000013241431560352000246560ustar00rootroot00000000000000// +build gofuzz package dhcpv6 import ( "bytes" "fmt" ) // Fuzz is an entrypoint for go-fuzz (github.com/dvyukov/go-fuzz) func Fuzz(data []byte) int { msg, err := FromBytes(data) if err != nil { return 0 } serialized := msg.ToBytes() if !bytes.Equal(data, serialized) { rtMsg, err := FromBytes(serialized) fmt.Printf("Input: %x\n", data) fmt.Printf("Round-trip: %x\n", serialized) fmt.Println("Message: ", msg.Summary()) fmt.Printf("Go repr: %#v\n", msg) fmt.Println("round-trip reserialized: ", rtMsg.Summary()) fmt.Printf("Go repr: %#v\n", rtMsg) if err != nil { fmt.Printf("failed to parse after deserialize-serialize: %v\n", err) } panic("round-trip different") } return 1 } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/iputils.go000066400000000000000000000054601431560352000253560ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" ) // InterfaceAddresses is used to fetch addresses of an interface with given name var InterfaceAddresses func(string) ([]net.Addr, error) = interfaceAddresses func interfaceAddresses(ifname string) ([]net.Addr, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } return iface.Addrs() } func getMatchingAddr(ifname string, matches func(net.IP) bool) (net.IP, error) { ifaddrs, err := InterfaceAddresses(ifname) if err != nil { return nil, err } for _, ifaddr := range ifaddrs { if ifaddr, ok := ifaddr.(*net.IPNet); ok && matches(ifaddr.IP) { return ifaddr.IP, nil } } return nil, fmt.Errorf("no matching address found for interface %s", ifname) } // GetLinkLocalAddr returns a link-local address for the interface func GetLinkLocalAddr(ifname string) (net.IP, error) { return getMatchingAddr(ifname, func(ip net.IP) bool { return ip.To4() == nil && ip.IsLinkLocalUnicast() }) } // GetGlobalAddr returns a global address for the interface func GetGlobalAddr(ifname string) (net.IP, error) { return getMatchingAddr(ifname, func(ip net.IP) bool { return ip.To4() == nil && ip.IsGlobalUnicast() }) } // GetMacAddressFromEUI64 will return a valid MAC address ONLY if it's a EUI-48 func GetMacAddressFromEUI64(ip net.IP) (net.HardwareAddr, error) { if ip.To16() == nil { return nil, fmt.Errorf("IP address shorter than 16 bytes") } if isEUI48 := ip[11] == 0xff && ip[12] == 0xfe; !isEUI48 { return nil, fmt.Errorf("IP address is not an EUI48 address") } mac := make(net.HardwareAddr, 6) copy(mac[0:3], ip[8:11]) copy(mac[3:6], ip[13:16]) mac[0] ^= 0x02 return mac, nil } // ExtractMAC looks into the inner most PeerAddr field in the RelayInfo header // which contains the EUI-64 address of the client making the request, populated // by the dhcp relay, it is possible to extract the mac address from that IP. // If that fails, it looks for the MAC addressed embededded in the DUID. // Note that this only works with type DuidLL and DuidLLT. // If a mac address cannot be found an error will be returned. func ExtractMAC(packet DHCPv6) (net.HardwareAddr, error) { msg := packet if packet.IsRelay() { inner, err := DecapsulateRelayIndex(packet, -1) if err != nil { return nil, err } relay := inner.(*RelayMessage) if _, mac := relay.Options.ClientLinkLayerAddress(); mac != nil { return mac, nil } if mac, err := GetMacAddressFromEUI64(relay.PeerAddr); err == nil { return mac, nil } msg, err = msg.(*RelayMessage).GetInnerMessage() if err != nil { return nil, err } } duid := msg.(*Message).Options.ClientID() if duid == nil { return nil, fmt.Errorf("client ID not found in packet") } if duid.LinkLayerAddr == nil { return nil, fmt.Errorf("failed to extract MAC") } return duid.LinkLayerAddr, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/iputils_test.go000066400000000000000000000121641431560352000264140ustar00rootroot00000000000000package dhcpv6 import ( "errors" "fmt" "net" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) var ErrDummy = errors.New("dummy error") type MatchingAddressTestSuite struct { suite.Suite m mock.Mock ips []net.IP addrs []net.Addr } func (s *MatchingAddressTestSuite) InterfaceAddresses(name string) ([]net.Addr, error) { args := s.m.Called(name) if args.Get(0) == nil { return nil, args.Error(1) } if ifaddrs, ok := args.Get(0).([]net.Addr); ok { return ifaddrs, args.Error(1) } panic(fmt.Sprintf("assert: arguments: InterfaceAddresses(0) failed because object wasn't correct type: %v", args.Get(0))) } func (s *MatchingAddressTestSuite) Match(ip net.IP) bool { args := s.m.Called(ip) return args.Bool(0) } func (s *MatchingAddressTestSuite) SetupTest() { InterfaceAddresses = s.InterfaceAddresses s.ips = []net.IP{ net.ParseIP("2401:db00:3020:70e1:face:0:7e:0"), net.ParseIP("2803:6080:890c:847e::1"), net.ParseIP("fe80::4a57:ddff:fe04:d8e9"), } s.addrs = []net.Addr{} for _, ip := range s.ips { s.addrs = append(s.addrs, &net.IPNet{IP: ip}) } } func (s *MatchingAddressTestSuite) TestGetMatchingAddr() { // Check if error from InterfaceAddresses immediately returns error s.m.On("InterfaceAddresses", "eth0").Return(nil, ErrDummy).Once() _, err := getMatchingAddr("eth0", s.Match) s.Assert().Equal(ErrDummy, err) s.m.AssertExpectations(s.T()) // Check if the looping is stopped after finding a matching address s.m.On("InterfaceAddresses", "eth0").Return(s.addrs, nil).Once() s.m.On("Match", s.ips[0]).Return(false).Once() s.m.On("Match", s.ips[1]).Return(true).Once() ip, err := getMatchingAddr("eth0", s.Match) s.Require().NoError(err) s.Assert().Equal(s.ips[1], ip) s.m.AssertExpectations(s.T()) // Check if the looping skips not matching addresses s.m.On("InterfaceAddresses", "eth0").Return(s.addrs, nil).Once() s.m.On("Match", s.ips[0]).Return(false).Once() s.m.On("Match", s.ips[1]).Return(false).Once() s.m.On("Match", s.ips[2]).Return(true).Once() ip, err = getMatchingAddr("eth0", s.Match) s.Require().NoError(err) s.Assert().Equal(s.ips[2], ip) s.m.AssertExpectations(s.T()) // Check if the error is returned if no matching address is found s.m.On("InterfaceAddresses", "eth0").Return(s.addrs, nil).Once() s.m.On("Match", s.ips[0]).Return(false).Once() s.m.On("Match", s.ips[1]).Return(false).Once() s.m.On("Match", s.ips[2]).Return(false).Once() _, err = getMatchingAddr("eth0", s.Match) s.Assert().EqualError(err, "no matching address found for interface eth0") s.m.AssertExpectations(s.T()) } func (s *MatchingAddressTestSuite) TestGetLinkLocalAddr() { s.m.On("InterfaceAddresses", "eth0").Return(s.addrs, nil).Once() ip, err := GetLinkLocalAddr("eth0") s.Require().NoError(err) s.Assert().Equal(s.ips[2], ip) s.m.AssertExpectations(s.T()) } func (s *MatchingAddressTestSuite) TestGetGlobalAddr() { s.m.On("InterfaceAddresses", "eth0").Return(s.addrs, nil).Once() ip, err := GetGlobalAddr("eth0") s.Require().NoError(err) s.Assert().Equal(s.ips[0], ip) s.m.AssertExpectations(s.T()) } func TestMatchingAddressTestSuite(t *testing.T) { suite.Run(t, new(MatchingAddressTestSuite)) } func Test_ExtractMAC(t *testing.T) { //SOLICIT message wrapped in Relay-Forw var relayForwBytesDuidUUID = []byte{ 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x8a, 0x07, 0xff, 0xfe, 0x56, 0xdc, 0xa4, 0x00, 0x12, 0x00, 0x06, 0x24, 0x8a, 0x07, 0x56, 0xdc, 0xa4, 0x00, 0x09, 0x00, 0x5a, 0x06, 0x7d, 0x9b, 0xca, 0x00, 0x01, 0x00, 0x12, 0x00, 0x04, 0xb7, 0xfd, 0x0a, 0x8c, 0x1b, 0x14, 0x10, 0xaa, 0xeb, 0x0a, 0x5b, 0x3f, 0xe8, 0x9d, 0x0f, 0x56, 0x00, 0x06, 0x00, 0x0a, 0x00, 0x17, 0x00, 0x18, 0x00, 0x17, 0x00, 0x18, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0xff, 0xff, 0x00, 0x03, 0x00, 0x28, 0x07, 0x56, 0xdc, 0xa4, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x15, 0x18, 0x00, 0x05, 0x00, 0x18, 0x26, 0x20, 0x01, 0x0d, 0xc0, 0x82, 0x90, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaf, 0xa0, 0x00, 0x00, 0x1c, 0x20, 0x00, 0x00, 0x1d, 0x4c} packet, err := FromBytes(relayForwBytesDuidUUID) require.NoError(t, err) mac, err := ExtractMAC(packet) require.NoError(t, err) require.Equal(t, mac.String(), "24:8a:07:56:dc:a4") // MAC extracted from DUID duid := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: []byte{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}, } solicit, err := NewMessage(WithClientID(duid)) require.NoError(t, err) relay, err := EncapsulateRelay(solicit, MessageTypeRelayForward, net.IPv6zero, net.IPv6zero) require.NoError(t, err) mac, err = ExtractMAC(relay) require.NoError(t, err) require.Equal(t, mac.String(), "aa:aa:aa:aa:aa:aa") // no client ID solicit, err = NewMessage() require.NoError(t, err) _, err = ExtractMAC(solicit) require.Error(t, err) // DUID is not DuidLL or DuidLLT duid = Duid{} solicit, err = NewMessage(WithClientID(duid)) require.NoError(t, err) _, err = ExtractMAC(solicit) require.Error(t, err) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/modifiers.go000066400000000000000000000110641431560352000256430ustar00rootroot00000000000000package dhcpv6 import ( "net" "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" ) // WithOption adds the specific option to the DHCPv6 message. func WithOption(o Option) Modifier { return func(d DHCPv6) { d.UpdateOption(o) } } // WithClientID adds a client ID option to a DHCPv6 packet func WithClientID(duid Duid) Modifier { return WithOption(OptClientID(duid)) } // WithServerID adds a client ID option to a DHCPv6 packet func WithServerID(duid Duid) Modifier { return WithOption(OptServerID(duid)) } // WithNetboot adds bootfile URL and bootfile param options to a DHCPv6 packet. func WithNetboot(d DHCPv6) { WithRequestedOptions(OptionBootfileURL, OptionBootfileParam)(d) } // WithFQDN adds a fully qualified domain name option to the packet func WithFQDN(flags uint8, domainname string) Modifier { return func(d DHCPv6) { d.UpdateOption(&OptFQDN{ Flags: flags, DomainName: &rfc1035label.Labels{ Labels: []string{domainname}, }, }) } } // WithUserClass adds a user class option to the packet func WithUserClass(uc []byte) Modifier { // TODO let the user specify multiple user classes return func(d DHCPv6) { ouc := OptUserClass{UserClasses: [][]byte{uc}} d.AddOption(&ouc) } } // WithArchType adds an arch type option to the packet func WithArchType(at iana.Arch) Modifier { return func(d DHCPv6) { d.AddOption(OptClientArchType(at)) } } // WithIANA adds or updates an OptIANA option with the provided IAAddress // options func WithIANA(addrs ...OptIAAddress) Modifier { return func(d DHCPv6) { if msg, ok := d.(*Message); ok { iana := msg.Options.OneIANA() if iana == nil { iana = &OptIANA{} } for _, addr := range addrs { iana.Options.Add(&addr) } msg.UpdateOption(iana) } } } // WithIAID updates an OptIANA option with the provided IAID func WithIAID(iaid [4]byte) Modifier { return func(d DHCPv6) { if msg, ok := d.(*Message); ok { iana := msg.Options.OneIANA() if iana == nil { iana = &OptIANA{ Options: IdentityOptions{Options: []Option{}}, } } copy(iana.IaId[:], iaid[:]) d.UpdateOption(iana) } } } // WithIATA adds or updates an OptIATA option with the provided IAID, // and IAAddress options func WithIATA(iaid [4]byte, addrs ...OptIAAddress) Modifier { return func(d DHCPv6) { if msg, ok := d.(*Message); ok { iata := msg.Options.OneIATA() if iata == nil { iata = &OptIATA{} } copy(iata.IaId[:], iaid[:]) for _, addr := range addrs { iata.Options.Add(&addr) } msg.UpdateOption(iata) } } } // WithDNS adds or updates an OptDNSRecursiveNameServer func WithDNS(dnses ...net.IP) Modifier { return WithOption(OptDNS(dnses...)) } // WithDomainSearchList adds or updates an OptDomainSearchList func WithDomainSearchList(searchlist ...string) Modifier { return func(d DHCPv6) { d.UpdateOption(OptDomainSearchList( &rfc1035label.Labels{ Labels: searchlist, }, )) } } // WithRapidCommit adds the rapid commit option to a message. func WithRapidCommit(d DHCPv6) { d.UpdateOption(&OptionGeneric{OptionCode: OptionRapidCommit}) } // WithRequestedOptions adds requested options to the packet func WithRequestedOptions(codes ...OptionCode) Modifier { return func(d DHCPv6) { if msg, ok := d.(*Message); ok { oro := msg.Options.RequestedOptions() for _, c := range codes { oro.Add(c) } d.UpdateOption(OptRequestedOption(oro...)) } } } // WithDHCP4oDHCP6Server adds or updates an OptDHCP4oDHCP6Server func WithDHCP4oDHCP6Server(addrs ...net.IP) Modifier { return func(d DHCPv6) { opt := OptDHCP4oDHCP6Server{ DHCP4oDHCP6Servers: addrs, } d.UpdateOption(&opt) } } // WithIAPD adds or updates an IAPD option with the provided IAID and // prefix options to a DHCPv6 packet. func WithIAPD(iaid [4]byte, prefixes ...*OptIAPrefix) Modifier { return func(d DHCPv6) { if msg, ok := d.(*Message); ok { opt := msg.Options.OneIAPD() if opt == nil { opt = &OptIAPD{} } copy(opt.IaId[:], iaid[:]) for _, prefix := range prefixes { opt.Options.Add(prefix) } d.UpdateOption(opt) } } } // WithClientLinkLayerAddress adds or updates the ClientLinkLayerAddress // option with provided HWType and HWAddress on a DHCPv6 packet func WithClientLinkLayerAddress(ht iana.HWType, lla net.HardwareAddr) Modifier { return WithOption(OptClientLinkLayerAddress(ht, lla)) } // WithInformationRefreshTime adds an optInformationRefreshTime to the DHCPv6 packet // using the provided duration func WithInformationRefreshTime(irt time.Duration) Modifier { return WithOption(OptInformationRefreshTime(irt)) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/modifiers_test.go000066400000000000000000000110351431560352000267000ustar00rootroot00000000000000package dhcpv6 import ( "log" "net" "testing" "time" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestWithClientID(t *testing.T) { duid := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr([]byte{0xfa, 0xce, 0xb0, 0x00, 0x00, 0x0c}), } m, err := NewMessage(WithClientID(duid)) require.NoError(t, err) cid := m.Options.ClientID() require.Equal(t, cid, &duid) } func TestWithServerID(t *testing.T) { duid := Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr([]byte{0xfa, 0xce, 0xb0, 0x00, 0x00, 0x0c}), } m, err := NewMessage(WithServerID(duid)) require.NoError(t, err) sid := m.Options.ServerID() require.Equal(t, sid, &duid) } func TestWithRequestedOptions(t *testing.T) { // Check if ORO is created when no ORO present m, err := NewMessage(WithRequestedOptions(OptionClientID)) require.NoError(t, err) oro := m.Options.RequestedOptions() require.ElementsMatch(t, oro, OptionCodes{OptionClientID}) // Check if already set options are preserved WithRequestedOptions(OptionServerID)(m) oro = m.Options.RequestedOptions() require.ElementsMatch(t, oro, OptionCodes{OptionClientID, OptionServerID}) } func TestWithIANA(t *testing.T) { var d Message WithIANA(OptIAAddress{ IPv6Addr: net.ParseIP("::1"), PreferredLifetime: 3600, ValidLifetime: 5200, })(&d) require.Equal(t, 1, len(d.Options.Options)) require.Equal(t, OptionIANA, d.Options.Options[0].Code()) } func TestWithDNS(t *testing.T) { var d Message WithDNS( net.ParseIP("fe80::1"), net.ParseIP("fe80::2"), )(&d) require.Equal(t, 1, len(d.Options.Options)) dns := d.Options.DNS() log.Printf("DNS %+v", dns) require.Equal(t, 2, len(dns)) require.Equal(t, net.ParseIP("fe80::1"), dns[0]) require.Equal(t, net.ParseIP("fe80::2"), dns[1]) require.NotEqual(t, net.ParseIP("fe80::1"), dns[1]) } func TestWithDomainSearchList(t *testing.T) { var d Message WithDomainSearchList("slackware.it", "dhcp.slackware.it")(&d) require.Equal(t, 1, len(d.Options.Options)) osl := d.Options.DomainSearchList() require.NotNil(t, osl) labels := osl.Labels require.Equal(t, 2, len(labels)) require.Equal(t, "slackware.it", labels[0]) require.Equal(t, "dhcp.slackware.it", labels[1]) } func TestWithFQDN(t *testing.T) { var d Message WithFQDN(4, "cnos.localhost")(&d) require.Equal(t, 1, len(d.Options.Options)) ofqdn := d.Options.FQDN() require.Equal(t, OptionFQDN, ofqdn.Code()) require.Equal(t, uint8(4), ofqdn.Flags) require.Equal(t, "cnos.localhost", ofqdn.DomainName.Labels[0]) } func TestWithDHCP4oDHCP6Server(t *testing.T) { var d Message WithDHCP4oDHCP6Server([]net.IP{ net.ParseIP("fe80::1"), net.ParseIP("fe80::2"), }...)(&d) require.Equal(t, 1, len(d.Options.Options)) opt := d.Options.DHCP4oDHCP6Server() require.Equal(t, OptionDHCP4oDHCP6Server, opt.Code()) require.Equal(t, 2, len(opt.DHCP4oDHCP6Servers)) require.Equal(t, net.ParseIP("fe80::1"), opt.DHCP4oDHCP6Servers[0]) require.Equal(t, net.ParseIP("fe80::2"), opt.DHCP4oDHCP6Servers[1]) require.NotEqual(t, net.ParseIP("fe80::1"), opt.DHCP4oDHCP6Servers[1]) } func TestWithIAPD(t *testing.T) { var d Message _, pre, _ := net.ParseCIDR("2001:DB8:7689::/48") prefix := &OptIAPrefix{ PreferredLifetime: 3600, ValidLifetime: 5200, Prefix: pre, } WithIAPD([4]byte{1, 2, 3, 4}, prefix)(&d) opt := d.Options.IAPD() require.Equal(t, 1, len(opt)) require.Equal(t, OptionIAPD, opt[0].Code()) } func TestWithClientLinkLayerAddress(t *testing.T) { var d RelayMessage mac, _ := net.ParseMAC("a4:83:e7:e3:df:88") WithClientLinkLayerAddress(iana.HWTypeEthernet, mac)(&d) opt := d.Options.GetOne(OptionClientLinkLayerAddr) require.Equal(t, OptionClientLinkLayerAddr, opt.Code()) llt, lla := d.Options.ClientLinkLayerAddress() require.Equal(t, iana.HWTypeEthernet, llt) require.Equal(t, mac, lla) } func TestWithIATA(t *testing.T) { var d Message WithIATA([4]byte{1, 2, 3, 4}, OptIAAddress{ IPv6Addr: net.ParseIP("2001:DB8:7689::1234"), PreferredLifetime: 3600, ValidLifetime: 5200, })(&d) require.Equal(t, 1, len(d.Options.Options)) iata := d.Options.OneIATA() iataOpts := iata.Options.Get(OptionIAAddr) iaAddr := iataOpts[0].(*OptIAAddress) require.Equal(t, OptionIATA, iata.Code()) require.Equal(t, [4]byte{1, 2, 3, 4}, iata.IaId) require.Equal(t, net.ParseIP("2001:DB8:7689::1234"), iaAddr.IPv6Addr) require.Equal(t, time.Duration(3600), iaAddr.PreferredLifetime) require.Equal(t, time.Duration(5200), iaAddr.ValidLifetime) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/nclient6/000077500000000000000000000000001431560352000250535ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/nclient6/client.go000066400000000000000000000312311431560352000266600ustar00rootroot00000000000000// Copyright 2018 the u-root Authors and Andrea Barberio. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package nclient6 is a minimum-functionality client for DHCPv6. package nclient6 import ( "context" "errors" "fmt" "log" "net" "os" "strings" "sync" "sync/atomic" "time" "github.com/insomniacslk/dhcp/dhcpv6" ) // Broadcast destination IP addresses as defined by RFC 3315 var ( AllDHCPRelayAgentsAndServers = &net.UDPAddr{ IP: net.ParseIP("ff02::1:2"), Port: dhcpv6.DefaultServerPort, } AllDHCPServers = &net.UDPAddr{ IP: net.ParseIP("ff05::1:3"), Port: dhcpv6.DefaultServerPort, } ) var ( // ErrNoResponse is returned when no response packet is received. ErrNoResponse = errors.New("no matching response packet received") ) // pendingCh is a channel associated with a pending TransactionID. type pendingCh struct { // SendAndRead closes done to indicate that it wishes for no more // messages for this particular XID. done <-chan struct{} // ch is used by the receive loop to distribute DHCP messages. ch chan<- *dhcpv6.Message } // Client is a DHCPv6 client. type Client struct { ifaceHWAddr net.HardwareAddr conn net.PacketConn timeout time.Duration retry int logger logger // bufferCap is the channel capacity for each TransactionID. bufferCap int // serverAddr is the UDP address to send all packets to. // // This may be an actual broadcast address, or a unicast address. serverAddr *net.UDPAddr // closed is an atomic bool set to 1 when done is closed. closed uint32 // done is closed to unblock the receive loop. done chan struct{} // wg protects the receiveLoop. wg sync.WaitGroup // printDropped logs dropped packets to logger if true. printDropped bool pendingMu sync.Mutex // pending stores the distribution channels for each pending // TransactionID. receiveLoop uses this map to determine which channel // to send a new DHCP message to. pending map[dhcpv6.TransactionID]*pendingCh } type logger interface { Printf(format string, v ...interface{}) PrintMessage(prefix string, message *dhcpv6.Message) } type emptyLogger struct{} func (e emptyLogger) Printf(format string, v ...interface{}) {} func (e emptyLogger) PrintMessage(prefix string, message *dhcpv6.Message) {} type shortSummaryLogger struct { *log.Logger } func (s shortSummaryLogger) Printf(format string, v ...interface{}) { s.Logger.Printf(format, v...) } func (s shortSummaryLogger) PrintMessage(prefix string, message *dhcpv6.Message) { s.Printf("%s: %s", prefix, message) } type debugLogger struct { *log.Logger } func (d debugLogger) Printf(format string, v ...interface{}) { d.Logger.Printf(format, v...) } func (d debugLogger) PrintMessage(prefix string, message *dhcpv6.Message) { d.Printf("%s: %s", prefix, message.Summary()) } // NewIPv6UDPConn returns a UDP connection bound to both the interface and port // given based on a IPv6 DGRAM socket. func NewIPv6UDPConn(iface string, port int) (net.PacketConn, error) { ip, err := dhcpv6.GetLinkLocalAddr(iface) if err != nil { return nil, err } return net.ListenUDP("udp6", &net.UDPAddr{ IP: ip, Port: port, Zone: iface, }) } // New returns a new DHCPv6 client for the given network interface. func New(iface string, opts ...ClientOpt) (*Client, error) { c, err := NewIPv6UDPConn(iface, dhcpv6.DefaultClientPort) if err != nil { return nil, err } i, err := net.InterfaceByName(iface) if err != nil { return nil, err } return NewWithConn(c, i.HardwareAddr, opts...) } // NewWithConn creates a new DHCP client that sends and receives packets on the // given interface. func NewWithConn(conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { c := &Client{ ifaceHWAddr: ifaceHWAddr, timeout: 5 * time.Second, retry: 3, serverAddr: AllDHCPRelayAgentsAndServers, bufferCap: 5, conn: conn, logger: emptyLogger{}, done: make(chan struct{}), pending: make(map[dhcpv6.TransactionID]*pendingCh), } for _, opt := range opts { opt(c) } if c.conn == nil { return nil, fmt.Errorf("require a connection") } c.receiveLoop() return c, nil } // Close closes the underlying connection. func (c *Client) Close() error { // Make sure not to close done twice. if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) { return nil } err := c.conn.Close() // Closing c.done sets off a chain reaction: // // Any SendAndRead unblocks trying to receive more messages, which // means rem() gets called. // // rem() should be unblocking receiveLoop if it is blocked. // // receiveLoop should then exit gracefully. close(c.done) // Wait for receiveLoop to stop. c.wg.Wait() return err } func isErrClosing(err error) bool { // Unfortunately, the epoll-connection-closed error is internal to the // net library. return strings.Contains(err.Error(), "use of closed network connection") } func (c *Client) receiveLoop() { c.wg.Add(1) go func() { defer c.wg.Done() for { // TODO: Clients can send a "max packet size" option in their // packets, IIRC. Choose a reasonable size and set it. b := make([]byte, 1500) n, _, err := c.conn.ReadFrom(b) if err != nil { if !isErrClosing(err) { c.logger.Printf("error reading from UDP connection: %v", err) } return } msg, err := dhcpv6.MessageFromBytes(b[:n]) if err != nil { // Not a valid DHCP packet; keep listening. if c.printDropped { if len(b) > 12 { b = b[:12] } c.logger.Printf("Invalid DHCPv6 message received (len %d bytes), first 12 bytes: %#x", n, b) } continue } c.pendingMu.Lock() p, ok := c.pending[msg.TransactionID] if ok { select { case <-p.done: close(p.ch) delete(c.pending, msg.TransactionID) // This send may block. case p.ch <- msg: } } else if c.printDropped { // The Stringer will print the transaction ID. c.logger.Printf("No client waiting for msg with this XID: %s", msg) } c.pendingMu.Unlock() } }() } // ClientOpt is a function that configures the Client. type ClientOpt func(*Client) // WithTimeout configures the retransmission timeout. // // Default is 5 seconds. func WithTimeout(d time.Duration) ClientOpt { return func(c *Client) { c.timeout = d } } // WithLogDroppedPackets logs a short message for dropped packets. func WithLogDroppedPackets() ClientOpt { return func(c *Client) { c.printDropped = true } } // WithRetry configures the number of retransmissions to attempt. // // Default is 3. func WithRetry(r int) ClientOpt { return func(c *Client) { c.retry = r } } // WithConn configures the packet connection to use. func WithConn(conn net.PacketConn) ClientOpt { return func(c *Client) { c.conn = conn } } // WithBroadcastAddr configures the address to broadcast to. func WithBroadcastAddr(n *net.UDPAddr) ClientOpt { return func(c *Client) { c.serverAddr = n } } // WithSummaryLogger logs one-line DHCPv6 message summarys when sent & received. func WithSummaryLogger() ClientOpt { return func(c *Client) { c.logger = shortSummaryLogger{ Logger: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // WithDebugLogger logs multi-line full DHCPv6 messages when sent & received. func WithDebugLogger() ClientOpt { return func(c *Client) { c.logger = debugLogger{ Logger: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // Matcher matches DHCP packets. type Matcher func(*dhcpv6.Message) bool // IsMessageType returns a matcher that checks for the message type. func IsMessageType(t dhcpv6.MessageType, tt ...dhcpv6.MessageType) Matcher { return func(p *dhcpv6.Message) bool { if p.MessageType == t { return true } for _, mt := range tt { if p.MessageType == mt { return true } } return false } } // RemoteAddr is the default DHCP server address this client sends messages to. func (c *Client) RemoteAddr() *net.UDPAddr { // Make a copy so the caller cannot modify the address once the client // is running. cop := *c.serverAddr return &cop } // InterfaceAddr returns the MAC address of the client's interface. func (c *Client) InterfaceAddr() net.HardwareAddr { b := make(net.HardwareAddr, len(c.ifaceHWAddr)) copy(b, c.ifaceHWAddr) return b } // RapidSolicit sends a solicitation message with the RapidCommit option and // returns the first valid reply received. func (c *Client) RapidSolicit(ctx context.Context, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { solicit, err := dhcpv6.NewSolicit(c.ifaceHWAddr, append(modifiers, dhcpv6.WithRapidCommit)...) if err != nil { return nil, err } msg, err := c.SendAndRead(ctx, c.serverAddr, solicit, IsMessageType(dhcpv6.MessageTypeReply, dhcpv6.MessageTypeAdvertise)) if err != nil { return nil, err } switch msg.MessageType { case dhcpv6.MessageTypeReply: // We got RapidCommitted. return msg, nil case dhcpv6.MessageTypeAdvertise: // We didn't get RapidCommitted. Request regular lease. return c.Request(ctx, msg, modifiers...) default: return nil, fmt.Errorf("invalid message type: cannot happen") } } // Solicit sends a solicitation message and returns the first valid // advertisement received. func (c *Client) Solicit(ctx context.Context, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { solicit, err := dhcpv6.NewSolicit(c.ifaceHWAddr, modifiers...) if err != nil { return nil, err } msg, err := c.SendAndRead(ctx, c.serverAddr, solicit, IsMessageType(dhcpv6.MessageTypeAdvertise)) if err != nil { return nil, err } return msg, nil } // Request requests an IP Assignment from peer given an advertise message. func (c *Client) Request(ctx context.Context, advertise *dhcpv6.Message, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { request, err := dhcpv6.NewRequestFromAdvertise(advertise, modifiers...) if err != nil { return nil, err } return c.SendAndRead(ctx, c.serverAddr, request, nil) } // send sends p to destination and returns a response channel. // // The returned function must be called after all desired responses have been // received. // // Responses will be matched by transaction ID. func (c *Client) send(dest net.Addr, msg *dhcpv6.Message) (<-chan *dhcpv6.Message, func(), error) { c.pendingMu.Lock() if _, ok := c.pending[msg.TransactionID]; ok { c.pendingMu.Unlock() return nil, nil, fmt.Errorf("transaction ID %s already in use", msg.TransactionID) } ch := make(chan *dhcpv6.Message, c.bufferCap) done := make(chan struct{}) c.pending[msg.TransactionID] = &pendingCh{done: done, ch: ch} c.pendingMu.Unlock() cancel := func() { // Why can't we just close ch here? // // Because receiveLoop may potentially be blocked trying to // send on ch. We gotta unblock it first, so it'll unlock the // lock, and then we can take the lock and remove the XID from // the pending transaction map. close(done) c.pendingMu.Lock() if p, ok := c.pending[msg.TransactionID]; ok { close(p.ch) delete(c.pending, msg.TransactionID) } c.pendingMu.Unlock() } if _, err := c.conn.WriteTo(msg.ToBytes(), dest); err != nil { cancel() return nil, nil, fmt.Errorf("error writing packet to connection: %v", err) } return ch, cancel, nil } // This should never be visible to a user. var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded") // SendAndRead sends a packet p to a destination dest and waits for the first // response matching `match` as well as its Transaction ID. // // If match is nil, the first packet matching the Transaction ID is returned. func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, msg *dhcpv6.Message, match Matcher) (*dhcpv6.Message, error) { var response *dhcpv6.Message err := c.retryFn(func(timeout time.Duration) error { ch, rem, err := c.send(dest, msg) if err != nil { return err } c.logger.PrintMessage("sent message", msg) defer rem() for { select { case <-c.done: return ErrNoResponse case <-time.After(timeout): return errDeadlineExceeded case <-ctx.Done(): return ctx.Err() case packet := <-ch: if match == nil || match(packet) { c.logger.PrintMessage("received message", packet) response = packet return nil } } } }) if err == errDeadlineExceeded { return nil, ErrNoResponse } if err != nil { return nil, err } return response, nil } func (c *Client) retryFn(fn func(timeout time.Duration) error) error { timeout := c.timeout // Each retry takes the amount of timeout at worst. for i := 0; i < c.retry || c.retry < 0; i++ { switch err := fn(timeout); err { case nil: // Got it! return nil case errDeadlineExceeded: // Double timeout, then retry. timeout *= 2 default: return err } } return errDeadlineExceeded } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/nclient6/client_test.go000066400000000000000000000151211431560352000277170ustar00rootroot00000000000000// Copyright 2018 the u-root Authors and Andrea Barberio. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build go1.12 package nclient6 import ( "bytes" "context" "fmt" "net" "sync" "testing" "time" "github.com/hugelgupf/socketpair" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/server6" "github.com/stretchr/testify/require" ) type handler struct { mu sync.Mutex received []*dhcpv6.Message // Each received packet can have more than one response (in theory, // from different servers sending different Advertise, for example). responses [][]*dhcpv6.Message } func (h *handler) handle(conn net.PacketConn, peer net.Addr, msg dhcpv6.DHCPv6) { h.mu.Lock() defer h.mu.Unlock() m := msg.(*dhcpv6.Message) h.received = append(h.received, m) if len(h.responses) > 0 { resps := h.responses[0] // What should we send in response? for _, resp := range resps { if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil { panic(err) } } h.responses = h.responses[1:] } } func serveAndClient(ctx context.Context, responses [][]*dhcpv6.Message, opt ...ClientOpt) (*Client, net.PacketConn) { // Fake connection between client and server. No raw sockets, no port // weirdness. clientRawConn, serverRawConn, err := socketpair.PacketSocketPair() if err != nil { panic(err) } o := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)} o = append(o, opt...) mc, err := NewWithConn(clientRawConn, net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, o...) if err != nil { panic(err) } h := &handler{ responses: responses, } s, err := server6.NewServer("", nil, h.handle, server6.WithConn(serverRawConn)) if err != nil { panic(err) } go func() { if err := s.Serve(); err != nil { panic(err) } }() return mc, serverRawConn } func ComparePacket(got *dhcpv6.Message, want *dhcpv6.Message) error { if got == nil && got == want { return nil } if (want == nil || got == nil) && (got != want) { return fmt.Errorf("packet got %v, want %v", got, want) } if !bytes.Equal(got.ToBytes(), want.ToBytes()) { return fmt.Errorf("packet got %v, want %v", got, want) } return nil } func pktsExpected(got []*dhcpv6.Message, want []*dhcpv6.Message) error { if len(got) != len(want) { return fmt.Errorf("got %d packets, want %d packets", len(got), len(want)) } for i := range got { if err := ComparePacket(got[i], want[i]); err != nil { return err } } return nil } func newPacket(xid dhcpv6.TransactionID) *dhcpv6.Message { p, err := dhcpv6.NewMessage() if err != nil { panic(fmt.Sprintf("newpacket: %v", err)) } p.TransactionID = xid return p } func withBufferCap(n int) ClientOpt { return func(c *Client) { c.bufferCap = n } } func TestSendAndReadUntil(t *testing.T) { for _, tt := range []struct { desc string send *dhcpv6.Message server []*dhcpv6.Message // If want is nil, we assume server contains what is wanted. want *dhcpv6.Message wantErr error }{ { desc: "two response packets", send: newPacket([3]byte{0x33, 0x33, 0x33}), server: []*dhcpv6.Message{ newPacket([3]byte{0x33, 0x33, 0x33}), newPacket([3]byte{0x33, 0x33, 0x33}), }, want: newPacket([3]byte{0x33, 0x33, 0x33}), }, { desc: "one response packet", send: newPacket([3]byte{0x33, 0x33, 0x33}), server: []*dhcpv6.Message{ newPacket([3]byte{0x33, 0x33, 0x33}), }, want: newPacket([3]byte{0x33, 0x33, 0x33}), }, { desc: "one response packet, one invalid XID", send: newPacket([3]byte{0x33, 0x33, 0x33}), server: []*dhcpv6.Message{ newPacket([3]byte{0x77, 0x33, 0x33}), newPacket([3]byte{0x33, 0x33, 0x33}), }, want: newPacket([3]byte{0x33, 0x33, 0x33}), }, { desc: "discard wrong XID", send: newPacket([3]byte{0x33, 0x33, 0x33}), server: []*dhcpv6.Message{ newPacket([3]byte{0, 0, 0}), }, want: nil, wantErr: ErrNoResponse, }, { desc: "no response, timeout", send: newPacket([3]byte{0x33, 0x33, 0x33}), wantErr: ErrNoResponse, }, } { t.Run(tt.desc, func(t *testing.T) { // Both server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, [][]*dhcpv6.Message{tt.server}, // Use an unbuffered channel to make sure we // have no deadlocks. withBufferCap(0)) defer mc.Close() rcvd, err := mc.SendAndRead(context.Background(), AllDHCPServers, tt.send, nil) if err != tt.wantErr { t.Error(err) } if err := ComparePacket(rcvd, tt.want); err != nil { t.Errorf("got unexpected packets: %v", err) } }) } } func TestSimpleSendAndReadDiscardGarbage(t *testing.T) { pkt := newPacket([3]byte{0x33, 0x33, 0x33}) responses := []*dhcpv6.Message{ newPacket([3]byte{0x33, 0x33, 0x33}), } // Both the server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, udpConn := serveAndClient(ctx, [][]*dhcpv6.Message{responses}) defer mc.Close() // Too short for valid DHCPv4 packet. _, err := udpConn.WriteTo([]byte{0x01}, nil) require.NoError(t, err) rcvd, err := mc.SendAndRead(context.Background(), AllDHCPServers, pkt, nil) if err != nil { t.Error(err) } if err := ComparePacket(rcvd, responses[0]); err != nil { t.Errorf("got unexpected packets: %v", err) } } func TestMultipleSendAndReadOne(t *testing.T) { for _, tt := range []struct { desc string send []*dhcpv6.Message server [][]*dhcpv6.Message wantErr []error }{ { desc: "two requests, two responses", send: []*dhcpv6.Message{ newPacket([3]byte{0x33, 0x33, 0x33}), newPacket([3]byte{0x44, 0x44, 0x44}), }, server: [][]*dhcpv6.Message{ []*dhcpv6.Message{ // Response for first packet. newPacket([3]byte{0x33, 0x33, 0x33}), }, []*dhcpv6.Message{ // Response for second packet. newPacket([3]byte{0x44, 0x44, 0x44}), }, }, wantErr: []error{ nil, nil, }, }, } { // Both server and client only get 2 seconds. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() mc, _ := serveAndClient(ctx, tt.server) defer mc.conn.Close() for i, send := range tt.send { rcvd, err := mc.SendAndRead(context.Background(), AllDHCPServers, send, nil) if wantErr := tt.wantErr[i]; err != wantErr { t.Errorf("SendAndReadOne(%v): got %v, want %v", send, err, wantErr) } if err := pktsExpected([]*dhcpv6.Message{rcvd}, tt.server[i]); err != nil { t.Errorf("got unexpected packets: %v", err) } } } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_4rd.go000066400000000000000000000124151431560352000257440ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "github.com/u-root/uio/uio" ) // Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options type Opt4RD Options // Code returns the Option Code for this option func (op *Opt4RD) Code() OptionCode { return Option4RD } // ToBytes serializes this option func (op *Opt4RD) ToBytes() []byte { return (*Options)(op).ToBytes() } // String returns a human-readable representation of the option func (op *Opt4RD) String() string { return fmt.Sprintf("Opt4RD{%v}", (*Options)(op)) } // ParseOpt4RD builds an Opt4RD structure from a sequence of bytes. // The input data does not include option code and length bytes func ParseOpt4RD(data []byte) (*Opt4RD, error) { var opt Options err := opt.FromBytes(data) return (*Opt4RD)(&opt), err } // Opt4RDMapRule represents a 4RD Mapping Rule option // The option is described in https://tools.ietf.org/html/rfc7600#section-4.9 // The 4RD mapping rules are described in https://tools.ietf.org/html/rfc7600#section-4.2 type Opt4RDMapRule struct { // Prefix4 is the IPv4 prefix mapped by this rule Prefix4 net.IPNet // Prefix6 is the IPv6 prefix mapped by this rule Prefix6 net.IPNet // EABitsLength is the number of bits of an address used in constructing the mapped address EABitsLength uint8 // WKPAuthorized determines if well-known ports are assigned to addresses in an A+P mapping // It can only be set if the length of Prefix4 + EABits > 32 WKPAuthorized bool } const ( // opt4RDWKPAuthorizedMask is the mask for the WKPAuthorized flag in its // byte in Opt4RDMapRule opt4RDWKPAuthorizedMask = 1 << 7 // opt4RDHubAndSpokeMask is the mask for the HubAndSpoke flag in its // byte in Opt4RDNonMapRule opt4RDHubAndSpokeMask = 1 << 7 // opt4RDTrafficClassMask is the mask for the TrafficClass flag in its // byte in Opt4RDNonMapRule opt4RDTrafficClassMask = 1 << 0 ) // Code returns the option code representing this option func (op *Opt4RDMapRule) Code() OptionCode { return Option4RDMapRule } // ToBytes serializes this option func (op *Opt4RDMapRule) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) p4Len, _ := op.Prefix4.Mask.Size() p6Len, _ := op.Prefix6.Mask.Size() buf.Write8(uint8(p4Len)) buf.Write8(uint8(p6Len)) buf.Write8(op.EABitsLength) if op.WKPAuthorized { buf.Write8(opt4RDWKPAuthorizedMask) } else { buf.Write8(0) } if op.Prefix4.IP.To4() == nil { // The API prevents us from returning an error here // We just write zeros instead, which is pretty bad behaviour buf.Write32(0) } else { buf.WriteBytes(op.Prefix4.IP.To4()) } if op.Prefix6.IP.To16() == nil { buf.Write64(0) buf.Write64(0) } else { buf.WriteBytes(op.Prefix6.IP.To16()) } return buf.Data() } // String returns a human-readable description of this option func (op *Opt4RDMapRule) String() string { return fmt.Sprintf("Opt4RDMapRule{Prefix4=%s, Prefix6=%s, EA-Bits=%d, WKPAuthorized=%t}", op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized) } // ParseOpt4RDMapRule builds an Opt4RDMapRule structure from a sequence of bytes. // The input data does not include option code and length bytes. func ParseOpt4RDMapRule(data []byte) (*Opt4RDMapRule, error) { var opt Opt4RDMapRule buf := uio.NewBigEndianBuffer(data) opt.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32) opt.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128) opt.EABitsLength = buf.Read8() opt.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0 opt.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len)) opt.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len)) return &opt, buf.FinError() } // Opt4RDNonMapRule represents 4RD parameters other than mapping rules type Opt4RDNonMapRule struct { // HubAndSpoke is whether the network topology is hub-and-spoke or meshed HubAndSpoke bool // TrafficClass is an optional 8-bit tunnel traffic class identifier TrafficClass *uint8 // DomainPMTU is the Path MTU for this 4RD domain DomainPMTU uint16 } // Code returns the option code for this option func (op *Opt4RDNonMapRule) Code() OptionCode { return Option4RDNonMapRule } // ToBytes serializes this option func (op *Opt4RDNonMapRule) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) var flags uint8 var trafficClassValue uint8 if op.HubAndSpoke { flags |= opt4RDHubAndSpokeMask } if op.TrafficClass != nil { flags |= opt4RDTrafficClassMask trafficClassValue = *op.TrafficClass } buf.Write8(flags) buf.Write8(trafficClassValue) buf.Write16(op.DomainPMTU) return buf.Data() } // String returns a human-readable description of this option func (op *Opt4RDNonMapRule) String() string { var tClass interface{} = false if op.TrafficClass != nil { tClass = *op.TrafficClass } return fmt.Sprintf("Opt4RDNonMapRule{HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}", op.HubAndSpoke, tClass, op.DomainPMTU) } // ParseOpt4RDNonMapRule builds an Opt4RDNonMapRule structure from a sequence of bytes. // The input data does not include option code and length bytes func ParseOpt4RDNonMapRule(data []byte) (*Opt4RDNonMapRule, error) { var opt Opt4RDNonMapRule buf := uio.NewBigEndianBuffer(data) flags := buf.Read8() opt.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0 tClass := buf.Read8() if flags&opt4RDTrafficClassMask != 0 { opt.TrafficClass = &tClass } opt.DomainPMTU = buf.Read16() return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_4rd_test.go000066400000000000000000000105121431560352000267770ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestOpt4RDNonMapRuleParse(t *testing.T) { data := []byte{0x81, 0xaa, 0x05, 0xd4} opt, err := ParseOpt4RDNonMapRule(data) require.NoError(t, err) require.True(t, opt.HubAndSpoke) require.NotNil(t, opt.TrafficClass) require.EqualValues(t, 0xaa, *opt.TrafficClass) require.EqualValues(t, 1492, opt.DomainPMTU) // Remove the TrafficClass flag and check value is ignored data[0] = 0x80 opt, err = ParseOpt4RDNonMapRule(data) require.NoError(t, err) require.True(t, opt.HubAndSpoke) require.Nil(t, opt.TrafficClass) require.EqualValues(t, 1492, opt.DomainPMTU) } func TestOpt4RDNonMapRuleToBytes(t *testing.T) { var tClass uint8 = 0xaa opt := Opt4RDNonMapRule{ HubAndSpoke: true, TrafficClass: &tClass, DomainPMTU: 1492, } expected := []byte{0x81, 0xaa, 0x05, 0xd4} require.Equal(t, expected, opt.ToBytes()) // Unsetting TrafficClass should zero the corresponding bytes in the output opt.TrafficClass = nil expected[0], expected[1] = 0x80, 0x00 require.Equal(t, expected, opt.ToBytes()) } func TestOpt4RDNonMapRuleString(t *testing.T) { var tClass uint8 = 120 opt := Opt4RDNonMapRule{ HubAndSpoke: true, TrafficClass: &tClass, DomainPMTU: 9000, } str := opt.String() require.Contains(t, str, "HubAndSpoke=true", "String() should contain the HubAndSpoke flag value") require.Contains(t, str, "TrafficClass=120", "String() should contain the TrafficClass flag value") require.Contains(t, str, "DomainPMTU=9000", "String() should contain the domain PMTU") } func TestOpt4RDMapRuleParse(t *testing.T) { ip6addr, ip6net, err := net.ParseCIDR("2001:db8::1234:5678:0:aabb/64") ip6net.IP = ip6addr // We want to keep the entire address however, not apply the mask require.NoError(t, err) ip4addr, ip4net, err := net.ParseCIDR("100.64.0.234/10") ip4net.IP = ip4addr.To4() require.NoError(t, err) data := append([]byte{ 10, // IPv4 prefix length 64, // IPv6 prefix length 32, // EA-bits 0x80, // WKPs authorized }, append(ip4addr.To4(), ip6addr...)..., ) opt, err := ParseOpt4RDMapRule(data) require.NoError(t, err) require.EqualValues(t, *ip6net, opt.Prefix6) require.EqualValues(t, *ip4net, opt.Prefix4) require.EqualValues(t, 32, opt.EABitsLength) require.True(t, opt.WKPAuthorized) } func TestOpt4RDMapRuleToBytes(t *testing.T) { opt := Opt4RDMapRule{ Prefix4: net.IPNet{ IP: net.IPv4(100, 64, 0, 238), Mask: net.CIDRMask(24, 32), }, Prefix6: net.IPNet{ IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), Mask: net.CIDRMask(80, 128), }, EABitsLength: 32, WKPAuthorized: true, } expected := append([]byte{ 24, // v4 prefix length 80, // v6 prefix length 32, // EA-bits 0x80, // WKPs authorized }, append(opt.Prefix4.IP.To4(), opt.Prefix6.IP.To16()...)..., ) require.Equal(t, expected, opt.ToBytes()) } // FIXME: Invalid packets are serialized without error func TestOpt4RDMapRuleString(t *testing.T) { opt := Opt4RDMapRule{ Prefix4: net.IPNet{ IP: net.IPv4(100, 64, 0, 238), Mask: net.CIDRMask(24, 32), }, Prefix6: net.IPNet{ IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), Mask: net.CIDRMask(80, 128), }, EABitsLength: 32, WKPAuthorized: true, } str := opt.String() require.Contains(t, str, "WKPAuthorized=true", "String() should write the flag values") require.Contains(t, str, "Prefix6=2001:db8::1234:5678:0:aabb/80", "String() should include the IPv6 prefix") require.Contains(t, str, "Prefix4=100.64.0.238/24", "String() should include the IPv4 prefix") require.Contains(t, str, "EA-Bits=32", "String() should include the value for EA-Bits") } // This test round-trip serialization/deserialization of both kinds of 4RD // options, and the container option func TestOpt4RDRoundTrip(t *testing.T) { var tClass uint8 = 0xaa opt := Opt4RD{ &Opt4RDMapRule{ Prefix4: net.IPNet{ IP: net.IPv4(100, 64, 0, 238).To4(), Mask: net.CIDRMask(24, 32), }, Prefix6: net.IPNet{ IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), Mask: net.CIDRMask(80, 128), }, EABitsLength: 32, WKPAuthorized: true, }, &Opt4RDNonMapRule{ HubAndSpoke: true, TrafficClass: &tClass, DomainPMTU: 9000, }, } rtOpt, err := ParseOpt4RD(opt.ToBytes()) require.NoError(t, err) require.NotNil(t, rtOpt) require.Equal(t, opt, *rtOpt) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_archtype.go000066400000000000000000000015211431560352000270660ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/insomniacslk/dhcp/iana" ) // OptClientArchType represents an option CLIENT_ARCH_TYPE. // // This module defines the OptClientArchType structure. // https://www.ietf.org/rfc/rfc5970.txt func OptClientArchType(a ...iana.Arch) Option { return &optClientArchType{Archs: a} } type optClientArchType struct { iana.Archs } func (op *optClientArchType) Code() OptionCode { return OptionClientArchType } func (op optClientArchType) String() string { return fmt.Sprintf("ClientArchType: %s", op.Archs.String()) } // parseOptClientArchType builds an OptClientArchType structure from // a sequence of bytes The input data does not include option code and // length bytes. func parseOptClientArchType(data []byte) (*optClientArchType, error) { var opt optClientArchType return &opt, opt.FromBytes(data) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_archtype_test.go000066400000000000000000000016251431560352000301320ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseOptClientArchType(t *testing.T) { data := []byte{ 0, 6, // EFI_IA32 } opt, err := parseOptClientArchType(data) require.NoError(t, err) require.Equal(t, iana.EFI_IA32, opt.Archs[0]) } func TestParseOptClientArchTypeInvalid(t *testing.T) { data := []byte{42} _, err := parseOptClientArchType(data) require.Error(t, err) } func TestOptClientArchTypeParseAndToBytes(t *testing.T) { data := []byte{ 0, 8, // EFI_XSCALE } opt, err := parseOptClientArchType(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } func TestOptClientArchType(t *testing.T) { opt := OptClientArchType(iana.EFI_ITANIUM) require.Equal(t, OptionClientArchType, opt.Code()) require.Contains(t, opt.String(), "EFI Itanium", "String() should contain the correct ArchType output") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_bootfileparam.go000066400000000000000000000033401431560352000300740ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // OptBootFileParam returns a BootfileParam option as defined in RFC 5970 // Section 3.2. func OptBootFileParam(args ...string) Option { return optBootFileParam(args) } type optBootFileParam []string // Code returns the option code func (optBootFileParam) Code() OptionCode { return OptionBootfileParam } // ToBytes serializes the option and returns it as a sequence of bytes func (op optBootFileParam) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, param := range op { if len(param) >= 1<<16 { // TODO: say something here instead of silently ignoring a parameter continue } buf.Write16(uint16(len(param))) buf.WriteBytes([]byte(param)) /*if err := buf.Error(); err != nil { // TODO: description of `WriteBytes` says it could return // an error via `buf.Error()`. But a quick look into implementation of // `WriteBytes` at the moment of this comment showed it does not set any // errors to `Error()` output. It's required to make a decision: // to fix `WriteBytes` or it's description or // to find a way to handle an error here. }*/ } return buf.Data() } func (op optBootFileParam) String() string { return fmt.Sprintf("BootFileParam: %v", ([]string)(op)) } // parseOptBootFileParam builds an OptBootFileParam structure from a sequence // of bytes. The input data does not include option code and length bytes. func parseOptBootFileParam(data []byte) (optBootFileParam, error) { buf := uio.NewBigEndianBuffer(data) var result optBootFileParam for buf.Has(2) { length := buf.Read16() result = append(result, string(buf.CopyN(int(length)))) } if err := buf.FinError(); err != nil { return nil, err } return result, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_bootfileparam_test.go000066400000000000000000000035261431560352000311410ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "encoding/binary" "fmt" "testing" "github.com/stretchr/testify/require" ) var ( testBootfileParams0Compiled = "\x00\x0eroot=/dev/sda1\x00\x00\x00\x02rw" testBootfileParams1 = []string{ "initrd=http://myserver.mycompany.local/initrd.xz", "", "root=/dev/sda1", "rw", "netconsole=..:\000:.something\000here.::..", string(make([]byte, (1<<16)-1)), } ) // compileTestBootfileParams is an independent implementation of bootfile param encoder func compileTestBootfileParams(t *testing.T, params []string) []byte { var length [2]byte buf := bytes.Buffer{} for _, param := range params { if len(param) >= 1<<16 { panic("a too long parameter") } binary.BigEndian.PutUint16(length[:], uint16(len(param))) _, err := buf.Write(length[:]) require.NoError(t, err) _, err = buf.WriteString(param) require.NoError(t, err) } return buf.Bytes() } func TestOptBootFileParam(t *testing.T) { expected := string(compileTestBootfileParams(t, testBootfileParams1)) opt, err := parseOptBootFileParam([]byte(expected)) if err != nil { t.Fatal(err) } if string(opt.ToBytes()) != expected { t.Fatalf("Invalid boot file parameter. Expected %v, got %v", expected, opt) } } func TestParsedTypeOptBootFileParam(t *testing.T) { tryParse := func(compiled []byte, expected []string) { opt, err := ParseOption(OptionBootfileParam, compiled) require.NoError(t, err) bootfileParamOpt, ok := opt.(optBootFileParam) require.True(t, ok, fmt.Sprintf("invalid type: %T instead of %T", opt, bootfileParamOpt)) require.Equal(t, compiled, bootfileParamOpt.ToBytes()) require.Equal(t, expected, ([]string)(bootfileParamOpt)) } tryParse( []byte(testBootfileParams0Compiled), []string{"root=/dev/sda1", "", "rw"}, ) tryParse( compileTestBootfileParams(t, testBootfileParams1), testBootfileParams1, ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_bootfileurl.go000066400000000000000000000014421431560352000275770ustar00rootroot00000000000000package dhcpv6 import ( "fmt" ) // OptBootFileURL returns a OptionBootfileURL as defined by RFC 5970. func OptBootFileURL(url string) Option { return optBootFileURL(url) } type optBootFileURL string // Code returns the option code func (op optBootFileURL) Code() OptionCode { return OptionBootfileURL } // ToBytes serializes the option and returns it as a sequence of bytes func (op optBootFileURL) ToBytes() []byte { return []byte(op) } func (op optBootFileURL) String() string { return fmt.Sprintf("BootFileURL: %s", string(op)) } // parseOptBootFileURL builds an optBootFileURL structure from a sequence // of bytes. The input data does not include option code and length bytes. func parseOptBootFileURL(data []byte) (optBootFileURL, error) { return optBootFileURL(string(data)), nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_bootfileurl_test.go000066400000000000000000000014131431560352000306340ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestOptBootFileURL(t *testing.T) { expected := "https://insomniac.slackware.it" opt, err := parseOptBootFileURL([]byte(expected)) if err != nil { t.Fatal(err) } if string(opt) != expected { t.Fatalf("Invalid boot file URL. Expected %v, got %v", expected, opt) } require.Contains(t, opt.String(), "https://insomniac.slackware.it", "String() should contain the correct BootFileUrl output") } func TestOptBootFileURLToBytes(t *testing.T) { urlString := "https://insomniac.slackware.it" opt := OptBootFileURL(urlString) toBytes := opt.ToBytes() if !bytes.Equal(toBytes, []byte(urlString)) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", urlString, toBytes) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_clientid.go000066400000000000000000000012721431560352000270450ustar00rootroot00000000000000package dhcpv6 import ( "fmt" ) // OptClientID represents a Client Identifier option as defined by RFC 3315 // Section 22.2. func OptClientID(d Duid) Option { return &optClientID{d} } type optClientID struct { Duid } func (*optClientID) Code() OptionCode { return OptionClientID } func (op *optClientID) String() string { return fmt.Sprintf("ClientID: %v", op.Duid.String()) } // parseOptClientID builds an OptClientId structure from a sequence // of bytes. The input data does not include option code and length // bytes. func parseOptClientID(data []byte) (*optClientID, error) { cid, err := DuidFromBytes(data) if err != nil { return nil, err } return &optClientID{*cid}, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_clientid_test.go000066400000000000000000000040601431560352000301020ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseOptClientID(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 0, 1, 2, 3, 4, 5, // hw addr } opt, err := parseOptClientID(data) require.NoError(t, err) require.Equal(t, DUID_LL, opt.Type) require.Equal(t, iana.HWTypeEthernet, opt.HwType) require.Equal(t, net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}), opt.LinkLayerAddr) } func TestOptClientIdToBytes(t *testing.T) { opt := OptClientID( Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr([]byte{5, 4, 3, 2, 1, 0}), }, ) expected := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } require.Equal(t, expected, opt.ToBytes()) } func TestOptClientIdDecodeEncode(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } opt, err := parseOptClientID(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } func TestOptionClientId(t *testing.T) { opt := OptClientID( Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr([]byte{0xde, 0xad, 0, 0, 0xbe, 0xef}), }, ) require.Equal(t, OptionClientID, opt.Code()) require.Contains( t, opt.String(), "ClientID: DUID{type=DUID-LL hwtype=Ethernet hwaddr=de:ad:00:00:be:ef}", "String() should contain the correct cid output", ) } func TestOptClientIdparseOptClientIDBogusDUID(t *testing.T) { data := []byte{ 0, 4, // DUID_UUID 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17 10, 11, 12, 13, 14, 15, 16, 17, } _, err := parseOptClientID(data) require.Error(t, err, "A truncated OptClientId DUID should return an error") } func TestOptClientIdparseOptClientIDInvalidTooShort(t *testing.T) { data := []byte{ 0, // truncated: DUIDs are at least 2 bytes } _, err := parseOptClientID(data) require.Error(t, err, "A truncated OptClientId should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_clientlinklayeraddress.go000066400000000000000000000026551431560352000320170ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "github.com/insomniacslk/dhcp/iana" "github.com/u-root/uio/uio" ) // OptClientLinkLayerAddress implements OptionClientLinkLayerAddr option. // https://tools.ietf.org/html/rfc6939 func OptClientLinkLayerAddress(ht iana.HWType, lla net.HardwareAddr) *optClientLinkLayerAddress { return &optClientLinkLayerAddress{LinkLayerType: ht, LinkLayerAddress: lla} } type optClientLinkLayerAddress struct { LinkLayerType iana.HWType LinkLayerAddress net.HardwareAddr } // Code returns the option code. func (op *optClientLinkLayerAddress) Code() OptionCode { return OptionClientLinkLayerAddr } // ToBytes serializes the option and returns it as a sequence of bytes func (op *optClientLinkLayerAddress) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(op.LinkLayerType)) buf.WriteBytes(op.LinkLayerAddress) return buf.Data() } func (op *optClientLinkLayerAddress) String() string { return fmt.Sprintf("ClientLinkLayerAddress: Type=%s LinkLayerAddress=%s", op.LinkLayerType, op.LinkLayerAddress) } // parseOptClientLinkLayerAddress deserializes from bytes // to build an optClientLinkLayerAddress structure. func parseOptClientLinkLayerAddress(data []byte) (*optClientLinkLayerAddress, error) { var opt optClientLinkLayerAddress buf := uio.NewBigEndianBuffer(data) opt.LinkLayerType = iana.HWType(buf.Read16()) opt.LinkLayerAddress = buf.ReadAll() return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_clientlinklayeraddress_test.go000066400000000000000000000017751431560352000330600ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "net" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseOptClientLinkLayerAddress(t *testing.T) { data := []byte{ 0, 1, // LinkLayerType 164, 131, 231, 227, 223, 136, } opt, err := parseOptClientLinkLayerAddress(data) require.NoError(t, err) require.Equal(t, OptionClientLinkLayerAddr, opt.Code()) require.Equal(t, iana.HWTypeEthernet, opt.LinkLayerType) require.Equal(t, net.HardwareAddr(data[2:]), opt.LinkLayerAddress) require.Equal(t, "ClientLinkLayerAddress: Type=Ethernet LinkLayerAddress=a4:83:e7:e3:df:88", opt.String()) } func TestOptClientLinkLayerAddressToBytes(t *testing.T) { mac, _ := net.ParseMAC("a4:83:e7:e3:df:88") opt := optClientLinkLayerAddress{ LinkLayerType: iana.HWTypeEthernet, LinkLayerAddress: mac, } want := []byte{ 0, 1, // LinkLayerType 164, 131, 231, 227, 223, 136, } b := opt.ToBytes() if !bytes.Equal(b, want) { t.Fatalf("opt.ToBytes()=%v, want %v", b, want) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dhcpv4_msg.go000066400000000000000000000015761431560352000273170ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/insomniacslk/dhcp/dhcpv4" ) // OptDHCPv4Msg represents a OptionDHCPv4Msg option // // This module defines the OptDHCPv4Msg structure. // https://www.ietf.org/rfc/rfc7341.txt type OptDHCPv4Msg struct { Msg *dhcpv4.DHCPv4 } // Code returns the option code func (op *OptDHCPv4Msg) Code() OptionCode { return OptionDHCPv4Msg } // ToBytes returns the option serialized to bytes. func (op *OptDHCPv4Msg) ToBytes() []byte { return op.Msg.ToBytes() } func (op *OptDHCPv4Msg) String() string { return fmt.Sprintf("OptDHCPv4Msg{%v}", op.Msg) } // ParseOptDHCPv4Msg builds an OptDHCPv4Msg structure // from a sequence of bytes. The input data does not include option code and length // bytes. func ParseOptDHCPv4Msg(data []byte) (*OptDHCPv4Msg, error) { var opt OptDHCPv4Msg var err error opt.Msg, err = dhcpv4.FromBytes(data) return &opt, err } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dhcpv4_msg_test.go000066400000000000000000000063771431560352000303620ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) var magicCookie = [4]byte{99, 130, 83, 99} func TestParseOptDHCPv4Msg(t *testing.T) { data := []byte{ 1, // dhcp request 1, // ethernet hw type 6, // hw addr length 3, // hop count 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network) 0, 3, // number of seconds 0, 1, // broadcast 0, 0, 0, 0, // client IP address 0, 0, 0, 0, // your IP address 0, 0, 0, 0, // server IP address 0, 0, 0, 0, // gateway IP address 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding } // server host name expectedHostname := []byte{} for i := 0; i < 64; i++ { expectedHostname = append(expectedHostname, 0) } data = append(data, expectedHostname...) // boot file name expectedBootfilename := []byte{} for i := 0; i < 128; i++ { expectedBootfilename = append(expectedBootfilename, 0) } data = append(data, expectedBootfilename...) // magic cookie, then no options data = append(data, magicCookie[:]...) opt, err := ParseOptDHCPv4Msg(data) d := opt.Msg require.NoError(t, err) require.Equal(t, d.OpCode, dhcpv4.OpcodeBootRequest) require.Equal(t, d.HWType, iana.HWTypeEthernet) require.Equal(t, d.HopCount, byte(3)) require.Equal(t, d.TransactionID, dhcpv4.TransactionID{0xaa, 0xbb, 0xcc, 0xdd}) require.Equal(t, d.NumSeconds, uint16(3)) require.Equal(t, d.Flags, uint16(1)) require.True(t, d.ClientIPAddr.Equal(net.IPv4zero)) require.True(t, d.YourIPAddr.Equal(net.IPv4zero)) require.True(t, d.GatewayIPAddr.Equal(net.IPv4zero)) require.Equal(t, d.ClientHWAddr, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) require.Equal(t, d.ServerHostName, "") require.Equal(t, d.BootFileName, "") // no need to check Magic Cookie as it is already validated in FromBytes // above } func TestOptDHCPv4MsgToBytes(t *testing.T) { // the following bytes match what dhcpv4.New would create. Keep them in // sync! expected := []byte{ 1, // Opcode BootRequest 1, // HwType Ethernet 6, // HwAddrLen 0, // HopCount 0x11, 0x22, 0x33, 0x44, // TransactionID 0, 0, // NumSeconds 0, 0, // Flags 0, 0, 0, 0, // ClientIPAddr 0, 0, 0, 0, // YourIPAddr 0, 0, 0, 0, // ServerIPAddr 0, 0, 0, 0, // GatewayIPAddr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ClientHwAddr } // ServerHostName expected = append(expected, bytes.Repeat([]byte{0}, 64)...) // BootFileName expected = append(expected, bytes.Repeat([]byte{0}, 128)...) // Magic Cookie expected = append(expected, magicCookie[:]...) // End expected = append(expected, 0xff) // Minimum message length padding. // // 236 + 4 byte cookie + 1 byte end + 59 bytes padding. expected = append(expected, bytes.Repeat([]byte{0}, 59)...) d, err := dhcpv4.New() require.NoError(t, err) // fix TransactionID to match the expected one, since it's randomly // generated in New() d.TransactionID = dhcpv4.TransactionID{0x11, 0x22, 0x33, 0x44} opt := OptDHCPv4Msg{Msg: d} require.Equal(t, expected, opt.ToBytes()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dhcpv4_o_dhcpv6_server.go000066400000000000000000000023471431560352000316240ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "github.com/u-root/uio/uio" ) // OptDHCP4oDHCP6Server represents a OptionDHCP4oDHCP6Server option // // This module defines the OptDHCP4oDHCP6Server structure. // https://www.ietf.org/rfc/rfc7341.txt type OptDHCP4oDHCP6Server struct { DHCP4oDHCP6Servers []net.IP } // Code returns the option code func (op *OptDHCP4oDHCP6Server) Code() OptionCode { return OptionDHCP4oDHCP6Server } // ToBytes returns the option serialized to bytes. func (op *OptDHCP4oDHCP6Server) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, addr := range op.DHCP4oDHCP6Servers { buf.WriteBytes(addr.To16()) } return buf.Data() } func (op *OptDHCP4oDHCP6Server) String() string { return fmt.Sprintf("OptDHCP4oDHCP6Server{4o6-servers=%v}", op.DHCP4oDHCP6Servers) } // ParseOptDHCP4oDHCP6Server builds an OptDHCP4oDHCP6Server structure // from a sequence of bytes. The input data does not include option code and length // bytes. func ParseOptDHCP4oDHCP6Server(data []byte) (*OptDHCP4oDHCP6Server, error) { var opt OptDHCP4oDHCP6Server buf := uio.NewBigEndianBuffer(data) for buf.Has(net.IPv6len) { opt.DHCP4oDHCP6Servers = append(opt.DHCP4oDHCP6Servers, buf.CopyN(net.IPv6len)) } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go000066400000000000000000000032111431560352000326520ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestParseOptDHCP4oDHCP6Server(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35, } expected := []net.IP{ net.IP(data), } opt, err := ParseOptDHCP4oDHCP6Server(data) require.NoError(t, err) require.Equal(t, expected, opt.DHCP4oDHCP6Servers) require.Equal(t, OptionDHCP4oDHCP6Server, opt.Code()) require.Contains(t, opt.String(), "4o6-servers=[2a03:2880:fffe:c:face:b00c:0:35]", "String() should contain the correct DHCP4-over-DHCP6 server output") } func TestOptDHCP4oDHCP6ServerToBytes(t *testing.T) { ip1 := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") ip2 := net.ParseIP("2001:4860:4860::8888") servers := []net.IP{ip1, ip2} expected := append([]byte{}, []byte(ip1)...) expected = append(expected, []byte(ip2)...) opt := OptDHCP4oDHCP6Server{DHCP4oDHCP6Servers: servers} require.Equal(t, expected, opt.ToBytes()) } func TestParseOptDHCP4oDHCP6ServerParseNoAddr(t *testing.T) { data := []byte{ } var expected []net.IP opt, err := ParseOptDHCP4oDHCP6Server(data) require.NoError(t, err) require.Equal(t, expected, opt.DHCP4oDHCP6Servers) } func TestOptDHCP4oDHCP6ServerToBytesNoAddr(t *testing.T) { expected := []byte(nil) opt := OptDHCP4oDHCP6Server{} require.Equal(t, expected, opt.ToBytes()) } func TestParseOptDHCP4oDHCP6ServerParseBogus(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address } _, err := ParseOptDHCP4oDHCP6Server(data) require.Error(t, err, "An invalid IPv6 address should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dns.go000066400000000000000000000020241431560352000260320ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "github.com/u-root/uio/uio" ) // OptDNS returns a DNS Recursive Name Server option as defined by RFC 3646. func OptDNS(ip ...net.IP) Option { return &optDNS{NameServers: ip} } type optDNS struct { NameServers []net.IP } // Code returns the option code func (op *optDNS) Code() OptionCode { return OptionDNSRecursiveNameServer } // ToBytes returns the option serialized to bytes. func (op *optDNS) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, ns := range op.NameServers { buf.WriteBytes(ns.To16()) } return buf.Data() } func (op *optDNS) String() string { return fmt.Sprintf("DNS: %v", op.NameServers) } // parseOptDNS builds an optDNS structure // from a sequence of bytes. The input data does not include option code and length // bytes. func parseOptDNS(data []byte) (*optDNS, error) { var opt optDNS buf := uio.NewBigEndianBuffer(data) for buf.Has(net.IPv6len) { opt.NameServers = append(opt.NameServers, buf.CopyN(net.IPv6len)) } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_dns_test.go000066400000000000000000000022511431560352000270730ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/stretchr/testify/require" ) func TestParseOptDNS(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35, } expected := []net.IP{ net.IP(data), } opt, err := parseOptDNS(data) require.NoError(t, err) require.Equal(t, expected, opt.NameServers) require.Equal(t, OptionDNSRecursiveNameServer, opt.Code()) require.Contains(t, opt.String(), "DNS: [2a03:2880:fffe:c:face:b00c:0:35]", "String() should contain the correct nameservers output") } func TestOptDNSRecursiveNameServerToBytes(t *testing.T) { ns1 := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") ns2 := net.ParseIP("2001:4860:4860::8888") nameservers := []net.IP{ns1, ns2} expected := append([]byte{}, []byte(ns1)...) expected = append(expected, []byte(ns2)...) opt := OptDNS(nameservers...) require.Equal(t, expected, opt.ToBytes()) } func TestParseOptDNSBogus(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address } _, err := parseOptDNS(data) require.Error(t, err, "An invalid nameserver IPv6 address should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_domainsearchlist.go000066400000000000000000000021001431560352000305720ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/insomniacslk/dhcp/rfc1035label" ) // OptDomainSearchList returns a DomainSearchList option as defined by RFC 3646. func OptDomainSearchList(labels *rfc1035label.Labels) Option { return &optDomainSearchList{DomainSearchList: labels} } type optDomainSearchList struct { DomainSearchList *rfc1035label.Labels } func (op *optDomainSearchList) Code() OptionCode { return OptionDomainSearchList } // ToBytes marshals this option to bytes. func (op *optDomainSearchList) ToBytes() []byte { return op.DomainSearchList.ToBytes() } func (op *optDomainSearchList) String() string { return fmt.Sprintf("DomainSearchList: %s", op.DomainSearchList) } // ParseOptDomainSearchList builds an OptDomainSearchList structure from a sequence // of bytes. The input data does not include option code and length bytes. func parseOptDomainSearchList(data []byte) (*optDomainSearchList, error) { var opt optDomainSearchList var err error opt.DomainSearchList, err = rfc1035label.FromBytes(data) if err != nil { return nil, err } return &opt, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_domainsearchlist_test.go000066400000000000000000000027731431560352000316510ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/stretchr/testify/require" ) func TestParseOptDomainSearchList(t *testing.T) { data := []byte{ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0, } opt, err := parseOptDomainSearchList(data) require.NoError(t, err) require.Equal(t, OptionDomainSearchList, opt.Code()) require.Equal(t, 2, len(opt.DomainSearchList.Labels)) require.Equal(t, "example.com", opt.DomainSearchList.Labels[0]) require.Equal(t, "subnet.example.org", opt.DomainSearchList.Labels[1]) require.Contains(t, opt.String(), "example.com subnet.example.org", "String() should contain the correct domain search output") } func TestOptDomainSearchListToBytes(t *testing.T) { expected := []byte{ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0, } opt := OptDomainSearchList( &rfc1035label.Labels{ Labels: []string{ "example.com", "subnet.example.org", }, }, ) require.Equal(t, expected, opt.ToBytes()) } func TestParseOptDomainSearchListInvalidLength(t *testing.T) { data := []byte{ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', // truncated } _, err := parseOptDomainSearchList(data) require.Error(t, err, "A truncated OptDomainSearchList should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_elapsedtime.go000066400000000000000000000020671431560352000275510ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "time" "github.com/u-root/uio/uio" ) // OptElapsedTime returns an Elapsed Time option as defined by RFC 3315 Section // 22.9. func OptElapsedTime(dur time.Duration) Option { return &optElapsedTime{ElapsedTime: dur} } type optElapsedTime struct { ElapsedTime time.Duration } func (*optElapsedTime) Code() OptionCode { return OptionElapsedTime } // ToBytes marshals this option to bytes. func (op *optElapsedTime) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(op.ElapsedTime.Round(10*time.Millisecond) / (10 * time.Millisecond))) return buf.Data() } func (op *optElapsedTime) String() string { return fmt.Sprintf("ElapsedTime: %s", op.ElapsedTime) } // build an optElapsedTime structure from a sequence of bytes. // The input data does not include option code and length bytes. func parseOptElapsedTime(data []byte) (*optElapsedTime, error) { var opt optElapsedTime buf := uio.NewBigEndianBuffer(data) opt.ElapsedTime = time.Duration(buf.Read16()) * 10 * time.Millisecond return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_elapsedtime_test.go000066400000000000000000000022661431560352000306110ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptElapsedTime(t *testing.T) { opt, err := parseOptElapsedTime([]byte{0xaa, 0xbb}) if err != nil { t.Fatal(err) } if elapsedTime := opt.ElapsedTime; elapsedTime != 0xaabb*10*time.Millisecond { t.Fatalf("Invalid elapsed time. Expected 0xaabb, got %v", elapsedTime) } } func TestOptElapsedTimeToBytes(t *testing.T) { opt := OptElapsedTime(0) expected := []byte{0, 0} if toBytes := opt.ToBytes(); !bytes.Equal(expected, toBytes) { t.Fatalf("Invalid ToBytes output. Expected %v, got %v", expected, toBytes) } } func TestOptElapsedTimeString(t *testing.T) { opt := OptElapsedTime(100 * time.Millisecond) expected := "ElapsedTime: 100ms" if optString := opt.String(); optString != expected { t.Fatalf("Invalid elapsed time string. Expected %v, got %v", expected, optString) } } func TestOptElapsedTimeParseInvalidOption(t *testing.T) { _, err := parseOptElapsedTime([]byte{0xaa}) require.Error(t, err, "A short option should return an error") _, err = parseOptElapsedTime([]byte{0xaa, 0xbb, 0xcc}) require.Error(t, err, "An option with too many bytes should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_fqdn.go000066400000000000000000000020431431560352000261770ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/uio" ) // OptFQDN implements OptionFQDN option. // // https://tools.ietf.org/html/rfc4704 type OptFQDN struct { Flags uint8 DomainName *rfc1035label.Labels } // Code returns the option code. func (op *OptFQDN) Code() OptionCode { return OptionFQDN } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptFQDN) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write8(op.Flags) buf.WriteBytes(op.DomainName.ToBytes()) return buf.Data() } func (op *OptFQDN) String() string { return fmt.Sprintf("OptFQDN{flags=%d, domainname=%s}", op.Flags, op.DomainName) } // ParseOptFQDN deserializes from bytes to build a OptFQDN structure. func ParseOptFQDN(data []byte) (*OptFQDN, error) { var opt OptFQDN var err error buf := uio.NewBigEndianBuffer(data) opt.Flags = buf.Read8() opt.DomainName, err = rfc1035label.FromBytes(buf.ReadAll()) if err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_fqdn_test.go000066400000000000000000000016531431560352000272440ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/stretchr/testify/require" ) func TestParseOptFQDN(t *testing.T) { data := []byte{ 0, // Flags 4, 'c', 'n', 'o', 's', 9, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', 0, } opt, err := ParseOptFQDN(data) require.NoError(t, err) require.Equal(t, OptionFQDN, opt.Code()) require.Equal(t, uint8(0), opt.Flags) require.Equal(t, "cnos.localhost", opt.DomainName.Labels[0]) require.Equal(t, "OptFQDN{flags=0, domainname=[cnos.localhost]}", opt.String()) } func TestOptFQDNToBytes(t *testing.T) { opt := OptFQDN{ Flags: 0, DomainName: &rfc1035label.Labels{ Labels: []string{"cnos.localhost"}, }, } want := []byte{ 0, // Flags 4, 'c', 'n', 'o', 's', 9, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', 0, } b := opt.ToBytes() if !bytes.Equal(b, want) { t.Fatalf("opt.ToBytes()=%v, want %v", b, want) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iaaddress.go000066400000000000000000000037501431560352000272140ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "time" "github.com/u-root/uio/uio" ) // AddressOptions are options valid for the IAAddress option field. // // RFC 8415 Appendix C lists only the Status Code option as valid. type AddressOptions struct { Options } // Status returns the status code associated with this option. func (ao AddressOptions) Status() *OptStatusCode { opt := ao.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // OptIAAddress represents an OptionIAAddr. // // This module defines the OptIAAddress structure. // https://www.ietf.org/rfc/rfc3633.txt type OptIAAddress struct { IPv6Addr net.IP PreferredLifetime time.Duration ValidLifetime time.Duration Options AddressOptions } // Code returns the option's code func (op *OptIAAddress) Code() OptionCode { return OptionIAAddr } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptIAAddress) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) write16(buf, op.IPv6Addr) t1 := Duration{op.PreferredLifetime} t1.Marshal(buf) t2 := Duration{op.ValidLifetime} t2.Marshal(buf) buf.WriteBytes(op.Options.ToBytes()) return buf.Data() } func (op *OptIAAddress) String() string { return fmt.Sprintf("IAAddress: IP=%v PreferredLifetime=%v ValidLifetime=%v Options=%v", op.IPv6Addr, op.PreferredLifetime, op.ValidLifetime, op.Options) } // ParseOptIAAddress builds an OptIAAddress structure from a sequence // of bytes. The input data does not include option code and length // bytes. func ParseOptIAAddress(data []byte) (*OptIAAddress, error) { var opt OptIAAddress buf := uio.NewBigEndianBuffer(data) opt.IPv6Addr = net.IP(buf.CopyN(net.IPv6len)) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) opt.PreferredLifetime = t1.Duration opt.ValidLifetime = t2.Duration if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iaaddress_test.go000066400000000000000000000052471431560352000302560ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIAAddressParse(t *testing.T) { ipaddr := []byte{0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} data := append(ipaddr, []byte{ 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime 0, 8, 0, 2, 0xaa, 0xbb, // options }...) opt, err := ParseOptIAAddress(data) require.NoError(t, err) require.Equal(t, net.IP(ipaddr), opt.IPv6Addr) require.Equal(t, 0x0a0b0c0d*time.Second, opt.PreferredLifetime) require.Equal(t, 0x0e0f0102*time.Second, opt.ValidLifetime) } func TestOptIAAddressParseInvalidTooShort(t *testing.T) { data := []byte{ 0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0xa, 0xb, 0xc, 0xd, // preferred lifetime // truncated here } _, err := ParseOptIAAddress(data) require.Error(t, err) } func TestOptIAAddressParseInvalidBrokenOptions(t *testing.T) { data := []byte{ 0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime 0, 8, 0, 2, 0xaa, // broken options } _, err := ParseOptIAAddress(data) require.Error(t, err) } func TestOptIAAddressToBytesDefault(t *testing.T) { want := []byte{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // IP 0, 0, 0, 0, // preferred lifetime 0, 0, 0, 0, // valid lifetime } opt := OptIAAddress{} require.Equal(t, opt.ToBytes(), want) } func TestOptIAAddressToBytes(t *testing.T) { ipBytes := []byte{0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} expected := append(ipBytes, []byte{ 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime 0, 8, 0, 2, 0x00, 0x01, // options }...) opt := OptIAAddress{ IPv6Addr: net.IP(ipBytes), PreferredLifetime: 0x0a0b0c0d * time.Second, ValidLifetime: 0x0e0f0102 * time.Second, Options: AddressOptions{[]Option{ OptElapsedTime(10 * time.Millisecond), }}, } require.Equal(t, expected, opt.ToBytes()) } func TestOptIAAddressString(t *testing.T) { ipaddr := []byte{0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} data := append(ipaddr, []byte{ 0x00, 0x00, 0x00, 70, // preferred lifetime 0x00, 0x00, 0x00, 50, // valid lifetime 0, 8, 0, 2, 0xaa, 0xbb, // options }...) opt, err := ParseOptIAAddress(data) require.NoError(t, err) str := opt.String() require.Contains( t, str, "IP=2401:203:405:607:809:a0b:c0d:e0f", "String() should return the ipv6addr", ) require.Contains( t, str, "PreferredLifetime=1m10s", "String() should return the preferredlifetime", ) require.Contains( t, str, "ValidLifetime=50s", "String() should return the validlifetime", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iapd.go000066400000000000000000000041721431560352000261710ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "time" "github.com/u-root/uio/uio" ) // PDOptions are options used with the IAPD (prefix delegation) option. // // RFC 3633 describes that IA_PD-options may contain the IAPrefix option and // the StatusCode option. type PDOptions struct { Options } // Prefixes are the prefixes associated with this delegation. func (po PDOptions) Prefixes() []*OptIAPrefix { opts := po.Options.Get(OptionIAPrefix) pre := make([]*OptIAPrefix, 0, len(opts)) for _, o := range opts { if iap, ok := o.(*OptIAPrefix); ok { pre = append(pre, iap) } } return pre } // Status returns the status code associated with this option. func (po PDOptions) Status() *OptStatusCode { opt := po.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // OptIAPD implements the identity association for prefix // delegation option defined by RFC 3633, Section 9. type OptIAPD struct { IaId [4]byte T1 time.Duration T2 time.Duration Options PDOptions } // Code returns the option code func (op *OptIAPD) Code() OptionCode { return OptionIAPD } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptIAPD) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.WriteBytes(op.IaId[:]) t1 := Duration{op.T1} t1.Marshal(buf) t2 := Duration{op.T2} t2.Marshal(buf) buf.WriteBytes(op.Options.ToBytes()) return buf.Data() } // String returns a string representation of the OptIAPD data func (op *OptIAPD) String() string { return fmt.Sprintf("IAPD: {IAID=%v, t1=%v, t2=%v, Options=[%v]}", op.IaId, op.T1, op.T2, op.Options) } // ParseOptIAPD builds an OptIAPD structure from a sequence of bytes. // The input data does not include option code and length bytes. func ParseOptIAPD(data []byte) (*OptIAPD, error) { var opt OptIAPD buf := uio.NewBigEndianBuffer(data) buf.ReadBytes(opt.IaId[:]) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) opt.T1 = t1.Duration opt.T2 = t2.Duration if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iapd_test.go000066400000000000000000000061061431560352000272270ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIAPDParseOptIAPD(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix } opt, err := ParseOptIAPD(data) require.NoError(t, err) require.Equal(t, OptionIAPD, opt.Code()) require.Equal(t, [4]byte{1, 0, 0, 0}, opt.IaId) require.Equal(t, time.Second, opt.T1) require.Equal(t, 2*time.Second, opt.T2) } func TestOptIAPDParseOptIAPDInvalidLength(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 // truncated from here } _, err := ParseOptIAPD(data) require.Error(t, err) } func TestOptIAPDParseOptIAPDInvalidOptions(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // IAPrefix ipv6Prefix missing last byte } _, err := ParseOptIAPD(data) require.Error(t, err) } func TestOptIAPDToBytes(t *testing.T) { oaddr := OptIAPrefix{ PreferredLifetime: 0xaabbccdd * time.Second, ValidLifetime: 0xeeff0011 * time.Second, Prefix: &net.IPNet{ Mask: net.CIDRMask(36, 128), IP: net.IPv6loopback, }, } opt := OptIAPD{ IaId: [4]byte{1, 2, 3, 4}, T1: 12345 * time.Second, T2: 54321 * time.Second, Options: PDOptions{[]Option{&oaddr}}, } expected := []byte{ 1, 2, 3, 4, // IA ID 0, 0, 0x30, 0x39, // T1 = 12345 0, 0, 0xd4, 0x31, // T2 = 54321 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix } require.Equal(t, expected, opt.ToBytes()) } func TestOptIAPDString(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix } opt, err := ParseOptIAPD(data) require.NoError(t, err) str := opt.String() require.Contains( t, str, "IAID=[1 0 0 0]", "String() should return the IAID", ) require.Contains( t, str, "t1=1s, t2=2s", "String() should return the T1/T2 options", ) require.Contains( t, str, "Options=[", "String() should return a list of options", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iaprefix.go000066400000000000000000000047621431560352000270700ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "time" "github.com/u-root/uio/uio" ) // PrefixOptions are the options valid for use with IAPrefix option field. // // RFC 3633 states that it's just the StatusCode option. // // RFC 8415 Appendix C does not list the Status Code option as valid, but it // does say that the previous text in RFC 8415 Section 21.22 supersedes that // table. Section 21.22 does mention the Status Code option. type PrefixOptions struct { Options } // Status returns the status code associated with this option. func (po PrefixOptions) Status() *OptStatusCode { opt := po.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // OptIAPrefix implements the IAPrefix option. // // This module defines the OptIAPrefix structure. // https://www.ietf.org/rfc/rfc3633.txt type OptIAPrefix struct { PreferredLifetime time.Duration ValidLifetime time.Duration Prefix *net.IPNet Options PrefixOptions } func (op *OptIAPrefix) Code() OptionCode { return OptionIAPrefix } // ToBytes marshals this option according to RFC 3633, Section 10. func (op *OptIAPrefix) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) t1 := Duration{op.PreferredLifetime} t1.Marshal(buf) t2 := Duration{op.ValidLifetime} t2.Marshal(buf) if op.Prefix != nil { // Even if Mask is nil, Size will return 0 without panicking. length, _ := op.Prefix.Mask.Size() buf.Write8(uint8(length)) write16(buf, op.Prefix.IP) } else { buf.Write8(0) write16(buf, nil) } buf.WriteBytes(op.Options.ToBytes()) return buf.Data() } func (op *OptIAPrefix) String() string { return fmt.Sprintf("IAPrefix: {PreferredLifetime=%v, ValidLifetime=%v, Prefix=%s, Options=%v}", op.PreferredLifetime, op.ValidLifetime, op.Prefix, op.Options) } // ParseOptIAPrefix an OptIAPrefix structure from a sequence of bytes. The // input data does not include option code and length bytes. func ParseOptIAPrefix(data []byte) (*OptIAPrefix, error) { buf := uio.NewBigEndianBuffer(data) var opt OptIAPrefix var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) opt.PreferredLifetime = t1.Duration opt.ValidLifetime = t2.Duration length := buf.Read8() ip := net.IP(buf.CopyN(net.IPv6len)) if length == 0 { opt.Prefix = nil } else { opt.Prefix = &net.IPNet{ Mask: net.CIDRMask(int(length), 128), IP: ip, } } if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_iaprefix_test.go000066400000000000000000000057401431560352000301240ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "net" "reflect" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIAPrefix(t *testing.T) { buf := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime 0xee, 0xff, 0x00, 0x11, // validLifetime 36, // prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // ipv6Prefix } opt, err := ParseOptIAPrefix(buf) if err != nil { t.Fatal(err) } want := &OptIAPrefix{ PreferredLifetime: 0xaabbccdd * time.Second, ValidLifetime: 0xeeff0011 * time.Second, Prefix: &net.IPNet{ Mask: net.CIDRMask(36, 128), IP: net.IPv6loopback, }, Options: PrefixOptions{[]Option{}}, } if !reflect.DeepEqual(want, opt) { t.Errorf("parseIAPrefix = %v, want %v", opt, want) } } func TestOptIAPrefixToBytes(t *testing.T) { buf := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime 0xee, 0xff, 0x00, 0x11, // validLifetime 36, // prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix 0, 8, 0, 2, 0x00, 0x01, // options } opt := OptIAPrefix{ PreferredLifetime: 0xaabbccdd * time.Second, ValidLifetime: 0xeeff0011 * time.Second, Prefix: &net.IPNet{ Mask: net.CIDRMask(36, 128), IP: net.IPv6zero, }, Options: PrefixOptions{[]Option{OptElapsedTime(10 * time.Millisecond)}}, } toBytes := opt.ToBytes() if !bytes.Equal(toBytes, buf) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", buf, toBytes) } } func TestOptIAPrefixToBytesDefault(t *testing.T) { buf := []byte{ 0, 0, 0, 0, // preferredLifetime 0, 0, 0, 0, // validLifetime 0, // prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix } opt := OptIAPrefix{} toBytes := opt.ToBytes() if !bytes.Equal(toBytes, buf) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", buf, toBytes) } } func TestOptIAPrefixParseInvalidTooShort(t *testing.T) { buf := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime 0xee, 0xff, 0x00, 0x11, // validLifetime 36, // prefixLength 0, 0, 0, 0, 0, 0, 0, // truncated ipv6Prefix } if opt, err := ParseOptIAPrefix(buf); err == nil { t.Fatalf("ParseOptIAPrefix: Expected error on truncated option, got %v", opt) } } func TestOptIAPrefixString(t *testing.T) { buf := []byte{ 0x00, 0x00, 0x00, 60, // preferredLifetime 0x00, 0x00, 0x00, 50, // validLifetime 36, // prefixLength 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix } opt, err := ParseOptIAPrefix(buf) require.NoError(t, err) str := opt.String() require.Contains( t, str, "Prefix=2001:db8::/36", "String() should return the ipv6addr", ) require.Contains( t, str, "PreferredLifetime=1m", "String() should return the preferredlifetime", ) require.Contains( t, str, "ValidLifetime=50s", "String() should return the validlifetime", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_informationrefreshtime.go000066400000000000000000000026331431560352000320370ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "time" "github.com/u-root/uio/uio" ) // OptInformationRefreshTime implements OptionInformationRefreshTime option. // https://tools.ietf.org/html/rfc8415#section-21.23 func OptInformationRefreshTime(irt time.Duration) *optInformationRefreshTime { return &optInformationRefreshTime{irt} } // optInformationRefreshTime represents an OptionInformationRefreshTime. type optInformationRefreshTime struct { InformationRefreshtime time.Duration } // Code returns the option's code func (op *optInformationRefreshTime) Code() OptionCode { return OptionInformationRefreshTime } // ToBytes serializes the option and returns it as a sequence of bytes func (op *optInformationRefreshTime) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) irt := Duration{op.InformationRefreshtime} irt.Marshal(buf) return buf.Data() } func (op *optInformationRefreshTime) String() string { return fmt.Sprintf("InformationRefreshTime: %v", op.InformationRefreshtime) } // parseOptInformationRefreshTime builds an optInformationRefreshTime structure from a sequence // of bytes. The input data does not include option code and length bytes. func parseOptInformationRefreshTime(data []byte) (*optInformationRefreshTime, error) { var opt optInformationRefreshTime buf := uio.NewBigEndianBuffer(data) var irt Duration irt.Unmarshal(buf) opt.InformationRefreshtime = irt.Duration return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_informationrefreshtime_test.go000066400000000000000000000017751431560352000331040ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "time" ) func TestOptInformationRefreshTime(t *testing.T) { opt, err := parseOptInformationRefreshTime([]byte{0xaa, 0xbb, 0xcc, 0xdd}) if err != nil { t.Fatal(err) } if informationRefreshTime := opt.InformationRefreshtime; informationRefreshTime != time.Duration(0xaabbccdd) * time.Second { t.Fatalf("Invalid information refresh time. Expected 0xaabb, got %v", informationRefreshTime) } } func TestOptInformationRefreshTimeToBytes(t *testing.T) { opt := OptInformationRefreshTime(0) expected := []byte{0, 0, 0, 0} if toBytes := opt.ToBytes(); !bytes.Equal(expected, toBytes) { t.Fatalf("Invalid ToBytes output. Expected %v, got %v", expected, toBytes) } } func TestOptInformationRefreshTimeString(t *testing.T) { opt := OptInformationRefreshTime(3600 * time.Second) expected := "InformationRefreshTime: 1h0m0s" if optString := opt.String(); optString != expected { t.Fatalf("Invalid elapsed time string. Expected %v, got %v", expected, optString) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_interfaceid.go000066400000000000000000000013531431560352000275270ustar00rootroot00000000000000package dhcpv6 import ( "fmt" ) // OptInterfaceID returns an interface id option as defined by RFC 3315, // Section 22.18. func OptInterfaceID(id []byte) Option { return &optInterfaceID{ID: id} } type optInterfaceID struct { ID []byte } func (*optInterfaceID) Code() OptionCode { return OptionInterfaceID } func (op *optInterfaceID) ToBytes() []byte { return op.ID } func (op *optInterfaceID) String() string { return fmt.Sprintf("InterfaceID: %v", op.ID) } // build an optInterfaceID structure from a sequence of bytes. // The input data does not include option code and length bytes. func parseOptInterfaceID(data []byte) (*optInterfaceID, error) { var opt optInterfaceID opt.ID = append([]byte(nil), data...) return &opt, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_interfaceid_test.go000066400000000000000000000014011431560352000305600ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestParseOptInterfaceID(t *testing.T) { expected := []byte("DSLAM01 eth2/1/01/21") opt, err := parseOptInterfaceID(expected) if err != nil { t.Fatal(err) } if url := opt.ID; !bytes.Equal(url, expected) { t.Fatalf("Invalid Interface ID. Expected %v, got %v", expected, url) } } func TestOptInterfaceID(t *testing.T) { want := []byte("DSLAM01 eth2/1/01/21") opt := OptInterfaceID(want) if got := opt.ToBytes(); !bytes.Equal(got, want) { t.Fatalf("%s.ToBytes() = %v, want %v", opt, got, want) } require.Contains( t, opt.String(), "68 83 76 65 77 48 49 32 101 116 104 50 47 49 47 48 49 47 50 49", "String() should return the interfaceId as bytes", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_nii.go000066400000000000000000000041301431560352000260250ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // NetworkInterfaceType is the NIC type as defined by RFC 4578 Section 2.2 type NetworkInterfaceType uint8 // see rfc4578 const ( NII_LANDESK_NOPXE NetworkInterfaceType = 0 NII_PXE_GEN_I NetworkInterfaceType = 1 NII_PXE_GEN_II NetworkInterfaceType = 2 NII_UNDI_NOEFI NetworkInterfaceType = 3 NII_UNDI_EFI_GEN_I NetworkInterfaceType = 4 NII_UNDI_EFI_GEN_II NetworkInterfaceType = 5 ) func (nit NetworkInterfaceType) String() string { if s, ok := niiToStringMap[nit]; ok { return s } return fmt.Sprintf("NetworkInterfaceType(%d, unknown)", nit) } var niiToStringMap = map[NetworkInterfaceType]string{ NII_LANDESK_NOPXE: "LANDesk service agent boot ROMs. No PXE", NII_PXE_GEN_I: "First gen. PXE boot ROMs", NII_PXE_GEN_II: "Second gen. PXE boot ROMs", NII_UNDI_NOEFI: "UNDI 32/64 bit. UEFI drivers, no UEFI runtime", NII_UNDI_EFI_GEN_I: "UNDI 32/64 bit. UEFI runtime 1st gen", NII_UNDI_EFI_GEN_II: "UNDI 32/64 bit. UEFI runtime 2nd gen", } // OptNetworkInterfaceID implements the NIC ID option for network booting as // defined by RFC 4578 Section 2.2 and RFC 5970 Section 3.4. type OptNetworkInterfaceID struct { Typ NetworkInterfaceType // Revision number Major, Minor uint8 } // Code implements Option.Code. func (*OptNetworkInterfaceID) Code() OptionCode { return OptionNII } // ToBytes implements Option.ToBytes. func (op *OptNetworkInterfaceID) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write8(uint8(op.Typ)) buf.Write8(op.Major) buf.Write8(op.Minor) return buf.Data() } func (op *OptNetworkInterfaceID) String() string { return fmt.Sprintf("NetworkInterfaceID: %s (Revision %d.%d)", op.Typ, op.Major, op.Minor) } // FromBytes builds an OptNetworkInterfaceID structure from a sequence of // bytes. The input data does not include option code and length bytes. func (op *OptNetworkInterfaceID) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) op.Typ = NetworkInterfaceType(buf.Read8()) op.Major = buf.Read8() op.Minor = buf.Read8() return buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_nii_test.go000066400000000000000000000034761431560352000271000ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptNetworkInterfaceIdParse(t *testing.T) { expected := []byte{ 1, // type (UNDI) 3, // major revision 20, // minor revision } var opt OptNetworkInterfaceID err := opt.FromBytes(expected) require.NoError(t, err, "ParseOptNetworkInterfaceId() should not return an error with correct bytes") require.Equal(t, OptionNII, opt.Code(), OptionNII, "Code() should return 62 for OptNetworkInterfaceId") require.Equal(t, NetworkInterfaceType(1), opt.Typ, "Typ should return 1 for UNDI") require.Equal(t, uint8(3), opt.Major, "Major should return 1 for UNDI") require.Equal(t, uint8(20), opt.Minor, "Minor should return 1 for UNDI") } func TestOptNetworkInterfaceIdToBytes(t *testing.T) { expected := []byte{ 1, // type (UNDI) 3, // major revision 20, // minor revision } var opt OptNetworkInterfaceID opt.Typ = NetworkInterfaceType(1) opt.Major = 3 opt.Minor = 20 require.Equal(t, expected, opt.ToBytes()) } func TestOptNetworkInterfaceIdTooShort(t *testing.T) { buf := []byte{ 1, // type (UNDI) // missing major/minor revision bytes } var opt OptNetworkInterfaceID err := opt.FromBytes(buf) require.Error(t, err, "ParseOptNetworkInterfaceId() should return an error on truncated options") } func TestOptNetworkInterfaceIdString(t *testing.T) { buf := []byte{ 1, // type (UNDI) 3, // major revision 20, // minor revision } var opt OptNetworkInterfaceID err := opt.FromBytes(buf) require.NoError(t, err) require.Contains( t, opt.String(), "First gen. PXE boot ROMs (Revision 3.20)", "String() should contain the type and revision", ) opt.Typ = NetworkInterfaceType(200) require.Contains( t, opt.String(), "NetworkInterfaceType(200, unknown)", "String() should contain unknown for an unknown type", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_nontemporaryaddress.go000066400000000000000000000053561431560352000313640ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "time" "github.com/u-root/uio/uio" ) // Duration is a duration as embedded in IA messages (IAPD, IANA, IATA). type Duration struct { time.Duration } // Marshal encodes the time in uint32 seconds as defined by RFC 3315 for IANA // messages. func (d Duration) Marshal(buf *uio.Lexer) { buf.Write32(uint32(d.Duration.Round(time.Second) / time.Second)) } // Unmarshal decodes time from uint32 seconds as defined by RFC 3315 for IANA // messages. func (d *Duration) Unmarshal(buf *uio.Lexer) { t := buf.Read32() d.Duration = time.Duration(t) * time.Second } // IdentityOptions implement the options allowed for IA_NA and IA_TA messages. // // The allowed options are identified in RFC 3315 Appendix B. type IdentityOptions struct { Options } // Addresses returns the addresses assigned to the identity. func (io IdentityOptions) Addresses() []*OptIAAddress { opts := io.Options.Get(OptionIAAddr) var iaAddrs []*OptIAAddress for _, o := range opts { iaAddrs = append(iaAddrs, o.(*OptIAAddress)) } return iaAddrs } // OneAddress returns one address (of potentially many) assigned to the identity. func (io IdentityOptions) OneAddress() *OptIAAddress { a := io.Addresses() if len(a) == 0 { return nil } return a[0] } // Status returns the status code associated with this option. func (io IdentityOptions) Status() *OptStatusCode { opt := io.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // OptIANA implements the identity association for non-temporary addresses // option. // // This module defines the OptIANA structure. // https://www.ietf.org/rfc/rfc3633.txt type OptIANA struct { IaId [4]byte T1 time.Duration T2 time.Duration Options IdentityOptions } func (op *OptIANA) Code() OptionCode { return OptionIANA } // ToBytes serializes IANA to DHCPv6 bytes. func (op *OptIANA) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.WriteBytes(op.IaId[:]) t1 := Duration{op.T1} t1.Marshal(buf) t2 := Duration{op.T2} t2.Marshal(buf) buf.WriteBytes(op.Options.ToBytes()) return buf.Data() } func (op *OptIANA) String() string { return fmt.Sprintf("IANA: {IAID=%v, t1=%v, t2=%v, options=%v}", op.IaId, op.T1, op.T2, op.Options) } // ParseOptIANA builds an OptIANA structure from a sequence of bytes. The // input data does not include option code and length bytes. func ParseOptIANA(data []byte) (*OptIANA, error) { var opt OptIANA buf := uio.NewBigEndianBuffer(data) buf.ReadBytes(opt.IaId[:]) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) opt.T1 = t1.Duration opt.T2 = t2.Duration if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_nontemporaryaddress_test.go000066400000000000000000000062671431560352000324250ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIANAParseOptIANA(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } opt, err := ParseOptIANA(data) require.NoError(t, err) require.Equal(t, OptionIANA, opt.Code()) } func TestOptIANAParseOptIANAInvalidLength(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 // truncated from here } _, err := ParseOptIANA(data) require.Error(t, err) } func TestOptIANAParseOptIANAInvalidOptions(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options } _, err := ParseOptIANA(data) require.Error(t, err) } func TestOptIANAGetOneOption(t *testing.T) { oaddr := &OptIAAddress{ IPv6Addr: net.ParseIP("::1"), } opt := OptIANA{ Options: IdentityOptions{[]Option{&OptStatusCode{}, oaddr}}, } require.Equal(t, oaddr, opt.Options.OneAddress()) } func TestOptIANAAddOption(t *testing.T) { opt := OptIANA{} opt.Options.Add(OptElapsedTime(0)) require.Equal(t, 1, len(opt.Options.Options)) require.Equal(t, OptionElapsedTime, opt.Options.Options[0].Code()) } func TestOptIANAGetOneOptionMissingOpt(t *testing.T) { oaddr := &OptIAAddress{ IPv6Addr: net.ParseIP("::1"), } opt := OptIANA{ Options: IdentityOptions{[]Option{&OptStatusCode{}, oaddr}}, } require.Equal(t, nil, opt.Options.GetOne(OptionDNSRecursiveNameServer)) } func TestOptIANADelOption(t *testing.T) { optiaaddr := OptIAAddress{} optsc := OptStatusCode{} iana1 := OptIANA{ Options: IdentityOptions{[]Option{ &optsc, &optiaaddr, &optiaaddr, }}, } iana1.Options.Del(OptionIAAddr) require.Equal(t, iana1.Options.Options, Options{&optsc}) iana2 := OptIANA{ Options: IdentityOptions{[]Option{ &optiaaddr, &optsc, &optiaaddr, }}, } iana2.Options.Del(OptionIAAddr) require.Equal(t, iana2.Options.Options, Options{&optsc}) } func TestOptIANAToBytes(t *testing.T) { opt := OptIANA{ IaId: [4]byte{1, 2, 3, 4}, T1: 12345 * time.Second, T2: 54321 * time.Second, Options: IdentityOptions{[]Option{ OptElapsedTime(10 * time.Millisecond), }}, } expected := []byte{ 1, 2, 3, 4, // IA ID 0, 0, 0x30, 0x39, // T1 = 12345 0, 0, 0xd4, 0x31, // T2 = 54321 0, 8, 0, 2, 0x00, 0x01, } require.Equal(t, expected, opt.ToBytes()) } func TestOptIANAString(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 0, 0, 1, // T1 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } opt, err := ParseOptIANA(data) require.NoError(t, err) str := opt.String() require.Contains( t, str, "IAID=[1 0 0 0]", "String() should return the IAID", ) require.Contains( t, str, "t1=1s, t2=2s", "String() should return the T1/T2 options", ) require.Contains( t, str, "options={", "String() should return a list of options", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_ntp_server.go000066400000000000000000000103301431560352000274340ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "net" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/uio" ) // NTPSuboptionSrvAddr is NTP_SUBOPTION_SRV_ADDR according to RFC 5908. type NTPSuboptionSrvAddr net.IP // Code returns the suboption code. func (n *NTPSuboptionSrvAddr) Code() OptionCode { return NTPSuboptionSrvAddrCode } // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionSrvAddr) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(NTPSuboptionSrvAddrCode)) buf.Write16(uint16(net.IPv6len)) buf.WriteBytes(net.IP(*n).To16()) return buf.Data() } func (n *NTPSuboptionSrvAddr) String() string { return fmt.Sprintf("Server Address: %s", net.IP(*n).String()) } // NTPSuboptionMCAddr is NTP_SUBOPTION_MC_ADDR according to RFC 5908. type NTPSuboptionMCAddr net.IP // Code returns the suboption code. func (n *NTPSuboptionMCAddr) Code() OptionCode { return NTPSuboptionMCAddrCode } // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionMCAddr) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(NTPSuboptionMCAddrCode)) buf.Write16(uint16(net.IPv6len)) buf.WriteBytes(net.IP(*n).To16()) return buf.Data() } func (n *NTPSuboptionMCAddr) String() string { return fmt.Sprintf("Multicast Address: %s", net.IP(*n).String()) } // NTPSuboptionSrvFQDN is NTP_SUBOPTION_SRV_FQDN according to RFC 5908. type NTPSuboptionSrvFQDN rfc1035label.Labels // Code returns the suboption code. func (n *NTPSuboptionSrvFQDN) Code() OptionCode { return NTPSuboptionSrvFQDNCode } // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionSrvFQDN) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(NTPSuboptionSrvFQDNCode)) l := rfc1035label.Labels(*n) buf.Write16(uint16(l.Length())) buf.WriteBytes(l.ToBytes()) return buf.Data() } func (n *NTPSuboptionSrvFQDN) String() string { l := rfc1035label.Labels(*n) return fmt.Sprintf("Server FQDN: %s", l.String()) } // NTPSuboptionSrvAddr is the value of NTP_SUBOPTION_SRV_ADDR according to RFC 5908. const ( NTPSuboptionSrvAddrCode = OptionCode(1) NTPSuboptionMCAddrCode = OptionCode(2) NTPSuboptionSrvFQDNCode = OptionCode(3) ) // parseNTPSuboption implements the OptionParser interface. func parseNTPSuboption(code OptionCode, data []byte) (Option, error) { //var o Options buf := uio.NewBigEndianBuffer(data) length := len(data) data, err := buf.ReadN(length) if err != nil { return nil, fmt.Errorf("failed to read %d bytes for suboption: %w", length, err) } switch code { case NTPSuboptionSrvAddrCode, NTPSuboptionMCAddrCode: if length != net.IPv6len { return nil, fmt.Errorf("invalid suboption length, want %d, got %d", net.IPv6len, length) } var so Option switch code { case NTPSuboptionSrvAddrCode: sos := NTPSuboptionSrvAddr(data) so = &sos case NTPSuboptionMCAddrCode: som := NTPSuboptionMCAddr(data) so = &som } return so, nil case NTPSuboptionSrvFQDNCode: l, err := rfc1035label.FromBytes(data) if err != nil { return nil, fmt.Errorf("failed to parse rfc1035 labels: %w", err) } // TODO according to rfc3315, this label must not be compressed. // Need to add support for compression detection to the // `rfc1035label` package in order to do that. so := NTPSuboptionSrvFQDN(*l) return &so, nil default: gopt := OptionGeneric{OptionCode: code, OptionData: data} return &gopt, nil } } // ParseOptNTPServer parses a sequence of bytes into an OptNTPServer object. func ParseOptNTPServer(data []byte) (*OptNTPServer, error) { var so Options if err := so.FromBytesWithParser(data, parseNTPSuboption); err != nil { return nil, err } return &OptNTPServer{ Suboptions: so, }, nil } // OptNTPServer is an option NTP server as defined by RFC 5908. type OptNTPServer struct { Suboptions Options } // Code returns the option code func (op *OptNTPServer) Code() OptionCode { return OptionNTPServer } // ToBytes returns the option serialized to bytes. func (op *OptNTPServer) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, so := range op.Suboptions { buf.WriteBytes(so.ToBytes()) } return buf.Data() } func (op *OptNTPServer) String() string { return fmt.Sprintf("NTP: %v", op.Suboptions) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_ntp_server_test.go000066400000000000000000000051351431560352000305020ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSuboptionSrvAddr(t *testing.T) { ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") so := NTPSuboptionSrvAddr(ip) assert.Equal(t, NTPSuboptionSrvAddrCode, so.Code()) expected := append([]byte{0x00, 0x01, 0x00, 0x10}, ip...) assert.Equal(t, expected, so.ToBytes()) } func TestSuboptionMCAddr(t *testing.T) { ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") so := NTPSuboptionMCAddr(ip) assert.Equal(t, NTPSuboptionMCAddrCode, so.Code()) expected := append([]byte{0x00, 0x02, 0x00, 0x10}, ip...) assert.Equal(t, expected, so.ToBytes()) } func TestSuboptionSrvFQDN(t *testing.T) { fqdn, err := rfc1035label.FromBytes([]byte("\x03ntp\x07example\x03com")) require.NoError(t, err) so := NTPSuboptionSrvFQDN(*fqdn) assert.Equal(t, NTPSuboptionSrvFQDNCode, so.Code()) expected := append([]byte{0x00, 0x03, 0x00, 0x10}, fqdn.ToBytes()...) assert.Equal(t, expected, so.ToBytes()) } func TestSuboptionGeneric(t *testing.T) { data := []byte{ 0xff, 0xff, // unknown sub-option type 0x00, 0x04, // length, 4 bytes 0x74, 0x65, 0x73, 0x74, // the ASCII bytes for the string "test" } o, err := ParseOptNTPServer(data) require.NoError(t, err) require.Equal(t, 1, len(o.Suboptions)) assert.IsType(t, &OptionGeneric{}, o.Suboptions[0]) og := o.Suboptions[0].(*OptionGeneric) assert.Equal(t, []byte("test"), og.ToBytes()) } func TestParseOptNTPServer(t *testing.T) { ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") fqdn, err := rfc1035label.FromBytes([]byte("\x03ntp\x07example\x03com")) require.NoError(t, err) // add server address sub-option data := []byte{ 0x00, 0x01, // sub-option type 0x00, 0x10, // length (16, IPv6 address) } data = append(data, []byte(ip)...) // add server FQDN sub-option data = append(data, []byte{ 0x00, 0x03, // sub-option type 0x00, 0x10, // length (16, the FQDN "ntp.example.com." as rfc1035 label) }...) data = append(data, fqdn.ToBytes()...) o, err := ParseOptNTPServer(data) require.NoError(t, err) require.NotNil(t, o) assert.Equal(t, 2, len(o.Suboptions)) optAddr, ok := o.Suboptions[0].(*NTPSuboptionSrvAddr) require.True(t, ok) assert.Equal(t, ip, net.IP(*optAddr)) optFQDN, ok := o.Suboptions[1].(*NTPSuboptionSrvFQDN) require.True(t, ok) assert.Equal(t, *fqdn, rfc1035label.Labels(*optFQDN)) var mo MessageOptions assert.Nil(t, mo.NTPServers()) mo.Add(o) // MessageOptions.NTPServers only returns server address values. assert.Equal(t, []net.IP{ip}, mo.NTPServers()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_relaymsg.go000066400000000000000000000015031431560352000270720ustar00rootroot00000000000000package dhcpv6 // This module defines the optRelayMsg structure. // https://www.ietf.org/rfc/rfc3315.txt import ( "fmt" ) // OptRelayMessage embeds a message in a relay option. func OptRelayMessage(msg DHCPv6) Option { return &optRelayMsg{Msg: msg} } type optRelayMsg struct { Msg DHCPv6 } func (op *optRelayMsg) Code() OptionCode { return OptionRelayMsg } func (op *optRelayMsg) ToBytes() []byte { return op.Msg.ToBytes() } func (op *optRelayMsg) String() string { return fmt.Sprintf("RelayMsg: %v", op.Msg) } // build an optRelayMsg structure from a sequence of bytes. // The input data does not include option code and length bytes. func parseOptRelayMsg(data []byte) (*optRelayMsg, error) { var err error var opt optRelayMsg opt.Msg, err = FromBytes(data) if err != nil { return nil, err } return &opt, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_relaymsg_test.go000066400000000000000000000117141431560352000301360ustar00rootroot00000000000000package dhcpv6 import ( "reflect" "testing" "time" "github.com/stretchr/testify/require" ) func TestRelayMsgParseOptRelayMsg(t *testing.T) { opt, err := parseOptRelayMsg([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time 0, 2, // option length 0, 0, // option value }) if err != nil { t.Fatal(err) } if code := opt.Code(); code != OptionRelayMsg { t.Fatalf("Invalid option code. Expected OptionRelayMsg (%v), got %v", OptionRelayMsg, code, ) } } func TestRelayMsgOptionsFromBytes(t *testing.T) { var opts Options err := opts.FromBytes([]byte{ 0, 9, // option: relay message 0, 10, // relayed message length 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time 0, 2, // option length 0, 0, // option value }) if err != nil { t.Fatal(err) } if len(opts) != 1 { t.Fatalf("Invalid number of options. Expected 1, got %v", len(opts)) } opt := opts[0] if code := opt.Code(); code != OptionRelayMsg { t.Fatalf("Invalid option code. Expected OptionRelayMsg (%v), got %v", OptionRelayMsg, code, ) } } func TestRelayMsgParseOptRelayMsgSingleEncapsulation(t *testing.T) { d, err := FromBytes([]byte{ 12, // RELAY-FORW 0, // hop count 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // linkAddr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, // peerAddr 0, 9, // option: relay message 0, 10, // relayed message length 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time 0, 2, // option length 0x00, 0x01, // option value }) if err != nil { t.Fatal(err) } r, ok := d.(*RelayMessage) if !ok { t.Fatalf("Invalid DHCPv6 type. Expected RelayMessage, got %v", reflect.TypeOf(d), ) } if mType := r.Type(); mType != MessageTypeRelayForward { t.Fatalf("Invalid messge type for relay. Expected %v, got %v", MessageTypeRelayForward, mType) } if len(r.Options.Options) != 1 { t.Fatalf("Invalid number of options. Expected 1, got %v", len(r.Options.Options)) } ro := r.Options.RelayMessage() if ro == nil { t.Fatalf("No relay message") } innerDHCP, ok := ro.(*Message) if !ok { t.Fatalf("Invalid relay message type. Expected Message, got %v", reflect.TypeOf(innerDHCP), ) } if dType := innerDHCP.Type(); dType != MessageTypeSolicit { t.Fatalf("Invalid inner DHCP type. Expected MessageTypeSolicit (%v), got %v", MessageTypeSolicit, dType, ) } xid := TransactionID{0xaa, 0xbb, 0xcc} if tID := innerDHCP.TransactionID; tID != xid { t.Fatalf("Invalid inner DHCP transaction ID. Expected 0xaabbcc, got %v", tID) } if len(innerDHCP.Options.Options) != 1 { t.Fatalf("Invalid inner DHCP options length. Expected 1, got %v", len(innerDHCP.Options.Options)) } eTime := innerDHCP.Options.ElapsedTime() if eTime != 10*time.Millisecond { t.Fatalf("Invalid elapsed time. Expected 0x1122, got 0x%04x", eTime) } } func TestSample(t *testing.T) { // Nested relay message. This test only checks if it parses correctly, but // could/should be extended to check all the fields like done in other tests buf := []byte{ 12, // relay 1, // hop count 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // linkAddr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // peerAddr // relay msg 0, 9, // opt relay msg 0, 66, // opt len // relay fwd 12, // relay 0, // hop count 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // linkAddr 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // peerAddr // opt interface ID 0, 18, // interface id 0, 6, // opt len 0xba, 0xbe, 0xb1, 0xb0, 0xbe, 0xbe, // opt value // relay msg 0, 9, // relay msg 0, 18, // msg len // dhcpv6 msg 1, // solicit 0xaa, 0xbb, 0xcc, // transaction ID // client ID 0, 1, // opt client id 0, 10, // opt len 0, 3, // duid type 0, 1, // hw type 5, 6, 7, 8, 9, 10, // duid value } _, err := FromBytes(buf) if err != nil { t.Fatal(err) } } func TestRelayMsgParseOptRelayMsgTooShort(t *testing.T) { _, err := parseOptRelayMsg([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time // no length/value for elapsed time option }) require.Error(t, err, "ParseOptRelayMsg() should return an error if the encapsulated message is invalid") } func TestRelayMsgString(t *testing.T) { opt, err := parseOptRelayMsg([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time 0, 2, // option length 0, 0, // option value }) require.NoError(t, err) require.Contains( t, opt.String(), "RelayMsg: Message", "String() should contain the relaymsg contents", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_relayport.go000066400000000000000000000017661431560352000273030ustar00rootroot00000000000000// This module defines the optRelayPort structure. // https://www.ietf.org/rfc/rfc8357.txt package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // OptRelayPort specifies an UDP port to use for the downstream relay func OptRelayPort(port uint16) Option { return &optRelayPort{DownstreamSourcePort: port} } type optRelayPort struct { DownstreamSourcePort uint16 } func (op *optRelayPort) Code() OptionCode { return OptionRelayPort } func (op *optRelayPort) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(op.DownstreamSourcePort) return buf.Data() } func (op *optRelayPort) String() string { return fmt.Sprintf("RelayPort: %d", op.DownstreamSourcePort) } // build an optRelayPort structure from a sequence of bytes. // The input data does not include option code and length bytes. func parseOptRelayPort(data []byte) (*optRelayPort, error) { var opt optRelayPort buf := uio.NewBigEndianBuffer(data) opt.DownstreamSourcePort = buf.Read16() return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_relayport_test.go000066400000000000000000000006061431560352000303320ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestParseRelayPort(t *testing.T) { opt, err := parseOptRelayPort([]byte{0x12, 0x32}) require.NoError(t, err) require.Equal(t, &optRelayPort{DownstreamSourcePort: 0x1232}, opt) } func TestRelayPortToBytes(t *testing.T) { op := OptRelayPort(0x3845) require.Equal(t, []byte{0x38, 0x45}, op.ToBytes()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_remoteid.go000066400000000000000000000020111431560352000270520ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // OptRemoteID implemens the Remote ID option as defined by RFC 4649. type OptRemoteID struct { EnterpriseNumber uint32 RemoteID []byte } // Code implements Option.Code. func (*OptRemoteID) Code() OptionCode { return OptionRemoteID } // ToBytes serializes this option to a byte stream. func (op *OptRemoteID) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write32(op.EnterpriseNumber) buf.WriteBytes(op.RemoteID) return buf.Data() } func (op *OptRemoteID) String() string { return fmt.Sprintf("RemoteID: EnterpriseNumber %d RemoteID %v", op.EnterpriseNumber, op.RemoteID, ) } // ParseOptRemoteId builds an OptRemoteId structure from a sequence of bytes. // The input data does not include option code and length bytes. func ParseOptRemoteID(data []byte) (*OptRemoteID, error) { var opt OptRemoteID buf := uio.NewBigEndianBuffer(data) opt.EnterpriseNumber = buf.Read32() opt.RemoteID = buf.ReadAll() return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_remoteid_test.go000066400000000000000000000030571431560352000301240ustar00rootroot00000000000000package dhcpv6 import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestOptRemoteID(t *testing.T) { expected := []byte{0xaa, 0xbb, 0xcc, 0xdd} remoteId := []byte("DSLAM01 eth2/1/01/21") expected = append(expected, remoteId...) opt, err := ParseOptRemoteID(expected) if err != nil { t.Fatal(err) } if en := opt.EnterpriseNumber; en != 0xaabbccdd { t.Fatalf("Invalid Enterprise Number. Expected 0xaabbccdd, got %v", en) } if rid := opt.RemoteID; !bytes.Equal(rid, remoteId) { t.Fatalf("Invalid Remote ID. Expected %v, got %v", expected, rid) } } func TestOptRemoteIDToBytes(t *testing.T) { remoteId := []byte("DSLAM01 eth2/1/01/21") expected := append([]byte{0, 0, 0, 0}, remoteId...) opt := OptRemoteID{ RemoteID: remoteId, } toBytes := opt.ToBytes() if !bytes.Equal(toBytes, expected) { t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes) } } func TestOptRemoteIDParseOptRemoteIDTooShort(t *testing.T) { buf := []byte{0xaa, 0xbb, 0xcc} _, err := ParseOptRemoteID(buf) require.Error(t, err, "A short option should return an error") } func TestOptRemoteIDString(t *testing.T) { buf := []byte{0xaa, 0xbb, 0xcc, 0xdd} remoteId := []byte("Test1234") buf = append(buf, remoteId...) opt, err := ParseOptRemoteID(buf) require.NoError(t, err) str := opt.String() require.Contains( t, str, "EnterpriseNumber 2864434397", "String() should contain the enterprisenum", ) require.Contains( t, str, "RemoteID [84 101 115 116 49 50 51 52]", "String() should contain the remoteid bytes", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_requestedoption.go000066400000000000000000000030061431560352000305010ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "strings" "github.com/u-root/uio/uio" ) // OptionCodes are a collection of option codes. type OptionCodes []OptionCode // Add adds an option to the list, ignoring duplicates. func (o *OptionCodes) Add(c OptionCode) { if !o.Contains(c) { *o = append(*o, c) } } // Contains returns whether the option codes contain c. func (o OptionCodes) Contains(c OptionCode) bool { for _, oo := range o { if oo == c { return true } } return false } // ToBytes implements Option.ToBytes. func (o OptionCodes) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, ro := range o { buf.Write16(uint16(ro)) } return buf.Data() } func (o OptionCodes) String() string { names := make([]string, 0, len(o)) for _, code := range o { names = append(names, code.String()) } return strings.Join(names, ", ") } // FromBytes populates o from binary-encoded data. func (o *OptionCodes) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) for buf.Has(2) { o.Add(OptionCode(buf.Read16())) } return buf.FinError() } // OptRequestedOption implements the requested options option as defined by RFC // 3315 Section 22.7. func OptRequestedOption(o ...OptionCode) Option { return &optRequestedOption{ OptionCodes: o, } } type optRequestedOption struct { OptionCodes } // Code implements Option.Code. func (*optRequestedOption) Code() OptionCode { return OptionORO } func (op *optRequestedOption) String() string { return fmt.Sprintf("RequestedOptions: %s", op.OptionCodes) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_requestedoption_test.go000066400000000000000000000017411431560352000315440ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptRequestedOption(t *testing.T) { expected := []byte{0, 1, 0, 2} var o optRequestedOption err := o.FromBytes(expected) require.NoError(t, err, "ParseOptRequestedOption() correct options should not error") } func TestOptRequestedOptionParseOptRequestedOptionTooShort(t *testing.T) { buf := []byte{0, 1, 0} var o optRequestedOption err := o.FromBytes(buf) require.Error(t, err, "A short option should return an error (must be divisible by 2)") } func TestOptRequestedOptionString(t *testing.T) { buf := []byte{0, 1, 0, 2} var o optRequestedOption err := o.FromBytes(buf) require.NoError(t, err) require.Contains( t, o.String(), "Client Identifier, Server Identifier", "String() should contain the options specified", ) o.OptionCodes = append(o.OptionCodes, 12345) require.Contains( t, o.String(), "unknown", "String() should contain 'Unknown' for an illegal option", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_serverid.go000066400000000000000000000012671431560352000271010ustar00rootroot00000000000000package dhcpv6 import ( "fmt" ) // OptServerID represents a Server Identifier option as defined by RFC 3315 // Section 22.1. func OptServerID(d Duid) Option { return &optServerID{d} } type optServerID struct { Duid } func (*optServerID) Code() OptionCode { return OptionServerID } func (op *optServerID) String() string { return fmt.Sprintf("ServerID: %v", op.Duid.String()) } // parseOptServerID builds an optServerID structure from a sequence of bytes. // The input data does not include option code and length bytes. func parseOptServerID(data []byte) (*optServerID, error) { sid, err := DuidFromBytes(data) if err != nil { return nil, err } return &optServerID{*sid}, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_serverid_test.go000066400000000000000000000040301431560352000301270ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseOptServerID(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 0, 1, 2, 3, 4, 5, // hw addr } opt, err := parseOptServerID(data) require.NoError(t, err) require.Equal(t, DUID_LL, opt.Type) require.Equal(t, iana.HWTypeEthernet, opt.HwType) require.Equal(t, net.HardwareAddr{0, 1, 2, 3, 4, 5}, opt.LinkLayerAddr) } func TestOptServerIdToBytes(t *testing.T) { opt := OptServerID( Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{5, 4, 3, 2, 1, 0}, }, ) expected := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } require.Equal(t, expected, opt.ToBytes()) } func TestOptServerIdDecodeEncode(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } opt, err := parseOptServerID(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } func TestOptionServerId(t *testing.T) { opt := OptServerID( Duid{ Type: DUID_LL, HwType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xde, 0xad, 0, 0, 0xbe, 0xef}, }, ) require.Equal(t, OptionServerID, opt.Code()) require.Contains( t, opt.String(), "ServerID: DUID{type=DUID-LL hwtype=Ethernet hwaddr=de:ad:00:00:be:ef}", "String() should contain the correct sid output", ) } func TestOptServerIdparseOptServerIDBogusDUID(t *testing.T) { data := []byte{ 0, 4, // DUID_UUID 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17 10, 11, 12, 13, 14, 15, 16, 17, } _, err := parseOptServerID(data) require.Error(t, err, "A truncated OptServerId DUID should return an error") } func TestOptServerIdparseOptServerIDInvalidTooShort(t *testing.T) { data := []byte{ 0, // truncated: DUIDs are at least 2 bytes } _, err := parseOptServerID(data) require.Error(t, err, "A truncated OptServerId should return an error") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_statuscode.go000066400000000000000000000024101431560352000274230ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/insomniacslk/dhcp/iana" "github.com/u-root/uio/uio" ) // OptStatusCode represents a DHCPv6 Status Code option // // This module defines the OptStatusCode structure. // https://www.ietf.org/rfc/rfc3315.txt type OptStatusCode struct { StatusCode iana.StatusCode StatusMessage string } // Code returns the option code. func (op *OptStatusCode) Code() OptionCode { return OptionStatusCode } // ToBytes serializes the option and returns it as a sequence of bytes. func (op *OptStatusCode) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write16(uint16(op.StatusCode)) buf.WriteBytes([]byte(op.StatusMessage)) return buf.Data() } // String returns a human-readable option. func (op *OptStatusCode) String() string { return fmt.Sprintf("StatusCode: Code: %s (%d); Message: %s", op.StatusCode, op.StatusCode, op.StatusMessage) } // ParseOptStatusCode builds an OptStatusCode structure from a sequence of // bytes. The input data does not include option code and length bytes. func ParseOptStatusCode(data []byte) (*OptStatusCode, error) { var opt OptStatusCode buf := uio.NewBigEndianBuffer(data) opt.StatusCode = iana.StatusCode(buf.Read16()) opt.StatusMessage = string(buf.ReadAll()) return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_statuscode_test.go000066400000000000000000000024321431560352000304660ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseOptStatusCode(t *testing.T) { data := []byte{ 0, 5, // StatusUseMulticast 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't', } opt, err := ParseOptStatusCode(data) require.NoError(t, err) require.Equal(t, iana.StatusUseMulticast, opt.StatusCode) require.Equal(t, "use multicast", opt.StatusMessage) } func TestOptStatusCodeToBytes(t *testing.T) { expected := []byte{ 0, 0, // StatusSuccess 's', 'u', 'c', 'c', 'e', 's', 's', } opt := OptStatusCode{ StatusCode: iana.StatusSuccess, StatusMessage: "success", } actual := opt.ToBytes() require.Equal(t, expected, actual) } func TestOptStatusCodeParseOptStatusCodeTooShort(t *testing.T) { _, err := ParseOptStatusCode([]byte{0}) require.Error(t, err, "ParseOptStatusCode: Expected error on truncated option") } func TestOptStatusCodeString(t *testing.T) { data := []byte{ 0, 5, // StatusUseMulticast 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't', } opt, err := ParseOptStatusCode(data) require.NoError(t, err) require.Contains( t, opt.String(), "Code: UseMulticast (5); Message: use multicast", "String() should contain the code and message", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_temporaryaddress.go000066400000000000000000000021351431560352000306410ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // OptIATA implements the identity association for non-temporary addresses // option. // // This module defines the OptIATA structure. // https://www.ietf.org/rfc/rfc8415.txt type OptIATA struct { IaId [4]byte Options IdentityOptions } // Code returns the option code for an IA_TA func (op *OptIATA) Code() OptionCode { return OptionIATA } // ToBytes serializes IATA to DHCPv6 bytes. func (op *OptIATA) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.WriteBytes(op.IaId[:]) buf.WriteBytes(op.Options.ToBytes()) return buf.Data() } func (op *OptIATA) String() string { return fmt.Sprintf("IATA: {IAID=%v, options=%v}", op.IaId, op.Options) } // ParseOptIATA builds an OptIATA structure from a sequence of bytes. The // input data does not include option code and length bytes. func ParseOptIATA(data []byte) (*OptIATA, error) { var opt OptIATA buf := uio.NewBigEndianBuffer(data) buf.ReadBytes(opt.IaId[:]) if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { return nil, err } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_temporaryaddress_test.go000066400000000000000000000054731431560352000317100ustar00rootroot00000000000000package dhcpv6 import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestOptIATAParseOptIATA(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } opt, err := ParseOptIATA(data) require.NoError(t, err) require.Equal(t, OptionIATA, opt.Code()) } func TestOptIATAParseOptIATAInvalidLength(t *testing.T) { data := []byte{ 1, 0, 0, // truncated IAID } _, err := ParseOptIATA(data) require.Error(t, err) } func TestOptIATAParseOptIATAInvalidOptions(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options } _, err := ParseOptIATA(data) require.Error(t, err) } func TestOptIATAGetOneOption(t *testing.T) { oaddr := &OptIAAddress{ IPv6Addr: net.ParseIP("::1"), } opt := OptIATA{ Options: IdentityOptions{[]Option{&OptStatusCode{}, oaddr}}, } require.Equal(t, oaddr, opt.Options.OneAddress()) } func TestOptIATAAddOption(t *testing.T) { opt := OptIATA{} opt.Options.Add(OptElapsedTime(0)) require.Equal(t, 1, len(opt.Options.Options)) require.Equal(t, OptionElapsedTime, opt.Options.Options[0].Code()) } func TestOptIATAGetOneOptionMissingOpt(t *testing.T) { oaddr := &OptIAAddress{ IPv6Addr: net.ParseIP("::1"), } opt := OptIATA{ Options: IdentityOptions{[]Option{&OptStatusCode{}, oaddr}}, } require.Equal(t, nil, opt.Options.GetOne(OptionDNSRecursiveNameServer)) } func TestOptIATADelOption(t *testing.T) { optiaaddr := OptIAAddress{} optsc := OptStatusCode{} iana1 := OptIATA{ Options: IdentityOptions{[]Option{ &optsc, &optiaaddr, &optiaaddr, }}, } iana1.Options.Del(OptionIAAddr) require.Equal(t, iana1.Options.Options, Options{&optsc}) iana2 := OptIATA{ Options: IdentityOptions{[]Option{ &optiaaddr, &optsc, &optiaaddr, }}, } iana2.Options.Del(OptionIAAddr) require.Equal(t, iana2.Options.Options, Options{&optsc}) } func TestOptIATAToBytes(t *testing.T) { opt := OptIATA{ IaId: [4]byte{1, 2, 3, 4}, Options: IdentityOptions{[]Option{ OptElapsedTime(10 * time.Millisecond), }}, } expected := []byte{ 1, 2, 3, 4, // IA ID 0, 8, 0, 2, 0x00, 0x01, } require.Equal(t, expected, opt.ToBytes()) } func TestOptIATAString(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } opt, err := ParseOptIATA(data) require.NoError(t, err) str := opt.String() require.Contains( t, str, "IAID=[1 0 0 0]", "String() should return the IAID", ) require.Contains( t, str, "options={", "String() should return a list of options", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_userclass.go000066400000000000000000000025671431560352000272660ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "strings" "github.com/u-root/uio/uio" ) // OptUserClass represent a DHCPv6 User Class option // // This module defines the OptUserClass structure. // https://www.ietf.org/rfc/rfc3315.txt type OptUserClass struct { UserClasses [][]byte } // Code returns the option code func (op *OptUserClass) Code() OptionCode { return OptionUserClass } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptUserClass) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, uc := range op.UserClasses { buf.Write16(uint16(len(uc))) buf.WriteBytes(uc) } return buf.Data() } func (op *OptUserClass) String() string { ucStrings := make([]string, 0, len(op.UserClasses)) for _, uc := range op.UserClasses { ucStrings = append(ucStrings, string(uc)) } return fmt.Sprintf("OptUserClass{userclass=[%s]}", strings.Join(ucStrings, ", ")) } // ParseOptUserClass builds an OptUserClass structure from a sequence of // bytes. The input data does not include option code and length bytes. func ParseOptUserClass(data []byte) (*OptUserClass, error) { var opt OptUserClass if len(data) == 0 { return nil, fmt.Errorf("user class option must not be empty") } buf := uio.NewBigEndianBuffer(data) for buf.Has(2) { len := buf.Read16() opt.UserClasses = append(opt.UserClasses, buf.CopyN(int(len))) } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_userclass_test.go000066400000000000000000000043401431560352000303140ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestParseOptUserClass(t *testing.T) { expected := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } opt, err := ParseOptUserClass(expected) require.NoError(t, err) require.Equal(t, 1, len(opt.UserClasses)) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) } func TestParseOptUserClassMultiple(t *testing.T) { expected := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } opt, err := ParseOptUserClass(expected) require.NoError(t, err) require.Equal(t, len(opt.UserClasses), 2) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) require.Equal(t, []byte("test"), opt.UserClasses[1]) } func TestParseOptUserClassNone(t *testing.T) { expected := []byte{} _, err := ParseOptUserClass(expected) require.Error(t, err) } func TestOptUserClassToBytes(t *testing.T) { opt := OptUserClass{ UserClasses: [][]byte{[]byte("linuxboot")}, } data := opt.ToBytes() expected := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } require.Equal(t, expected, data) } func TestOptUserClassToBytesMultiple(t *testing.T) { opt := OptUserClass{ UserClasses: [][]byte{ []byte("linuxboot"), []byte("test"), }, } data := opt.ToBytes() expected := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } require.Equal(t, expected, data) } func TestOptUserClassParseOptUserClassTooShort(t *testing.T) { buf := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', } _, err := ParseOptUserClass(buf) require.Error(t, err, "ParseOptUserClass() should error if given truncated user classes") buf = []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, } _, err = ParseOptUserClass(buf) require.Error(t, err, "ParseOptUserClass() should error if given a truncated length") } func TestOptUserClassString(t *testing.T) { data := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } opt, err := ParseOptUserClass(data) require.NoError(t, err) require.Contains( t, opt.String(), "userclass=[linuxboot, test]", "String() should contain the list of user classes", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_vendor_opts.go000066400000000000000000000033011431560352000276070ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // OptVendorOpts represents a DHCPv6 Status Code option // // This module defines the OptVendorOpts structure. // https://tools.ietf.org/html/rfc3315#section-22.17 type OptVendorOpts struct { EnterpriseNumber uint32 VendorOpts Options } // Code returns the option code func (op *OptVendorOpts) Code() OptionCode { return OptionVendorOpts } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptVendorOpts) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write32(op.EnterpriseNumber) buf.WriteData(op.VendorOpts.ToBytes()) return buf.Data() } // String returns a string representation of the VendorOpts data func (op *OptVendorOpts) String() string { return fmt.Sprintf("OptVendorOpts{enterprisenum=%v, vendorOpts=%v}", op.EnterpriseNumber, op.VendorOpts, ) } // ParseOptVendorOpts builds an OptVendorOpts structure from a sequence of bytes. // The input data does not include option code and length bytes. func ParseOptVendorOpts(data []byte) (*OptVendorOpts, error) { var opt OptVendorOpts buf := uio.NewBigEndianBuffer(data) opt.EnterpriseNumber = buf.Read32() if err := opt.VendorOpts.FromBytesWithParser(buf.ReadAll(), vendParseOption); err != nil { return nil, err } return &opt, buf.FinError() } // vendParseOption builds a GenericOption from a slice of bytes // We cannot use the existing ParseOption function in options.go because the // sub-options include codes specific to each vendor. There are overlaps in these // codes with RFC standard codes. func vendParseOption(code OptionCode, data []byte) (Option, error) { return &OptionGeneric{OptionCode: code, OptionData: data}, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_vendor_opts_test.go000066400000000000000000000024771431560352000306630ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestOptVendorOpts(t *testing.T) { optData := []byte("Arista;DCS-7304;01.00;HSH14425148") // NOTE: this should be aware of endianness expected := []byte{0xaa, 0xbb, 0xcc, 0xdd} expected = append(expected, []byte{0, 1, //code 0, byte(len(optData)), //length }...) expected = append(expected, optData...) expectedOpts := OptVendorOpts{} var vendorOpts []Option expectedOpts.VendorOpts = append(vendorOpts, &OptionGeneric{OptionCode: 1, OptionData: optData}) opt, err := ParseOptVendorOpts(expected) require.NoError(t, err) require.Equal(t, uint32(0xaabbccdd), opt.EnterpriseNumber) require.Equal(t, expectedOpts.VendorOpts, opt.VendorOpts) shortData := make([]byte, 1) _, err = ParseOptVendorOpts(shortData) require.Error(t, err) } func TestOptVendorOptsToBytes(t *testing.T) { optData := []byte("Arista;DCS-7304;01.00;HSH14425148") var opts []Option opts = append(opts, &OptionGeneric{OptionCode: 1, OptionData: optData}) expected := append([]byte{ 0, 0, 0, 0, // EnterpriseNumber 0, 1, // Sub-Option code from vendor 0, byte(len(optData)), // Length of optionData only }, optData...) opt := OptVendorOpts{ EnterpriseNumber: 0000, VendorOpts: opts, } toBytes := opt.ToBytes() require.Equal(t, expected, toBytes) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_vendorclass.go000066400000000000000000000027771431560352000276100ustar00rootroot00000000000000package dhcpv6 import ( "errors" "fmt" "strings" "github.com/u-root/uio/uio" ) // OptVendorClass represents a DHCPv6 Vendor Class option type OptVendorClass struct { EnterpriseNumber uint32 Data [][]byte } // Code returns the option code func (op *OptVendorClass) Code() OptionCode { return OptionVendorClass } // ToBytes serializes the option and returns it as a sequence of bytes func (op *OptVendorClass) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write32(op.EnterpriseNumber) for _, data := range op.Data { buf.Write16(uint16(len(data))) buf.WriteBytes(data) } return buf.Data() } // String returns a string representation of the VendorClass data func (op *OptVendorClass) String() string { vcStrings := make([]string, 0) for _, data := range op.Data { vcStrings = append(vcStrings, string(data)) } return fmt.Sprintf("OptVendorClass{enterprisenum=%d, data=[%s]}", op.EnterpriseNumber, strings.Join(vcStrings, ", ")) } // ParseOptVendorClass builds an OptVendorClass structure from a sequence of // bytes. The input data does not include option code and length bytes. func ParseOptVendorClass(data []byte) (*OptVendorClass, error) { var opt OptVendorClass buf := uio.NewBigEndianBuffer(data) opt.EnterpriseNumber = buf.Read32() for buf.Has(2) { len := buf.Read16() opt.Data = append(opt.Data, buf.CopyN(int(len))) } if len(opt.Data) < 1 { return nil, errors.New("ParseOptVendorClass: at least one vendor class data is required") } return &opt, buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/option_vendorclass_test.go000066400000000000000000000046411431560352000306370ustar00rootroot00000000000000package dhcpv6 import ( "testing" "github.com/stretchr/testify/require" ) func TestParseOptVendorClass(t *testing.T) { data := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber 0, 10, 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', 0, 4, 't', 'e', 's', 't', } opt, err := ParseOptVendorClass(data) require.NoError(t, err) require.Equal(t, OptionVendorClass, opt.Code()) require.Equal(t, 2, len(opt.Data)) require.Equal(t, uint32(0xaabbccdd), opt.EnterpriseNumber) require.Equal(t, []byte("HTTPClient"), opt.Data[0]) require.Equal(t, []byte("test"), opt.Data[1]) } func TestOptVendorClassToBytes(t *testing.T) { opt := OptVendorClass{ EnterpriseNumber: uint32(0xaabbccdd), Data: [][]byte{ []byte("HTTPClient"), []byte("test"), }, } data := opt.ToBytes() expected := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber 0, 10, 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', 0, 4, 't', 'e', 's', 't', } require.Equal(t, expected, data) } func TestOptVendorClassParseOptVendorClassMalformed(t *testing.T) { buf := []byte{ 0xaa, 0xbb, // truncated EnterpriseNumber } _, err := ParseOptVendorClass(buf) require.Error(t, err, "ParseOptVendorClass() should error if given truncated EnterpriseNumber") buf = []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber } _, err = ParseOptVendorClass(buf) require.Error(t, err, "ParseOptVendorClass() should error if given no vendor classes") buf = []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', } _, err = ParseOptVendorClass(buf) require.Error(t, err, "ParseOptVendorClass() should error if given truncated vendor classes") buf = []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, } _, err = ParseOptVendorClass(buf) require.Error(t, err, "ParseOptVendorClass() should error if given a truncated length") } func TestOptVendorClassString(t *testing.T) { data := []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } opt, err := ParseOptVendorClass(data) require.NoError(t, err) str := opt.String() require.Contains( t, str, "enterprisenum=2864434397", "String() should contain the enterprisenum", ) require.Contains( t, str, "data=[linuxboot, test]", "String() should contain the list of vendor classes", ) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/options.go000066400000000000000000000125511431560352000253570ustar00rootroot00000000000000package dhcpv6 import ( "fmt" "github.com/u-root/uio/uio" ) // Option is an interface that all DHCPv6 options adhere to. type Option interface { Code() OptionCode ToBytes() []byte String() string } type OptionGeneric struct { OptionCode OptionCode OptionData []byte } func (og *OptionGeneric) Code() OptionCode { return og.OptionCode } func (og *OptionGeneric) ToBytes() []byte { return og.OptionData } func (og *OptionGeneric) String() string { return fmt.Sprintf("%s -> %v", og.OptionCode, og.OptionData) } // ParseOption parses data according to the given code. func ParseOption(code OptionCode, optData []byte) (Option, error) { // Parse a sequence of bytes as a single DHCPv6 option. // Returns the option structure, or an error if any. var ( err error opt Option ) switch code { case OptionClientID: opt, err = parseOptClientID(optData) case OptionServerID: opt, err = parseOptServerID(optData) case OptionIANA: opt, err = ParseOptIANA(optData) case OptionIATA: opt, err = ParseOptIATA(optData) case OptionIAAddr: opt, err = ParseOptIAAddress(optData) case OptionORO: var o optRequestedOption err = o.FromBytes(optData) opt = &o case OptionElapsedTime: opt, err = parseOptElapsedTime(optData) case OptionRelayMsg: opt, err = parseOptRelayMsg(optData) case OptionStatusCode: opt, err = ParseOptStatusCode(optData) case OptionUserClass: opt, err = ParseOptUserClass(optData) case OptionVendorClass: opt, err = ParseOptVendorClass(optData) case OptionVendorOpts: opt, err = ParseOptVendorOpts(optData) case OptionInterfaceID: opt, err = parseOptInterfaceID(optData) case OptionDNSRecursiveNameServer: opt, err = parseOptDNS(optData) case OptionDomainSearchList: opt, err = parseOptDomainSearchList(optData) case OptionIAPD: opt, err = ParseOptIAPD(optData) case OptionIAPrefix: opt, err = ParseOptIAPrefix(optData) case OptionInformationRefreshTime: opt, err = parseOptInformationRefreshTime(optData) case OptionRemoteID: opt, err = ParseOptRemoteID(optData) case OptionFQDN: opt, err = ParseOptFQDN(optData) case OptionNTPServer: opt, err = ParseOptNTPServer(optData) case OptionBootfileURL: opt, err = parseOptBootFileURL(optData) case OptionBootfileParam: opt, err = parseOptBootFileParam(optData) case OptionClientArchType: opt, err = parseOptClientArchType(optData) case OptionNII: var o OptNetworkInterfaceID err = o.FromBytes(optData) opt = &o case OptionClientLinkLayerAddr: opt, err = parseOptClientLinkLayerAddress(optData) case OptionDHCPv4Msg: opt, err = ParseOptDHCPv4Msg(optData) case OptionDHCP4oDHCP6Server: opt, err = ParseOptDHCP4oDHCP6Server(optData) case Option4RD: opt, err = ParseOpt4RD(optData) case Option4RDMapRule: opt, err = ParseOpt4RDMapRule(optData) case Option4RDNonMapRule: opt, err = ParseOpt4RDNonMapRule(optData) case OptionRelayPort: opt, err = parseOptRelayPort(optData) default: opt = &OptionGeneric{OptionCode: code, OptionData: optData} } if err != nil { return nil, err } return opt, nil } // Options is a collection of options. type Options []Option // Get returns all options matching the option code. func (o Options) Get(code OptionCode) []Option { var ret []Option for _, opt := range o { if opt.Code() == code { ret = append(ret, opt) } } return ret } // GetOne returns the first option matching the option code. func (o Options) GetOne(code OptionCode) Option { for _, opt := range o { if opt.Code() == code { return opt } } return nil } // Add appends one option. func (o *Options) Add(option Option) { *o = append(*o, option) } // Del deletes all options matching the option code. func (o *Options) Del(code OptionCode) { newOpts := make(Options, 0, len(*o)) for _, opt := range *o { if opt.Code() != code { newOpts = append(newOpts, opt) } } *o = newOpts } // Update replaces the first option of the same type as the specified one. func (o *Options) Update(option Option) { for idx, opt := range *o { if opt.Code() == option.Code() { (*o)[idx] = option // don't look further return } } // if not found, add it o.Add(option) } // ToBytes marshals all options to bytes. func (o Options) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, opt := range o { buf.Write16(uint16(opt.Code())) val := opt.ToBytes() buf.Write16(uint16(len(val))) buf.WriteBytes(val) } return buf.Data() } // FromBytes reads data into o and returns an error if the options are not a // valid serialized representation of DHCPv6 options per RFC 3315. func (o *Options) FromBytes(data []byte) error { return o.FromBytesWithParser(data, ParseOption) } // OptionParser is a function signature for option parsing type OptionParser func(code OptionCode, data []byte) (Option, error) // FromBytesWithParser parses Options from byte sequences using the parsing // function that is passed in as a paremeter func (o *Options) FromBytesWithParser(data []byte, parser OptionParser) error { *o = make(Options, 0, 10) if len(data) == 0 { // no options, no party return nil } buf := uio.NewBigEndianBuffer(data) for buf.Has(4) { code := OptionCode(buf.Read16()) length := int(buf.Read16()) // Consume, but do not Copy. Each parser will make a copy of // pertinent data. optData := buf.Consume(length) opt, err := parser(code, optData) if err != nil { return err } *o = append(*o, opt) } return buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/000077500000000000000000000000001431560352000247255ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/conn_unix.go000066400000000000000000000047031431560352000272600ustar00rootroot00000000000000// +build !windows package server6 import ( "errors" "fmt" "net" "os" "github.com/insomniacslk/dhcp/interfaces" "golang.org/x/sys/unix" ) // NewIPv6UDPConn returns a UDPv6-only connection bound to both the interface and port // given based on a IPv6 DGRAM socket. // As a bonus, you can actually listen on a multicast address instead of being punted to the wildcard // // The interface must already be configured. func NewIPv6UDPConn(iface string, addr *net.UDPAddr) (*net.UDPConn, error) { fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) if err != nil { return nil, fmt.Errorf("cannot get a UDP socket: %v", err) } f := os.NewFile(uintptr(fd), "") // net.FilePacketConn dups the FD, so we have to close this in any case. defer f.Close() // Allow broadcasting. if err := unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, 1); err != nil { if errno, ok := err.(unix.Errno); !ok { return nil, fmt.Errorf("unexpected socket error: %v", err) } else if errno != unix.ENOPROTOOPT { // Unsupported on some OSes (but in that case v6only is default), so we ignore ENOPROTOOPT return nil, fmt.Errorf("cannot bind socket v6only %v", err) } } // Allow reusing the addr to aid debugging. if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { return nil, fmt.Errorf("cannot set reuseaddr on socket: %v", err) } if len(iface) != 0 { // Bind directly to the interface. if err := interfaces.BindToInterface(fd, iface); err != nil { if errno, ok := err.(unix.Errno); ok && errno == unix.EACCES { // Return a more helpful error message in this (fairly common) case return nil, errors.New("Cannot bind to interface without CAP_NET_RAW or root permissions. " + "Restart with elevated privilege, or run without specifying an interface to bind to all available interfaces.") } return nil, fmt.Errorf("cannot bind to interface %s: %v", iface, err) } } if addr == nil { return nil, errors.New("An address to listen on needs to be specified") } // Bind to the port. saddr := unix.SockaddrInet6{Port: addr.Port} copy(saddr.Addr[:], addr.IP) if err := unix.Bind(fd, &saddr); err != nil { return nil, fmt.Errorf("cannot bind to address %v: %v", addr, err) } conn, err := net.FilePacketConn(f) if err != nil { return nil, err } udpconn, ok := conn.(*net.UDPConn) if !ok { return nil, errors.New("BUG(dhcp6): incorrect socket type, expected UDP") } return udpconn, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/conn_windows.go000066400000000000000000000004211431560352000277600ustar00rootroot00000000000000// +build windows package server6 import ( "errors" "net" ) // NewIPv6UDPConn fails on Windows. Use WithConn() to pass the connection. func NewIPv6UDPConn(iface string, addr *net.UDPAddr) (*net.UDPConn, error) { return nil, errors.New("not implemented on Windows") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/logger.go000066400000000000000000000041001431560352000265260ustar00rootroot00000000000000package server6 import ( "github.com/insomniacslk/dhcp/dhcpv6" ) // Logger is a handler which will be used to output logging messages type Logger interface { // PrintMessage print _all_ DHCP messages PrintMessage(prefix string, message *dhcpv6.Message) // Printf is use to print the rest debugging information Printf(format string, v ...interface{}) } // EmptyLogger prints nothing type EmptyLogger struct{} // Printf is just a dummy function that does nothing func (e EmptyLogger) Printf(format string, v ...interface{}) {} // PrintMessage is just a dummy function that does nothing func (e EmptyLogger) PrintMessage(prefix string, message *dhcpv6.Message) {} // Printfer is used for actual output of the logger. For example *log.Logger is a Printfer. type Printfer interface { // Printf is the function for logging output. Arguments are handled in the manner of fmt.Printf. Printf(format string, v ...interface{}) } // ShortSummaryLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the short format. type ShortSummaryLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (s ShortSummaryLogger) Printf(format string, v ...interface{}) { s.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the short format via predefined Printfer func (s ShortSummaryLogger) PrintMessage(prefix string, message *dhcpv6.Message) { s.Printf("%s: %s", prefix, message) } // DebugLogger is a wrapper for Printfer to implement interface Logger. // DHCP messages are printed in the long format. type DebugLogger struct { // Printfer is used for actual output of the logger Printfer } // Printf prints a log message as-is via predefined Printfer func (d DebugLogger) Printf(format string, v ...interface{}) { d.Printfer.Printf(format, v...) } // PrintMessage prints a DHCP message in the long format via predefined Printfer func (d DebugLogger) PrintMessage(prefix string, message *dhcpv6.Message) { d.Printf("%s: %s", prefix, message.Summary()) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/logger_test.go000066400000000000000000000014451431560352000275760ustar00rootroot00000000000000package server6 import( "log" "os" "testing" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/stretchr/testify/require" ) func TestEmptyLogger(t *testing.T) { l := EmptyLogger{} msg, err := dhcpv6.NewMessage() require.Nil(t, err) l.Printf("test") l.PrintMessage("prefix", msg) } func TestShortSummaryLogger(t *testing.T) { l := ShortSummaryLogger{ Printfer: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } msg, err := dhcpv6.NewMessage() require.Nil(t, err) require.NotNil(t, msg) l.Printf("test") l.PrintMessage("prefix", msg) } func TestDebugLogger(t *testing.T) { l := DebugLogger{ Printfer: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } msg, err := dhcpv6.NewMessage() require.Nil(t, err) require.NotNil(t, msg) l.Printf("test") l.PrintMessage("prefix", msg) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/server.go000066400000000000000000000131351431560352000265650ustar00rootroot00000000000000// Package server6 is a basic, extensible DHCPv6 server. // // To use the DHCPv6 server code you have to call NewServer with three arguments: // - an interface to listen on, // - an address to listen on, and // - a handler function, that will be called every time a valid DHCPv6 packet is // received. // // The address to listen on is used to know IP address, port and optionally the // scope to create and UDP socket to listen on for DHCPv6 traffic. // // The handler is a function that takes as input a packet connection, that can be // used to reply to the client; a peer address, that identifies the client sending // the request, and the DHCPv6 packet itself. Just implement your custom logic in // the handler. // // Optionally, NewServer can receive options that will modify the server object. // Some options already exist, for example WithConn. If this option is passed with // a valid connection, the listening address argument is ignored. // // Example program: // // package main // // import ( // "log" // "net" // // "github.com/insomniacslk/dhcp/dhcpv6" // "github.com/insomniacslk/dhcp/dhcpv6/server6" // ) // // func handler(conn net.PacketConn, peer net.Addr, m dhcpv6.DHCPv6) { // // this function will just print the received DHCPv6 message, without replying // log.Print(m.Summary()) // } // // func main() { // laddr := net.UDPAddr{ // IP: net.ParseIP("::1"), // Port: 547, // } // server, err := server6.NewServer("eth0", &laddr, handler) // if err != nil { // log.Fatal(err) // } // // // This never returns. If you want to do other stuff, dump it into a // // goroutine. // server.Serve() // } // package server6 import ( "log" "net" "os" "github.com/insomniacslk/dhcp/dhcpv6" "golang.org/x/net/ipv6" ) // Handler is a type that defines the handler function to be called every time a // valid DHCPv6 message is received type Handler func(conn net.PacketConn, peer net.Addr, m dhcpv6.DHCPv6) // Server represents a DHCPv6 server object type Server struct { conn net.PacketConn handler Handler logger Logger } // Serve starts the DHCPv6 server. The listener will run in background, and can // be interrupted with `Server.Close`. func (s *Server) Serve() error { s.logger.Printf("Server listening on %s", s.conn.LocalAddr()) s.logger.Printf("Ready to handle requests") defer s.Close() for { rbuf := make([]byte, 4096) // FIXME this is bad n, peer, err := s.conn.ReadFrom(rbuf) if err != nil { s.logger.Printf("Error reading from packet conn: %v", err) return err } s.logger.Printf("Handling request from %v", peer) d, err := dhcpv6.FromBytes(rbuf[:n]) if err != nil { s.logger.Printf("Error parsing DHCPv6 request: %v", err) continue } go s.handler(s.conn, peer, d) } } // Close sends a termination request to the server, and closes the UDP listener func (s *Server) Close() error { return s.conn.Close() } // A ServerOpt configures a Server. type ServerOpt func(s *Server) // WithConn configures a server with the given connection. func WithConn(conn net.PacketConn) ServerOpt { return func(s *Server) { s.conn = conn } } // NewServer initializes and returns a new Server object, listening on `addr`. // * If `addr` is a multicast group, the group will be additionally joined // * If `addr` is the wildcard address on the DHCPv6 server port (`[::]:547), the // multicast groups All_DHCP_Relay_Agents_and_Servers(`[ff02::1:2]`) and // All_DHCP_Servers(`[ff05::1:3]:547`) will be joined. // * If `addr` is nil, IPv6 unspec on the DHCP server port is used and the above // behaviour applies // If `WithConn` is used with a non-nil address, `addr` and `ifname` have // no effect. In such case, joining the multicast group is the caller's // responsibility. func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerOpt) (*Server, error) { s := &Server{ handler: handler, logger: EmptyLogger{}, } for _, o := range opt { o(s) } if s.conn != nil { return s, nil } if addr == nil { addr = &net.UDPAddr{ IP: net.IPv6unspecified, Port: dhcpv6.DefaultServerPort, } } var ( err error iface *net.Interface ) if ifname == "" { iface = nil } else { iface, err = net.InterfaceByName(ifname) if err != nil { return nil, err } } // no connection provided by the user, create a new one s.conn, err = NewIPv6UDPConn(ifname, addr) if err != nil { return nil, err } p := ipv6.NewPacketConn(s.conn) if addr.IP.IsMulticast() { if err := p.JoinGroup(iface, addr); err != nil { return nil, err } } else if (addr.IP == nil || addr.IP.IsUnspecified()) && addr.Port == dhcpv6.DefaultServerPort { // For wildcard addresses on the correct port, listen on both multicast // addresses defined in the RFC as a "default" behaviour for _, g := range []net.IP{dhcpv6.AllDHCPRelayAgentsAndServers, dhcpv6.AllDHCPServers} { group := net.UDPAddr{ IP: g, Port: dhcpv6.DefaultServerPort, } if err := p.JoinGroup(iface, &group); err != nil { return nil, err } } } return s, nil } // WithSummaryLogger logs one-line DHCPv6 message summaries when sent & received. func WithSummaryLogger() ServerOpt { return func(s *Server) { s.logger = ShortSummaryLogger{ Printfer: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // WithDebugLogger logs multi-line full DHCPv6 messages when sent & received. func WithDebugLogger() ServerOpt { return func(s *Server) { s.logger = DebugLogger{ Printfer: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // WithLogger set the logger (see interface Logger). func WithLogger(newLogger Logger) ServerOpt { return func(s *Server) { s.logger = newLogger } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/server6/server_test.go000066400000000000000000000035531431560352000276270ustar00rootroot00000000000000package server6 import ( "context" "log" "net" "testing" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/nclient6" "github.com/insomniacslk/dhcp/interfaces" "github.com/stretchr/testify/require" ) // Turns a connected UDP conn into an "unconnected" UDP conn. type unconnectedConn struct { *net.UDPConn } func (f unconnectedConn) WriteTo(b []byte, _ net.Addr) (int, error) { return f.UDPConn.Write(b) } func (f unconnectedConn) ReadFrom(b []byte) (int, net.Addr, error) { n, err := f.Read(b) return n, nil, err } // utility function to set up a client and a server instance and run it in // background. The caller needs to call Server.Close() once finished. func setUpClientAndServer(handler Handler) (*nclient6.Client, *Server) { laddr := &net.UDPAddr{ IP: net.ParseIP("::1"), Port: 0, } s, err := NewServer("", laddr, handler) if err != nil { panic(err) } go func() { _ = s.Serve() }() clientConn, err := net.DialUDP("udp6", &net.UDPAddr{IP: net.ParseIP("::1")}, s.conn.LocalAddr().(*net.UDPAddr)) if err != nil { panic(err) } c, err := nclient6.NewWithConn(unconnectedConn{clientConn}, net.HardwareAddr{1, 2, 3, 4, 5, 6}) if err != nil { panic(err) } return c, s } func TestServer(t *testing.T) { handler := func(conn net.PacketConn, peer net.Addr, m dhcpv6.DHCPv6) { msg := m.(*dhcpv6.Message) adv, err := dhcpv6.NewAdvertiseFromSolicit(msg) if err != nil { log.Printf("NewAdvertiseFromSolicit failed: %v", err) return } if _, err := conn.WriteTo(adv.ToBytes(), peer); err != nil { log.Printf("Cannot reply to client: %v", err) } } c, s := setUpClientAndServer(handler) defer s.Close() ifaces, err := interfaces.GetLoopbackInterfaces() require.NoError(t, err) require.NotEqual(t, 0, len(ifaces)) _, err = c.Solicit(context.Background(), dhcpv6.WithRapidCommit) require.NoError(t, err) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/types.go000066400000000000000000000533671431560352000250420ustar00rootroot00000000000000package dhcpv6 import ( "fmt" ) // TransactionID is a DHCPv6 Transaction ID defined by RFC 3315, Section 6. type TransactionID [3]byte // String prints the transaction ID as a hex value. func (xid TransactionID) String() string { return fmt.Sprintf("0x%x", xid[:]) } // MessageType represents the kind of DHCPv6 message. type MessageType uint8 // The DHCPv6 message types defined per RFC 3315, Section 5.3. const ( // MessageTypeNone is used internally and is not part of the RFC. MessageTypeNone MessageType = 0 MessageTypeSolicit MessageType = 1 MessageTypeAdvertise MessageType = 2 MessageTypeRequest MessageType = 3 MessageTypeConfirm MessageType = 4 MessageTypeRenew MessageType = 5 MessageTypeRebind MessageType = 6 MessageTypeReply MessageType = 7 MessageTypeRelease MessageType = 8 MessageTypeDecline MessageType = 9 MessageTypeReconfigure MessageType = 10 MessageTypeInformationRequest MessageType = 11 MessageTypeRelayForward MessageType = 12 MessageTypeRelayReply MessageType = 13 MessageTypeLeaseQuery MessageType = 14 MessageTypeLeaseQueryReply MessageType = 15 MessageTypeLeaseQueryDone MessageType = 16 MessageTypeLeaseQueryData MessageType = 17 _ MessageType = 18 _ MessageType = 19 MessageTypeDHCPv4Query MessageType = 20 MessageTypeDHCPv4Response MessageType = 21 ) // String prints the message type name. func (m MessageType) String() string { if s, ok := messageTypeToStringMap[m]; ok { return s } return fmt.Sprintf("unknown (%d)", m) } // messageTypeToStringMap contains the mapping of MessageTypes to // human-readable strings. var messageTypeToStringMap = map[MessageType]string{ MessageTypeSolicit: "SOLICIT", MessageTypeAdvertise: "ADVERTISE", MessageTypeRequest: "REQUEST", MessageTypeConfirm: "CONFIRM", MessageTypeRenew: "RENEW", MessageTypeRebind: "REBIND", MessageTypeReply: "REPLY", MessageTypeRelease: "RELEASE", MessageTypeDecline: "DECLINE", MessageTypeReconfigure: "RECONFIGURE", MessageTypeInformationRequest: "INFORMATION-REQUEST", MessageTypeRelayForward: "RELAY-FORW", MessageTypeRelayReply: "RELAY-REPL", MessageTypeLeaseQuery: "LEASEQUERY", MessageTypeLeaseQueryReply: "LEASEQUERY-REPLY", MessageTypeLeaseQueryDone: "LEASEQUERY-DONE", MessageTypeLeaseQueryData: "LEASEQUERY-DATA", MessageTypeDHCPv4Query: "DHCPv4-QUERY", MessageTypeDHCPv4Response: "DHCPv4-RESPONSE", } // OptionCode is a single byte representing the code for a given Option. type OptionCode uint16 // String returns the option code name. func (o OptionCode) String() string { if s, ok := optionCodeToString[o]; ok { return s } return fmt.Sprintf("unknown (%d)", o) } // All DHCPv6 options. const ( OptionClientID OptionCode = 1 OptionServerID OptionCode = 2 OptionIANA OptionCode = 3 OptionIATA OptionCode = 4 OptionIAAddr OptionCode = 5 OptionORO OptionCode = 6 OptionPreference OptionCode = 7 OptionElapsedTime OptionCode = 8 OptionRelayMsg OptionCode = 9 _ OptionCode = 10 OptionAuth OptionCode = 11 OptionUnicast OptionCode = 12 OptionStatusCode OptionCode = 13 OptionRapidCommit OptionCode = 14 OptionUserClass OptionCode = 15 OptionVendorClass OptionCode = 16 OptionVendorOpts OptionCode = 17 OptionInterfaceID OptionCode = 18 OptionReconfMessage OptionCode = 19 OptionReconfAccept OptionCode = 20 OptionSIPServersDomainNameList OptionCode = 21 OptionSIPServersIPv6AddressList OptionCode = 22 OptionDNSRecursiveNameServer OptionCode = 23 OptionDomainSearchList OptionCode = 24 OptionIAPD OptionCode = 25 OptionIAPrefix OptionCode = 26 OptionNISServers OptionCode = 27 OptionNISPServers OptionCode = 28 OptionNISDomainName OptionCode = 29 OptionNISPDomainName OptionCode = 30 OptionSNTPServerList OptionCode = 31 OptionInformationRefreshTime OptionCode = 32 OptionBCMCSControllerDomainNameList OptionCode = 33 OptionBCMCSControllerIPv6AddressList OptionCode = 34 _ OptionCode = 35 OptionGeoConfCivic OptionCode = 36 OptionRemoteID OptionCode = 37 OptionRelayAgentSubscriberID OptionCode = 38 OptionFQDN OptionCode = 39 OptionPANAAuthenticationAgent OptionCode = 40 OptionNewPOSIXTimezone OptionCode = 41 OptionNewTZDBTimezone OptionCode = 42 OptionEchoRequest OptionCode = 43 OptionLQQuery OptionCode = 44 OptionClientData OptionCode = 45 OptionCLTTime OptionCode = 46 OptionLQRelayData OptionCode = 47 OptionLQClientLink OptionCode = 48 OptionMIPv6HomeNetworkIDFQDN OptionCode = 49 OptionMIPv6VisitedHomeNetworkInformation OptionCode = 50 OptionLoSTServer OptionCode = 51 OptionCAPWAPAccessControllerAddresses OptionCode = 52 OptionRelayID OptionCode = 53 OptionIPv6AddressMOS OptionCode = 54 OptionIPv6FQDNMOS OptionCode = 55 OptionNTPServer OptionCode = 56 OptionV6AccessDomain OptionCode = 57 OptionSIPUACSList OptionCode = 58 OptionBootfileURL OptionCode = 59 OptionBootfileParam OptionCode = 60 OptionClientArchType OptionCode = 61 OptionNII OptionCode = 62 OptionGeolocation OptionCode = 63 OptionAFTRName OptionCode = 64 OptionERPLocalDomainName OptionCode = 65 OptionRSOO OptionCode = 66 OptionPDExclude OptionCode = 67 OptionVirtualSubnetSelection OptionCode = 68 OptionMIPv6IdentifiedHomeNetworkInformation OptionCode = 69 OptionMIPv6UnrestrictedHomeNetworkInformation OptionCode = 70 OptionMIPv6HomeNetworkPrefix OptionCode = 71 OptionMIPv6HomeAgentAddress OptionCode = 72 OptionMIPv6HomeAgentFQDN OptionCode = 73 OptionRDNSSSelection OptionCode = 74 OptionKRBPrincipalName OptionCode = 75 OptionKRBRealmName OptionCode = 76 OptionKRBDefaultRealmName OptionCode = 77 OptionKRBKDC OptionCode = 78 OptionClientLinkLayerAddr OptionCode = 79 OptionLinkAddress OptionCode = 80 OptionRadius OptionCode = 81 OptionSolMaxRT OptionCode = 82 OptionInfMaxRT OptionCode = 83 OptionAddrSel OptionCode = 84 OptionAddrSelTable OptionCode = 85 OptionV6PCPServer OptionCode = 86 OptionDHCPv4Msg OptionCode = 87 OptionDHCP4oDHCP6Server OptionCode = 88 OptionS46Rule OptionCode = 89 OptionS46BR OptionCode = 90 OptionS46DMR OptionCode = 91 OptionS46V4V6Bind OptionCode = 92 OptionS46PortParams OptionCode = 93 OptionS46ContMapE OptionCode = 94 OptionS46ContMapT OptionCode = 95 OptionS46ContLW OptionCode = 96 Option4RD OptionCode = 97 Option4RDMapRule OptionCode = 98 Option4RDNonMapRule OptionCode = 99 OptionLQBaseTime OptionCode = 100 OptionLQStartTime OptionCode = 101 OptionLQEndTime OptionCode = 102 OptionCaptivePortal OptionCode = 103 OptionMPLParameters OptionCode = 104 OptionANIAccessTechType OptionCode = 105 OptionANINetworkName OptionCode = 106 OptionANIAccessPointName OptionCode = 107 OptionANIAccessPointBSSID OptionCode = 108 OptionANIOperatorID OptionCode = 109 OptionANIOperatorRealm OptionCode = 110 OptionS46Priority OptionCode = 111 OptionMUDUrlV6 OptionCode = 112 OptionV6Prefix64 OptionCode = 113 OptionFailoverBindingStatus OptionCode = 114 OptionFailoverConnectFlags OptionCode = 115 OptionFailoverDNSRemovalInfo OptionCode = 116 OptionFailoverDNSHostName OptionCode = 117 OptionFailoverDNSZoneName OptionCode = 118 OptionFailoverDNSFlags OptionCode = 119 OptionFailoverExpirationTime OptionCode = 120 OptionFailoverMaxUnackedBNDUPD OptionCode = 121 OptionFailoverMCLT OptionCode = 122 OptionFailoverPartnerLifetime OptionCode = 123 OptionFailoverPartnerLifetimeSent OptionCode = 124 OptionFailoverPartnerDownTime OptionCode = 125 OptionFailoverPartnerRawCLTTime OptionCode = 126 OptionFailoverProtocolVersion OptionCode = 127 OptionFailoverKeepaliveTime OptionCode = 128 OptionFailoverReconfigureData OptionCode = 129 OptionFailoverRelationshipName OptionCode = 130 OptionFailoverServerFlags OptionCode = 131 OptionFailoverServerState OptionCode = 132 OptionFailoverStartTimeOfState OptionCode = 133 OptionFailoverStateExpirationTime OptionCode = 134 OptionRelayPort OptionCode = 135 OptionV6SZTPRedirect OptionCode = 136 OptionS46BindIPv6Prefix OptionCode = 137 _ OptionCode = 138 _ OptionCode = 139 _ OptionCode = 140 _ OptionCode = 141 _ OptionCode = 142 OptionIPv6AddressANDSF OptionCode = 143 ) // optionCodeToString maps DHCPv6 OptionCodes to human-readable strings. var optionCodeToString = map[OptionCode]string{ OptionClientID: "Client Identifier", OptionServerID: "Server Identifier", OptionIANA: "IA_NA", OptionIATA: "IA_TA", OptionIAAddr: "IA IP Address", OptionORO: "Requested Options", OptionPreference: "Preference", OptionElapsedTime: "Elapsed Time", OptionRelayMsg: "Relay Message", OptionAuth: "Auth", OptionUnicast: "Unicast", OptionStatusCode: "Status Code", OptionRapidCommit: "Rapid Commit", OptionUserClass: "User Class", OptionVendorClass: "Vendor Class", OptionVendorOpts: "Vendor Options", OptionInterfaceID: "Interface ID", OptionReconfMessage: "Reconfig Message", OptionReconfAccept: "Reconfig Accept", OptionSIPServersDomainNameList: "SIP Servers Domain Name List", OptionSIPServersIPv6AddressList: "SIP Servers IPv6 Address List", OptionDNSRecursiveNameServer: "DNS Recursive Name Server", OptionDomainSearchList: "Domain Search List", OptionIAPD: "IA_PD", OptionIAPrefix: "IA Prefix", OptionNISServers: "NIS Servers", OptionNISPServers: "NISP Servers", OptionNISDomainName: "NIS Domain Name", OptionNISPDomainName: "NISP Domain Name", OptionSNTPServerList: "SNTP Server List", OptionInformationRefreshTime: "Information Refresh Time", OptionBCMCSControllerDomainNameList: "BCMCS Controller Domain Name List", OptionBCMCSControllerIPv6AddressList: "BCMCS Controller IPv6 Address List", OptionGeoConfCivic: "Geoconf", OptionRemoteID: "Remote ID", OptionRelayAgentSubscriberID: "Relay-Agent Subscriber ID", OptionFQDN: "FQDN", OptionPANAAuthenticationAgent: "PANA Authentication Agent", OptionNewPOSIXTimezone: "New POSIX Timezone", OptionNewTZDBTimezone: "New TZDB Timezone", OptionEchoRequest: "Echo Request", OptionLQQuery: "OPTION_LQ_QUERY", OptionClientData: "OPTION_CLIENT_DATA", OptionCLTTime: "OPTION_CLT_TIME", OptionLQRelayData: "OPTION_LQ_RELAY_DATA", OptionLQClientLink: "OPTION_LQ_CLIENT_LINK", OptionMIPv6HomeNetworkIDFQDN: "MIPv6 Home Network ID FQDN", OptionMIPv6VisitedHomeNetworkInformation: "MIPv6 Visited Home Network Information", OptionLoSTServer: "LoST Server", OptionCAPWAPAccessControllerAddresses: "CAPWAP Access Controller Addresses", OptionRelayID: "RELAY_ID", OptionIPv6AddressMOS: "OPTION-IPv6_Address-MoS", OptionIPv6FQDNMOS: "OPTION-IPv6-FQDN-MoS", OptionNTPServer: "NTP Server", OptionV6AccessDomain: "OPTION_V6_ACCESS_DOMAIN", OptionSIPUACSList: "OPTION_SIP_UA_CS_LIST", OptionBootfileURL: "Boot File URL", OptionBootfileParam: "Boot File Parameters", OptionClientArchType: "Client Architecture", OptionNII: "Network Interface ID", OptionGeolocation: "OPTION_GEOLOCATION", OptionAFTRName: "OPTION_AFTR_NAME", OptionERPLocalDomainName: "OPTION_ERP_LOCAL_DOMAIN_NAME", OptionRSOO: "OPTION_RSOO", OptionPDExclude: "OPTION_PD_EXCLUDE", OptionVirtualSubnetSelection: "Virtual Subnet Selection", OptionMIPv6IdentifiedHomeNetworkInformation: "MIPv6 Identified Home Network Information", OptionMIPv6UnrestrictedHomeNetworkInformation: "MIPv6 Unrestricted Home Network Information", OptionMIPv6HomeNetworkPrefix: "MIPv6 Home Network Prefix", OptionMIPv6HomeAgentAddress: "MIPv6 Home Agent Address", OptionMIPv6HomeAgentFQDN: "MIPv6 Home Agent FQDN", OptionRDNSSSelection: "RDNSS Selection", OptionKRBPrincipalName: "Kerberos Principal Name", OptionKRBRealmName: "Kerberos Realm Name", OptionKRBDefaultRealmName: "Kerberos Default Realm Name", OptionKRBKDC: "Kerberos KDC", OptionClientLinkLayerAddr: "Client Link-Layer Address", OptionLinkAddress: "Link Address", OptionRadius: "OPTION_RADIUS", OptionSolMaxRT: "Max Solicit Timeout Value", OptionInfMaxRT: "Max Information-Request Timeout Value", OptionAddrSel: "Address Selection", OptionAddrSelTable: "Address Selection Policy Table", OptionV6PCPServer: "Port Control Protocol Server", OptionDHCPv4Msg: "Encapsulated DHCPv4 Message", OptionDHCP4oDHCP6Server: "DHCPv4-over-DHCPv6 Server", OptionS46Rule: "Softwire46 Rule", OptionS46BR: "Softwire46 Border Relay", OptionS46DMR: "Softwire46 Default Mapping Rule", OptionS46V4V6Bind: "Softwire46 IPv4/IPv6 Address Binding", OptionS46PortParams: "Softwire46 Port Parameters", OptionS46ContMapE: "Softwire46 MAP-E Container", OptionS46ContMapT: "Softwire46 MAP-T Container", OptionS46ContLW: "Softwire46 Lightweight 4over6 Container", Option4RD: "IPv4 Residual Deployment", Option4RDMapRule: "IPv4 Residual Deployment Mapping Rule", Option4RDNonMapRule: "IPv4 Residual Deployment Non-Mapping Rule", OptionLQBaseTime: "Leasequery Server Base time", OptionLQStartTime: "Leasequery Server Query Start Time", OptionLQEndTime: "Leasequery Server Query End Time", OptionCaptivePortal: "Captive Portal URI", OptionMPLParameters: "MPL Parameters", OptionANIAccessTechType: "Access-Network-Information Access-Technology-Type", OptionANINetworkName: "Access-Network-Information Network-Name", OptionANIAccessPointName: "Access-Network-Information Access-Point-Name", OptionANIAccessPointBSSID: "Access-Network-Information Access-Point-BSSID", OptionANIOperatorID: "Access-Network-Information Operator-Identifier", OptionANIOperatorRealm: "Access-Network-Information Operator-Realm", OptionS46Priority: "Softwire46 Priority", OptionMUDUrlV6: "Manufacturer Usage Description URL", OptionV6Prefix64: "OPTION_V6_PREFIX64", OptionFailoverBindingStatus: "Failover Binding Status", OptionFailoverConnectFlags: "Failover Connection Flags", OptionFailoverDNSRemovalInfo: "Failover DNS Removal Info", OptionFailoverDNSHostName: "Failover DNS Removal Host Name", OptionFailoverDNSZoneName: "Failover DNS Removal Zone Name", OptionFailoverDNSFlags: "Failover DNS Removal Flags", OptionFailoverExpirationTime: "Failover Maximum Expiration Time", OptionFailoverMaxUnackedBNDUPD: "Failover Maximum Unacked BNDUPD Messages", OptionFailoverMCLT: "Failover Maximum Client Lead Time", OptionFailoverPartnerLifetime: "Failover Partner Lifetime", OptionFailoverPartnerLifetimeSent: "Failover Received Partner Lifetime", OptionFailoverPartnerDownTime: "Failover Last Partner Down Time", OptionFailoverPartnerRawCLTTime: "Failover Last Client Time", OptionFailoverProtocolVersion: "Failover Protocol Version", OptionFailoverKeepaliveTime: "Failover Keepalive Time", OptionFailoverReconfigureData: "Failover Reconfigure Data", OptionFailoverRelationshipName: "Failover Relationship Name", OptionFailoverServerFlags: "Failover Server Flags", OptionFailoverServerState: "Failover Server State", OptionFailoverStartTimeOfState: "Failover State Start Time", OptionFailoverStateExpirationTime: "Failover State Expiration Time", OptionRelayPort: "Relay Source Port", OptionV6SZTPRedirect: "IPv6 Secure Zerotouch Provisioning Redirect", OptionS46BindIPv6Prefix: "Softwire46 Source Binding Prefix Hint", OptionIPv6AddressANDSF: "IPv6 Access Network Discovery and Selection Function Address", } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/000077500000000000000000000000001431560352000244225ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/README.md000066400000000000000000000023071431560352000257030ustar00rootroot00000000000000# Zero Touch Provisioning (ZTP) DHCPv6 Parsing for Network Hardware Vendors ## Currently Supported Vendors For DHCPv6 ZTP - Arista - ZPE - Ciena ## Why Do We Need This? Many network hardware vendors support features that allow network devices to provision themselves with proper supporting automation/tools. Network devices can rely on DHCP and other methods to gather bootfile info, IPs, etc. DHCPv6 Vendor options provides us Vendor Name, Make, Model, and Serial Number data. This data can be used to uniquely identify individual network devices at provisioning time and can be used by tooling to make decisions necessary to correctly and reliably provision a network device. For more details on a large-scale ZTP deployment, check out how this is done at Facebook, [Scaling Backbone Networks Through Zero Touch Provisioning](https://code.fb.com/networking-traffic/scaling-the-facebook-backbone-through-zero-touch-provisioning/). ### Example Data Vendor specific data is commonly in a delimiter separated format containing Vendor Name, Model, Make, and Serial Number. This of course will vary per vendor and there could be more or less data. Vendor;Model;Version;SerialNumber `Arista;DCS-7060;01.011;ZZZ00000000` golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/parse_remote_id.go000066400000000000000000000047261431560352000301230ustar00rootroot00000000000000package ztpv6 import ( "errors" "fmt" "regexp" "github.com/insomniacslk/dhcp/dhcpv6" ) var ( // Arista Port, Vlan Pattern aristaPVPattern = regexp.MustCompile("Ethernet(?P[0-9]+):(?P[0-9]+)") // Arista Slot, Mod, Port Pattern aristaSMPPattern = regexp.MustCompile("Ethernet(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)") ) // CircuitID represents the structure of network vendor interface formats type CircuitID struct { Slot string Module string Port string SubPort string Vlan string } // ParseRemoteId will parse the RemoteId Option data for Vendor Specific data func ParseRemoteID(packet dhcpv6.DHCPv6) (*CircuitID, error) { // Need to decapsulate the packet after multiple relays in order to reach RemoteId data inner, err := dhcpv6.DecapsulateRelayIndex(packet, -1) if err != nil { return nil, fmt.Errorf("failed to decapsulate relay index: %v", err) } if rm, ok := inner.(*dhcpv6.RelayMessage); ok { if rid := rm.Options.RemoteID(); rid != nil { remoteID := string(rid.RemoteID) circ, err := matchCircuitId(remoteID) if err == nil { return circ, nil } } // if we fail to find circuit id from remote id try to use interface ID option if iid := rm.Options.InterfaceID(); iid != nil { interfaceID := string(iid) circ, err := matchCircuitId(interfaceID) if err == nil { return circ, nil } } } return nil, errors.New("failed to parse RemoteID and InterfaceID option data") } func matchCircuitId(circuitInfo string) (*CircuitID, error) { var names, matches []string switch { case aristaPVPattern.MatchString(circuitInfo): matches = aristaPVPattern.FindStringSubmatch(circuitInfo) names = aristaPVPattern.SubexpNames() case aristaSMPPattern.MatchString(circuitInfo): matches = aristaSMPPattern.FindStringSubmatch(circuitInfo) names = aristaSMPPattern.SubexpNames() } if len(matches) == 0 { return nil, fmt.Errorf("no circuitId regex matches for %v", circuitInfo) } var circuit CircuitID for i, match := range matches { switch names[i] { case "port": circuit.Port = match case "slot": circuit.Slot = match case "module": circuit.Module = match case "subport": circuit.SubPort = match case "vlan": circuit.Vlan = match } } return &circuit, nil } // FormatCircuitID is the CircuitID format we send in our Bootfile URL for ZTP devices func (c *CircuitID) FormatCircuitID() string { return fmt.Sprintf("%v,%v,%v,%v,%v", c.Slot, c.Module, c.Port, c.SubPort, c.Vlan) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/parse_remote_id_test.go000066400000000000000000000071311431560352000311530ustar00rootroot00000000000000package ztpv6 import ( "testing" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/stretchr/testify/require" ) func TestCircuitID(t *testing.T) { tt := []struct { name string circuit string want *CircuitID fail bool }{ {name: "Bogus string", circuit: "ope/1/2/3:ope", fail: true, want: nil}, {name: "Arista Port Vlan Pattern", circuit: "Ethernet13:2001", want: &CircuitID{Port: "13", Vlan: "2001"}}, {name: "Arista Slot Module Port Pattern", circuit: "Ethernet1/3/4", want: &CircuitID{Slot: "1", Module: "3", Port: "4"}}, {name: "Arista Slot Module Port Pattern InterfaceID", circuit: "Ethernet1/3/4:default", want: &CircuitID{Slot: "1", Module: "3", Port: "4"}}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { circuit, err := matchCircuitId(tc.circuit) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if circuit != nil { require.Equal(t, *tc.want, *circuit, "comparing remoteID data") } }) } } func TestFormatCircuitID(t *testing.T) { tt := []struct { name string circuit *CircuitID want string }{ {name: "empty", circuit: &CircuitID{}, want: ",,,,"}, {name: "Arista format Port/Vlan", circuit: &CircuitID{Port: "13", Vlan: "2001"}, want: ",,13,,2001"}, {name: "Arista format Slot/Module/Port", circuit: &CircuitID{Slot: "1", Module: "3", Port: "4"}, want: "1,3,4,,"}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { circuit := tc.circuit.FormatCircuitID() require.Equal(t, tc.want, circuit, "FormatRemoteID data") }) } } func TestParseRemoteID(t *testing.T) { tt := []struct { name string circuit []byte want *CircuitID fail bool }{ {name: "Bogus string", circuit: []byte("ope/1/2/3:ope.1"), fail: true, want: nil}, {name: "Arista Port Vlan Pattern", circuit: []byte("Ethernet13:2001"), want: &CircuitID{Port: "13", Vlan: "2001"}}, {name: "Arista Slot Module Port Pattern", circuit: []byte("Ethernet1/3/4"), want: &CircuitID{Slot: "1", Module: "3", Port: "4"}}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { m := &dhcpv6.RelayMessage{ MessageType: dhcpv6.MessageTypeRelayForward, } // Has to be a well-formed relay message with the OptRelayMsg. m.Options.Add(dhcpv6.OptRelayMessage(&dhcpv6.Message{})) m.Options.Add(&dhcpv6.OptRemoteID{RemoteID: tc.circuit, EnterpriseNumber: 1234}) circuit, err := ParseRemoteID(m) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if circuit != nil { require.Equal(t, *tc.want, *circuit, "ZTPRemoteID data") } else { require.Equal(t, tc.want, circuit, "ZTPRemoteID data") } }) } } func TestParseRemoteIDWithInterfaceID(t *testing.T) { tt := []struct { name string circuit []byte want *CircuitID fail bool }{ {name: "Bogus string", circuit: []byte("ope/1/2/3:ope.1"), fail: true, want: nil}, {name: "Arista Slot Module Port Pattern", circuit: []byte("Ethernet1/3/4:default"), want: &CircuitID{Slot: "1", Module: "3", Port: "4"}}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { m := &dhcpv6.RelayMessage{ MessageType: dhcpv6.MessageTypeRelayForward, } // Has to be a well-formed relay message with the OptRelayMsg. m.Options.Add(dhcpv6.OptRelayMessage(&dhcpv6.Message{})) m.Options.Add(dhcpv6.OptInterfaceID(tc.circuit)) circuit, err := ParseRemoteID(m) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if circuit != nil { require.Equal(t, *tc.want, *circuit, "ZTPRemoteID data") } else { require.Equal(t, tc.want, circuit, "ZTPRemoteID data") } }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/parse_vendor_options.go000066400000000000000000000051741431560352000312220ustar00rootroot00000000000000package ztpv6 import ( "errors" "strconv" "strings" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" ) var ( errVendorOptionMalformed = errors.New("malformed vendor option") ) // VendorData contains fields extracted from Option 17 data type VendorData struct { VendorName, Model, Serial string } // ParseVendorData will try to parse dhcp6 Vendor Specific Information options // ( 16 and 17) data looking for more specific vendor data (like model, serial // number, etc). If the options are missing we will just return nil func ParseVendorData(packet dhcpv6.DHCPv6) (*VendorData, error) { // check for both options 16 and 17 if both are present will use opt 17 opt16 := packet.GetOneOption(dhcpv6.OptionVendorClass) opt17 := packet.GetOneOption(dhcpv6.OptionVendorOpts) if (opt16 == nil) && (opt17 == nil) { return nil, errors.New("no vendor options or vendor class found") } vd := VendorData{} vData := []string{} if opt17 != nil { vo := opt17.(*dhcpv6.OptVendorOpts).VendorOpts for _, opt := range vo { vData = append(vData, string(opt.(*dhcpv6.OptionGeneric).OptionData)) } } else { data := opt16.(*dhcpv6.OptVendorClass).Data for _, d := range data { vData = append(vData, string(d)) } } for _, d := range vData { switch { // Arista;DCS-0000;00.00;ZZZ00000000 // Cisco;8800;12.34;FOC00000000 case strings.HasPrefix(d, "Arista;"), strings.HasPrefix(d, "Cisco;"): p := strings.Split(d, ";") if len(p) < 4 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[3] return &vd, nil // ZPESystems:NSC:000000000 case strings.HasPrefix(d, "ZPESystems:"): p := strings.Split(d, ":") if len(p) < 3 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[2] return &vd, nil // For Ciena the class identifier (opt 60) is written in the following format: // {vendor iana code}-{product}-{type} // For Ciena the iana code is 1271 // The product type is a number that maps to a Ciena product // The type is used to identified different subtype of the product. // An example can be ‘1271-23422Z11-123’. case strings.HasPrefix(d, strconv.Itoa(int(iana.EnterpriseIDCienaCorporation))): v := strings.Split(d, "-") if len(v) < 3 { return nil, errVendorOptionMalformed } duid := packet.(*dhcpv6.Message).Options.ClientID() vd.VendorName = iana.EnterpriseIDCienaCorporation.String() vd.Model = v[1] + "-" + v[2] vd.Serial = string(duid.EnterpriseIdentifier) return &vd, nil } } return nil, errors.New("failed to parse vendor option data") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/dhcpv6/ztpv6/parse_vendor_options_test.go000066400000000000000000000066231431560352000322610ustar00rootroot00000000000000package ztpv6 import ( "testing" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) func TestParseVendorDataWithVendorOpts(t *testing.T) { tt := []struct { name string vc string want *VendorData fail bool }{ {name: "empty", fail: true}, {name: "unknownVendor", vc: "VendorX;BFR10K;XX12345", fail: true, want: nil}, {name: "truncatedArista", vc: "Arista;1234", fail: true, want: nil}, {name: "truncatedCisco", vc: "Cisco;1234", fail: true, want: nil}, {name: "truncatedZPE", vc: "ZPESystems:1234", fail: true, want: nil}, { name: "arista", vc: "Arista;DCS-7050S-64;01.23;JPE12345678", want: &VendorData{VendorName: "Arista", Model: "DCS-7050S-64", Serial: "JPE12345678"}, }, { name: "cisco", vc: "Cisco;SYS-8801;01.23;FOC12345678", want: &VendorData{VendorName: "Cisco", Model: "SYS-8801", Serial: "FOC12345678"}, }, { name: "zpe", vc: "ZPESystems:NSC:001234567", want: &VendorData{VendorName: "ZPESystems", Model: "NSC", Serial: "001234567"}, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { packet, err := dhcpv6.NewMessage() if err != nil { t.Fatalf("failed to creat dhcpv6 packet object: %v", err) } opts := []dhcpv6.Option{&dhcpv6.OptionGeneric{OptionData: []byte(tc.vc), OptionCode: 1}} packet.AddOption(&dhcpv6.OptVendorOpts{ VendorOpts: opts, EnterpriseNumber: 0000}) vd, err := ParseVendorData(packet) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if vd != nil { require.Equal(t, *tc.want, *vd, "comparing vendor option data") } else { require.Equal(t, tc.want, vd, "comparing vendor option data") } }) } } func TestParseVendorDataWithVendorClass(t *testing.T) { tt := []struct { name string vc string clientId *dhcpv6.Duid want *VendorData fail bool }{ {name: "empty", fail: true}, {name: "unknownVendor", vc: "VendorX;BFR10K;XX12345", fail: true, want: nil}, {name: "truncatedArista", vc: "Arista;1234", fail: true, want: nil}, {name: "truncatedZPE", vc: "ZPESystems:1234", fail: true, want: nil}, { name: "arista", vc: "Arista;DCS-7050S-64;01.23;JPE12345678", want: &VendorData{VendorName: "Arista", Model: "DCS-7050S-64", Serial: "JPE12345678"}, }, { name: "zpe", vc: "ZPESystems:NSC:001234567", want: &VendorData{VendorName: "ZPESystems", Model: "NSC", Serial: "001234567"}, }, { name: "Ciena", vc: "1271-23422Z11-123", clientId: &dhcpv6.Duid{ Type: dhcpv6.DUID_EN, EnterpriseIdentifier: []byte("001234567"), }, want: &VendorData{VendorName: iana.EnterpriseIDCienaCorporation.String(), Model: "23422Z11-123", Serial: "001234567"}, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { packet, err := dhcpv6.NewMessage() if err != nil { t.Fatalf("failed to creat dhcpv6 packet object: %v", err) } packet.AddOption(&dhcpv6.OptVendorClass{ EnterpriseNumber: 0000, Data: [][]byte{[]byte(tc.vc)}}) if tc.clientId != nil { packet.AddOption(dhcpv6.OptClientID(*tc.clientId)) } vd, err := ParseVendorData(packet) if err != nil && !tc.fail { t.Errorf("unexpected failure: %v", err) } if vd != nil { require.Equal(t, *tc.want, *vd, "comparing vendor class data") } else { require.Equal(t, tc.want, vd, "comparing vendor class data") } }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/000077500000000000000000000000001431560352000237555ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/client6/000077500000000000000000000000001431560352000253215ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/client6/README.md000066400000000000000000000003601431560352000265770ustar00rootroot00000000000000# DHCPv6 client A minimal DHCPv6 client can be implemented in a few lines of code, by using the default client parameters. The example in [main.go](./main.go) lets you specify the interface to send packets through, and defaults to `eth0`. golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/client6/main.go000066400000000000000000000024411431560352000265750ustar00rootroot00000000000000package main import ( "flag" "log" "github.com/insomniacslk/dhcp/dhcpv6/client6" ) var ( iface = flag.String("i", "eth0", "Interface to configure via DHCPv6") ) func main() { flag.Parse() log.Printf("Starting DHCPv6 client on interface %s", *iface) // NewClient sets up a new DHCPv6 client with default values // for read and write timeouts, for destination address and listening // address client := client6.NewClient() // Exchange runs a Solicit-Advertise-Request-Reply transaction on the // specified network interface, and returns a list of DHCPv6 packets // (a "conversation") and an error if any. Notice that Exchange may // return a non-empty packet list even if there is an error. This is // intended, because the transaction may fail at any point, and we // still want to know what packets were exchanged until then. // A default Solicit packet will be used during the "conversation", // which can be manipulated by using modifiers. conversation, err := client.Exchange(*iface) // Summary() prints a verbose representation of the exchanged packets. for _, packet := range conversation { log.Print(packet.Summary()) } // error handling is done *after* printing, so we still print the // exchanged packets if any, as explained above. if err != nil { log.Fatal(err) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/packetcrafting6/000077500000000000000000000000001431560352000270305ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/packetcrafting6/README.md000066400000000000000000000020021431560352000303010ustar00rootroot00000000000000# DHCPv6 packet crafting It is easy to parse, create and manipulate DHCPv6 packets. The `DHCPv6` interface is the central way to move packets around. Two concrete implementations, `DHCPv6Message` and `DHCPv6Relay` take care of the details. The example in [main.go](./main.go) shows how to craft a DHCP6 Solicit message with a custom DUID_LLT, encapsulate it in a Relay message, and print its details. The output (slightly modified for readability) is ``` $ go run main.go 2018/11/08 13:56:31 DHCPv6Relay messageType=RELAY-FORW hopcount=0 linkaddr=2001:db8::1 peeraddr=2001:db8::2 options=[OptRelayMsg{relaymsg=DHCPv6Message(messageType=SOLICIT transactionID=0x9e0242, 4 options)}] 2018/11/08 13:56:31 [12 0 32 1 13 184 0 0 0 0 0 0 0 0 0 0 0 1 32 1 13 184 0 0 0 0 0 0 0 0 0 0 0 2 0 9 0 52 1 158 2 66 0 1 0 14 0 1 0 1 35 118 253 15 0 250 206 176 12 0 0 6 0 4 0 23 0 24 0 8 0 2 0 0 0 3 0 12 250 206 176 12 0 0 14 16 0 0 21 24] ``` golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/packetcrafting6/main.go000066400000000000000000000051061431560352000303050ustar00rootroot00000000000000package main import ( "log" "net" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" ) func main() { // In this example we create and manipulate a DHCPv6 solicit packet // and encapsulate it in a relay packet. To to this, we use // `dhcpv6.Message` and `dhcpv6.DHCPv6Relay`, two structures // that implement the `dhcpv6.DHCPv6` interface. // Then print the wire-format representation of the packet. iface, err := net.InterfaceByName("eth0") if err != nil { log.Fatal(err) } // Create the DHCPv6 Solicit first, using the interface "eth0" // to get the MAC address msg, err := dhcpv6.NewSolicit(iface.HardwareAddr) if err != nil { log.Fatal(err) } // In this example I want to redact the MAC address of my // network interface, so instead of replacing it manually, // I will show how to use modifiers for the purpose. // A Modifier is simply a function that can be applied on // a DHCPv6 object to manipulate it. Here we use it to // replace the MAC address with a dummy one. // Modifiers can be passed to many functions, for example // to constructors, `Exchange()`, `Solicit()`, etc. Check // the source code to know where to use them. // Existing modifiers are implemented in dhcpv6/modifiers.go . mac, err := net.ParseMAC("00:fa:ce:b0:0c:00") if err != nil { log.Fatal(err) } duid := dhcpv6.Duid{ Type: dhcpv6.DUID_LLT, HwType: iana.HWTypeEthernet, Time: dhcpv6.GetTime(), LinkLayerAddr: mac, } // As suggested above, an alternative is to call // dhcpv6.NewSolicitForInterface("eth0", dhcpv6.WithClientID(duid)) dhcpv6.WithClientID(duid)(msg) // Now encapsulate the message in a DHCPv6 relay. // As per RFC3315, the link-address and peer-address have // to be set by the relay agent. We use dummy values here. linkAddr := net.ParseIP("2001:0db8::1") peerAddr := net.ParseIP("2001:0db8::2") relay, err := dhcpv6.EncapsulateRelay(msg, dhcpv6.MessageTypeRelayForward, linkAddr, peerAddr) if err != nil { log.Fatal(err) } // Print a verbose representation of the relay packet, that will also // show a short representation of the inner Solicit message. // To print a detailed summary of the inner packet, extract it // first from the relay using `relay.GetInnerMessage()`. log.Print(relay.Summary()) // And finally, print the bytes that would be sent on the wire log.Print(relay.ToBytes()) // Note: there are many more functions in the library, check them // out in the source code. For example, if you want to decode a // byte stream into a DHCPv6 message or relay, you can use // `dhcpv6.FromBytes`. } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/server6/000077500000000000000000000000001431560352000253515ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/server6/README.md000066400000000000000000000006741431560352000266370ustar00rootroot00000000000000# DHCPv6 server A DHCPv6 server requires the user to implement a request handler. Basically the user has to provide the logic to answer to each packet. The library offers a few facilities to forge response packets, e.g. `NewAdvertiseFromSolicit`, `NewReplyFromDHCPv6Message` and so on. Look at the source code to see what's available. An example server that will print (but not reply to) the client's request is shown in [main.go](./main.go) golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/examples/server6/main.go000066400000000000000000000010031431560352000266160ustar00rootroot00000000000000package main import ( "log" "net" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/server6" ) func handler(conn net.PacketConn, peer net.Addr, m dhcpv6.DHCPv6) { // this function will just print the received DHCPv6 message, without replying log.Print(m.Summary()) } func main() { laddr := &net.UDPAddr{ IP: net.ParseIP("::1"), Port: dhcpv6.DefaultServerPort, } server, err := server6.NewServer("", laddr, handler) if err != nil { log.Fatal(err) } server.Serve() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/go.mod000066400000000000000000000012431431560352000232450ustar00rootroot00000000000000module github.com/insomniacslk/dhcp go 1.13 require ( github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 github.com/mdlayher/netlink v1.1.1 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.6.1 github.com/u-root/uio v0.0.0-20210528114334-82958018845c golang.org/x/net v0.0.0-20201110031124-69a78807bb2b golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea ) golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/go.sum000066400000000000000000000177431431560352000233060ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 h1:0eU/faU2oDIB2BkQVM02hgRLJjGzzUuRf19HUhp0394= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c h1:7cpGGTQO6+OuYQWkueqeXuErSjs1NZtpALpv1x7Mq4g= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1 h1:VqG+Voq9V4uZ+04vjIrcSCWDpf91B1xxbP4QBUmUJE8= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/000077500000000000000000000000001431560352000230475ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/archtype.go000066400000000000000000000102471431560352000252210ustar00rootroot00000000000000package iana import ( "fmt" "strings" "github.com/u-root/uio/uio" ) // Arch encodes an architecture type per RFC 4578, Section 2.1. type Arch uint16 // See RFC 4578, 5970, and http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#processor-architecture const ( INTEL_X86PC Arch = 0 NEC_PC98 Arch = 1 EFI_ITANIUM Arch = 2 DEC_ALPHA Arch = 3 ARC_X86 Arch = 4 INTEL_LEAN_CLIENT Arch = 5 EFI_IA32 Arch = 6 EFI_X86_64 Arch = 7 EFI_XSCALE Arch = 8 EFI_BC Arch = 9 EFI_ARM32 Arch = 10 EFI_ARM64 Arch = 11 PPC_OPEN_FIRMWARE Arch = 12 PPC_EPAPR Arch = 13 PPC_OPAL Arch = 14 EFI_X86_HTTP Arch = 15 EFI_X86_64_HTTP Arch = 16 EFI_BC_HTTP Arch = 17 EFI_ARM32_HTTP Arch = 18 EFI_ARM64_HTTP Arch = 19 INTEL_X86PC_HTTP Arch = 20 UBOOT_ARM32 Arch = 21 UBOOT_ARM64 Arch = 22 UBOOT_ARM32_HTTP Arch = 23 UBOOT_ARM64_HTTP Arch = 24 EFI_RISCV32 Arch = 25 EFI_RISCV32_HTTP Arch = 26 EFI_RISCV64 Arch = 27 EFI_RISCV64_HTTP Arch = 28 EFI_RISCV128 Arch = 29 EFI_RISCV128_HTTP Arch = 30 S390_BASIC Arch = 31 S390_EXTENDED Arch = 32 EFI_MIPS32 Arch = 33 EFI_MIPS64 Arch = 34 EFI_SUNWAY32 Arch = 35 EFI_SUNWAY64 Arch = 36 ) // archTypeToStringMap maps an Arch to a mnemonic name var archTypeToStringMap = map[Arch]string{ INTEL_X86PC: "Intel x86PC", NEC_PC98: "NEC/PC98", EFI_ITANIUM: "EFI Itanium", DEC_ALPHA: "DEC Alpha", ARC_X86: "Arc x86", INTEL_LEAN_CLIENT: "Intel Lean Client", EFI_IA32: "EFI IA32", EFI_XSCALE: "EFI Xscale", EFI_X86_64: "EFI x86-64", EFI_BC: "EFI BC", EFI_ARM32: "EFI ARM32", EFI_ARM64: "EFI ARM64", PPC_OPEN_FIRMWARE: "PowerPC Open Firmware", PPC_EPAPR: "PowerPC ePAPR", PPC_OPAL: "POWER OPAL v3", EFI_X86_HTTP: "EFI x86 boot from HTTP", EFI_X86_64_HTTP: "EFI x86-64 boot from HTTP", EFI_BC_HTTP: "EFI BC boot from HTTP", EFI_ARM32_HTTP: "EFI ARM32 boot from HTTP", EFI_ARM64_HTTP: "EFI ARM64 boot from HTTP", INTEL_X86PC_HTTP: "Intel x86PC boot from HTTP", UBOOT_ARM32: "U-Boot ARM32", UBOOT_ARM64: "U-Boot ARM64", UBOOT_ARM32_HTTP: "U-boot ARM32 boot from HTTP", UBOOT_ARM64_HTTP: "U-Boot ARM64 boot from HTTP", EFI_RISCV32: "EFI RISC-V 32-bit", EFI_RISCV32_HTTP: "EFI RISC-V 32-bit boot from HTTP", EFI_RISCV64: "EFI RISC-V 64-bit", EFI_RISCV64_HTTP: "EFI RISC-V 64-bit boot from HTTP", EFI_RISCV128: "EFI RISC-V 128-bit", EFI_RISCV128_HTTP: "EFI RISC-V 128-bit boot from HTTP", S390_BASIC: "s390 Basic", S390_EXTENDED: "s390 Extended", EFI_MIPS32: "EFI MIPS32", EFI_MIPS64: "EFI MIPS64", EFI_SUNWAY32: "EFI Sunway 32-bit", EFI_SUNWAY64: "EFI Sunway 64-bit", } // String returns a mnemonic name for a given architecture type. func (a Arch) String() string { if at := archTypeToStringMap[a]; at != "" { return at } return "unknown" } // Archs represents multiple Arch values. type Archs []Arch // Contains returns whether b is one of the Archs in a. func (a Archs) Contains(b Arch) bool { for _, t := range a { if t == b { return true } } return false } // ToBytes returns the serialized option defined by RFC 4578 (DHCPv4) and RFC // 5970 (DHCPv6) as the Client System Architecture Option. func (a Archs) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) for _, at := range a { buf.Write16(uint16(at)) } return buf.Data() } // String returns the list of archs in a human-readable manner. func (a Archs) String() string { s := make([]string, 0, len(a)) for _, arch := range a { s = append(s, arch.String()) } return strings.Join(s, ", ") } // FromBytes parses a DHCP list of architecture types as defined by RFC 4578 // and RFC 5970. func (a *Archs) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) if buf.Len() == 0 { return fmt.Errorf("must have at least one archtype if option is present") } *a = make([]Arch, 0, buf.Len()/2) for buf.Has(2) { *a = append(*a, Arch(buf.Read16())) } return buf.FinError() } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/entid.go000066400000000000000000000012111431560352000244740ustar00rootroot00000000000000package iana // EnterpriseID represents the Enterprise IDs as set by IANA type EnterpriseID int // See https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers for values const ( EnterpriseIDCiscoSystems EnterpriseID = 9 EnterpriseIDCienaCorporation EnterpriseID = 1271 ) var enterpriseIDToStringMap = map[EnterpriseID]string{ EnterpriseIDCiscoSystems: "Cisco Systems", EnterpriseIDCienaCorporation: "Ciena Corporation", } // String returns the vendor name for a given Enterprise ID func (e EnterpriseID) String() string { if vendor := enterpriseIDToStringMap[e]; vendor != "" { return vendor } return "Unknown" } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/hwtypes.go000066400000000000000000000046721431560352000251120ustar00rootroot00000000000000package iana // HWType is a hardware type as per RFC 2132 and defined by the IANA. type HWType uint16 // See IANA for values. const ( _ HWType = iota // skip 0 HWTypeEthernet HWTypeExperimentalEthernet HWTypeAmateurRadioAX25 HWTypeProteonTokenRing HWTypeChaos HWTypeIEEE802 HWTypeARCNET HWTypeHyperchannel HWTypeLanstar HWTypeAutonet HWTypeLocalTalk HWTypeLocalNet HWTypeUltraLink HWTypeSMDS HWTypeFrameRelay HWTypeATM HWTypeHDLC HWTypeFibreChannel HWTypeATM2 HWTypeSerialLine HWTypeATM3 HWTypeMILSTD188220 HWTypeMetricom HWTypeIEEE1394 HWTypeMAPOS HWTypeTwinaxial HWTypeEUI64 HWTypeHIPARP HWTypeISO7816 HWTypeARPSec HWTypeIPsec HWTypeInfiniband HWTypeCAI HWTypeWiegandInterface HWTypePureIP ) var hwTypeToString = map[HWType]string{ HWTypeEthernet: "Ethernet", HWTypeExperimentalEthernet: "Experimental Ethernet", HWTypeAmateurRadioAX25: "Amateur Radio AX.25", HWTypeProteonTokenRing: "Proteon ProNET Token Ring", HWTypeChaos: "Chaos", HWTypeIEEE802: "IEEE 802", HWTypeARCNET: "ARCNET", HWTypeHyperchannel: "Hyperchannel", HWTypeLanstar: "Lanstar", HWTypeAutonet: "Autonet Short Address", HWTypeLocalTalk: "LocalTalk", HWTypeLocalNet: "LocalNet", HWTypeUltraLink: "Ultra link", HWTypeSMDS: "SMDS", HWTypeFrameRelay: "Frame Relay", HWTypeATM: "ATM", HWTypeHDLC: "HDLC", HWTypeFibreChannel: "Fibre Channel", HWTypeATM2: "ATM 2", HWTypeSerialLine: "Serial Line", HWTypeATM3: "ATM 3", HWTypeMILSTD188220: "MIL-STD-188-220", HWTypeMetricom: "Metricom", HWTypeIEEE1394: "IEEE 1394.1995", HWTypeMAPOS: "MAPOS", HWTypeTwinaxial: "Twinaxial", HWTypeEUI64: "EUI-64", HWTypeHIPARP: "HIPARP", HWTypeISO7816: "IP and ARP over ISO 7816-3", HWTypeARPSec: "ARPSec", HWTypeIPsec: "IPsec tunnel", HWTypeInfiniband: "Infiniband", HWTypeCAI: "CAI, TIA-102 Project 125 Common Air Interface", HWTypeWiegandInterface: "Wiegand Interface", HWTypePureIP: "Pure IP", } // String implements fmt.Stringer. func (h HWType) String() string { hwtype := hwTypeToString[h] if hwtype == "" { hwtype = "unknown" } return hwtype } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/iana.go000066400000000000000000000001011431560352000242760ustar00rootroot00000000000000// Package iana contains constants defined by IANA. package iana golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/iana/statuscodes.go000066400000000000000000000051451431560352000257440ustar00rootroot00000000000000package iana // StatusCode represents a IANA status code for DHCPv6 // // IANA Status Codes for DHCPv6 // https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#dhcpv6-parameters-5 type StatusCode uint16 // IANA status codes const ( // RFC 3315 par. 24..4 StatusSuccess StatusCode = 0 StatusUnspecFail StatusCode = 1 StatusNoAddrsAvail StatusCode = 2 StatusNoBinding StatusCode = 3 StatusNotOnLink StatusCode = 4 StatusUseMulticast StatusCode = 5 StatusNoPrefixAvail StatusCode = 6 // RFC 5007 StatusUnknownQueryType StatusCode = 7 StatusMalformedQuery StatusCode = 8 StatusNotConfigured StatusCode = 9 StatusNotAllowed StatusCode = 10 // RFC 5460 StatusQueryTerminated StatusCode = 11 // RFC 7653 StatusDataMissing StatusCode = 12 StatusCatchUpComplete StatusCode = 13 StatusNotSupported StatusCode = 14 StatusTLSConnectionRefused StatusCode = 15 // RFC 8156 StatusAddressInUse StatusCode = 16 StatusConfigurationConflict StatusCode = 17 StatusMissingBindingInformation StatusCode = 18 StatusOutdatedBindingInformation StatusCode = 19 StatusServerShuttingDown StatusCode = 20 StatusDNSUpdateNotSupported StatusCode = 21 StatusExcessiveTimeSkew StatusCode = 22 ) // String returns a mnemonic name for a given status code func (s StatusCode) String() string { if sc := statusCodeToStringMap[s]; sc != "" { return sc } return "Unknown" } var statusCodeToStringMap = map[StatusCode]string{ StatusSuccess: "Success", StatusUnspecFail: "UnspecFail", StatusNoAddrsAvail: "NoAddrsAvail", StatusNoBinding: "NoBinding", StatusNotOnLink: "NotOnLink", StatusUseMulticast: "UseMulticast", StatusNoPrefixAvail: "NoPrefixAvail", // RFC 5007 StatusUnknownQueryType: "UnknownQueryType", StatusMalformedQuery: "MalformedQuery", StatusNotConfigured: "NotConfigured", StatusNotAllowed: "NotAllowed", // RFC 5460 StatusQueryTerminated: "QueryTerminated", // RFC 7653 StatusDataMissing: "DataMissing", StatusCatchUpComplete: "CatchUpComplete", StatusNotSupported: "NotSupported", StatusTLSConnectionRefused: "TLSConnectionRefused", // RFC 8156 StatusAddressInUse: "AddressInUse", StatusConfigurationConflict: "ConfigurationConflict", StatusMissingBindingInformation: "MissingBindingInformation", StatusOutdatedBindingInformation: "OutdatedBindingInformation", StatusServerShuttingDown: "ServerShuttingDown", StatusDNSUpdateNotSupported: "DNSUpdateNotSupported", StatusExcessiveTimeSkew: "ExcessiveTimeSkew", } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/000077500000000000000000000000001431560352000242625ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/bindtodevice_bsd.go000066400000000000000000000006251431560352000301030ustar00rootroot00000000000000// +build aix freebsd openbsd netbsd package interfaces import ( "net" "golang.org/x/sys/unix" ) // BindToInterface emulates linux's SO_BINDTODEVICE option for a socket by using // IP_RECVIF. func BindToInterface(fd int, ifname string) error { iface, err := net.InterfaceByName(ifname) if err != nil { return err } return unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVIF, iface.Index) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/bindtodevice_darwin.go000066400000000000000000000006051431560352000306150ustar00rootroot00000000000000// +build darwin package interfaces import ( "net" "golang.org/x/sys/unix" ) // BindToInterface emulates linux's SO_BINDTODEVICE option for a socket by using // IP_BOUND_IF. func BindToInterface(fd int, ifname string) error { iface, err := net.InterfaceByName(ifname) if err != nil { return err } return unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/bindtodevice_linux.go000066400000000000000000000002411431560352000304640ustar00rootroot00000000000000// +build linux package interfaces import "golang.org/x/sys/unix" func BindToInterface(fd int, ifname string) error { return unix.BindToDevice(fd, ifname) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/bindtodevice_windows.go000066400000000000000000000002611431560352000310210ustar00rootroot00000000000000package interfaces import "errors" // BindToInterface fails on Windows. func BindToInterface(fd int, ifname string) error { return errors.New("not implemented on Windows") } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/interfaces.go000066400000000000000000000022751431560352000267420ustar00rootroot00000000000000package interfaces import "net" // InterfaceMatcher is a function type used to match the interfaces we want. See // GetInterfacesFunc below for usage. type InterfaceMatcher func(net.Interface) bool // interfaceGetter is used for testing purposes var interfaceGetter = net.Interfaces // GetInterfacesFunc loops through the available network interfaces, and returns // a list of interfaces for which the passed InterfaceMatcher function returns // true. func GetInterfacesFunc(matcher InterfaceMatcher) ([]net.Interface, error) { ifaces, err := interfaceGetter() if err != nil { return nil, err } ret := make([]net.Interface, 0) for _, iface := range ifaces { if matcher(iface) { ret = append(ret, iface) } } return ret, nil } // GetLoopbackInterfaces returns a list of loopback interfaces. func GetLoopbackInterfaces() ([]net.Interface, error) { return GetInterfacesFunc(func(iface net.Interface) bool { return iface.Flags&net.FlagLoopback != 0 }) } // GetNonLoopbackInterfaces returns a list of non-loopback interfaces. func GetNonLoopbackInterfaces() ([]net.Interface, error) { return GetInterfacesFunc(func(iface net.Interface) bool { return iface.Flags&net.FlagLoopback == 0 }) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/interfaces/interfaces_test.go000066400000000000000000000035271431560352000300020ustar00rootroot00000000000000package interfaces import ( "errors" "net" "testing" "github.com/stretchr/testify/require" ) func fakeIface(idx int, name string, loopback bool) net.Interface { var flags net.Flags if loopback { flags |= net.FlagLoopback } return net.Interface{ Index: idx, MTU: 1500, Name: name, HardwareAddr: []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, Flags: flags, } } func TestGetLoopbackInterfaces(t *testing.T) { interfaceGetter = func() ([]net.Interface, error) { return []net.Interface{ fakeIface(0, "lo", true), fakeIface(1, "eth0", false), fakeIface(2, "eth1", false), }, nil } ifaces, err := GetLoopbackInterfaces() // this has to be reassigned before any require.* call interfaceGetter = net.Interfaces require.NoError(t, err) require.Equal(t, 1, len(ifaces)) } func TestGetLoopbackInterfacesError(t *testing.T) { interfaceGetter = func() ([]net.Interface, error) { return nil, errors.New("expected error") } _, err := GetLoopbackInterfaces() // this has to be reassigned before any require.* call interfaceGetter = net.Interfaces require.Error(t, err) } func TestGetNonLoopbackInterfaces(t *testing.T) { interfaceGetter = func() ([]net.Interface, error) { return []net.Interface{ fakeIface(0, "lo", true), fakeIface(1, "eth0", false), fakeIface(2, "eth1", false), }, nil } ifaces, err := GetNonLoopbackInterfaces() // this has to be reassigned before any require.* call interfaceGetter = net.Interfaces require.NoError(t, err) require.Equal(t, 2, len(ifaces)) } func TestGetNonLoopbackInterfacesError(t *testing.T) { interfaceGetter = func() ([]net.Interface, error) { return nil, errors.New("expected error") } _, err := GetNonLoopbackInterfaces() // this has to be reassigned before any require.* call interfaceGetter = net.Interfaces require.Error(t, err) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/000077500000000000000000000000001431560352000236115ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/netboot.go000066400000000000000000000116371431560352000256220ustar00rootroot00000000000000package netboot import ( "errors" "fmt" "log" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/client4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" ) var sleeper = func(d time.Duration) { time.Sleep(d) } // BootConf is a structure describes everything a host needs to know to boot over network type BootConf struct { // NetConf is the network configuration of the client NetConf // BootfileURL is "where is the image (kernel)". // See RFC5970 section 3.1 for IPv6 and RFC2132 section 9.5 ("Bootfile name") for IPv4 BootfileURL string // BootfileParam is "what arguments should we pass (cmdline)". // See RFC5970 section 3.2 for IPv6. BootfileParam []string } // RequestNetbootv6 sends a netboot request via DHCPv6 and returns the exchanged packets. Additional modifiers // can be passed to manipulate both solicit and advertise packets. func RequestNetbootv6(ifname string, timeout time.Duration, retries int, modifiers ...dhcpv6.Modifier) ([]dhcpv6.DHCPv6, error) { var ( conversation []dhcpv6.DHCPv6 err error ) modifiers = append(modifiers, dhcpv6.WithNetboot) delay := 2 * time.Second for i := 0; i <= retries; i++ { log.Printf("sending request, attempt #%d", i+1) client := client6.NewClient() client.ReadTimeout = timeout conversation, err = client.Exchange(ifname, modifiers...) if err != nil { log.Printf("Client.Exchange failed: %v", err) if i >= retries { // don't wait at the end of the last attempt return nil, fmt.Errorf("netboot failed after %d attempts: %v", retries+1, err) } log.Printf("sleeping %v before retrying", delay) sleeper(delay) // TODO add random splay delay = delay * 2 continue } break } return conversation, nil } // RequestNetbootv4 sends a netboot request via DHCPv4 and returns the exchanged packets. Additional modifiers // can be passed to manipulate both the discover and offer packets. func RequestNetbootv4(ifname string, timeout time.Duration, retries int, modifiers ...dhcpv4.Modifier) ([]*dhcpv4.DHCPv4, error) { var ( conversation []*dhcpv4.DHCPv4 err error ) delay := 2 * time.Second modifiers = append(modifiers, dhcpv4.WithNetboot) for i := 0; i <= retries; i++ { log.Printf("sending request, attempt #%d", i+1) client := client4.NewClient() client.ReadTimeout = timeout conversation, err = client.Exchange(ifname, modifiers...) if err != nil { log.Printf("Client.Exchange failed: %v", err) log.Printf("sleeping %v before retrying", delay) if i >= retries { // don't wait at the end of the last attempt break } sleeper(delay) // TODO add random splay delay = delay * 2 continue } break } return conversation, nil } // ConversationToNetconf extracts network configuration and boot file URL from a // DHCPv6 4-way conversation and returns them, or an error if any. func ConversationToNetconf(conversation []dhcpv6.DHCPv6) (*BootConf, error) { var advertise, reply *dhcpv6.Message for _, m := range conversation { switch m.Type() { case dhcpv6.MessageTypeAdvertise: advertise = m.(*dhcpv6.Message) case dhcpv6.MessageTypeReply: reply = m.(*dhcpv6.Message) } } if reply == nil { return nil, errors.New("no REPLY received") } bootconf := &BootConf{} netconf, err := GetNetConfFromPacketv6(reply) if err != nil { return nil, fmt.Errorf("cannot get netconf from packet: %v", err) } bootconf.NetConf = *netconf if u := reply.Options.BootFileURL(); len(u) > 0 { bootconf.BootfileURL = u bootconf.BootfileParam = reply.Options.BootFileParam() } else { log.Printf("no bootfile URL option found in REPLY, fallback to ADVERTISE's value") if u := advertise.Options.BootFileURL(); len(u) > 0 { bootconf.BootfileURL = u bootconf.BootfileParam = advertise.Options.BootFileParam() } } if len(bootconf.BootfileURL) == 0 { return nil, errors.New("no bootfile URL option found") } return bootconf, nil } // ConversationToNetconfv4 extracts network configuration and boot file URL from a // DHCPv4 4-way conversation and returns them, or an error if any. func ConversationToNetconfv4(conversation []*dhcpv4.DHCPv4) (*BootConf, error) { var reply *dhcpv4.DHCPv4 for _, m := range conversation { // look for a BootReply packet of type Offer containing the bootfile URL. // Normally both packets with Message Type OFFER or ACK do contain // the bootfile URL. if m.OpCode == dhcpv4.OpcodeBootReply && m.MessageType() == dhcpv4.MessageTypeOffer { reply = m break } } if reply == nil { return nil, errors.New("no OFFER with valid bootfile URL received") } bootconf := &BootConf{} netconf, err := GetNetConfFromPacketv4(reply) if err != nil { return nil, fmt.Errorf("could not get netconf: %v", err) } bootconf.NetConf = *netconf bootconf.BootfileURL = reply.BootFileName // TODO: should we support bootfile parameters here somehow? (see netconf.BootfileParam) return bootconf, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/netconf.go000066400000000000000000000174521431560352000256050ustar00rootroot00000000000000package netboot import ( "errors" "fmt" "io/ioutil" "net" "os" "strings" "syscall" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/jsimonetti/rtnetlink" "github.com/jsimonetti/rtnetlink/rtnl" "github.com/mdlayher/netlink" ) // AddrConf holds a single IP address configuration for a NIC type AddrConf struct { IPNet net.IPNet PreferredLifetime time.Duration ValidLifetime time.Duration } // NetConf holds multiple IP configuration for a NIC, and DNS configuration type NetConf struct { Addresses []AddrConf DNSServers []net.IP DNSSearchList []string Routers []net.IP NTPServers []net.IP } // GetNetConfFromPacketv6 extracts network configuration information from a DHCPv6 // Reply packet and returns a populated NetConf structure func GetNetConfFromPacketv6(d *dhcpv6.Message) (*NetConf, error) { iana := d.Options.OneIANA() if iana == nil { return nil, errors.New("no option IA NA found") } netconf := NetConf{} for _, iaaddr := range iana.Options.Addresses() { netconf.Addresses = append(netconf.Addresses, AddrConf{ IPNet: net.IPNet{ IP: iaaddr.IPv6Addr, // This mask tells Linux which addresses we know to be // "on-link" (i.e., reachable on this interface without // having to talk to a router). // // Since DHCPv6 does not give us that information, we // have to assume that no addresses are on-link. To do // that, we use /128. (See also RFC 5942 Section 5, // "Observed Incorrect Implementation Behavior".) Mask: net.CIDRMask(128, 128), }, PreferredLifetime: iaaddr.PreferredLifetime, ValidLifetime: iaaddr.ValidLifetime, }) } // get DNS configuration netconf.DNSServers = d.Options.DNS() // get domain search list domains := d.Options.DomainSearchList() if domains != nil { netconf.DNSSearchList = domains.Labels } // get NTP servers netconf.NTPServers = d.Options.NTPServers() return &netconf, nil } // GetNetConfFromPacketv4 extracts network configuration information from a DHCPv4 // Reply packet and returns a populated NetConf structure func GetNetConfFromPacketv4(d *dhcpv4.DHCPv4) (*NetConf, error) { // extract the address from the DHCPv4 address ipAddr := d.YourIPAddr if ipAddr == nil || ipAddr.Equal(net.IPv4zero) { return nil, errors.New("ip address is null (0.0.0.0)") } netconf := NetConf{} // get the subnet mask from OptionSubnetMask. If the netmask is not defined // in the packet, an error is returned netmask := d.SubnetMask() if netmask == nil { return nil, errors.New("no netmask option in response packet") } ones, _ := netmask.Size() if ones == 0 { return nil, errors.New("netmask extracted from OptSubnetMask options is null") } // netconf struct requires a valid lifetime to be specified. ValidLifetime is a dhcpv6 // concept, the closest mapping in dhcpv4 world is "IP Address Lease Time". If the lease // time option is nil, we set it to 0 leaseTime := d.IPAddressLeaseTime(0) netconf.Addresses = append(netconf.Addresses, AddrConf{ IPNet: net.IPNet{ IP: ipAddr, Mask: netmask, }, PreferredLifetime: 0, ValidLifetime: leaseTime, }) // get DNS configuration netconf.DNSServers = d.DNS() // get domain search list dnsSearchList := d.DomainSearch() if dnsSearchList != nil { if len(dnsSearchList.Labels) == 0 { return nil, errors.New("dns search list is empty") } netconf.DNSSearchList = dnsSearchList.Labels } // get default gateway routersList := d.Router() if len(routersList) == 0 { return nil, errors.New("no routers specified in the corresponding option") } netconf.Routers = routersList // get NTP servers netconf.NTPServers = d.NTPServers() return &netconf, nil } // IfUp brings up an interface by name, and waits for it to come up until a timeout expires func IfUp(ifname string, timeout time.Duration) (_ *net.Interface, err error) { start := time.Now() rt, err := rtnl.Dial(nil) if err != nil { return nil, err } defer func() { if cerr := rt.Close(); cerr != nil { err = cerr } }() for time.Since(start) < timeout { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } // If the interface is up, return. According to kernel documentation OperState may // be either Up or Unknown: // Interface is in RFC2863 operational state UP or UNKNOWN. This is for // backward compatibility, routing daemons, dhcp clients can use this // flag to determine whether they should use the interface. // Source: https://www.kernel.org/doc/Documentation/networking/operstates.txt operState, err := getOperState(iface.Index) if err != nil { return nil, err } if operState == rtnetlink.OperStateUp || operState == rtnetlink.OperStateUnknown { // XXX despite the OperUp state, upon the first attempt I // consistently get a "cannot assign requested address" error. Need // to investigate more. time.Sleep(time.Second) return iface, nil } // otherwise try to bring it up if err := rt.LinkUp(iface); err != nil { return nil, fmt.Errorf("interface %q: %v can't bring it up: %v", ifname, iface, err) } time.Sleep(10 * time.Millisecond) } return nil, fmt.Errorf("timed out while waiting for %s to come up", ifname) } // ConfigureInterface configures a network interface with the configuration held by a // NetConf structure func ConfigureInterface(ifname string, netconf *NetConf) (err error) { iface, err := net.InterfaceByName(ifname) if err != nil { return err } rt, err := rtnl.Dial(nil) if err != nil { return err } defer func() { if cerr := rt.Close(); err != nil { err = cerr } }() // configure interfaces for _, addr := range netconf.Addresses { if err := rt.AddrAdd(iface, &addr.IPNet); err != nil { return fmt.Errorf("cannot configure %s on %s: %v", ifname, addr.IPNet, err) } } // configure /etc/resolv.conf resolvconf := "" for _, ns := range netconf.DNSServers { resolvconf += fmt.Sprintf("nameserver %s\n", ns) } if len(netconf.DNSSearchList) > 0 { resolvconf += fmt.Sprintf("search %s\n", strings.Join(netconf.DNSSearchList, " ")) } if err = ioutil.WriteFile("/etc/resolv.conf", []byte(resolvconf), 0644); err != nil { return fmt.Errorf("could not write resolv.conf file %v", err) } // FIXME wut? No IPv6 here? // add default route information for v4 space. only one default route is allowed // so ignore the others if there are multiple ones if len(netconf.Routers) > 0 { // if there is a default v4 route, remove it, as we want to add the one we just got during // the dhcp transaction. if the route is not present, which is the final state we want, // an error is returned so ignore it dst := net.IPNet{ IP: net.IPv4zero, Mask: net.CIDRMask(0, 32), } // Remove a possible default route (dst 0.0.0.0) to the L2 domain (gw: 0.0.0.0), which is what // a client would want to add before initiating the DHCP transaction in order not to fail with // ENETUNREACH. If this default route has a specific metric assigned, it doesn't get removed. // The code doesn't remove any other default route (i.e. gw != 0.0.0.0). if err := rt.RouteDel(iface, net.IPNet{IP: net.IPv4zero}); err != nil { switch err := err.(type) { case *netlink.OpError: // ignore the error if it's -EEXIST or -ESRCH if !os.IsExist(err.Err) && err.Err != syscall.ESRCH { return fmt.Errorf("could not delete default route on interface %s: %v", ifname, err) } default: return fmt.Errorf("could not delete default route on interface %s: %v", ifname, err) } } src := netconf.Addresses[0].IPNet // TODO handle the remaining Routers if more than one if err := rt.RouteAdd(iface, dst, netconf.Routers[0], rtnl.WithRouteSrc(&src)); err != nil { return fmt.Errorf("could not add gateway %s for src %s dst %s to interface %s: %v", netconf.Routers[0], src, dst, ifname, err) } } return nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/netconf_integ_test.go000066400000000000000000000040321431560352000300200ustar00rootroot00000000000000// +build integration package netboot import ( "fmt" "io/ioutil" "log" "net" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // The test assumes that the interface exists and is configurable. // If you are running this test locally, you may need to adjust this value. var ifname = "eth0" func TestIfUp(t *testing.T) { iface, err := IfUp(ifname, 2*time.Second) require.NoError(t, err) assert.Equal(t, ifname, iface.Name) } func TestIfUpTimeout(t *testing.T) { _, err := IfUp(ifname, 0*time.Second) require.Error(t, err) } func TestConfigureInterface(t *testing.T) { // Linux-only. `netboot.ConfigureInterface` writes to /etc/resolv.conf when // `NetConf.DNSServers` is set. In this test we make a backup of resolv.conf // and subsequently restore it. This is really ugly, and not safe if // multiple tests do the same. resolvconf, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { panic(fmt.Sprintf("Failed to read /etc/resolv.conf: %v", err)) } type testCase struct { Name string NetConf *NetConf } testCases := []testCase{ { Name: "just IP addr", NetConf: &NetConf{ Addresses: []AddrConf{ AddrConf{IPNet: net.IPNet{IP: net.ParseIP("10.20.30.40")}}, }, }, }, { Name: "IP addr, DNS, and routers", NetConf: &NetConf{ Addresses: []AddrConf{ AddrConf{IPNet: net.IPNet{IP: net.ParseIP("10.20.30.40")}}, }, DNSServers: []net.IP{net.ParseIP("8.8.8.8")}, DNSSearchList: []string{"slackware.it"}, Routers: []net.IP{net.ParseIP("10.20.30.254")}, }, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { require.NoError(t, ConfigureInterface(ifname, tc.NetConf)) // after the test, restore the content of /etc/resolv.conf . The permissions // are used only if it didn't exist. if err = ioutil.WriteFile("/etc/resolv.conf", resolvconf, 0644); err != nil { panic(fmt.Sprintf("Failed to restore /etc/resolv.conf: %v", err)) } log.Printf("Restored /etc/resolv.conf") }) } } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/netconf_test.go000066400000000000000000000140041431560352000266320ustar00rootroot00000000000000package netboot import ( "log" "net" "testing" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/stretchr/testify/require" ) func getAdv(advModifiers ...dhcpv6.Modifier) *dhcpv6.Message { hwaddr, err := net.ParseMAC("aa:bb:cc:dd:ee:ff") if err != nil { log.Panic(err) } sol, err := dhcpv6.NewSolicit(hwaddr) if err != nil { log.Panic(err) } d, err := dhcpv6.NewAdvertiseFromSolicit(sol, advModifiers...) if err != nil { log.Panic(err) } return d } func TestGetNetConfFromPacketv6Invalid(t *testing.T) { adv := getAdv() _, err := GetNetConfFromPacketv6(adv) require.Error(t, err) } func TestGetNetConfFromPacketv6NoSearchList(t *testing.T) { addrs := []dhcpv6.OptIAAddress{ dhcpv6.OptIAAddress{ IPv6Addr: net.ParseIP("::1"), PreferredLifetime: 3600 * time.Second, ValidLifetime: 5200 * time.Second, }, } adv := getAdv( dhcpv6.WithIANA(addrs...), dhcpv6.WithDNS(net.ParseIP("fe80::1")), ) _, err := GetNetConfFromPacketv6(adv) require.NoError(t, err) } func TestGetNetConfFromPacketv6(t *testing.T) { addrs := []dhcpv6.OptIAAddress{ dhcpv6.OptIAAddress{ IPv6Addr: net.ParseIP("::1"), PreferredLifetime: 3600 * time.Second, ValidLifetime: 5200 * time.Second, }, } adv := getAdv( dhcpv6.WithIANA(addrs...), dhcpv6.WithDNS(net.ParseIP("fe80::1")), dhcpv6.WithDomainSearchList("slackware.it"), ) netconf, err := GetNetConfFromPacketv6(adv) require.NoError(t, err) // check addresses require.Equal(t, 1, len(netconf.Addresses)) require.Equal(t, net.ParseIP("::1"), netconf.Addresses[0].IPNet.IP) require.Equal(t, 3600*time.Second, netconf.Addresses[0].PreferredLifetime) require.Equal(t, 5200*time.Second, netconf.Addresses[0].ValidLifetime) // check DNSes require.Equal(t, 1, len(netconf.DNSServers)) require.Equal(t, net.ParseIP("fe80::1"), netconf.DNSServers[0]) // check DNS search list require.Equal(t, 1, len(netconf.DNSSearchList)) require.Equal(t, "slackware.it", netconf.DNSSearchList[0]) // check routers require.Equal(t, 0, len(netconf.Routers)) } func TestGetNetConfFromPacketv4AddrZero(t *testing.T) { d, _ := dhcpv4.New(dhcpv4.WithYourIP(net.IPv4zero)) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4NoMask(t *testing.T) { d, _ := dhcpv4.New(dhcpv4.WithYourIP(net.ParseIP("10.0.0.1"))) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4NullMask(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(0, 0, 0, 0)), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4NoLeaseTime(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4EmptyDNSList(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(0)), dhcpv4.WithDNS(), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4NoSearchList(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(0)), dhcpv4.WithDNS(net.ParseIP("10.10.0.1"), net.ParseIP("10.10.0.2")), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4EmptySearchList(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(0)), dhcpv4.WithDNS(net.ParseIP("10.10.0.1"), net.ParseIP("10.10.0.2")), dhcpv4.WithDomainSearchList(), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4NoRouter(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(0)), dhcpv4.WithDNS(net.ParseIP("10.10.0.1"), net.ParseIP("10.10.0.2")), dhcpv4.WithDomainSearchList("slackware.it", "dhcp.slackware.it"), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4EmptyRouter(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(0)), dhcpv4.WithDNS(net.ParseIP("10.10.0.1"), net.ParseIP("10.10.0.2")), dhcpv4.WithDomainSearchList("slackware.it", "dhcp.slackware.it"), dhcpv4.WithRouter(), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) _, err := GetNetConfFromPacketv4(d) require.Error(t, err) } func TestGetNetConfFromPacketv4(t *testing.T) { d, _ := dhcpv4.New( dhcpv4.WithNetmask(net.IPv4Mask(255, 255, 255, 0)), dhcpv4.WithLeaseTime(uint32(5200)), dhcpv4.WithDNS(net.ParseIP("10.10.0.1"), net.ParseIP("10.10.0.2")), dhcpv4.WithDomainSearchList("slackware.it", "dhcp.slackware.it"), dhcpv4.WithRouter(net.ParseIP("10.0.0.254")), dhcpv4.WithYourIP(net.ParseIP("10.0.0.1")), ) netconf, err := GetNetConfFromPacketv4(d) require.NoError(t, err) // check addresses require.Equal(t, 1, len(netconf.Addresses)) require.Equal(t, net.ParseIP("10.0.0.1"), netconf.Addresses[0].IPNet.IP) require.Equal(t, time.Duration(0), netconf.Addresses[0].PreferredLifetime) require.Equal(t, 5200*time.Second, netconf.Addresses[0].ValidLifetime) // check DNSes require.Equal(t, 2, len(netconf.DNSServers)) require.Equal(t, net.ParseIP("10.10.0.1").To4(), netconf.DNSServers[0]) require.Equal(t, net.ParseIP("10.10.0.2").To4(), netconf.DNSServers[1]) // check DNS search list require.Equal(t, 2, len(netconf.DNSSearchList)) require.Equal(t, "slackware.it", netconf.DNSSearchList[0]) require.Equal(t, "dhcp.slackware.it", netconf.DNSSearchList[1]) // check routers require.Equal(t, 1, len(netconf.Routers)) require.Equal(t, net.ParseIP("10.0.0.254").To4(), netconf.Routers[0]) } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/netboot/rtnetlink_linux.go000066400000000000000000000010341431560352000273670ustar00rootroot00000000000000package netboot import ( "log" "github.com/jsimonetti/rtnetlink" ) // getOperState returns the operational state for the given interface index. func getOperState(iface int) (rtnetlink.OperationalState, error) { conn, err := rtnetlink.Dial(nil) if err != nil { return 0, err } defer func() { err := conn.Close() if err != nil { log.Printf("failed to close rtnetlink connection: %v", err) } }() msg, err := conn.Link.Get(uint32(iface)) if err != nil { return 0, err } return msg.Attributes.OperationalState, nil } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/rfc1035label/000077500000000000000000000000001431560352000242225ustar00rootroot00000000000000golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/rfc1035label/label.go000066400000000000000000000103651431560352000256350ustar00rootroot00000000000000package rfc1035label import ( "errors" "fmt" "strings" ) // Labels represents RFC1035 labels // // This implements RFC 1035 labels, including compression. // https://tools.ietf.org/html/rfc1035#section-4.1.4 type Labels struct { // original contains the original bytes if the object was parsed from a byte // sequence, or nil otherwise. The `original` field is necessary to deal // with compressed labels. If the labels are further modified, the original // content is invalidated and no compression will be used. original []byte // Labels contains the parsed labels. A change here invalidates the // `original` object. Labels []string } // same compares two string arrays func same(a, b []string) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } // String prints labels. func (l *Labels) String() string { return fmt.Sprintf("%v", l.Labels) } // ToBytes returns a byte sequence representing the labels. If the original // sequence is modified, the labels are parsed again, otherwise the original // byte sequence is returned. func (l *Labels) ToBytes() []byte { // if the original byte sequence has been modified, invalidate it and // serialize again. // NOTE: this function is not thread-safe. If multiple threads modify // the `Labels` field, the result may be wrong. originalLabels, err := labelsFromBytes(l.original) // if the original object has not been modified, or we cannot parse it, // return the original bytes. if err != nil || (l.original != nil && same(originalLabels, l.Labels)) { return l.original } return labelsToBytes(l.Labels) } // Length returns the length in bytes of the serialized labels func (l *Labels) Length() int { return len(l.ToBytes()) } // NewLabels returns an initialized Labels object. func NewLabels() *Labels { return &Labels{ Labels: make([]string, 0), } } // FromBytes reads labels from a bytes stream according to RFC 1035. func (l *Labels) FromBytes(data []byte) error { labs, err := labelsFromBytes(data) if err != nil { return err } l.original = data l.Labels = labs return nil } // FromBytes returns a Labels object from the given byte sequence, or an error if // any. func FromBytes(data []byte) (*Labels, error) { var l Labels if err := l.FromBytes(data); err != nil { return nil, err } return &l, nil } // fromBytes decodes a serialized stream and returns a list of labels func labelsFromBytes(buf []byte) ([]string, error) { var ( labels = make([]string, 0) pos, oldPos int label string handlingPointer bool ) for { if pos >= len(buf) { // interpret label without trailing zero-length byte as a partial // domain name field as per RFC 4704 Section 4.2 if label != "" { labels = append(labels, label) } break } length := int(buf[pos]) pos++ var chunk string if length == 0 { labels = append(labels, label) label = "" if handlingPointer { pos = oldPos handlingPointer = false } } else if length&0xc0 == 0xc0 { // compression pointer if handlingPointer { return nil, errors.New("rfc1035label: cannot handle nested pointers") } handlingPointer = true if pos+1 > len(buf) { return nil, errors.New("rfc1035label: pointer buffer too short") } off := int(buf[pos-1]&^0xc0)<<8 + int(buf[pos]) oldPos = pos + 1 pos = off } else { if pos+length > len(buf) { return nil, errors.New("rfc1035label: buffer too short") } chunk = string(buf[pos : pos+length]) if label != "" { label += "." } label += chunk pos += length } } return labels, nil } // labelToBytes encodes a label and returns a serialized stream of bytes func labelToBytes(label string) []byte { var encodedLabel []byte if len(label) == 0 { return []byte{0} } for _, part := range strings.Split(label, ".") { encodedLabel = append(encodedLabel, byte(len(part))) encodedLabel = append(encodedLabel, []byte(part)...) } return append(encodedLabel, 0) } // labelsToBytes encodes a list of labels and returns a serialized stream of // bytes func labelsToBytes(labels []string) []byte { var encodedLabels []byte for _, label := range labels { encodedLabels = append(encodedLabels, labelToBytes(label)...) } return encodedLabels } golang-github-insomniacslk-dhcp-0.0~git20220915.043f172/rfc1035label/label_test.go000066400000000000000000000064051431560352000266740ustar00rootroot00000000000000package rfc1035label import ( "testing" "github.com/stretchr/testify/require" ) func TestLabelsFromBytes(t *testing.T) { expected := []byte{ 0x9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 0x2, 'i', 't', 0x0, } labels, err := FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(labels.Labels)) require.Equal(t, len(expected), labels.Length()) require.Equal(t, expected, labels.ToBytes()) require.Equal(t, "slackware.it", labels.Labels[0]) } func TestLabelsFromBytesZeroLength(t *testing.T) { labels, err := FromBytes([]byte{}) require.NoError(t, err) require.Equal(t, 0, len(labels.Labels)) require.Equal(t, 0, labels.Length()) require.Equal(t, []byte{}, labels.ToBytes()) } func TestLabelsFromBytesPartialDomainName(t *testing.T) { // Partial domain name without trailing zero-length byte as per RFC 4704 // Section 4.2 expected := []byte{ 0x8, 'h', 'o', 's', 't', 'n', 'a', 'm', 'e', } labels, err := FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(labels.Labels)) require.Equal(t, len(expected), labels.Length()) require.Equal(t, expected, labels.ToBytes()) require.Equal(t, "hostname", labels.Labels[0]) } func TestLabelsFromBytesInvalidLength(t *testing.T) { _, err := FromBytes([]byte{0x5, 0xaa, 0xbb}) // short length require.Error(t, err) } func TestLabelsFromBytesInvalidLengthOffByOne(t *testing.T) { _, err := FromBytes([]byte{0x3, 0xaa, 0xbb}) // short length require.Error(t, err) } func TestLabelsToBytes(t *testing.T) { expected := []byte{ 9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 2, 'i', 't', 0, 9, 'i', 'n', 's', 'o', 'm', 'n', 'i', 'a', 'c', 9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 2, 'i', 't', 0, } labels := Labels{ Labels: []string{ "slackware.it", "insomniac.slackware.it", }, } require.Equal(t, expected, labels.ToBytes()) } func TestLabelToBytesZeroLength(t *testing.T) { labels := Labels{ Labels: []string{""}, } require.Equal(t, []byte{0}, labels.ToBytes()) } func TestCompressedLabel(t *testing.T) { data := []byte{ // slackware.it 9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 2, 'i', 't', 0, // insomniac.slackware.it 9, 'i', 'n', 's', 'o', 'm', 'n', 'i', 'a', 'c', 192, 0, // mail.systemboot.org 4, 'm', 'a', 'i', 'l', 10, 's', 'y', 's', 't', 'e', 'm', 'b', 'o', 'o', 't', 3, 'o', 'r', 'g', 0, // systemboot.org 192, 31, } expected := []string{ "slackware.it", "insomniac.slackware.it", "mail.systemboot.org", "systemboot.org", } labels, err := FromBytes(data) require.NoError(t, err) require.Equal(t, 4, len(labels.Labels)) require.Equal(t, expected, labels.Labels) require.Equal(t, len(data), labels.Length()) } func TestShortCompressedLabel(t *testing.T) { data := []byte{ // slackware.it 9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 2, 'i', 't', 0, // insomniac.slackware.it 9, 'i', 'n', 's', 'o', 'm', 'n', 'i', 'a', 'c', 192, } _, err := FromBytes(data) require.Error(t, err) } func TestNestedCompressedLabel(t *testing.T) { data := []byte{ // it 3, 'i', 't', 0, // slackware.it 9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', 192, 0, // insomniac.slackware.it 9, 'i', 'n', 's', 'o', 'm', 'n', 'i', 'a', 'c', 192, 5, } _, err := FromBytes(data) require.Error(t, err) }