conn,device: implement InitiationAwareEndpoint

To be implemented by [magicsock.lazyEndpoint], which is responsible for
triggering JIT peer configuration.

Updates tailscale/corp#20732
Updates tailscale/corp#30042

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2025-07-03 20:54:49 -07:00 committed by Jordan Whited
parent 24483d7a00
commit 1f398ae148
4 changed files with 52 additions and 3 deletions

View file

@ -86,6 +86,21 @@ type Endpoint interface {
SrcIP() netip.Addr SrcIP() netip.Addr
} }
// InitiationAwareEndpoint is an optional [Endpoint] specialization for
// integrations that want to know when a WireGuard handshake initiation
// message has been received, enabling just-in-time peer configuration before
// attempted decryption.
//
// It's most useful when used in combination with [PeerAwareEndpoint], enabling
// JIT peer configuration and post-decryption peer verification from a single
// implementer.
type InitiationAwareEndpoint interface {
// InitiationMessagePublicKey is called when a handshake initiation message
// has been received, and the sender's public key has been identified, but
// BEFORE an attempt has been made to verify it.
InitiationMessagePublicKey(peerPublicKey [32]byte)
}
// PeerAwareEndpoint is an optional Endpoint specialization for // PeerAwareEndpoint is an optional Endpoint specialization for
// integrations that want to know about the outcome of Cryptokey Routing // integrations that want to know about the outcome of Cryptokey Routing
// identification. // identification.

View file

@ -16,6 +16,7 @@ import (
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305" "golang.org/x/crypto/poly1305"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/tai64n" "github.com/tailscale/wireguard-go/tai64n"
) )
@ -338,7 +339,7 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
return &msg, nil return &msg, nil
} }
func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation, endpoint conn.Endpoint) *Peer {
var ( var (
hash [blake2s.Size]byte hash [blake2s.Size]byte
chainKey [blake2s.Size]byte chainKey [blake2s.Size]byte
@ -372,6 +373,11 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
// lookup peer // lookup peer
initEP, ok := endpoint.(conn.InitiationAwareEndpoint)
if ok {
initEP.InitiationMessagePublicKey(peerPK)
}
peer := device.LookupPeer(peerPK) peer := device.LookupPeer(peerPK)
if peer == nil || !peer.isRunning.Load() { if peer == nil || !peer.isRunning.Load() {
return nil return nil

View file

@ -8,6 +8,7 @@ package device
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"net/netip"
"testing" "testing"
"github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/conn"
@ -56,6 +57,26 @@ func assertEqual(t *testing.T, a, b []byte) {
} }
} }
type initAwareEP struct {
calledWith *[32]byte
}
var _ conn.Endpoint = (*initAwareEP)(nil)
var _ conn.InitiationAwareEndpoint = (*initAwareEP)(nil)
func (i *initAwareEP) ClearSrc() {}
func (i *initAwareEP) SrcToString() string { return "" }
func (i *initAwareEP) DstToString() string { return "" }
func (i *initAwareEP) DstToBytes() []byte { return nil }
func (i *initAwareEP) DstIP() netip.Addr { return netip.Addr{} }
func (i *initAwareEP) SrcIP() netip.Addr { return netip.Addr{} }
func (i *initAwareEP) InitiationMessagePublicKey(peerPublicKey [32]byte) {
calledWith := [32]byte{}
copy(calledWith[:], peerPublicKey[:])
i.calledWith = &calledWith
}
func TestNoiseHandshake(t *testing.T) { func TestNoiseHandshake(t *testing.T) {
dev1 := randDevice(t) dev1 := randDevice(t)
dev2 := randDevice(t) dev2 := randDevice(t)
@ -93,10 +114,17 @@ func TestNoiseHandshake(t *testing.T) {
writer := bytes.NewBuffer(packet) writer := bytes.NewBuffer(packet)
err = binary.Write(writer, binary.LittleEndian, msg1) err = binary.Write(writer, binary.LittleEndian, msg1)
assertNil(t, err) assertNil(t, err)
peer := dev2.ConsumeMessageInitiation(msg1) initEP := &initAwareEP{}
peer := dev2.ConsumeMessageInitiation(msg1, initEP)
if peer == nil { if peer == nil {
t.Fatal("handshake failed at initiation message") t.Fatal("handshake failed at initiation message")
} }
if initEP.calledWith == nil {
t.Fatal("initAwareEP never called")
}
if *initEP.calledWith != dev1.staticIdentity.publicKey {
t.Fatal("initAwareEP called with unexpected public key")
}
assertEqual( assertEqual(
t, t,

View file

@ -359,7 +359,7 @@ func (device *Device) RoutineHandshake(id int) {
// consume initiation // consume initiation
peer := device.ConsumeMessageInitiation(&msg) peer := device.ConsumeMessageInitiation(&msg, elem.endpoint)
if peer == nil { if peer == nil {
device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString()) device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString())
goto skip goto skip