|
|
|
@@ -84,14 +84,14 @@ type RosenpassConfig struct {
|
|
|
|
|
type PQHandshaker interface {
|
|
|
|
|
// OfferPayload returns the KEM offer to embed in an outgoing offer (nil if this
|
|
|
|
|
// peer is not the KEM initiator) and the local PQ data-path port to announce.
|
|
|
|
|
OfferPayload(remoteKey string) (payload []byte, port int)
|
|
|
|
|
OfferPayload(remoteKey string) (payload []byte, port uint16)
|
|
|
|
|
// ShouldSendBootstrapOffer reports whether, as the controller, we should reply to a
|
|
|
|
|
// received responder offer with our own KEM offer (true only when no exchange is
|
|
|
|
|
// already in flight — so we kick the KEM once and ignore further offers).
|
|
|
|
|
ShouldSendBootstrapOffer(remoteKey string) bool
|
|
|
|
|
// AnswerPayload processes a received KEM offer (nil if absent) and returns the KEM
|
|
|
|
|
// answer to embed in the outgoing answer (nil if none) and the local PQ port.
|
|
|
|
|
AnswerPayload(remoteKey string, recvOffer []byte) (payload []byte, port int)
|
|
|
|
|
AnswerPayload(remoteKey string, recvOffer []byte) (payload []byte, port uint16)
|
|
|
|
|
// OnAnswer feeds a received KEM answer (nil if absent).
|
|
|
|
|
OnAnswer(remoteKey string, recvAnswer []byte)
|
|
|
|
|
// PSK returns the peer's latest derived post-quantum PSK to program at WG
|
|
|
|
@@ -468,6 +468,9 @@ func (conn *Conn) ConnID() id.ConnID {
|
|
|
|
|
|
|
|
|
|
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
|
|
|
|
|
func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConnInfo ICEConnInfo) {
|
|
|
|
|
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
|
|
|
|
|
pqPSK, pqOK := conn.pqPSK()
|
|
|
|
|
|
|
|
|
|
conn.mu.Lock()
|
|
|
|
|
defer conn.mu.Unlock()
|
|
|
|
|
|
|
|
|
@@ -485,7 +488,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|
|
|
|
if conn.currentConnPriority > priority {
|
|
|
|
|
conn.Log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority)
|
|
|
|
|
conn.statusICE.SetConnected()
|
|
|
|
|
conn.updateIceState(iceConnInfo, time.Now())
|
|
|
|
|
conn.updateIceState(iceConnInfo, pqOK, time.Now())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -528,7 +531,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|
|
|
|
updateTime := time.Now()
|
|
|
|
|
conn.enableWgWatcherIfNeeded(updateTime)
|
|
|
|
|
|
|
|
|
|
presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey)
|
|
|
|
|
presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey, pqPSK)
|
|
|
|
|
if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil {
|
|
|
|
|
conn.handleConfigurationFailure(err, wgProxy)
|
|
|
|
|
return
|
|
|
|
@@ -544,11 +547,14 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|
|
|
|
|
|
|
|
|
conn.currentConnPriority = priority
|
|
|
|
|
conn.statusICE.SetConnected()
|
|
|
|
|
conn.updateIceState(iceConnInfo, updateTime)
|
|
|
|
|
conn.updateIceState(iceConnInfo, pqOK, updateTime)
|
|
|
|
|
conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr, updateTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
|
|
|
|
|
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
|
|
|
|
|
pqPSK, _ := conn.pqPSK()
|
|
|
|
|
|
|
|
|
|
conn.mu.Lock()
|
|
|
|
|
defer conn.mu.Unlock()
|
|
|
|
|
|
|
|
|
@@ -575,7 +581,7 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
|
|
|
|
|
// todo consider to move after the ConfigureWGEndpoint
|
|
|
|
|
conn.wgProxyRelay.Work()
|
|
|
|
|
|
|
|
|
|
presharedKey := conn.presharedKey(conn.rosenpassRemoteKey)
|
|
|
|
|
presharedKey := conn.presharedKey(conn.rosenpassRemoteKey, pqPSK)
|
|
|
|
|
if err := conn.endpointUpdater.SwitchWGEndpoint(conn.wgProxyRelay.EndpointAddr(), presharedKey); err != nil {
|
|
|
|
|
conn.Log.Errorf("failed to switch to relay conn: %v", err)
|
|
|
|
|
}
|
|
|
|
@@ -613,6 +619,9 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|
|
|
|
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
|
|
|
|
|
pqPSK, pqOK := conn.pqPSK()
|
|
|
|
|
|
|
|
|
|
conn.mu.Lock()
|
|
|
|
|
defer conn.mu.Unlock()
|
|
|
|
|
|
|
|
|
@@ -641,7 +650,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|
|
|
|
conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String())
|
|
|
|
|
conn.setRelayedProxy(wgProxy)
|
|
|
|
|
conn.statusRelay.SetConnected()
|
|
|
|
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, time.Now())
|
|
|
|
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, pqOK, time.Now())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -652,7 +661,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|
|
|
|
}
|
|
|
|
|
updateTime := time.Now()
|
|
|
|
|
conn.enableWgWatcherIfNeeded(updateTime)
|
|
|
|
|
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), conn.presharedKey(rci.rosenpassPubKey)); err != nil {
|
|
|
|
|
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), conn.presharedKey(rci.rosenpassPubKey, pqPSK)); err != nil {
|
|
|
|
|
if err := wgProxy.CloseConn(); err != nil {
|
|
|
|
|
conn.Log.Warnf("Failed to close relay connection: %v", err)
|
|
|
|
|
}
|
|
|
|
@@ -671,7 +680,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|
|
|
|
conn.currentConnPriority = conntype.Relay
|
|
|
|
|
conn.statusRelay.SetConnected()
|
|
|
|
|
conn.setRelayedProxy(wgProxy)
|
|
|
|
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, updateTime)
|
|
|
|
|
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, pqOK, updateTime)
|
|
|
|
|
conn.Log.Infof("start to communicate with peer via relay")
|
|
|
|
|
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr, updateTime)
|
|
|
|
|
}
|
|
|
|
@@ -751,19 +760,15 @@ func (conn *Conn) RequestReoffer() {
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) onWGDisconnected(watcherCtx context.Context) {
|
|
|
|
|
conn.mu.Lock()
|
|
|
|
|
defer conn.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// watcherCtx guards against a stale watcher tearing down a connection that already superseded it.
|
|
|
|
|
if conn.ctx.Err() != nil || watcherCtx.Err() != nil {
|
|
|
|
|
conn.mu.Unlock()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn.Log.Warnf("WireGuard handshake timeout detected, closing current connection")
|
|
|
|
|
|
|
|
|
|
if conn.config.PQ != nil {
|
|
|
|
|
conn.config.PQ.OnDataPathDown(conn.config.Key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close the active connection based on current priority
|
|
|
|
|
switch conn.currentConnPriority {
|
|
|
|
|
case conntype.Relay:
|
|
|
|
@@ -776,6 +781,15 @@ func (conn *Conn) onWGDisconnected(watcherCtx context.Context) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn.escalateWGTimeoutLocked()
|
|
|
|
|
pq := conn.config.PQ
|
|
|
|
|
key := conn.config.Key
|
|
|
|
|
conn.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Signal the PQ manager outside conn.mu: it may re-enter Conn (reoffer) under
|
|
|
|
|
// conn.mu, so calling it while holding the lock would invert the lock order.
|
|
|
|
|
if pq != nil {
|
|
|
|
|
pq.OnDataPathDown(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// escalateWGTimeoutLocked resets the peer's rosenpass state after repeated
|
|
|
|
@@ -799,14 +813,14 @@ func (conn *Conn) escalateWGTimeoutLocked() {
|
|
|
|
|
conn.onDisconnected(conn.config.WgConfig.RemoteKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, updateTime time.Time) {
|
|
|
|
|
func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, pqEstablished bool, updateTime time.Time) {
|
|
|
|
|
peerState := State{
|
|
|
|
|
PubKey: conn.config.Key,
|
|
|
|
|
ConnStatusUpdate: updateTime,
|
|
|
|
|
ConnStatus: conn.evalStatus(),
|
|
|
|
|
Relayed: conn.isRelayed(),
|
|
|
|
|
RelayServerAddress: relayServerAddr,
|
|
|
|
|
RosenpassEnabled: conn.quantumResistant(rosenpassPubKey),
|
|
|
|
|
RosenpassEnabled: conn.quantumResistant(rosenpassPubKey, pqEstablished),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := conn.statusRecorder.UpdatePeerRelayedState(peerState)
|
|
|
|
@@ -815,7 +829,7 @@ func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []by
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time) {
|
|
|
|
|
func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, pqEstablished bool, updateTime time.Time) {
|
|
|
|
|
peerState := State{
|
|
|
|
|
PubKey: conn.config.Key,
|
|
|
|
|
ConnStatusUpdate: updateTime,
|
|
|
|
@@ -825,7 +839,7 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time)
|
|
|
|
|
RemoteIceCandidateType: iceConnInfo.RemoteIceCandidateType,
|
|
|
|
|
LocalIceCandidateEndpoint: iceConnInfo.LocalIceCandidateEndpoint,
|
|
|
|
|
RemoteIceCandidateEndpoint: iceConnInfo.RemoteIceCandidateEndpoint,
|
|
|
|
|
RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey),
|
|
|
|
|
RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey, pqEstablished),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := conn.statusRecorder.UpdatePeerICEState(peerState)
|
|
|
|
@@ -1094,13 +1108,31 @@ func (conn *Conn) AgentVersionString() string {
|
|
|
|
|
return conn.config.AgentVersion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|
|
|
|
// pqPSK returns the post-quantum PSK derived for this peer, if the ML-KEM exchange has
|
|
|
|
|
// produced one. It reads the manager WITHOUT conn.mu, so callers fetch it before taking
|
|
|
|
|
// conn.mu: the lock order is always conn.mu -> manager, never the reverse (the manager's
|
|
|
|
|
// reoffer callback re-enters Conn under conn.mu).
|
|
|
|
|
func (conn *Conn) pqPSK() (*wgtypes.Key, bool) {
|
|
|
|
|
if conn.config.PQ == nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
psk, ok := conn.config.PQ.PSK(conn.config.Key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return &psk, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// presharedKey resolves the WireGuard preshared key for the peer. pqPSK is the
|
|
|
|
|
// post-quantum PSK looked up out of band via pqPSK (nil when none is derived yet), passed
|
|
|
|
|
// in so the manager lock is never taken under conn.mu.
|
|
|
|
|
func (conn *Conn) presharedKey(remoteRosenpassKey []byte, pqPSK *wgtypes.Key) *wgtypes.Key {
|
|
|
|
|
// Post-quantum: once the ML-KEM exchange has derived a PSK for this peer, program
|
|
|
|
|
// it here so the peer's next WireGuard handshake adopts it. Applied at peer-config
|
|
|
|
|
// time (bootstrap / reconnect); steady-state rotation is pushed separately.
|
|
|
|
|
if conn.config.PQ != nil {
|
|
|
|
|
if psk, ok := conn.config.PQ.PSK(conn.config.Key); ok {
|
|
|
|
|
return &psk
|
|
|
|
|
if pqPSK != nil {
|
|
|
|
|
return pqPSK
|
|
|
|
|
}
|
|
|
|
|
if conn.config.PQStrict && conn.pqStrictSentinelKey != nil {
|
|
|
|
|
// Fail closed: program a non-matching sentinel so no session forms on a
|
|
|
|
@@ -1154,16 +1186,8 @@ func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
|
|
|
|
// quantumResistant reports whether the peer's tunnel is post-quantum protected, for
|
|
|
|
|
// the status "Quantum resistance" field: either Rosenpass (the remote advertised a
|
|
|
|
|
// Rosenpass key) or the ML-KEM exchange (a PQ PSK has been derived for this peer).
|
|
|
|
|
func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte) bool {
|
|
|
|
|
if isRosenpassEnabled(remoteRosenpassPubKey) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if conn.config.PQ != nil {
|
|
|
|
|
if _, ok := conn.config.PQ.PSK(conn.config.Key); ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte, pqEstablished bool) bool {
|
|
|
|
|
return isRosenpassEnabled(remoteRosenpassPubKey) || pqEstablished
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func evalConnStatus(in connStatusInputs) guard.ConnStatus {
|
|
|
|
|