Skip to content

Commit 35c7432

Browse files
authored
Integrate DTLS (pion/dtls) configuration using options instead of deprecated *dtls.Config{} (#645)
* chore: update Go version and pion/dtls in go.mod and go.sum * feat: introduce options-based DTLS API and deprecate legacy methods * feat: refactor DTLS API to use generic options-based configuration and deprecate legacy methods * feat: update DTLS implementation to use options-based configuration across examples * test: enhance error assertion in TestTLSListenerCheckForInfinitLoop for macOS compatibility * docs: update README to reflect deprecation of *dtls.Config and introduce options-based DTLS API * fix: ensure DTLS connection is closed on error in Dial function * feat: update README and add example client for options-based DTLS API * fix: clean up main function in DTLS client example * fix: update indirect dependencies to latest versions
1 parent 361db49 commit 35c7432

File tree

20 files changed

+662
-119
lines changed

20 files changed

+662
-119
lines changed

README.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,12 @@ The go-coap provides servers and clients for DTLS, TCP-TLS, UDP, TCP in golang l
8080
// for tcp-tls
8181
// log.Fatal(coap.ListenAndServeTLS("tcp", ":5688", &tls.Config{...}, r))
8282

83-
// for udp-dtls
83+
// for udp-dtls (deprecated: *dtls.Config is deprecated in pion/dtls v3, prefer options-based API)
8484
// log.Fatal(coap.ListenAndServeDTLS("udp", ":5688", &dtls.Config{...}, r))
85+
86+
// for udp-dtls (options-based API — pion/dtls v3)
87+
// log.Fatal(coap.ListenAndServeDTLS("udp", ":5688",
88+
// net.NewDTLSServerOptions(dtls.WithPSK(...), dtls.WithCertificates(...)), r))
8589
}
8690
```
8791

@@ -99,8 +103,12 @@ The go-coap provides servers and clients for DTLS, TCP-TLS, UDP, TCP in golang l
99103
// for tcp-tls
100104
// co, err := tcp.Dial("localhost:5688", tcp.WithTLS(&tls.Config{...}))
101105

102-
// for dtls
103-
// co, err := dtls.Dial("localhost:5688", &dtls.Config{...}))
106+
// for dtls (deprecated: *dtls.Config is deprecated in pion/dtls v3, prefer options-based API)
107+
// co, err := dtls.Dial("localhost:5688", &dtls.Config{...})
108+
109+
// for dtls (options-based API — pion/dtls v3)
110+
// co, err := dtls.Dial("localhost:5688",
111+
// dtlscoap.NewDTLSClientOptions(piondtls.WithPSK(...), piondtls.WithCertificates(...)))
104112

105113
if err != nil {
106114
log.Fatalf("Error dialing: %v", err)
@@ -116,6 +124,43 @@ The go-coap provides servers and clients for DTLS, TCP-TLS, UDP, TCP in golang l
116124
}
117125
```
118126

127+
### DTLS Options-based API (pion/dtls v3)
128+
129+
> **Deprecation notice:** `*dtls.Config` is [deprecated in pion/dtls v3](https://github.com/pion/dtls) in favour of immutable options-based configurations. Its use in go-coap is consequently deprecated as well and may be removed in a future major version. Migrate to `net.NewDTLSServerOptions` / `dtls.NewDTLSClientOptions` as shown below.
130+
131+
In addition to the legacy `*dtls.Config`, go-coap supports the **options-based API** introduced in pion/dtls v3. Both approaches are currently accepted by the same generic functions (`ListenAndServeDTLS`, `Dial`, `NewDTLSListener`).
132+
133+
Use `net.NewDTLSServerOptions(...)` / `dtls.NewDTLSClientOptions(...)` to compose DTLS configurations from individual options:
134+
135+
```go
136+
import (
137+
coap "github.com/plgd-dev/go-coap/v3"
138+
coapnet "github.com/plgd-dev/go-coap/v3/net"
139+
coapdtls "github.com/plgd-dev/go-coap/v3/dtls"
140+
piondtls "github.com/pion/dtls/v3"
141+
)
142+
143+
// Server
144+
serverOpts := coapnet.NewDTLSServerOptions(
145+
piondtls.WithPSK(func(hint []byte) ([]byte, error) {
146+
return []byte{0xAB, 0xC1, 0x23}, nil
147+
}),
148+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
149+
)
150+
log.Fatal(coap.ListenAndServeDTLS("udp", ":5688", serverOpts, r))
151+
152+
// Client
153+
clientOpts := coapdtls.NewDTLSClientOptions(
154+
piondtls.WithPSK(func(hint []byte) ([]byte, error) {
155+
return []byte{0xAB, 0xC1, 0x23}, nil
156+
}),
157+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
158+
)
159+
co, err := coapdtls.Dial("localhost:5688", clientOpts)
160+
```
161+
162+
See [examples/options/server/](examples/options/server/) and [examples/options/client/](examples/options/client/) for complete working examples using the options-based API.
163+
119164
### Observe / Notify
120165

121166
[Server](examples/observe/server/main.go) example.

dtls/client.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,30 @@ var DefaultConfig = func() udpClient.Config {
3333
}()
3434

3535
// Dial creates a client connection to the given target.
36-
func Dial(target string, dtlsCfg *dtls.Config, opts ...udp.Option) (*udpClient.Conn, error) {
37-
cfg := DefaultConfig
36+
// cfg accepts either a *dtls.Config (backward-compatible legacy path) or a
37+
// DTLSClientOptions value built with NewDTLSClientOptions (recommended).
38+
func Dial[T DTLSClientConfig](target string, cfg T, opts ...udp.Option) (*udpClient.Conn, error) {
39+
defaultCfg := DefaultConfig
3840
for _, o := range opts {
39-
o.UDPClientApply(&cfg)
41+
o.UDPClientApply(&defaultCfg)
4042
}
4143

42-
c, err := cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target)
44+
c, err := defaultCfg.Dialer.DialContext(defaultCfg.Ctx, defaultCfg.Net, target)
4345
if err != nil {
4446
return nil, err
4547
}
4648

47-
conn, err := dtls.Client(dtlsnet.PacketConnFromConn(c), c.RemoteAddr(), dtlsCfg)
49+
var conn *dtls.Conn
50+
switch v := any(cfg).(type) {
51+
case *dtls.Config:
52+
conn, err = dtls.Client(dtlsnet.PacketConnFromConn(c), c.RemoteAddr(), v) //nolint:staticcheck
53+
case DTLSClientOptions:
54+
conn, err = dtls.ClientWithOptions(dtlsnet.PacketConnFromConn(c), c.RemoteAddr(), v.opts...)
55+
default:
56+
panic("unreachable: unexpected type in DTLSClientConfig constraint")
57+
}
4858
if err != nil {
59+
_ = c.Close()
4960
return nil, err
5061
}
5162
opts = append(opts, options.WithCloseSocket())

dtls/client_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,142 @@ func TestClientKeepAliveMonitor(t *testing.T) {
797797
require.NoError(t, err)
798798
require.True(t, inactivityDetected.Load())
799799
}
800+
801+
// TestConnGetWithOptions mirrors TestConnGet but uses the new
802+
// generic Dial / NewDTLSListener API with DTLSClientOptions / DTLSServerOptions.
803+
func TestConnGetWithOptions(t *testing.T) {
804+
type args struct {
805+
path string
806+
opts message.Options
807+
}
808+
tests := []struct {
809+
name string
810+
args args
811+
wantCode codes.Code
812+
wantContentFormat *message.MediaType
813+
wantPayload interface{}
814+
wantErr bool
815+
}{
816+
{
817+
name: "ok-a",
818+
args: args{path: "/a"},
819+
wantCode: codes.BadRequest,
820+
wantContentFormat: &message.TextPlain,
821+
wantPayload: make([]byte, 5330),
822+
},
823+
{
824+
name: "ok-b",
825+
args: args{path: "/b"},
826+
wantCode: codes.Content,
827+
wantContentFormat: &message.TextPlain,
828+
wantPayload: []byte("b"),
829+
},
830+
{
831+
name: "notfound",
832+
args: args{path: "/c"},
833+
wantCode: codes.NotFound,
834+
},
835+
}
836+
837+
pskCallback := func(hint []byte) ([]byte, error) {
838+
fmt.Printf("Hint: %s \n", hint)
839+
return []byte{0xAB, 0xC1, 0x23}, nil
840+
}
841+
serverOpts := coapNet.NewDTLSServerOptions(
842+
piondtls.WithPSK(pskCallback),
843+
piondtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
844+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
845+
)
846+
clientOpts := dtls.NewDTLSClientOptions(
847+
piondtls.WithPSK(pskCallback),
848+
piondtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
849+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
850+
)
851+
852+
l, err := coapNet.NewDTLSListener("udp", "", serverOpts)
853+
require.NoError(t, err)
854+
defer func() {
855+
errC := l.Close()
856+
require.NoError(t, errC)
857+
}()
858+
859+
var wg sync.WaitGroup
860+
defer wg.Wait()
861+
862+
m := mux.NewRouter()
863+
err = m.Handle("/a", mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) {
864+
assert.Equal(t, codes.GET, r.Code())
865+
errS := w.SetResponse(codes.BadRequest, message.TextPlain, bytes.NewReader(make([]byte, 5330)))
866+
require.NoError(t, errS)
867+
require.NotEmpty(t, w.Conn())
868+
}))
869+
require.NoError(t, err)
870+
err = m.Handle("/b", mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) {
871+
assert.Equal(t, codes.GET, r.Code())
872+
errS := w.SetResponse(codes.Content, message.TextPlain, bytes.NewReader([]byte("b")))
873+
require.NoError(t, errS)
874+
require.NotEmpty(t, w.Conn())
875+
}))
876+
require.NoError(t, err)
877+
878+
s := dtls.NewServer(options.WithMux(m))
879+
defer s.Stop()
880+
881+
wg.Add(1)
882+
go func() {
883+
defer wg.Done()
884+
errS := s.Serve(l)
885+
assert.NoError(t, errS)
886+
}()
887+
888+
cc, err := dtls.Dial(l.Addr().String(), clientOpts)
889+
require.NoError(t, err)
890+
defer func() {
891+
errC := cc.Close()
892+
require.NoError(t, errC)
893+
}()
894+
895+
for _, tt := range tests {
896+
t.Run(tt.name, func(t *testing.T) {
897+
ctx, cancel := context.WithTimeout(context.Background(), Timeout)
898+
defer cancel()
899+
got, err := cc.Get(ctx, tt.args.path, tt.args.opts...)
900+
if tt.wantErr {
901+
require.Error(t, err)
902+
return
903+
}
904+
require.NoError(t, err)
905+
require.Equal(t, tt.wantCode, got.Code())
906+
if tt.wantContentFormat != nil {
907+
ct, err := got.ContentFormat()
908+
require.NoError(t, err)
909+
require.Equal(t, *tt.wantContentFormat, ct)
910+
buf := bytes.NewBuffer(nil)
911+
_, err = buf.ReadFrom(got.Body())
912+
require.NoError(t, err)
913+
require.Equal(t, tt.wantPayload, buf.Bytes())
914+
}
915+
})
916+
}
917+
}
918+
919+
// TestNewDTLSListenerInvalidAddr verifies that
920+
// NewDTLSListener propagates address-resolution errors.
921+
func TestNewDTLSListenerInvalidAddr(t *testing.T) {
922+
_, err := coapNet.NewDTLSListener("udp", "!!invalid!!", coapNet.NewDTLSServerOptions(
923+
piondtls.WithPSK(func([]byte) ([]byte, error) { return []byte{0x01}, nil }),
924+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
925+
))
926+
require.Error(t, err)
927+
}
928+
929+
// TestDialConnectRefused ensures Dial returns an error
930+
// when nothing is listening on the target address.
931+
func TestDialConnectRefused(t *testing.T) {
932+
// port 1 is reserved and will never have a DTLS listener
933+
_, err := dtls.Dial("127.0.0.1:1", dtls.NewDTLSClientOptions(
934+
piondtls.WithPSK(func([]byte) ([]byte, error) { return []byte{0x01}, nil }),
935+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
936+
))
937+
require.Error(t, err)
938+
}

dtls/clientoptions.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dtls
2+
3+
import piondtls "github.com/pion/dtls/v3"
4+
5+
// DTLSClientOptions holds DTLS client-side configuration options for use with
6+
// Dial. It wraps the options-based API of pion/dtls, keeping
7+
// pion/dtls option types out of go-coap function signatures.
8+
type DTLSClientOptions struct {
9+
opts []piondtls.ClientOption
10+
}
11+
12+
// NewDTLSClientOptions creates a DTLSClientOptions from the provided pion/dtls
13+
// ClientOption values (e.g. piondtls.WithPSK, piondtls.WithCertificates, …).
14+
//
15+
// Most pion/dtls options implement the shared piondtls.Option interface, which
16+
// satisfies both ServerOption and ClientOption, so they can be passed here
17+
// directly.
18+
func NewDTLSClientOptions(opts ...piondtls.ClientOption) DTLSClientOptions {
19+
return DTLSClientOptions{opts: opts}
20+
}
21+
22+
// DTLSClientConfig is a type constraint accepted by Dial.
23+
// It allows callers to pass either the legacy *piondtls.Config
24+
// (backward-compatible) or the recommended DTLSClientOptions wrapper
25+
// (built via NewDTLSClientOptions).
26+
type DTLSClientConfig interface {
27+
*piondtls.Config | DTLSClientOptions
28+
}

dtls/example_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,32 @@ func ExampleConn_Get() {
3939
fmt.Printf("%v", data)
4040
}
4141

42+
func ExampleDial_withOptions() {
43+
conn, err := dtls.Dial("pluggedin.cloud:5684", dtls.NewDTLSClientOptions(
44+
piondtls.WithPSK(func(hint []byte) ([]byte, error) {
45+
fmt.Printf("Hint: %s \n", hint)
46+
return []byte{0xAB, 0xC1, 0x23}, nil
47+
}),
48+
piondtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
49+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
50+
))
51+
if err != nil {
52+
log.Fatal(err)
53+
}
54+
defer conn.Close()
55+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
56+
defer cancel()
57+
res, err := conn.Get(ctx, "/oic/res")
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
data, err := io.ReadAll(res.Body())
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
fmt.Printf("%v", data)
66+
}
67+
4268
func ExampleServer() {
4369
dtlsCfg := &piondtls.Config{
4470
PSK: func(hint []byte) ([]byte, error) {
@@ -57,3 +83,21 @@ func ExampleServer() {
5783
defer s.Stop()
5884
log.Fatal(s.Serve(l))
5985
}
86+
87+
func ExampleNewDTLSListener_withOptions() {
88+
l, err := net.NewDTLSListener("udp", "0.0.0.0:5683", net.NewDTLSServerOptions(
89+
piondtls.WithPSK(func(hint []byte) ([]byte, error) {
90+
fmt.Printf("Hint: %s \n", hint)
91+
return []byte{0xAB, 0xC1, 0x23}, nil
92+
}),
93+
piondtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
94+
piondtls.WithCipherSuites(piondtls.TLS_PSK_WITH_AES_128_CCM_8),
95+
))
96+
if err != nil {
97+
log.Fatal(err)
98+
}
99+
defer l.Close()
100+
s := dtls.NewServer()
101+
defer s.Stop()
102+
log.Fatal(s.Serve(l))
103+
}

0 commit comments

Comments
 (0)