Scope first-packet reinjection to kernel mode and drop userspace bind capture

This commit is contained in:
Viktor Liu
2026-06-28 16:27:12 +02:00
parent 3a6d4cfeab
commit 52dd8703fa
5 changed files with 10 additions and 43 deletions

View File

@@ -136,14 +136,8 @@ func (p *ProxyBind) CloseConn() error {
return p.close()
}
// InjectPacket writes b to the remote peer over the underlying transport.
func (p *ProxyBind) InjectPacket(b []byte) error {
if p.remoteConn == nil {
return errors.New("proxy not started")
}
if _, err := p.remoteConn.Write(b); err != nil {
return err
}
// InjectPacket is a no-op for the userspace proxy: first-packet reinjection is kernel-only.
func (p *ProxyBind) InjectPacket(_ []byte) error {
return nil
}

View File

@@ -20,6 +20,7 @@ type Proxy interface {
SetDisconnectListener(disconnected func())
// InjectPacket writes a raw packet directly to the remote peer over the underlying transport,
// bypassing WireGuard. Used to replay the captured lazyconn handshake initiation.
// bypassing WireGuard. Used to replay the captured lazyconn handshake initiation. Only the
// kernel-mode proxies act on it; the userspace proxy is a no-op since reinjection is kernel-only.
InjectPacket(b []byte) error
}

View File

@@ -4,22 +4,15 @@ import (
"context"
"io"
"net"
"slices"
"sync"
"time"
)
// lazyConn detects activity when WireGuard attempts to send packets.
// It does not deliver packets, only signals that activity occurred. The first packet WG-go writes
// is captured so the lazyconn manager can reinject it through the real bind endpoint, avoiding the
// handshake-retry wait when the real connection comes up.
// It does not deliver packets, only signals that activity occurred.
type lazyConn struct {
activityCh chan struct{}
ctx context.Context
cancel context.CancelFunc
mu sync.Mutex
capturedPacket []byte
}
// newLazyConn creates a new lazyConn for activity detection.
@@ -38,19 +31,12 @@ func (c *lazyConn) Read(_ []byte) (n int, err error) {
return 0, io.EOF
}
// Write signals activity detection when ICEBind routes packets to this endpoint. The first packet
// is cloned and stored so it can be replayed on the real endpoint once ICE/relay comes up.
// Write signals activity detection when ICEBind routes packets to this endpoint.
func (c *lazyConn) Write(b []byte) (n int, err error) {
if c.ctx.Err() != nil {
return 0, io.EOF
}
c.mu.Lock()
if c.capturedPacket == nil && len(b) > 0 {
c.capturedPacket = slices.Clone(b)
}
c.mu.Unlock()
select {
case c.activityCh <- struct{}{}:
default:
@@ -59,14 +45,6 @@ func (c *lazyConn) Write(b []byte) (n int, err error) {
return len(b), nil
}
// CapturedPacket returns the first packet written to this connection, or nil. The slice aliases
// the internal buffer, which is written once and never mutated, so the caller must not modify it.
func (c *lazyConn) CapturedPacket() []byte {
c.mu.Lock()
defer c.mu.Unlock()
return c.capturedPacket
}
// ActivityChan returns the channel that signals when activity is detected.
func (c *lazyConn) ActivityChan() <-chan struct{} {
return c.activityCh

View File

@@ -118,21 +118,15 @@ func (d *BindListener) ReadPackets() {
d.peerCfg.Log.Infof("exit from activity listener")
}
// Leave the peer in place. ConfigureWGEndpoint will UpdatePeer with the real endpoint;
// removing the peer here wipes wireguard-go's staged queue and drops the user packet that
// triggered activation.
d.peerCfg.Log.Debugf("removing lazy endpoint for peer %s", d.peerCfg.PublicKey)
_ = d.lazyConn.Close()
d.bind.RemoveEndpoint(d.fakeIP)
d.done.Done()
}
// CapturedPacket returns the first packet that triggered activity, or nil if none was captured.
// Safe to call after ReadPackets returns.
// CapturedPacket is unused in userspace bind mode: first-packet reinjection is kernel-only.
func (d *BindListener) CapturedPacket() []byte {
if d.lazyConn == nil {
return nil
}
return d.lazyConn.CapturedPacket()
return nil
}
// Close stops the listener and cleans up resources.

View File

@@ -213,7 +213,7 @@ func TestManager_BindMode(t *testing.T) {
select {
case ev := <-mgr.OnActivityChan:
assert.Equal(t, cfg.PeerConnID, ev.PeerConnID, "Received peer connection ID should match")
assert.Equal(t, []byte{0x01, 0x02, 0x03}, ev.FirstPacket, "First packet should be captured for reinjection")
assert.Nil(t, ev.FirstPacket, "Bind mode does not capture packets: reinjection is kernel-only")
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for activity notification")
}