From da6c9119caf3977b08fba47b567923c559fadd9b Mon Sep 17 00:00:00 2001 From: Jaroslaw Mroz Date: Thu, 6 Jun 2024 13:41:51 +0200 Subject: [PATCH 1/6] feat: add dhcpv6 relay msg support in nclient6 Signed-off-by: Jaroslaw Mroz --- .gitlab-ci.yml | 12 +++++++ dhcpv6/nclient6/client.go | 65 +++++++++++++++++++++++----------- dhcpv6/nclient6/client_test.go | 20 +++++++++-- 3 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..7157912e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,12 @@ +# dummy cicd that walways passes +image: golang:latest + + +stages: + - test + + +test: + stage: test + script: + - go test -v ./... \ No newline at end of file diff --git a/dhcpv6/nclient6/client.go b/dhcpv6/nclient6/client.go index 8705a6ab..842e5886 100644 --- a/dhcpv6/nclient6/client.go +++ b/dhcpv6/nclient6/client.go @@ -44,7 +44,7 @@ type pendingCh struct { done <-chan struct{} // ch is used by the receive loop to distribute DHCP messages. - ch chan<- *dhcpv6.Message + ch chan<- dhcpv6.DHCPv6 } // Client is a DHCPv6 client. @@ -84,13 +84,13 @@ type Client struct { type logger interface { Printf(format string, v ...interface{}) - PrintMessage(prefix string, message *dhcpv6.Message) + PrintMessage(prefix string, message dhcpv6.DHCPv6) } type emptyLogger struct{} -func (e emptyLogger) Printf(format string, v ...interface{}) {} -func (e emptyLogger) PrintMessage(prefix string, message *dhcpv6.Message) {} +func (e emptyLogger) Printf(format string, v ...interface{}) {} +func (e emptyLogger) PrintMessage(prefix string, message dhcpv6.DHCPv6) {} type shortSummaryLogger struct { *log.Logger @@ -99,7 +99,7 @@ type shortSummaryLogger struct { func (s shortSummaryLogger) Printf(format string, v ...interface{}) { s.Logger.Printf(format, v...) } -func (s shortSummaryLogger) PrintMessage(prefix string, message *dhcpv6.Message) { +func (s shortSummaryLogger) PrintMessage(prefix string, message dhcpv6.DHCPv6) { s.Printf("%s: %s", prefix, message) } @@ -110,7 +110,7 @@ type debugLogger struct { func (d debugLogger) Printf(format string, v ...interface{}) { d.Logger.Printf(format, v...) } -func (d debugLogger) PrintMessage(prefix string, message *dhcpv6.Message) { +func (d debugLogger) PrintMessage(prefix string, message dhcpv6.DHCPv6) { d.Printf("%s: %s", prefix, message.Summary()) } @@ -355,14 +355,19 @@ func (c *Client) RapidSolicit(ctx context.Context, modifiers ...dhcpv6.Modifier) return nil, err } - switch msg.MessageType { + inner, err := msg.GetInnerMessage() + if err != nil { + return nil, err + } + + switch msg.Type() { case dhcpv6.MessageTypeReply: // We got RapidCommitted. - return msg, nil + return inner, nil case dhcpv6.MessageTypeAdvertise: // We didn't get RapidCommitted. Request regular lease. - return c.Request(ctx, msg, modifiers...) + return c.Request(ctx, inner, modifiers...) default: return nil, fmt.Errorf("invalid message type: cannot happen") @@ -380,7 +385,11 @@ func (c *Client) Solicit(ctx context.Context, modifiers ...dhcpv6.Modifier) (*dh if err != nil { return nil, err } - return msg, nil + inner, err := msg.GetInnerMessage() + if err != nil { + return nil, err + } + return inner, nil } // Request requests an IP Assignment from peer given an advertise message. @@ -389,7 +398,12 @@ func (c *Client) Request(ctx context.Context, advertise *dhcpv6.Message, modifie if err != nil { return nil, err } - return c.SendAndRead(ctx, c.serverAddr, request, nil) + msg, err := c.SendAndRead(ctx, c.serverAddr, request, nil) + inner, err := msg.GetInnerMessage() + if err != nil { + return nil, err + } + return inner, nil } // send sends p to destination and returns a response channel. @@ -398,16 +412,21 @@ func (c *Client) Request(ctx context.Context, advertise *dhcpv6.Message, modifie // received. // // Responses will be matched by transaction ID. -func (c *Client) send(dest net.Addr, msg *dhcpv6.Message) (<-chan *dhcpv6.Message, func(), error) { +func (c *Client) send(dest net.Addr, msg dhcpv6.DHCPv6) (<-chan dhcpv6.DHCPv6, func(), error) { + inner, err := msg.GetInnerMessage() + if err != nil { + return nil, nil, err + } + c.pendingMu.Lock() - if _, ok := c.pending[msg.TransactionID]; ok { + if _, ok := c.pending[inner.TransactionID]; ok { c.pendingMu.Unlock() - return nil, nil, fmt.Errorf("transaction ID %s already in use", msg.TransactionID) + return nil, nil, fmt.Errorf("transaction ID %s already in use", inner.TransactionID) } - ch := make(chan *dhcpv6.Message, c.bufferCap) + ch := make(chan dhcpv6.DHCPv6, c.bufferCap) done := make(chan struct{}) - c.pending[msg.TransactionID] = &pendingCh{done: done, ch: ch} + c.pending[inner.TransactionID] = &pendingCh{done: done, ch: ch} c.pendingMu.Unlock() cancel := func() { @@ -420,9 +439,9 @@ func (c *Client) send(dest net.Addr, msg *dhcpv6.Message) (<-chan *dhcpv6.Messag close(done) c.pendingMu.Lock() - if p, ok := c.pending[msg.TransactionID]; ok { + if p, ok := c.pending[inner.TransactionID]; ok { close(p.ch) - delete(c.pending, msg.TransactionID) + delete(c.pending, inner.TransactionID) } c.pendingMu.Unlock() } @@ -441,8 +460,8 @@ var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded") // 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 +func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, msg dhcpv6.DHCPv6, match Matcher) (dhcpv6.DHCPv6, error) { + var response dhcpv6.DHCPv6 err := c.retryFn(func(timeout time.Duration) error { ch, rem, err := c.send(dest, msg) if err != nil { @@ -463,7 +482,11 @@ func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, msg *dhcpv6 return ctx.Err() case packet := <-ch: - if match == nil || match(packet) { + inner, err := packet.GetInnerMessage() + if err != nil { + return err + } + if match == nil || match(inner) { c.logger.PrintMessage("received message", packet) response = packet return nil diff --git a/dhcpv6/nclient6/client_test.go b/dhcpv6/nclient6/client_test.go index 8d55faf2..27d2c694 100644 --- a/dhcpv6/nclient6/client_test.go +++ b/dhcpv6/nclient6/client_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.12 // +build go1.12 package nclient6 @@ -189,7 +190,13 @@ func TestSendAndReadUntil(t *testing.T) { t.Error(err) } - if err := ComparePacket(rcvd, tt.want); err != nil { + var inner *dhcpv6.Message + if rcvd != nil { + inner, err = rcvd.GetInnerMessage() + require.NoError(t, err) + } + + if err := ComparePacket(inner, tt.want); err != nil { t.Errorf("got unexpected packets: %v", err) } }) @@ -219,7 +226,10 @@ func TestSimpleSendAndReadDiscardGarbage(t *testing.T) { t.Error(err) } - if err := ComparePacket(rcvd, responses[0]); err != nil { + inner, err := rcvd.GetInnerMessage() + require.NoError(t, err) + + if err := ComparePacket(inner, responses[0]); err != nil { t.Errorf("got unexpected packets: %v", err) } } @@ -264,7 +274,11 @@ func TestMultipleSendAndReadOne(t *testing.T) { 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 { + + inner, err := rcvd.GetInnerMessage() + require.NoError(t, err) + + if err := pktsExpected([]*dhcpv6.Message{inner}, tt.server[i]); err != nil { t.Errorf("got unexpected packets: %v", err) } } From cee5d2f12061889f61e477da1fa61086ce3f53db Mon Sep 17 00:00:00 2001 From: Jaroslaw Mroz Date: Sat, 8 Jun 2024 17:11:23 +0200 Subject: [PATCH 2/6] Make receive loop handle RelayMessage Signed-off-by: Jaroslaw Mroz --- dhcpv6/nclient6/client.go | 17 ++++- dhcpv6/nclient6/client_test.go | 136 +++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/dhcpv6/nclient6/client.go b/dhcpv6/nclient6/client.go index 842e5886..cf34045e 100644 --- a/dhcpv6/nclient6/client.go +++ b/dhcpv6/nclient6/client.go @@ -218,7 +218,7 @@ func (c *Client) receiveLoop() { return } - msg, err := dhcpv6.MessageFromBytes(b[:n]) + msg, err := dhcpv6.FromBytes(b[:n]) if err != nil { // Not a valid DHCP packet; keep listening. if c.printDropped { @@ -230,13 +230,24 @@ func (c *Client) receiveLoop() { continue } + inner, err := msg.GetInnerMessage() + if err != nil { + 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] + p, ok := c.pending[inner.TransactionID] if ok { select { case <-p.done: close(p.ch) - delete(c.pending, msg.TransactionID) + delete(c.pending, inner.TransactionID) // This send may block. case p.ch <- msg: diff --git a/dhcpv6/nclient6/client_test.go b/dhcpv6/nclient6/client_test.go index 27d2c694..5d37fede 100644 --- a/dhcpv6/nclient6/client_test.go +++ b/dhcpv6/nclient6/client_test.go @@ -51,6 +51,35 @@ func (h *handler) handle(conn net.PacketConn, peer net.Addr, msg dhcpv6.DHCPv6) } } +type relayHandler struct { + mu sync.Mutex + received []*dhcpv6.RelayMessage + + // Each received packet can have more than one response (in theory, + // from different servers sending different Advertise, for example). + responses [][]*dhcpv6.RelayMessage +} + +func (h *relayHandler) handle(conn net.PacketConn, peer net.Addr, msg dhcpv6.DHCPv6) { + h.mu.Lock() + defer h.mu.Unlock() + + m := msg.(*dhcpv6.RelayMessage) + + 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. @@ -82,6 +111,37 @@ func serveAndClient(ctx context.Context, responses [][]*dhcpv6.Message, opt ...C return mc, serverRawConn } +func serveAndRelay(ctx context.Context, responses [][]*dhcpv6.RelayMessage, 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 := &relayHandler{ + 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 @@ -108,6 +168,32 @@ func pktsExpected(got []*dhcpv6.Message, want []*dhcpv6.Message) error { return nil } +func CompareRelayPacket(got *dhcpv6.RelayMessage, want *dhcpv6.RelayMessage) 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 relayPktsExpected(got []*dhcpv6.RelayMessage, want []*dhcpv6.RelayMessage) 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 := CompareRelayPacket(got[i], want[i]); err != nil { + return err + } + } + return nil +} + func newPacket(xid dhcpv6.TransactionID) *dhcpv6.Message { p, err := dhcpv6.NewMessage() if err != nil { @@ -117,6 +203,14 @@ func newPacket(xid dhcpv6.TransactionID) *dhcpv6.Message { return p } +func newRelayPacket(mType dhcpv6.MessageType, p dhcpv6.DHCPv6) *dhcpv6.RelayMessage { + r, err := dhcpv6.EncapsulateRelay(p, mType, net.ParseIP("fe80::1"), net.ParseIP("fe80::2")) + if err != nil { + panic(fmt.Sprintf("newrelaypacket: %v", err)) + } + return r +} + func withBufferCap(n int) ClientOpt { return func(c *Client) { c.bufferCap = n @@ -284,3 +378,45 @@ func TestMultipleSendAndReadOne(t *testing.T) { } } } + +func TestSendAndReadRelay(t *testing.T) { + for _, tt := range []struct { + desc string + send *dhcpv6.RelayMessage + server []*dhcpv6.RelayMessage + wantErr error + }{ + { + desc: "single relay message, single response", + send: newRelayPacket(dhcpv6.MessageTypeRelayForward, newPacket([3]byte{0x33, 0x33, 0x33})), + + server: []*dhcpv6.RelayMessage{ + newRelayPacket(dhcpv6.MessageTypeRelayReply, newPacket([3]byte{0x33, 0x33, 0x33})), + }, + wantErr: nil, + }, + } { + // Both server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, _ := serveAndRelay(ctx, [][]*dhcpv6.RelayMessage{tt.server}) + defer mc.conn.Close() + + rcvd, err := mc.SendAndRead(context.Background(), AllDHCPServers, tt.send, nil) + + if err != tt.wantErr { + t.Errorf("SendAndReadOne(%v): got %v, want %v", tt.send, err, tt.wantErr) + } + + relayResp, ok := rcvd.(*dhcpv6.RelayMessage) + if !ok { + t.Errorf("expected relay message, got %v", rcvd) + } + + if err := relayPktsExpected([]*dhcpv6.RelayMessage{relayResp}, tt.server); err != nil { + t.Errorf("got unexpected packets: %v", err) + } + + } +} From 4eb44c77216db75f0ae0c859e8dd13005bd4a4f7 Mon Sep 17 00:00:00 2001 From: Jaroslaw Mroz Date: Mon, 10 Jun 2024 17:50:44 +0200 Subject: [PATCH 3/6] Change module name to allow imports from vitrifi gitlab Signed-off-by: Jaroslaw Mroz --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index f93fee90..1090c036 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ +replace github.com/insomniacslk/dhcp => gitlab.com/Vitrifi/cne/insomniacslk-dhcp v0.0.1-cne + module github.com/insomniacslk/dhcp go 1.20 From e229577e8521ceef69a39296c2be60191eb9a641 Mon Sep 17 00:00:00 2001 From: Jaroslaw Mroz Date: Tue, 11 Jun 2024 11:24:17 +0200 Subject: [PATCH 4/6] Change module path after move to other gitlab location Signed-off-by: Jaroslaw Mroz --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1090c036..4070e24c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -replace github.com/insomniacslk/dhcp => gitlab.com/Vitrifi/cne/insomniacslk-dhcp v0.0.1-cne +replace github.com/insomniacslk/dhcp => gitlab.com/Vitrifi/insomniacslk-dhcp v0.0.1-cne module github.com/insomniacslk/dhcp From 26ba165abcbd362b73f7219bf8ab52689556f5d8 Mon Sep 17 00:00:00 2001 From: Claudio Segal Date: Fri, 23 Aug 2024 16:59:32 +0000 Subject: [PATCH 5/6] Add SetReadBuffer option to nclient4. Signed-off-by: Jaroslaw Mroz --- dhcpv4/nclient4/client.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/dhcpv4/nclient4/client.go b/dhcpv4/nclient4/client.go index b4e4b567..93cf1e6e 100644 --- a/dhcpv4/nclient4/client.go +++ b/dhcpv4/nclient4/client.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.12 // +build go1.12 // Package nclient4 is a small, minimum-functionality client for DHCPv4. @@ -62,6 +63,12 @@ var ( // ErrNoIfaceHWAddr is returned when NewWithConn is called with nil-value as ifaceHWAddr ErrNoIfaceHWAddr = errors.New("ifaceHWAddr is nil") + + // ErrNotAUDPSocket is returned when WithReadBuffer is called but the underlying socket is not UDP. + ErrNotAUDPSocket = errors.New("the underlying socket is not UDP") + + // ErrZeroReadBufferSize is returned when WithReadBuffer is called with a buffer size of zero. + ErrZeroReadBufferSize = errors.New("read buffer size cannot be zero") ) // pendingCh is a channel associated with a pending TransactionID. @@ -362,6 +369,26 @@ func WithUnicast(srcAddr *net.UDPAddr) ClientOpt { } } +// WithReadBuffer sets the size of the read buffer for the underlying socket. +// This has the effect of setting the SO_RCVBUF option to the given value. +// The underlying socket must be UDP. +// The buffer size must be a positive integer. +func WithReadBuffer(bufSize uint) ClientOpt { + return func(c *Client) (err error) { + if bufSize == 0 { + return ErrZeroReadBufferSize + } + udpConn, ok := c.conn.(*net.UDPConn) + if !ok { + return ErrNotAUDPSocket + } + if err := udpConn.SetReadBuffer(int(bufSize)); err != nil { + return fmt.Errorf("unable to set read buffer: %w", err) + } + return + } +} + // WithHWAddr tells to the Client to receive messages destinated to selected // hardware address func WithHWAddr(hwAddr net.HardwareAddr) ClientOpt { From abaae812c644485f84eb27e7f946efbd3af0b460 Mon Sep 17 00:00:00 2001 From: Jaroslaw Mroz Date: Mon, 2 Sep 2024 14:30:16 +0200 Subject: [PATCH 6/6] chore: remove gitlab related config Signed-off-by: Jaroslaw Mroz --- .gitlab-ci.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 7157912e..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,12 +0,0 @@ -# dummy cicd that walways passes -image: golang:latest - - -stages: - - test - - -test: - stage: test - script: - - go test -v ./... \ No newline at end of file