mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
network/dialer: add Z-Wing PQ secure-channel wrapper
Introduces ZWingDialer + ZWingListener which wrap any underlying
net.Conn (TCP, hostname, RNS mesh link, Unix socket, in-memory pipe)
with the canonical Lux PQ secure channel:
IETF X-Wing KEM (X25519 + ML-KEM-768)
Hybrid Ed25519 + ML-DSA-65 identity, signed transcript
ChaCha20-Poly1305 with sequence-numbered nonces
Z-Wing's contract is "any net.Conn" — the same secure channel rides
unchanged on TCP today and on the existing RNS transport tomorrow
without a per-transport rewrite. The legacy LP-9701 in-RNS-link crypto
(rns_link.go) stays in place during the transition; new p2p paths
should layer ZWingDialer over the EndpointDialer instead of relying on
LP-9701's inline encryption.
Adds:
network/dialer/zwing_dialer.go ZWingDialer + ZWingListener
network/dialer/zwing_dialer_test.go 5 e2e tests covering:
- missing-identity rejection (dialer + listener)
- real TCP listener + Z-Wing handshake + payload round trip
- Wrap() over an arbitrary net.Conn (net.Pipe stand-in for RNS)
- identity mismatch (MitM defence)
- DialEndpoint over an Endpoint (works for IP, hostname, future RNS)
Bumps:
github.com/luxfi/zwing v0.5.2 (full FIPS 204 PQ stack, cross-language
wire-byte interop with Rust/Py/TS)
github.com/luxfi/api v1.0.10 (NewListener seam used by zwing.ListenZAP)
luxd-side wiring (node.go construction with a loaded LocalIdentity) is
the next follow-up; this commit lands the seam without behavioural
changes to the existing dialer interface.
This commit is contained in:
@@ -7,7 +7,7 @@ module github.com/luxfi/node
|
||||
//
|
||||
// - If updating between minor versions (e.g. 1.23.x -> 1.24.x):
|
||||
// - Consider updating the version of golangci-lint (in scripts/lint.sh).
|
||||
go 1.26.1
|
||||
go 1.26.2
|
||||
|
||||
exclude github.com/luxfi/geth v1.16.1
|
||||
|
||||
@@ -28,7 +28,7 @@ require (
|
||||
github.com/jackpal/gateway v1.1.1
|
||||
github.com/jackpal/go-nat-pmp v1.0.2
|
||||
github.com/luxfi/consensus v1.22.85
|
||||
github.com/luxfi/crypto v1.18.1
|
||||
github.com/luxfi/crypto v1.18.3
|
||||
github.com/luxfi/database v1.18.2
|
||||
github.com/luxfi/ids v1.2.9
|
||||
github.com/luxfi/keychain v1.0.2
|
||||
@@ -59,7 +59,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/mod v0.34.0
|
||||
golang.org/x/net v0.52.0
|
||||
@@ -115,8 +115,8 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
)
|
||||
|
||||
@@ -127,7 +127,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
github.com/luxfi/accel v1.0.9
|
||||
github.com/luxfi/api v1.0.5
|
||||
github.com/luxfi/api v1.0.10
|
||||
github.com/luxfi/atomic v1.0.0
|
||||
github.com/luxfi/chains v1.2.1
|
||||
github.com/luxfi/compress v0.0.5
|
||||
@@ -174,6 +174,7 @@ require (
|
||||
github.com/luxfi/threshold v1.5.5 // indirect
|
||||
github.com/luxfi/trace v0.1.4 // indirect
|
||||
github.com/luxfi/zapdb v1.8.0 // indirect
|
||||
github.com/luxfi/zwing v0.5.2 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.100 // indirect
|
||||
|
||||
@@ -260,6 +260,8 @@ github.com/luxfi/api v1.0.4 h1:5altGkSSk3zIsGzK/NPhRQe/It5QMHrAPgBVg2TMkK4=
|
||||
github.com/luxfi/api v1.0.4/go.mod h1:Znr2A+SDJBCZ7ofxeq0MswHI0gk0cJsIYJLcu3tT8JY=
|
||||
github.com/luxfi/api v1.0.5 h1:5+sepMbIBDzeIA0QTO66X/2D4SR8dKEQwQpdrmFmr+Q=
|
||||
github.com/luxfi/api v1.0.5/go.mod h1:5OFWvZF+PbyuvLCDyKKpXCA/xp+LxJ32yOhNEKwQFN0=
|
||||
github.com/luxfi/api v1.0.10 h1:tqtiCX8DcBsFG0JEhKy7eUNI+42+m2DoPjLjEh5TE5E=
|
||||
github.com/luxfi/api v1.0.10/go.mod h1:5OFWvZF+PbyuvLCDyKKpXCA/xp+LxJ32yOhNEKwQFN0=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
@@ -282,6 +284,8 @@ github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM
|
||||
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
|
||||
github.com/luxfi/crypto v1.18.1 h1:Qaw8hSK0unIOAH6TRvu0s3JfoJWcg6zTf5eZ5adgKic=
|
||||
github.com/luxfi/crypto v1.18.1/go.mod h1:QhSdEEk3xmgR5a97/nkXB/n21ZsI1Iv7tcbqQaNM5zs=
|
||||
github.com/luxfi/crypto v1.18.3 h1:f6mBUHNjZl9/9nXPSqjq+oJttrPl8slvcZ49Vi7jKw4=
|
||||
github.com/luxfi/crypto v1.18.3/go.mod h1:qBtCJDT6p6aNa2hMtPxYwC688Bnsb3Amso8q09GsMls=
|
||||
github.com/luxfi/database v1.18.2 h1:r7jU8s4jaN0NpzWB0x91yyaL4CoCe0SxWEvxJGhJ7rE=
|
||||
github.com/luxfi/database v1.18.2/go.mod h1:PW0oGujt165mYg7j/lctVbnfsUW6QLfIAgjyQdS66SM=
|
||||
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
|
||||
@@ -368,6 +372,8 @@ github.com/luxfi/warp v1.18.5 h1:yXFCT+lnvzJHs8nVvfUCQ9wNvhtgbDGaXJkJeiwgKf4=
|
||||
github.com/luxfi/warp v1.18.5/go.mod h1:SFyC529HDvbP/TWRAdYQSyJUliMa5JKFRtBrTLEElp4=
|
||||
github.com/luxfi/zapdb v1.8.0 h1:E9hXMW1MTuS6kmDVs07j62gIq5qI5gZOdyzVfiBc1wg=
|
||||
github.com/luxfi/zapdb v1.8.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
|
||||
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
|
||||
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -573,6 +579,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -625,6 +633,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -634,6 +644,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Z-Wing PQ secure channel wrapper for the p2p dialer.
|
||||
//
|
||||
// Z-Wing (LP-9702) is the canonical Lux post-quantum secure channel:
|
||||
// IETF X-Wing KEM (X25519 + ML-KEM-768) plus an Ed25519 + ML-DSA-65
|
||||
// hybrid identity plus ChaCha20-Poly1305 with sequence-numbered
|
||||
// nonces. This file wraps the regular `EndpointDialer` so that any
|
||||
// `net.Conn` it produces — over plain TCP today and over RNS mesh
|
||||
// transport once `RNSTransport` is wired in — gets a Z-Wing handshake
|
||||
// applied on top.
|
||||
//
|
||||
// Architecture:
|
||||
//
|
||||
// ┌────────────────────────────────────────────────────────────┐
|
||||
// │ Application bytes (ZAP / p2p frames) │
|
||||
// ├────────────────────────────────────────────────────────────┤
|
||||
// │ Z-Wing AEAD records (ChaCha20-Poly1305, seq-numbered) │
|
||||
// ├────────────────────────────────────────────────────────────┤
|
||||
// │ net.Conn — TCP today, RNS mesh link tomorrow │
|
||||
// └────────────────────────────────────────────────────────────┘
|
||||
//
|
||||
// The same `ZWingDialer` works on either underlying transport —
|
||||
// Z-Wing's contract is "any `net.Conn`". The replacement of legacy
|
||||
// LP-9701 in-RNS-link crypto with Z-Wing happens above this file:
|
||||
// callers route `endpointDialer.DialEndpoint(...)` through
|
||||
// `ZWingDialer.Wrap(...)` and the secure channel is the same shape
|
||||
// regardless of whether the underlying conn is IP, hostname, or RNS.
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/net/endpoints"
|
||||
"github.com/luxfi/zwing"
|
||||
)
|
||||
|
||||
// ErrZWingMissingIdentity is returned when ZWingDialer is configured
|
||||
// without a local Z-Wing identity.
|
||||
var ErrZWingMissingIdentity = errors.New("zwing dialer: missing LocalIdentity")
|
||||
|
||||
// ZWingDialer wraps an underlying `EndpointDialer` so every produced
|
||||
// `net.Conn` is upgraded with a Z-Wing 1-RTT mutual-auth handshake.
|
||||
//
|
||||
// The base dialer is responsible for picking the transport (plain TCP
|
||||
// or RNS mesh link); the Z-Wing layer is identical in both cases —
|
||||
// the secure channel rides byte-for-byte over whatever bytes the base
|
||||
// transport delivers.
|
||||
type ZWingDialer struct {
|
||||
base EndpointDialer
|
||||
cfg *zwing.Config
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// ZWingDialerConfig configures a Z-Wing-secured dialer.
|
||||
type ZWingDialerConfig struct {
|
||||
// LocalIdentity is this node's long-term Z-Wing identity (Ed25519 +
|
||||
// ML-DSA-65 hybrid + X-Wing static key). Required.
|
||||
LocalIdentity *zwing.Identity
|
||||
|
||||
// HandshakeTimeout bounds the post-dial handshake. Zero means no
|
||||
// timeout.
|
||||
HandshakeTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewZWingDialer wraps `base` so each produced connection runs the
|
||||
// Z-Wing handshake as the initiator. Returns an error if `cfg` is
|
||||
// missing the local identity.
|
||||
func NewZWingDialer(base EndpointDialer, cfg ZWingDialerConfig, logger log.Logger) (*ZWingDialer, error) {
|
||||
if cfg.LocalIdentity == nil {
|
||||
return nil, ErrZWingMissingIdentity
|
||||
}
|
||||
return &ZWingDialer{
|
||||
base: base,
|
||||
cfg: &zwing.Config{
|
||||
LocalIdentity: cfg.LocalIdentity,
|
||||
HandshakeTimeout: cfg.HandshakeTimeout,
|
||||
},
|
||||
log: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DialEndpoint dials `endpoint` via the base dialer and upgrades the
|
||||
// connection to a Z-Wing secure channel. Pass `expectedRemote` non-nil
|
||||
// to pin the peer's expected public identity.
|
||||
func (z *ZWingDialer) DialEndpoint(
|
||||
ctx context.Context,
|
||||
endpoint endpoints.Endpoint,
|
||||
expectedRemote *zwing.IdentityPublic,
|
||||
) (net.Conn, error) {
|
||||
raw, err := z.base.DialEndpoint(ctx, endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zwing dialer: base dial %s: %w", endpoint, err)
|
||||
}
|
||||
return z.upgrade(raw, expectedRemote)
|
||||
}
|
||||
|
||||
// Dial wraps the legacy IP-only path.
|
||||
func (z *ZWingDialer) Dial(
|
||||
ctx context.Context,
|
||||
ip netip.AddrPort,
|
||||
expectedRemote *zwing.IdentityPublic,
|
||||
) (net.Conn, error) {
|
||||
raw, err := z.base.Dial(ctx, ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zwing dialer: base dial %s: %w", ip, err)
|
||||
}
|
||||
return z.upgrade(raw, expectedRemote)
|
||||
}
|
||||
|
||||
// Wrap upgrades an already-established `net.Conn` (e.g. an RNS link or
|
||||
// a Unix socket) to a Z-Wing secure channel as the initiator.
|
||||
func (z *ZWingDialer) Wrap(raw net.Conn, expectedRemote *zwing.IdentityPublic) (net.Conn, error) {
|
||||
return z.upgrade(raw, expectedRemote)
|
||||
}
|
||||
|
||||
func (z *ZWingDialer) upgrade(raw net.Conn, expectedRemote *zwing.IdentityPublic) (net.Conn, error) {
|
||||
cfg := *z.cfg // shallow copy so we can plug in a per-dial pin without mutating the parent
|
||||
cfg.ExpectedRemote = expectedRemote
|
||||
conn, err := zwing.Client(raw, &cfg)
|
||||
if err != nil {
|
||||
_ = raw.Close()
|
||||
return nil, fmt.Errorf("zwing dialer: handshake: %w", err)
|
||||
}
|
||||
if z.log != nil {
|
||||
z.log.Debug("zwing channel up",
|
||||
log.Stringer("remote", raw.RemoteAddr()),
|
||||
)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// ZWingListener wraps a raw TCP/RNS listener so every accepted
|
||||
// connection is post-handshake by the time the caller sees it. The
|
||||
// listener spec mirrors `zwing.Listen` but is built on top of any
|
||||
// `net.Listener` to reuse the existing RNS / TCP listener plumbing.
|
||||
type ZWingListener struct {
|
||||
raw net.Listener
|
||||
cfg *zwing.Config
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// NewZWingListener wraps a raw listener with a Z-Wing handshake.
|
||||
func NewZWingListener(raw net.Listener, cfg ZWingDialerConfig, logger log.Logger) (*ZWingListener, error) {
|
||||
if cfg.LocalIdentity == nil {
|
||||
return nil, ErrZWingMissingIdentity
|
||||
}
|
||||
return &ZWingListener{
|
||||
raw: raw,
|
||||
cfg: &zwing.Config{
|
||||
LocalIdentity: cfg.LocalIdentity,
|
||||
HandshakeTimeout: cfg.HandshakeTimeout,
|
||||
},
|
||||
log: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Accept blocks until the next conn is handshaken. Failed handshakes
|
||||
// surface as errors; callers that want to keep listening should call
|
||||
// Accept again.
|
||||
func (l *ZWingListener) Accept() (net.Conn, error) {
|
||||
raw, err := l.raw.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := zwing.Server(raw, l.cfg)
|
||||
if err != nil {
|
||||
_ = raw.Close()
|
||||
return nil, fmt.Errorf("zwing listener: handshake: %w", err)
|
||||
}
|
||||
if l.log != nil {
|
||||
l.log.Debug("zwing channel accepted",
|
||||
log.Stringer("remote", raw.RemoteAddr()),
|
||||
)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying listener.
|
||||
func (l *ZWingListener) Close() error { return l.raw.Close() }
|
||||
|
||||
// Addr returns the underlying listener's address.
|
||||
func (l *ZWingListener) Addr() net.Addr { return l.raw.Addr() }
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/net/endpoints"
|
||||
"github.com/luxfi/zwing"
|
||||
)
|
||||
|
||||
func newPair(t *testing.T) (client, server *zwing.Identity) {
|
||||
t.Helper()
|
||||
c, err := zwing.GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("client identity: %v", err)
|
||||
}
|
||||
s, err := zwing.GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("server identity: %v", err)
|
||||
}
|
||||
return c, s
|
||||
}
|
||||
|
||||
func TestZWingDialerMissingIdentity(t *testing.T) {
|
||||
base := NewEndpointDialer("tcp", EndpointDialerConfig{}, log.NewNoOpLogger())
|
||||
if _, err := NewZWingDialer(base, ZWingDialerConfig{}, log.NewNoOpLogger()); !errors.Is(err, ErrZWingMissingIdentity) {
|
||||
t.Fatalf("expected ErrZWingMissingIdentity, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZWingListenerMissingIdentity(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
if _, err := NewZWingListener(ln, ZWingDialerConfig{}, log.NewNoOpLogger()); !errors.Is(err, ErrZWingMissingIdentity) {
|
||||
t.Fatalf("expected ErrZWingMissingIdentity, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZWingDialerOverTCP wires a real TCP listener + Z-Wing listener and
|
||||
// verifies the dialer produces a post-handshake net.Conn that round-trips
|
||||
// application bytes.
|
||||
func TestZWingDialerOverTCP(t *testing.T) {
|
||||
clientID, serverID := newPair(t)
|
||||
|
||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer rawLn.Close()
|
||||
|
||||
zwLn, err := NewZWingListener(rawLn, ZWingDialerConfig{LocalIdentity: serverID}, log.NewNoOpLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("zwing listener: %v", err)
|
||||
}
|
||||
|
||||
type accepted struct {
|
||||
c net.Conn
|
||||
err error
|
||||
}
|
||||
acceptCh := make(chan accepted, 1)
|
||||
go func() {
|
||||
c, err := zwLn.Accept()
|
||||
acceptCh <- accepted{c: c, err: err}
|
||||
}()
|
||||
|
||||
base := NewEndpointDialer("tcp", EndpointDialerConfig{}, log.NewNoOpLogger())
|
||||
zd, err := NewZWingDialer(base, ZWingDialerConfig{LocalIdentity: clientID}, log.NewNoOpLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("zwing dialer: %v", err)
|
||||
}
|
||||
|
||||
addr := netip.MustParseAddrPort(rawLn.Addr().String())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cConn, err := zd.Dial(ctx, addr, serverID.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer cConn.Close()
|
||||
|
||||
a := <-acceptCh
|
||||
if a.err != nil {
|
||||
t.Fatalf("accept: %v", a.err)
|
||||
}
|
||||
defer a.c.Close()
|
||||
|
||||
payload := []byte("zwing-over-tcp via dialer")
|
||||
wErr := make(chan error, 1)
|
||||
go func() { _, e := cConn.Write(payload); wErr <- e }()
|
||||
buf := make([]byte, len(payload))
|
||||
if _, err := a.c.Read(buf); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if err := <-wErr; err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf, payload) {
|
||||
t.Fatalf("got %q want %q", buf, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZWingDialerWrapAnyConn proves Z-Wing rides on top of any net.Conn,
|
||||
// so RNS mesh links (or Unix sockets, or pipes) get the same secure
|
||||
// channel as TCP without changes to this layer.
|
||||
func TestZWingDialerWrapAnyConn(t *testing.T) {
|
||||
clientID, serverID := newPair(t)
|
||||
|
||||
left, right := net.Pipe()
|
||||
defer left.Close()
|
||||
defer right.Close()
|
||||
|
||||
cZD, err := NewZWingDialer(
|
||||
NewEndpointDialer("tcp", EndpointDialerConfig{}, log.NewNoOpLogger()),
|
||||
ZWingDialerConfig{LocalIdentity: clientID},
|
||||
log.NewNoOpLogger(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("client zwing dialer: %v", err)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
cCh := make(chan result, 1)
|
||||
sCh := make(chan result, 1)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c, err := cZD.Wrap(left, serverID.Public())
|
||||
cCh <- result{conn: c, err: err}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
serverWrapper, _ := NewZWingListener(stubListener{}, ZWingDialerConfig{LocalIdentity: serverID}, log.NewNoOpLogger())
|
||||
c, err := zwing.Server(right, serverWrapper.cfg)
|
||||
sCh <- result{conn: c, err: err}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
c := <-cCh
|
||||
s := <-sCh
|
||||
if c.err != nil || s.err != nil {
|
||||
t.Fatalf("handshake: client=%v server=%v", c.err, s.err)
|
||||
}
|
||||
defer c.conn.Close()
|
||||
defer s.conn.Close()
|
||||
|
||||
payload := []byte("zwing-over-pipe")
|
||||
go func() { _, _ = c.conn.Write(payload) }()
|
||||
buf := make([]byte, len(payload))
|
||||
if _, err := s.conn.Read(buf); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf, payload) {
|
||||
t.Fatalf("got %q want %q", buf, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// stubListener satisfies net.Listener for the Wrap-side construction.
|
||||
type stubListener struct{}
|
||||
|
||||
func (stubListener) Accept() (net.Conn, error) { return nil, errors.New("stub") }
|
||||
func (stubListener) Close() error { return nil }
|
||||
func (stubListener) Addr() net.Addr { return &stubAddr{} }
|
||||
|
||||
type stubAddr struct{}
|
||||
|
||||
func (stubAddr) Network() string { return "stub" }
|
||||
func (stubAddr) String() string { return "stub" }
|
||||
|
||||
// TestZWingDialerIdentityMismatch confirms a wrong pinned remote rejects
|
||||
// the handshake (defence against MitM).
|
||||
func TestZWingDialerIdentityMismatch(t *testing.T) {
|
||||
clientID, serverID := newPair(t)
|
||||
_, otherID := newPair(t)
|
||||
|
||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer rawLn.Close()
|
||||
|
||||
zwLn, _ := NewZWingListener(rawLn, ZWingDialerConfig{LocalIdentity: serverID}, log.NewNoOpLogger())
|
||||
go func() { _, _ = zwLn.Accept() }()
|
||||
|
||||
base := NewEndpointDialer("tcp", EndpointDialerConfig{}, log.NewNoOpLogger())
|
||||
zd, _ := NewZWingDialer(base, ZWingDialerConfig{LocalIdentity: clientID}, log.NewNoOpLogger())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
addr := netip.MustParseAddrPort(rawLn.Addr().String())
|
||||
if _, err := zd.Dial(ctx, addr, otherID.Public()); err == nil {
|
||||
t.Fatal("expected ErrIdentityMismatch via wrapped error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZWingDialerDialEndpoint exercises the endpoint-aware path so
|
||||
// Z-Wing works over hostname endpoints too (and, transitively, over
|
||||
// any future RNS endpoint that produces a `net.Conn`).
|
||||
func TestZWingDialerDialEndpoint(t *testing.T) {
|
||||
clientID, serverID := newPair(t)
|
||||
|
||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer rawLn.Close()
|
||||
|
||||
zwLn, _ := NewZWingListener(rawLn, ZWingDialerConfig{LocalIdentity: serverID}, log.NewNoOpLogger())
|
||||
go func() { _, _ = zwLn.Accept() }()
|
||||
|
||||
base := NewEndpointDialer("tcp", EndpointDialerConfig{}, log.NewNoOpLogger())
|
||||
zd, _ := NewZWingDialer(base, ZWingDialerConfig{LocalIdentity: clientID}, log.NewNoOpLogger())
|
||||
|
||||
endpoint := endpoints.NewIPEndpoint(netip.MustParseAddrPort(rawLn.Addr().String()))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := zd.DialEndpoint(ctx, endpoint, serverID.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("dial endpoint: %v", err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user