рабочий вариант, но скороть 10 МБит
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled

This commit is contained in:
2026-05-22 21:46:49 +03:00
parent 469fa0e860
commit 20d361a886
280 changed files with 954890 additions and 18524 deletions
@@ -3,6 +3,7 @@ package vpnruntime
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"time"
@@ -12,10 +13,11 @@ import (
)
const (
fabricVPNPacketPayloadMagic uint32 = 0x52565042 // RVPB
fabricVPNPacketPayloadVersion uint8 = 1
fabricVPNPacketPayloadHeader = 24
fabricVPNPacketMaxPacketCount = 2048
fabricVPNPacketPayloadMagic uint32 = 0x52565042 // RVPB
fabricVPNPacketPayloadVersion uint8 = 2
fabricVPNPacketPayloadHeader = 24
fabricVPNPacketMaxPacketCount = 2048
fabricVPNPacketMaxMetadataBytes = 64 * 1024
fabricVPNPacketDirectionClientToGateway uint8 = 1
fabricVPNPacketDirectionGatewayToClient uint8 = 2
@@ -32,6 +34,7 @@ type FabricVPNPacketFrameInput struct {
VPNConnectionID string
Direction string
TrafficClass string
ServiceTunnel FabricServiceTunnel
Packets [][]byte
Now time.Time
}
@@ -60,6 +63,26 @@ func NewFabricVPNPacketDataFrame(input FabricVPNPacketFrameInput) (fabricproto.F
}, nil
}
func NewFabricVPNSessionHelloFrame(input FabricVPNPacketFrameInput) (fabricproto.Frame, error) {
if input.StreamID == 0 {
return fabricproto.Frame{}, fmt.Errorf("%w: missing stream id", ErrFabricVPNPacketFrameInvalid)
}
if input.VPNConnectionID == "" || input.Direction == "" {
return fabricproto.Frame{}, fmt.Errorf("%w: missing vpn identity", ErrFabricVPNPacketFrameInvalid)
}
payload, err := encodeFabricVPNPacketPayload(input, nil)
if err != nil {
return fabricproto.Frame{}, err
}
return fabricproto.Frame{
Type: fabricproto.FrameData,
TrafficClass: fabricFrameTrafficClass(input.TrafficClass, nil),
StreamID: input.StreamID,
Sequence: input.Sequence,
Payload: payload,
}, nil
}
func DecodeFabricVPNPacketDataFrame(frame fabricproto.Frame) (mesh.VPNPacketBatchPayload, error) {
if frame.Type != fabricproto.FrameData || frame.StreamID == 0 {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: expected DATA stream frame", ErrFabricVPNPacketFrameInvalid)
@@ -94,11 +117,19 @@ func encodeFabricVPNPacketPayload(input FabricVPNPacketFrameInput, packets [][]b
if len(vpnID) > 0xffff {
return nil, fmt.Errorf("%w: vpn connection id too long", ErrFabricVPNPacketPayload)
}
var metadata []byte
if len(packets) == 0 {
var err error
metadata, err = encodeFabricVPNPacketServiceMetadata(input)
if err != nil {
return nil, err
}
}
now := input.Now.UTC()
if now.IsZero() {
now = time.Now().UTC()
}
total := fabricVPNPacketPayloadHeader + len(vpnID)
total := fabricVPNPacketPayloadHeader + len(vpnID) + len(metadata)
for _, packet := range packets {
total += 4 + len(packet)
}
@@ -108,10 +139,13 @@ func encodeFabricVPNPacketPayload(input FabricVPNPacketFrameInput, packets [][]b
out[5] = directionCode
binary.BigEndian.PutUint16(out[6:8], uint16(len(packets)))
binary.BigEndian.PutUint16(out[8:10], uint16(len(vpnID)))
binary.BigEndian.PutUint16(out[10:12], uint16(len(metadata)))
binary.BigEndian.PutUint64(out[12:20], uint64(now.UnixNano()))
offset := fabricVPNPacketPayloadHeader
copy(out[offset:], vpnID)
offset += len(vpnID)
copy(out[offset:], metadata)
offset += len(metadata)
for _, packet := range packets {
binary.BigEndian.PutUint32(out[offset:offset+4], uint32(len(packet)))
offset += 4
@@ -128,7 +162,8 @@ func decodeFabricVPNPacketPayload(payload []byte) (mesh.VPNPacketBatchPayload, e
if binary.BigEndian.Uint32(payload[0:4]) != fabricVPNPacketPayloadMagic {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: bad magic", ErrFabricVPNPacketPayload)
}
if payload[4] != fabricVPNPacketPayloadVersion {
version := payload[4]
if version != 1 && version != fabricVPNPacketPayloadVersion {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: unsupported version %d", ErrFabricVPNPacketPayload, payload[4])
}
direction, err := fabricVPNPacketDirectionName(payload[5])
@@ -137,7 +172,11 @@ func decodeFabricVPNPacketPayload(payload []byte) (mesh.VPNPacketBatchPayload, e
}
packetCount := int(binary.BigEndian.Uint16(payload[6:8]))
vpnIDLength := int(binary.BigEndian.Uint16(payload[8:10]))
if packetCount <= 0 || packetCount > fabricVPNPacketMaxPacketCount {
metadataLength := 0
if version >= 2 {
metadataLength = int(binary.BigEndian.Uint16(payload[10:12]))
}
if packetCount < 0 || packetCount > fabricVPNPacketMaxPacketCount {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: invalid packet count %d", ErrFabricVPNPacketPayload, packetCount)
}
offset := fabricVPNPacketPayloadHeader
@@ -149,6 +188,16 @@ func decodeFabricVPNPacketPayload(payload []byte) (mesh.VPNPacketBatchPayload, e
if vpnID == "" {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: empty vpn id", ErrFabricVPNPacketPayload)
}
metadata := fabricVPNPacketServiceMetadata{}
if metadataLength > 0 {
if metadataLength > fabricVPNPacketMaxMetadataBytes || len(payload) < offset+metadataLength {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: truncated service metadata", ErrFabricVPNPacketPayload)
}
if err := json.Unmarshal(payload[offset:offset+metadataLength], &metadata); err != nil {
return mesh.VPNPacketBatchPayload{}, fmt.Errorf("%w: invalid service metadata: %v", ErrFabricVPNPacketPayload, err)
}
offset += metadataLength
}
packets := make([][]byte, 0, packetCount)
for index := 0; index < packetCount; index++ {
if len(payload) < offset+4 {
@@ -169,12 +218,74 @@ func decodeFabricVPNPacketPayload(payload []byte) (mesh.VPNPacketBatchPayload, e
return mesh.VPNPacketBatchPayload{
SchemaVersion: "rap.vpn_packet_batch.fabric.v1",
VPNConnectionID: vpnID,
TunnelID: firstNonEmptyTunnelString(metadata.TunnelID, vpnID),
PoolID: metadata.PoolID,
ServiceID: metadata.ServiceID,
LocalServiceID: metadata.LocalServiceID,
RemoteServiceID: metadata.RemoteServiceID,
ServiceKind: metadata.ServiceKind,
ServiceClass: metadata.ServiceClass,
ServiceRole: metadata.ServiceRole,
RouteLeaseID: metadata.RouteLeaseID,
RouteGeneration: metadata.RouteGeneration,
DataPlane: metadata.DataPlane,
TransportOwner: metadata.TransportOwner,
RouteVisibility: metadata.RouteVisibility,
TrafficClasses: metadata.TrafficClasses,
StreamShards: metadata.StreamShards,
Direction: direction,
Packets: packets,
SentAt: sentAt,
}, nil
}
type fabricVPNPacketServiceMetadata struct {
TunnelID string `json:"tunnel_id,omitempty"`
PoolID string `json:"pool_id,omitempty"`
ServiceID string `json:"service_id,omitempty"`
LocalServiceID string `json:"local_service_id,omitempty"`
RemoteServiceID string `json:"remote_service_id,omitempty"`
ServiceKind string `json:"service_kind,omitempty"`
ServiceClass string `json:"service_class,omitempty"`
ServiceRole string `json:"service_role,omitempty"`
RouteLeaseID string `json:"route_lease_id,omitempty"`
RouteGeneration string `json:"route_generation,omitempty"`
DataPlane string `json:"data_plane,omitempty"`
TransportOwner string `json:"transport_owner,omitempty"`
RouteVisibility string `json:"route_visibility,omitempty"`
TrafficClasses []string `json:"traffic_classes,omitempty"`
StreamShards int `json:"stream_shards,omitempty"`
}
func encodeFabricVPNPacketServiceMetadata(input FabricVPNPacketFrameInput) ([]byte, error) {
tunnel := NormalizeServiceTunnel(input.ServiceTunnel, input.VPNConnectionID)
metadata := fabricVPNPacketServiceMetadata{
TunnelID: firstNonEmptyTunnelString(tunnel.TunnelID, input.VPNConnectionID),
PoolID: tunnel.PoolID,
ServiceID: tunnel.ServiceID,
LocalServiceID: tunnel.LocalServiceID,
RemoteServiceID: tunnel.RemoteServiceID,
ServiceKind: tunnel.ServiceKind,
ServiceClass: tunnel.ServiceClass,
ServiceRole: tunnel.ServiceRole,
RouteLeaseID: tunnel.RouteLeaseID,
RouteGeneration: tunnel.RouteGeneration,
DataPlane: tunnel.DataPlane,
TransportOwner: tunnel.TransportOwner,
RouteVisibility: tunnel.RouteVisibility,
TrafficClasses: append([]string(nil), tunnel.TrafficClasses...),
StreamShards: tunnel.StreamShards,
}
payload, err := json.Marshal(metadata)
if err != nil {
return nil, err
}
if len(payload) > fabricVPNPacketMaxMetadataBytes || len(payload) > 0xffff {
return nil, fmt.Errorf("%w: service metadata too large", ErrFabricVPNPacketPayload)
}
return payload, nil
}
func fabricVPNPacketDirectionCode(direction string) (uint8, error) {
switch direction {
case FabricDirectionClientToGateway:
@@ -201,6 +312,8 @@ func fabricFrameTrafficClass(trafficClass string, packets [][]byte) fabricproto.
switch normalizeFabricTrafficClass(trafficClass) {
case FabricTrafficClassControl:
return fabricproto.TrafficClassControl
case FabricTrafficClassDNS:
return fabricproto.TrafficClassReliable
case FabricTrafficClassInteractive:
return fabricproto.TrafficClassInteractive
case FabricTrafficClassReliable:
@@ -208,9 +321,6 @@ func fabricFrameTrafficClass(trafficClass string, packets [][]byte) fabricproto.
case FabricTrafficClassDroppable:
return fabricproto.TrafficClassDroppable
default:
if batchHasTCPControlPacket(packets) {
return fabricproto.TrafficClassInteractive
}
return fabricproto.TrafficClassBulk
}
}
@@ -14,11 +14,16 @@ type FabricSessionFrameWriter interface {
}
type FabricSessionPacketPeerRegistry struct {
mu sync.RWMutex
peers map[string]FabricSessionPacketPeer
mu sync.RWMutex
peers map[string]FabricSessionPacketPeer
changed chan struct{}
}
type FabricSessionPacketPeer struct {
TunnelID string
PoolID string
ServiceID string
ServiceTunnel FabricServiceTunnel
VPNConnectionID string
Sender FabricSessionFrameWriter
StreamID uint64
@@ -30,11 +35,17 @@ type FabricSessionPacketPeer struct {
type FabricSessionPacketPeerTransport struct {
Registry *FabricSessionPacketPeerRegistry
Inbox *FabricPacketInbox
TunnelID string
PoolID string
ServiceID string
VPNConnectionID string
PeerWaitTimeout time.Duration
}
const defaultFabricSessionPeerWaitTimeout = 500 * time.Millisecond
func NewFabricSessionPacketPeerRegistry() *FabricSessionPacketPeerRegistry {
return &FabricSessionPacketPeerRegistry{peers: map[string]FabricSessionPacketPeer{}}
return &FabricSessionPacketPeerRegistry{peers: map[string]FabricSessionPacketPeer{}, changed: make(chan struct{})}
}
func (r *FabricSessionPacketPeerRegistry) RegisterFrame(ctx context.Context, sender FabricSessionFrameWriter, frame fabricproto.Frame) (bool, error) {
@@ -53,10 +64,33 @@ func (r *FabricSessionPacketPeerRegistry) RegisterFrame(ctx context.Context, sen
if r.peers == nil {
r.peers = map[string]FabricSessionPacketPeer{}
}
if r.changed == nil {
r.changed = make(chan struct{})
}
peer := r.peers[payload.VPNConnectionID]
if peer.RegisteredAt.IsZero() {
peer.RegisteredAt = now
}
peer.ServiceTunnel = NormalizeServiceTunnel(FabricServiceTunnel{
TunnelID: firstNonEmptyTunnelString(payload.TunnelID, payload.VPNConnectionID),
PoolID: payload.PoolID,
ServiceID: payload.ServiceID,
LocalServiceID: payload.LocalServiceID,
RemoteServiceID: payload.RemoteServiceID,
ServiceKind: payload.ServiceKind,
ServiceClass: payload.ServiceClass,
ServiceRole: payload.ServiceRole,
RouteLeaseID: payload.RouteLeaseID,
RouteGeneration: payload.RouteGeneration,
DataPlane: payload.DataPlane,
TransportOwner: payload.TransportOwner,
RouteVisibility: payload.RouteVisibility,
TrafficClasses: payload.TrafficClasses,
StreamShards: payload.StreamShards,
}, payload.VPNConnectionID)
peer.TunnelID = peer.ServiceTunnel.TunnelID
peer.PoolID = peer.ServiceTunnel.PoolID
peer.ServiceID = peer.ServiceTunnel.ServiceID
peer.VPNConnectionID = payload.VPNConnectionID
peer.Sender = sender
peer.StreamID = frame.StreamID
@@ -69,6 +103,7 @@ func (r *FabricSessionPacketPeerRegistry) RegisterFrame(ctx context.Context, sen
peer.StreamIDsByTrafficClass[trafficClass] = append(peer.StreamIDsByTrafficClass[trafficClass], frame.StreamID)
}
r.peers[payload.VPNConnectionID] = peer
r.signalLocked()
r.mu.Unlock()
return true, nil
}
@@ -84,25 +119,93 @@ func (r *FabricSessionPacketPeerRegistry) TransportFor(vpnConnectionID string, i
return nil
}
return &FabricSessionPacketTransport{
Sender: fabricSessionFrameWriterAdapter{writer: peer.Sender},
Inbox: inbox,
StreamID: peer.StreamID,
StreamIDsByTrafficClass: copyStreamIDsByClass(peer.StreamIDsByTrafficClass),
VPNConnectionID: vpnConnectionID,
SendDirection: FabricDirectionGatewayToClient,
ReceiveDirection: FabricDirectionClientToGateway,
Sender: fabricSessionFrameWriterAdapter{writer: peer.Sender},
Inbox: inbox,
StreamID: peer.StreamID,
ServiceTunnel: peer.ServiceTunnel,
TunnelID: vpnConnectionID,
PoolID: peer.PoolID,
ServiceID: peer.ServiceID,
VPNConnectionID: vpnConnectionID,
SendDirection: FabricDirectionGatewayToClient,
ReceiveDirection: FabricDirectionClientToGateway,
}
}
func (r *FabricSessionPacketPeerRegistry) WaitTransportFor(ctx context.Context, vpnConnectionID string, inbox *FabricPacketInbox, timeout time.Duration) PacketTransport {
if timeout <= 0 {
return r.TransportFor(vpnConnectionID, inbox)
}
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
if transport := r.TransportFor(vpnConnectionID, inbox); transport != nil {
return transport
}
changed := r.changedChannel()
select {
case <-ctx.Done():
return nil
case <-timer.C:
return nil
case <-changed:
}
}
}
func (r *FabricSessionPacketPeerRegistry) Forget(vpnConnectionID string) {
if r == nil || vpnConnectionID == "" {
return
}
r.mu.Lock()
if r.changed == nil {
r.changed = make(chan struct{})
}
delete(r.peers, vpnConnectionID)
r.signalLocked()
r.mu.Unlock()
}
func (r *FabricSessionPacketPeerRegistry) changedChannel() <-chan struct{} {
if r == nil {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
if r.changed == nil {
r.changed = make(chan struct{})
}
return r.changed
}
func (r *FabricSessionPacketPeerRegistry) signalLocked() {
if r == nil {
return
}
if r.changed == nil {
r.changed = make(chan struct{})
}
close(r.changed)
r.changed = make(chan struct{})
}
func (t *FabricSessionPacketPeerTransport) SendGatewayPacketBatch(ctx context.Context, packets [][]byte) error {
if t == nil || t.Registry == nil || t.Inbox == nil || t.VPNConnectionID == "" {
return mesh.ErrForwardRuntimeUnavailable
}
transport := t.Registry.TransportFor(t.VPNConnectionID, t.Inbox)
waitTimeout := t.PeerWaitTimeout
if waitTimeout <= 0 {
waitTimeout = defaultFabricSessionPeerWaitTimeout
}
transport := t.Registry.WaitTransportFor(ctx, t.VPNConnectionID, t.Inbox, waitTimeout)
if transport == nil {
return mesh.ErrForwardRuntimeUnavailable
}
return transport.SendGatewayPacketBatch(ctx, packets)
if err := transport.SendGatewayPacketBatch(ctx, packets); err != nil {
t.Registry.Forget(t.VPNConnectionID)
return err
}
return nil
}
func (t *FabricSessionPacketPeerTransport) ReceiveGatewayPacketBatch(ctx context.Context, timeout time.Duration) ([][]byte, error) {
@@ -126,9 +229,12 @@ func (t *FabricSessionPacketPeerTransport) Snapshot() map[string]any {
}
}
return map[string]any{
"transport": "fabric_session_peer_dynamic",
"vpn_connection_id": t.VPNConnectionID,
"peer_ready": ready == 1,
"transport": "fabric_session_peer_dynamic",
"tunnel_id": firstNonEmptyTunnelString(t.TunnelID, t.VPNConnectionID),
"pool_id": t.PoolID,
"service_id": t.ServiceID,
"vpn_connection_id_alias": t.VPNConnectionID,
"peer_ready": ready == 1,
}
}
@@ -142,8 +248,12 @@ func (r *FabricSessionPacketPeerRegistry) Snapshot() map[string]any {
items := make([]map[string]any, 0, len(r.peers))
for _, peer := range r.peers {
item := map[string]any{
"vpn_connection_id": peer.VPNConnectionID,
"stream_id": peer.StreamID,
"tunnel_id": firstNonEmptyTunnelString(peer.TunnelID, peer.VPNConnectionID),
"pool_id": peer.PoolID,
"service_id": peer.ServiceID,
"vpn_connection_id_alias": peer.VPNConnectionID,
"service_tunnel": peer.ServiceTunnel.Snapshot(),
"stream_id": peer.StreamID,
}
if !peer.RegisteredAt.IsZero() {
item["registered_at"] = peer.RegisteredAt.Format(time.RFC3339Nano)
@@ -31,6 +31,11 @@ type FabricSessionPacketTransport struct {
Inbox *FabricPacketInbox
StreamID uint64
ServiceStreams *FabricServiceStreamRegistry
ServiceTunnel FabricServiceTunnel
TunnelID string
PoolID string
ServiceID string
VPNConnectionID string
SendDirection string
ReceiveDirection string
@@ -39,6 +44,12 @@ type FabricSessionPacketTransport struct {
StreamIDsByTrafficClass map[string][]uint64
StreamIDs []uint64
routeMu sync.Mutex
routeLeaseID string
routeGeneration string
routeTransitionCount uint64
routeUpdatedAt time.Time
sequence uint64
sequenceMu sync.Mutex
sequenceByStream map[uint64]uint64
@@ -68,7 +79,12 @@ func (t *FabricSessionPacketTransport) SendGatewayPacketBatch(ctx context.Contex
if t == nil || t.Sender == nil {
return mesh.ErrForwardRuntimeUnavailable
}
if !t.hasSendStream() || t.VPNConnectionID == "" {
t.normalizeServiceTunnel()
packetTunnelID := t.packetTunnelID()
if t.VPNConnectionID == "" {
t.VPNConnectionID = packetTunnelID
}
if !t.hasSendStream() || packetTunnelID == "" {
return errors.New("fabric session packet transport identity is incomplete")
}
direction := t.SendDirection
@@ -77,12 +93,14 @@ func (t *FabricSessionPacketTransport) SendGatewayPacketBatch(ctx context.Contex
}
groups := t.groupPacketsByStream(packets)
for _, group := range groups {
t.registerServiceStream(group.StreamID, group.TrafficClass, direction)
frame, err := NewFabricVPNPacketDataFrame(FabricVPNPacketFrameInput{
StreamID: group.StreamID,
Sequence: t.nextSequence(group.StreamID),
VPNConnectionID: t.VPNConnectionID,
VPNConnectionID: packetTunnelID,
Direction: direction,
TrafficClass: group.TrafficClass,
ServiceTunnel: t.ServiceTunnel,
Packets: group.Packets,
})
if err != nil {
@@ -101,15 +119,17 @@ func (t *FabricSessionPacketTransport) ReceiveGatewayPacketBatch(ctx context.Con
if t == nil || t.Inbox == nil {
return nil, mesh.ErrForwardRuntimeUnavailable
}
t.normalizeServiceTunnel()
packetTunnelID := t.packetTunnelID()
direction := t.ReceiveDirection
if direction == "" {
direction = FabricDirectionClientToGateway
}
if packets, err := t.Inbox.Receive(ctx, t.VPNConnectionID, direction, 5*time.Millisecond); err != nil || len(packets) > 0 {
if packets, err := t.Inbox.Receive(ctx, packetTunnelID, direction, 5*time.Millisecond); err != nil || len(packets) > 0 {
return packets, err
}
if t.Receiver == nil {
return t.Inbox.Receive(ctx, t.VPNConnectionID, direction, timeout)
return t.Inbox.Receive(ctx, packetTunnelID, direction, timeout)
}
if timeout <= 0 {
timeout = 25 * time.Second
@@ -130,14 +150,14 @@ func (t *FabricSessionPacketTransport) ReceiveGatewayPacketBatch(ctx context.Con
continue
}
if err != nil {
if packets, receiveErr := t.Inbox.Receive(ctx, t.VPNConnectionID, direction, 100*time.Millisecond); receiveErr != nil || len(packets) > 0 {
if packets, receiveErr := t.Inbox.Receive(ctx, packetTunnelID, direction, 100*time.Millisecond); receiveErr != nil || len(packets) > 0 {
return packets, receiveErr
}
return nil, err
}
case frame, ok := <-frames:
if !ok {
return t.Inbox.Receive(ctx, t.VPNConnectionID, direction, 100*time.Millisecond)
return t.Inbox.Receive(ctx, packetTunnelID, direction, 100*time.Millisecond)
}
if frame.Type != fabricproto.FrameData || !t.acceptsStream(frame.StreamID) {
continue
@@ -146,7 +166,7 @@ func (t *FabricSessionPacketTransport) ReceiveGatewayPacketBatch(ctx context.Con
if err != nil {
return nil, err
}
if payload.VPNConnectionID == t.VPNConnectionID && payload.Direction == direction {
if payload.VPNConnectionID == packetTunnelID && payload.Direction == direction {
t.recordReceive(frame.StreamID, fabricSessionTrafficClassName(frame.TrafficClass), len(payload.Packets))
return cleanPacketBatch(payload.Packets), nil
}
@@ -222,7 +242,8 @@ func (t *FabricSessionPacketTransport) Close() error {
if t.closeErr == nil {
t.closeErr = err
}
} else if err == nil {
} else {
t.markServiceStreamClosed(streamID)
t.recordCloseStream()
}
}
@@ -334,7 +355,13 @@ func (t *FabricSessionPacketTransport) streamIDsForTrafficClass(trafficClass str
if ids := t.StreamIDsByTrafficClass[normalizeFabricTrafficClass(trafficClass)]; len(ids) > 0 {
return ids
}
if normalizeFabricTrafficClass(trafficClass) == FabricTrafficClassReliable {
switch normalizeFabricTrafficClass(trafficClass) {
case FabricTrafficClassDNS:
if ids := t.StreamIDsByTrafficClass[FabricTrafficClassReliable]; len(ids) > 0 {
return ids
}
return t.StreamIDsByTrafficClass[FabricTrafficClassBulk]
case FabricTrafficClassReliable:
return t.StreamIDsByTrafficClass[FabricTrafficClassBulk]
}
return nil
@@ -444,6 +471,7 @@ func (t *FabricSessionPacketTransport) Snapshot() map[string]any {
if t == nil {
return nil
}
t.normalizeServiceTunnel()
t.statsMu.Lock()
sendFramesByClass := copyStringUint64Map(t.sendFramesByClass)
sendPacketsByClass := copyStringUint64Map(t.sendPacketsByClass)
@@ -471,9 +499,23 @@ func (t *FabricSessionPacketTransport) Snapshot() map[string]any {
receivePacketsByStream[fmt.Sprintf("%d", streamID)] = count
}
t.statsMu.Unlock()
t.routeMu.Lock()
routeLeaseID := firstNonEmptyTunnelString(t.routeLeaseID, t.ServiceTunnel.RouteLeaseID)
routeGeneration := firstNonEmptyTunnelString(t.routeGeneration, t.ServiceTunnel.RouteGeneration)
routeTransitionCount := t.routeTransitionCount
routeUpdatedAt := t.routeUpdatedAt
t.routeMu.Unlock()
streamIDsByClass := copyStreamIDsByTrafficClass(t.StreamIDsByTrafficClass)
return map[string]any{
out := map[string]any{
"schema_version": "rap.vpn_fabric_session_packet_transport.v1",
"tunnel_id": t.packetTunnelID(),
"pool_id": t.PoolID,
"service_id": t.ServiceID,
"route_lease_id": routeLeaseID,
"route_generation": routeGeneration,
"route_transition_count": routeTransitionCount,
"vpn_connection_id_alias": t.VPNConnectionID,
"service_tunnel": t.ServiceTunnel.Snapshot(),
"stream_id": t.StreamID,
"stream_ids_by_class": streamIDsByClass,
"stream_class_count": len(streamIDsByClass),
@@ -495,6 +537,92 @@ func (t *FabricSessionPacketTransport) Snapshot() map[string]any {
"receive_frames_by_stream_id": receiveFramesByStream,
"receive_packets_by_stream_id": receivePacketsByStream,
}
if t.ServiceStreams != nil {
out["service_stream_registry"] = t.ServiceStreams.Snapshot()
out["service_streams"] = serviceStreamsSnapshotItems(t.ServiceStreams.StreamsForTunnel(t.packetTunnelID()))
}
if !routeUpdatedAt.IsZero() {
out["route_updated_at"] = routeUpdatedAt.UTC().Format(time.RFC3339Nano)
}
return out
}
func (t *FabricSessionPacketTransport) UpdateServiceTunnel(tunnel FabricServiceTunnel) (bool, error) {
if t == nil {
return false, mesh.ErrForwardRuntimeUnavailable
}
currentID := t.packetTunnelID()
tunnel = NormalizeServiceTunnel(tunnel, currentID)
if currentID != "" && tunnel.TunnelID != "" && tunnel.TunnelID != currentID {
return false, fmt.Errorf("service tunnel id changed from %q to %q", currentID, tunnel.TunnelID)
}
t.routeMu.Lock()
defer t.routeMu.Unlock()
previousLeaseID := firstNonEmptyTunnelString(t.routeLeaseID, t.ServiceTunnel.RouteLeaseID)
previousGeneration := firstNonEmptyTunnelString(t.routeGeneration, t.ServiceTunnel.RouteGeneration)
changed := previousLeaseID != tunnel.RouteLeaseID || previousGeneration != tunnel.RouteGeneration
t.ServiceTunnel = tunnel
t.TunnelID = firstNonEmptyTunnelString(t.TunnelID, tunnel.TunnelID)
t.PoolID = firstNonEmptyTunnelString(tunnel.PoolID, t.PoolID)
t.ServiceID = firstNonEmptyTunnelString(tunnel.ServiceID, t.ServiceID)
t.routeLeaseID = tunnel.RouteLeaseID
t.routeGeneration = tunnel.RouteGeneration
if changed {
t.routeTransitionCount++
t.routeUpdatedAt = time.Now().UTC()
}
return changed, nil
}
func (t *FabricSessionPacketTransport) normalizeServiceTunnel() {
if t == nil {
return
}
fallbackID := firstNonEmptyTunnelString(t.ServiceTunnel.TunnelID, t.TunnelID, t.VPNConnectionID)
t.ServiceTunnel = NormalizeServiceTunnel(t.ServiceTunnel, fallbackID)
t.TunnelID = firstNonEmptyTunnelString(t.TunnelID, t.ServiceTunnel.TunnelID)
t.PoolID = firstNonEmptyTunnelString(t.PoolID, t.ServiceTunnel.PoolID)
t.ServiceID = firstNonEmptyTunnelString(t.ServiceID, t.ServiceTunnel.ServiceID)
t.routeMu.Lock()
if t.routeLeaseID == "" {
t.routeLeaseID = t.ServiceTunnel.RouteLeaseID
}
if t.routeGeneration == "" {
t.routeGeneration = t.ServiceTunnel.RouteGeneration
}
t.routeMu.Unlock()
}
func (t *FabricSessionPacketTransport) packetTunnelID() string {
if t == nil {
return ""
}
return firstNonEmptyTunnelString(t.ServiceTunnel.TunnelID, t.TunnelID, t.VPNConnectionID)
}
func (t *FabricSessionPacketTransport) registerServiceStream(streamID uint64, trafficClass string, direction string) {
if t == nil || t.ServiceStreams == nil || streamID == 0 {
return
}
t.normalizeServiceTunnel()
t.ServiceStreams.Register(FabricServiceStream{
TunnelID: t.packetTunnelID(),
ServiceID: t.ServiceID,
StreamID: streamID,
TrafficClass: trafficClass,
Direction: direction,
ServiceTunnel: t.ServiceTunnel,
Metadata: map[string]string{
"adapter": "vpn",
},
})
}
func (t *FabricSessionPacketTransport) markServiceStreamClosed(streamID uint64) {
if t == nil || t.ServiceStreams == nil || streamID == 0 {
return
}
t.ServiceStreams.MarkClosed(t.packetTunnelID(), streamID)
}
func (t *FabricSessionPacketTransport) recordCloseStream() {
@@ -516,12 +644,9 @@ func (t *FabricSessionPacketTransport) recordCloseError() {
}
func fabricSessionTrafficClassForPackets(fallback string, packets [][]byte) string {
if fallback = normalizeFabricTrafficClass(fallback); fallback != "" && fallback != FabricTrafficClassBulk {
if fallback = normalizeFabricTrafficClass(fallback); fallback != "" {
return fallback
}
if batchHasTCPControlPacket(packets) {
return FabricTrafficClassInteractive
}
return FabricTrafficClassBulk
}
@@ -35,6 +35,9 @@ type FabricPacketTransport struct {
Inbox *FabricPacketInbox
ClusterID string
TunnelID string
PoolID string
ServiceID string
VPNConnectionID string
RouteID string
LocalNodeID string
@@ -46,16 +49,16 @@ type FabricPacketTransport struct {
}
type FabricClientPacketIngress struct {
ForwardTransport mesh.ProductionForwardTransport
Inbox *FabricPacketInbox
Routes func() []mesh.SyntheticRoute
LocalGateway func(vpnConnectionID string) bool
AllowLegacyLocalGatewayFallback bool
FlowScheduler *FabricFlowScheduler
MaxParallelFlowSends int
RecoveryPolicyFingerprint string
AdaptivePolicyFingerprint string
PreventLastRouteWithdrawal bool
ForwardTransport mesh.ProductionForwardTransport
Inbox *FabricPacketInbox
Routes func() []mesh.SyntheticRoute
LocalGateway func(vpnConnectionID string) bool
AllowLocalGatewayBypass bool
FlowScheduler *FabricFlowScheduler
MaxParallelFlowSends int
RecoveryPolicyFingerprint string
AdaptivePolicyFingerprint string
PreventLastRouteWithdrawal bool
ClusterID string
LocalNodeID string
@@ -159,6 +162,7 @@ type FabricServiceChannelAdaptivePolicy struct {
const (
FabricTrafficClassControl = "control"
FabricTrafficClassDNS = "dns"
FabricTrafficClassInteractive = "interactive"
FabricTrafficClassReliable = "reliable"
FabricTrafficClassBulk = "bulk"
@@ -370,6 +374,7 @@ func defaultFabricServiceChannelAdaptivePolicy() FabricServiceChannelAdaptivePol
QueuePressureMaxInFlight: defaultFabricFlowParallelSendWindow * 4,
ClassWindows: map[string]int{
FabricTrafficClassControl: defaultFabricFlowParallelSendWindow,
FabricTrafficClassDNS: defaultFabricFlowParallelSendWindow,
FabricTrafficClassInteractive: defaultFabricFlowParallelSendWindow,
FabricTrafficClassReliable: 6,
FabricTrafficClassBulk: 4,
@@ -399,6 +404,7 @@ func normalizeFabricServiceChannelAdaptivePolicy(policy FabricServiceChannelAdap
}
defaults := map[string]int{
FabricTrafficClassControl: policy.MaxParallelWindow,
FabricTrafficClassDNS: policy.MaxParallelWindow,
FabricTrafficClassInteractive: policy.MaxParallelWindow,
FabricTrafficClassReliable: minPositive(policy.MaxParallelWindow, 6),
FabricTrafficClassBulk: minPositive(policy.MaxParallelWindow, 4),
@@ -466,7 +472,7 @@ func (s *FabricFlowScheduler) scheduleClientPackets(vpnConnectionID string, traf
FlowID: flowID,
Shard: shard,
TrafficClass: trafficClass,
Classifier: "ip_5tuple_or_packet_hash",
Classifier: "opaque_packet_hash",
ServiceMode: "application_protocol_agnostic",
}
grouped[channelID] = batch
@@ -1277,6 +1283,8 @@ func normalizeFabricTrafficClass(value string) string {
switch strings.TrimSpace(strings.ToLower(value)) {
case FabricTrafficClassControl:
return FabricTrafficClassControl
case FabricTrafficClassDNS:
return FabricTrafficClassDNS
case FabricTrafficClassInteractive:
return FabricTrafficClassInteractive
case FabricTrafficClassReliable:
@@ -1294,16 +1302,18 @@ func fabricTrafficClassPriority(value string) int {
switch normalizeFabricTrafficClass(value) {
case FabricTrafficClassControl:
return 0
case FabricTrafficClassInteractive:
case FabricTrafficClassDNS:
return 1
case FabricTrafficClassReliable:
case FabricTrafficClassInteractive:
return 2
case FabricTrafficClassReliable:
return 3
case FabricTrafficClassBulk:
return 3
case FabricTrafficClassDroppable:
return 4
case FabricTrafficClassDroppable:
return 5
default:
return 3
return 4
}
}
@@ -1932,7 +1942,7 @@ func (i *FabricClientPacketIngress) ReceiveClientPacketBatch(ctx context.Context
}
func (i *FabricClientPacketIngress) localGatewayReady(vpnConnectionID string) bool {
if i == nil || !i.AllowLegacyLocalGatewayFallback || i.inbox() == nil || vpnConnectionID == "" {
if i == nil || !i.AllowLocalGatewayBypass || i.inbox() == nil || vpnConnectionID == "" {
return false
}
localGateway := i.localGateway()
@@ -2224,9 +2234,6 @@ func (i *FabricPacketInbox) Receive(ctx context.Context, vpnConnectionID, direct
func (i *FabricPacketInbox) enqueue(payload mesh.VPNPacketBatchPayload) error {
queue := i.queue(payload.VPNConnectionID, payload.Direction)
target := queue.normal
if payload.Direction == FabricDirectionGatewayToClient && batchHasTCPControlPacket(payload.Packets) {
target = queue.priority
}
select {
case target <- payload:
default:
@@ -2256,15 +2263,6 @@ func (i *FabricPacketInbox) queue(vpnConnectionID, direction string) *fabricPack
return queue
}
func batchHasTCPControlPacket(packets [][]byte) bool {
for _, packet := range packets {
if isTCPControlPacket(packet) {
return true
}
}
return false
}
func maxInt(a, b int) int {
if a > b {
return a
@@ -2976,7 +2974,7 @@ func classifyPacketFlow(packet []byte, shardCount int) (string, int) {
if shardCount <= 0 {
shardCount = defaultFabricFlowShardCount
}
key := packetFlowKey(packet)
key := packetHashFlowKey("opaque", packet)
hash := fnv.New32a()
_, _ = hash.Write([]byte(key))
shard := int(hash.Sum32() % uint32(shardCount))
@@ -234,6 +234,7 @@ func TestFabricSessionPacketTransportSendsDataFrame(t *testing.T) {
}
func TestFabricSessionPacketTransportShardsStreamsByTrafficClass(t *testing.T) {
t.Skip("retired: base VPN fabric channel is opaque and no longer classifies TCP control packets")
sender := &captureFabricSessionSender{}
transport := &FabricSessionPacketTransport{
Sender: sender,
@@ -284,7 +285,245 @@ func TestFabricSessionPacketTransportShardsStreamsByTrafficClass(t *testing.T) {
}
}
func TestFabricSessionPacketTransportUsesTunnelIDAsServiceIdentity(t *testing.T) {
sender := &captureFabricSessionSender{}
transport := &FabricSessionPacketTransport{
Sender: sender,
StreamID: 700,
TunnelID: "fabric-tunnel-1",
VPNConnectionID: "legacy-vpn-1",
SendDirection: FabricDirectionClientToGateway,
ServiceTunnel: FabricServiceTunnel{
TunnelID: "fabric-tunnel-1",
PoolID: "ipv4-egress",
ServiceID: "svc-vpn-1",
ServiceKind: "ipv4-tunnel",
ServiceClass: "vpn_packets",
},
}
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{[]byte("packet")}); err != nil {
t.Fatalf("send packet: %v", err)
}
if len(sender.frames) != 1 {
t.Fatalf("sent frames = %d, want 1", len(sender.frames))
}
payload, err := DecodeFabricVPNPacketDataFrame(sender.frames[0])
if err != nil {
t.Fatalf("decode payload: %v", err)
}
if payload.VPNConnectionID != "fabric-tunnel-1" {
t.Fatalf("payload tunnel identity = %q, want fabric-tunnel-1", payload.VPNConnectionID)
}
if payload.TunnelID != "fabric-tunnel-1" || payload.PoolID != "" || payload.ServiceID != "" {
t.Fatalf("hot data frame should carry only tunnel identity, got %+v", payload)
}
snapshot := transport.Snapshot()
if snapshot["tunnel_id"] != "fabric-tunnel-1" || snapshot["vpn_connection_id_alias"] != "legacy-vpn-1" {
t.Fatalf("snapshot should expose tunnel id and legacy alias: %+v", snapshot)
}
serviceTunnel, ok := snapshot["service_tunnel"].(map[string]any)
if !ok || serviceTunnel["transport_owner"] != DefaultFabricTransportOwner || serviceTunnel["route_visibility"] != DefaultFabricRouteVisibility {
t.Fatalf("service tunnel snapshot missing fabric ownership: %+v", snapshot["service_tunnel"])
}
}
func TestFabricSessionPacketTransportUsesOpaqueBulkChannelForPacketContents(t *testing.T) {
sender := &captureFabricSessionSender{}
transport := &FabricSessionPacketTransport{
Sender: sender,
TunnelID: "fabric-tunnel-1",
VPNConnectionID: "legacy-vpn-1",
SendDirection: FabricDirectionClientToGateway,
StreamIDsByTrafficClass: map[string][]uint64{
FabricTrafficClassReliable: []uint64{701},
FabricTrafficClassInteractive: []uint64{801},
FabricTrafficClassBulk: []uint64{901},
},
}
dns := testDNSIPv4PacketForFabricRuntime()
tcpControl := testIPv4TCPPacket([4]byte{10, 77, 0, 2}, [4]byte{192, 168, 200, 95}, 51001, 3389)
tcpControl[33] = 0x02
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{dns, tcpControl}); err != nil {
t.Fatalf("send opaque packets: %v", err)
}
if len(sender.frames) != 1 {
t.Fatalf("frames = %d, want one opaque bulk frame", len(sender.frames))
}
if sender.frames[0].TrafficClass != fabricproto.TrafficClassBulk || sender.frames[0].StreamID != 901 {
t.Fatalf("opaque packets should use bulk stream without protocol analysis: %+v", sender.frames[0])
}
payload, err := DecodeFabricVPNPacketDataFrame(sender.frames[0])
if err != nil {
t.Fatalf("decode opaque frame: %v", err)
}
if len(payload.Packets) != 2 {
t.Fatalf("opaque frame packets = %d, want 2", len(payload.Packets))
}
}
func TestFabricSessionPacketPeerRegistryKeepsServiceTunnelFromHello(t *testing.T) {
registry := NewFabricSessionPacketPeerRegistry()
sender := &recordingFrameSender{}
frame, err := NewFabricVPNSessionHelloFrame(FabricVPNPacketFrameInput{
StreamID: 711,
VPNConnectionID: "fabric-tunnel-1",
Direction: FabricDirectionClientToGateway,
TrafficClass: FabricTrafficClassInteractive,
ServiceTunnel: FabricServiceTunnel{
TunnelID: "fabric-tunnel-1",
PoolID: "home-ipv4",
ServiceID: "svc-vpn-1",
ServiceKind: "ipv4-tunnel",
ServiceClass: "vpn_packets",
ServiceRole: "ipv4-egress",
RouteLeaseID: "lease-1",
RouteGeneration: "route-gen-1",
},
})
if err != nil {
t.Fatalf("hello frame: %v", err)
}
handled, err := registry.RegisterFrame(context.Background(), sender, frame)
if err != nil || !handled {
t.Fatalf("register hello handled=%v err=%v", handled, err)
}
snapshot := registry.Snapshot()
peers := snapshot["peers"].([]map[string]any)
if len(peers) != 1 {
t.Fatalf("peers = %+v", peers)
}
serviceTunnel := peers[0]["service_tunnel"].(map[string]any)
if serviceTunnel["pool_id"] != "home-ipv4" ||
serviceTunnel["service_id"] != "svc-vpn-1" ||
serviceTunnel["route_visibility"] != DefaultFabricRouteVisibility ||
serviceTunnel["route_lease_id"] != "lease-1" ||
serviceTunnel["route_generation"] != "route-gen-1" {
t.Fatalf("peer service tunnel not preserved: %+v", serviceTunnel)
}
}
func TestFabricSessionPacketTransportRegistersServiceStreams(t *testing.T) {
t.Skip("retired: base VPN fabric channel is opaque and no longer derives service stream class from packet contents")
sender := &captureFabricSessionSender{}
registry := NewFabricServiceStreamRegistry()
transport := &FabricSessionPacketTransport{
Sender: sender,
TunnelID: "fabric-tunnel-1",
VPNConnectionID: "legacy-vpn-1",
ServiceID: "svc-vpn-1",
SendDirection: FabricDirectionClientToGateway,
ServiceStreams: registry,
StreamIDsByTrafficClass: map[string][]uint64{
FabricTrafficClassInteractive: []uint64{801},
FabricTrafficClassBulk: []uint64{901},
},
ServiceTunnel: FabricServiceTunnel{
TunnelID: "fabric-tunnel-1",
PoolID: "ipv4-egress",
ServiceID: "svc-vpn-1",
ServiceKind: "ipv4-tunnel",
ServiceClass: "vpn_packets",
},
}
packet := testIPv4TCPPacket([4]byte{10, 77, 0, 2}, [4]byte{192, 168, 200, 95}, 51001, 3389)
packet[33] = 0x02
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{packet}); err != nil {
t.Fatalf("send packet: %v", err)
}
streams := registry.StreamsForTunnel("fabric-tunnel-1")
if len(streams) != 1 {
t.Fatalf("registered streams = %+v, want one", streams)
}
if streams[0].StreamID != 801 ||
streams[0].TrafficClass != FabricTrafficClassInteractive ||
streams[0].ServiceID != "svc-vpn-1" ||
streams[0].State != FabricServiceStreamStateOpen {
t.Fatalf("unexpected service stream: %+v", streams[0])
}
snapshot := transport.Snapshot()
serviceStreams, ok := snapshot["service_streams"].([]map[string]any)
if !ok || len(serviceStreams) != 1 || serviceStreams[0]["stream_id"] != uint64(801) {
t.Fatalf("transport snapshot missing service streams: %+v", snapshot["service_streams"])
}
if err := transport.Close(); err != nil {
t.Fatalf("close transport: %v", err)
}
streams = registry.StreamsForTunnel("fabric-tunnel-1")
if len(streams) != 1 || streams[0].State != FabricServiceStreamStateClosed {
t.Fatalf("service stream not closed with transport: %+v", streams)
}
}
func TestFabricSessionPacketTransportUpdatesRouteLeaseWithoutChangingTunnel(t *testing.T) {
transport := &FabricSessionPacketTransport{
TunnelID: "fabric-tunnel-1",
ServiceTunnel: FabricServiceTunnel{
TunnelID: "fabric-tunnel-1",
PoolID: "home-ipv4",
ServiceID: "svc-vpn-1",
RouteLeaseID: "lease-1",
RouteGeneration: "route-gen-1",
},
}
changed, err := transport.UpdateServiceTunnel(FabricServiceTunnel{
TunnelID: "fabric-tunnel-1",
PoolID: "home-ipv4",
ServiceID: "svc-vpn-1",
RouteLeaseID: "lease-2",
RouteGeneration: "route-gen-2",
})
if err != nil || !changed {
t.Fatalf("update service tunnel changed=%v err=%v", changed, err)
}
snapshot := transport.Snapshot()
if snapshot["tunnel_id"] != "fabric-tunnel-1" ||
snapshot["route_lease_id"] != "lease-2" ||
snapshot["route_generation"] != "route-gen-2" ||
snapshot["route_transition_count"] != uint64(1) {
t.Fatalf("route lease update not reflected without tunnel change: %+v", snapshot)
}
if _, err := transport.UpdateServiceTunnel(FabricServiceTunnel{TunnelID: "other-tunnel"}); err == nil {
t.Fatal("expected changing tunnel id to be rejected")
}
}
func TestFabricSessionPacketTransportRoutesDNSOnReliableClass(t *testing.T) {
t.Skip("retired: base VPN fabric channel is opaque and no longer detects DNS packets")
sender := &captureFabricSessionSender{}
registry := NewFabricServiceStreamRegistry()
transport := &FabricSessionPacketTransport{
Sender: sender,
TunnelID: "fabric-tunnel-1",
VPNConnectionID: "legacy-vpn-1",
SendDirection: FabricDirectionClientToGateway,
ServiceStreams: registry,
StreamIDsByTrafficClass: map[string][]uint64{
FabricTrafficClassReliable: []uint64{701},
FabricTrafficClassBulk: []uint64{901},
},
}
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{testDNSIPv4PacketForFabricRuntime()}); err != nil {
t.Fatalf("send dns packet: %v", err)
}
if len(sender.frames) != 1 {
t.Fatalf("frames = %d, want 1", len(sender.frames))
}
if sender.frames[0].StreamID != 701 || sender.frames[0].TrafficClass != fabricproto.TrafficClassReliable {
t.Fatalf("dns packet should use reliable stream: %+v", sender.frames[0])
}
streams := registry.StreamsForTunnel("fabric-tunnel-1")
if len(streams) != 1 || streams[0].TrafficClass != FabricTrafficClassDNS {
t.Fatalf("dns service stream not tracked separately: %+v", streams)
}
}
func TestFabricSessionPacketTransportSplitsMixedBatchByStream(t *testing.T) {
t.Skip("retired: base VPN fabric channel is opaque and no longer splits batches by packet protocol")
sender := &captureFabricSessionSender{}
transport := &FabricSessionPacketTransport{
Sender: sender,
@@ -470,15 +709,91 @@ func TestFabricSessionPacketPeerTransportSendsReplyToLatestRegisteredPeer(t *tes
}
}
func TestFabricSessionPacketPeerTransportForgetsClosedPeerAndRebinds(t *testing.T) {
inbox := NewFabricPacketInbox(4)
registry := NewFabricSessionPacketPeerRegistry()
firstSender := &recordingFrameSender{err: errors.New("closed")}
registerFabricSessionPeerForTest(t, registry, firstSender, "vpn-1", 7)
transport := &FabricSessionPacketPeerTransport{
Registry: registry,
Inbox: inbox,
VPNConnectionID: "vpn-1",
PeerWaitTimeout: 250 * time.Millisecond,
}
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{[]byte("reply-1")}); err == nil {
t.Fatal("send through closed peer succeeded")
}
if ready := registry.TransportFor("vpn-1", inbox); ready != nil {
t.Fatal("closed peer remained registered")
}
secondSender := &recordingFrameSender{}
go func() {
time.Sleep(25 * time.Millisecond)
registerFabricSessionPeerForTest(t, registry, secondSender, "vpn-1", 11)
}()
if err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{[]byte("reply-2")}); err != nil {
t.Fatalf("send after peer rebind: %v", err)
}
if len(secondSender.frames) != 1 {
t.Fatalf("second sender frames = %d, want 1", len(secondSender.frames))
}
payload, err := DecodeFabricVPNPacketDataFrame(secondSender.frames[0])
if err != nil {
t.Fatalf("decode rebound reply: %v", err)
}
if string(payload.Packets[0]) != "reply-2" {
t.Fatalf("rebound payload = %+v", payload)
}
}
func TestFabricSessionPacketPeerTransportFailsFastWithoutPeer(t *testing.T) {
inbox := NewFabricPacketInbox(4)
registry := NewFabricSessionPacketPeerRegistry()
transport := &FabricSessionPacketPeerTransport{
Registry: registry,
Inbox: inbox,
VPNConnectionID: "vpn-1",
PeerWaitTimeout: 20 * time.Millisecond,
}
startedAt := time.Now()
err := transport.SendGatewayPacketBatch(context.Background(), [][]byte{[]byte("reply")})
if err == nil {
t.Fatal("send without peer succeeded")
}
if elapsed := time.Since(startedAt); elapsed > 250*time.Millisecond {
t.Fatalf("send without peer took %s, want fast failure", elapsed)
}
}
type recordingFrameSender struct {
err error
frames []fabricproto.Frame
}
func (s *recordingFrameSender) SendFrame(_ context.Context, frame fabricproto.Frame) error {
if s.err != nil {
return s.err
}
s.frames = append(s.frames, frame)
return nil
}
func registerFabricSessionPeerForTest(t *testing.T, registry *FabricSessionPacketPeerRegistry, sender FabricSessionFrameWriter, vpnConnectionID string, streamID uint64) {
t.Helper()
frame, err := NewFabricVPNSessionHelloFrame(FabricVPNPacketFrameInput{
StreamID: streamID,
VPNConnectionID: vpnConnectionID,
Direction: FabricDirectionClientToGateway,
})
if err != nil {
t.Fatalf("hello frame: %v", err)
}
handled, err := registry.RegisterFrame(context.Background(), sender, frame)
if err != nil || !handled {
t.Fatalf("register peer handled=%v err=%v", handled, err)
}
}
func TestFabricSessionPacketTransportReceiveReadsPumpFrames(t *testing.T) {
inbox := NewFabricPacketInbox(4)
receiver := memoryFabricSessionReceiver{
@@ -684,7 +999,7 @@ func TestFabricPacketInboxReceivesFabricSessionFrame(t *testing.T) {
}
}
func TestFabricVPNPacketDataFrameInfersInteractiveTCPControl(t *testing.T) {
func TestFabricVPNPacketDataFrameKeepsExplicitBulkForTCPControlContents(t *testing.T) {
packet := testIPv4TCPPacket([4]byte{192, 168, 200, 95}, [4]byte{10, 77, 0, 2}, 3389, 57032)
packet[33] = 0x12
frame, err := NewFabricVPNPacketDataFrame(FabricVPNPacketFrameInput{
@@ -698,12 +1013,13 @@ func TestFabricVPNPacketDataFrameInfersInteractiveTCPControl(t *testing.T) {
if err != nil {
t.Fatalf("new fabric vpn frame: %v", err)
}
if frame.TrafficClass != fabricproto.TrafficClassInteractive {
t.Fatalf("traffic class = %v, want interactive", frame.TrafficClass)
if frame.TrafficClass != fabricproto.TrafficClassBulk {
t.Fatalf("traffic class = %v, want opaque bulk", frame.TrafficClass)
}
}
func TestFabricPacketInboxPrioritizesGatewayTCPControlPackets(t *testing.T) {
t.Skip("retired: base VPN fabric channel preserves arrival order and no longer prioritizes TCP control packets")
inbox := NewFabricPacketInbox(4)
normal := testIPv4TCPPacket([4]byte{185, 16, 148, 89}, [4]byte{10, 77, 0, 2}, 443, 56000)
priority := testIPv4TCPPacket([4]byte{192, 168, 200, 95}, [4]byte{10, 77, 0, 2}, 3389, 57032)
@@ -726,6 +1042,7 @@ func TestFabricPacketInboxPrioritizesGatewayTCPControlPackets(t *testing.T) {
}
func TestFabricPacketInboxWaitsBrieflyForGatewayTCPControlPackets(t *testing.T) {
t.Skip("retired: base VPN fabric channel preserves arrival order and no longer waits for TCP control packets")
inbox := NewFabricPacketInbox(4)
normal := testIPv4TCPPacket([4]byte{185, 16, 148, 89}, [4]byte{10, 77, 0, 2}, 443, 56000)
priority := testIPv4TCPPacket([4]byte{192, 168, 200, 95}, [4]byte{10, 77, 0, 2}, 3389, 57032)
@@ -774,6 +1091,7 @@ func TestLocalPacketTransportUsesFabricInboxDirections(t *testing.T) {
}
func TestFabricFlowSchedulerKeepsReverseFiveTupleTogether(t *testing.T) {
t.Skip("retired: base VPN fabric channel uses opaque packet sharding instead of inspecting 5-tuples")
scheduler := NewFabricFlowScheduler(8, 8)
forward := testIPv4TCPPacket([4]byte{10, 77, 0, 2}, [4]byte{192, 168, 200, 95}, 51000, 3389)
reverse := testIPv4TCPPacket([4]byte{192, 168, 200, 95}, [4]byte{10, 77, 0, 2}, 3389, 51000)
@@ -826,6 +1144,18 @@ func TestFabricFlowSchedulerPrioritizesExplicitTrafficClass(t *testing.T) {
}
}
func TestFabricFlowSchedulerUsesOpaquePacketHashClassifier(t *testing.T) {
scheduler := NewFabricFlowScheduler(8, 0)
packet := testIPv4TCPPacket([4]byte{10, 77, 0, 2}, [4]byte{192, 168, 200, 95}, 51000, 3389)
batches := scheduler.ScheduleClientPacketsForConnection("vpn-1", [][]byte{packet})
if len(batches) != 1 {
t.Fatalf("batches = %d, want 1", len(batches))
}
if batches[0].Classifier != "opaque_packet_hash" || !strings.HasPrefix(batches[0].FlowID, "opaque:") {
t.Fatalf("scheduler should not expose protocol-derived flow keys: %+v", batches[0])
}
}
func TestFabricFlowSchedulerDropsWhenChannelQueueIsFull(t *testing.T) {
scheduler := NewFabricFlowScheduler(1, 1)
packetA := testIPv4TCPPacket([4]byte{10, 77, 0, 2}, [4]byte{192, 168, 200, 95}, 51000, 3389)
@@ -1032,7 +1362,7 @@ func TestFabricClientPacketIngressUsesLeasePreferredRouteBeforeConfigOrder(t *te
}
}
func TestFabricClientPacketIngressTriesAlternateRouteBeforeBackendFallback(t *testing.T) {
func TestFabricClientPacketIngressTriesAlternateRouteBeforeCompatFallback(t *testing.T) {
transport := &failoverProductionTransport{failNextHop: "relay-bad"}
ingress := &FabricClientPacketIngress{
ForwardTransport: transport,
@@ -2617,10 +2947,10 @@ func TestFabricClientPacketIngressBoundedLoadReportsPerChannelDrops(t *testing.T
func TestFabricClientPacketIngressUsesLocalGatewayShortcutWithoutRoute(t *testing.T) {
inbox := NewFabricPacketInbox(4)
ingress := &FabricClientPacketIngress{
Inbox: inbox,
ClusterID: "cluster-1",
LocalNodeID: "entry-1",
AllowLegacyLocalGatewayFallback: true,
Inbox: inbox,
ClusterID: "cluster-1",
LocalNodeID: "entry-1",
AllowLocalGatewayBypass: true,
LocalGateway: func(vpnConnectionID string) bool {
return vpnConnectionID == "vpn-1"
},
@@ -2642,10 +2972,10 @@ func TestFabricClientPacketIngressUsesLocalGatewayShortcutWithoutRoute(t *testin
func TestFabricClientPacketIngressReceivesLocalGatewayReplyWithoutRoute(t *testing.T) {
inbox := NewFabricPacketInbox(4)
ingress := &FabricClientPacketIngress{
Inbox: inbox,
ClusterID: "cluster-1",
LocalNodeID: "entry-1",
AllowLegacyLocalGatewayFallback: true,
Inbox: inbox,
ClusterID: "cluster-1",
LocalNodeID: "entry-1",
AllowLocalGatewayBypass: true,
LocalGateway: func(vpnConnectionID string) bool {
return vpnConnectionID == "vpn-1"
},
@@ -2705,6 +3035,24 @@ func packetSourcePort(packet []byte) uint16 {
return uint16(packet[20])<<8 | uint16(packet[21])
}
func testDNSIPv4PacketForFabricRuntime() []byte {
packet := make([]byte, 28)
packet[0] = 0x45
packet[2] = 0
packet[3] = byte(len(packet))
packet[8] = 64
packet[9] = 17
copy(packet[12:16], []byte{10, 77, 0, 2})
copy(packet[16:20], []byte{1, 1, 1, 1})
packet[20] = 0xc0
packet[21] = 0x00
packet[22] = 0x00
packet[23] = 0x35
packet[24] = 0
packet[25] = 8
return packet
}
func testFlowChannelID(vpnConnectionID string, packet []byte, shardCount int) string {
return fabricFlowChannelID(vpnConnectionID, packetShard(packet, shardCount))
}
@@ -18,6 +18,7 @@ type Gateway struct {
Transport PacketTransport
ClusterID string
VPNConnectionID string
ServiceTunnel FabricServiceTunnel
InterfaceName string
AddressCIDR string
RouteCIDR string
@@ -73,20 +74,6 @@ type packetTransportCloser interface {
Close() error
}
type BackendPacketTransport struct {
API *client.Client
ClusterID string
VPNConnectionID string
}
func (t BackendPacketTransport) SendGatewayPacketBatch(ctx context.Context, packets [][]byte) error {
return t.API.SendVPNGatewayPacketBatch(ctx, t.ClusterID, t.VPNConnectionID, packets)
}
func (t BackendPacketTransport) ReceiveGatewayPacketBatch(ctx context.Context, timeout time.Duration) ([][]byte, error) {
return t.API.ReceiveVPNGatewayPacketBatch(ctx, t.ClusterID, t.VPNConnectionID, timeout)
}
func (g *Gateway) EnsureStarted(ctx context.Context) error {
g.mu.Lock()
if g.running {
@@ -120,7 +107,7 @@ func (g *Gateway) EnsureStarted(ctx context.Context) error {
go func() {
if err := g.run(runCtx, tun); err != nil && runCtx.Err() == nil {
log.Printf("vpn gateway runtime stopped: vpn_connection_id=%s error=%v", g.VPNConnectionID, err)
log.Printf("vpn gateway runtime stopped: tunnel_id=%s error=%v", g.tunnelID(), err)
g.setStopped(err)
return
}
@@ -152,7 +139,8 @@ func (g *Gateway) Status() (bool, string) {
func (g *Gateway) IsReadyForConnection(vpnConnectionID string) bool {
g.mu.Lock()
defer g.mu.Unlock()
return g.running && g.VPNConnectionID == vpnConnectionID && vpnConnectionID != ""
tunnelID := g.tunnelIDLocked()
return g.running && (g.VPNConnectionID == vpnConnectionID || tunnelID == vpnConnectionID) && vpnConnectionID != ""
}
func (g *Gateway) Snapshot() map[string]any {
@@ -169,8 +157,14 @@ func (g *Gateway) Snapshot() map[string]any {
out := map[string]any{
"running": running,
"service_role": "ipv4-egress",
"service_class": "vpn_packets",
"tunnel_id": g.ServiceTunnel.TunnelID,
"pool_id": g.ServiceTunnel.PoolID,
"service_id": g.ServiceTunnel.ServiceID,
"local_service_id": g.ServiceTunnel.LocalServiceID,
"remote_service_id": g.ServiceTunnel.RemoteServiceID,
"service_kind": g.ServiceTunnel.ServiceKind,
"service_role": firstNonEmptyTunnelString(g.ServiceTunnel.ServiceRole, DefaultFabricTunnelRole),
"service_class": firstNonEmptyTunnelString(g.ServiceTunnel.ServiceClass, DefaultFabricTunnelClass),
"adapter_contract": "fabric_channel_to_ipv4_nat",
"transport": g.transportName(),
"poll_timeout_ms": g.PollTimeout.Milliseconds(),
@@ -196,6 +190,7 @@ func (g *Gateway) Snapshot() map[string]any {
if !lastRuntimeActivityAt.IsZero() {
out["last_runtime_activity_at"] = lastRuntimeActivityAt.UTC().Format(time.RFC3339Nano)
}
out["service_tunnel"] = g.ServiceTunnel.Snapshot()
if platform := gatewayPlatformSnapshot(g.InterfaceName, g.RouteCIDR); len(platform) > 0 {
out["platform"] = platform
}
@@ -216,9 +211,7 @@ func (g *Gateway) transportName() string {
case *LocalPacketTransport:
return "local_fabric_inbox"
case *AdaptivePacketTransport:
return "adaptive_fabric_backend"
case BackendPacketTransport:
return "backend_http_packet_relay"
return "adaptive_fabric"
default:
if g.Transport == nil {
return "none"
@@ -237,10 +230,14 @@ func (g *Gateway) setStopped(err error) {
func (g *Gateway) normalize() error {
if g.Transport == nil {
return fmt.Errorf("fabric packet transport is required; backend packet relay fallback is disabled")
return fmt.Errorf("fabric packet transport is required")
}
g.ServiceTunnel = NormalizeServiceTunnel(g.ServiceTunnel, g.VPNConnectionID)
if g.VPNConnectionID == "" {
g.VPNConnectionID = g.ServiceTunnel.TunnelID
}
if g.ClusterID == "" || g.VPNConnectionID == "" {
return fmt.Errorf("cluster id and vpn connection id are required")
return fmt.Errorf("cluster id and tunnel id are required")
}
if g.InterfaceName == "" {
g.InterfaceName = "rapvpn0"
@@ -257,6 +254,19 @@ func (g *Gateway) normalize() error {
return nil
}
func (g *Gateway) tunnelIDLocked() string {
return firstNonEmptyTunnelString(g.ServiceTunnel.TunnelID, g.VPNConnectionID)
}
func (g *Gateway) tunnelID() string {
if g == nil {
return ""
}
g.mu.Lock()
defer g.mu.Unlock()
return g.tunnelIDLocked()
}
func (g *Gateway) run(ctx context.Context, tun readWriteCloser) error {
defer tun.Close()
if closer, ok := g.Transport.(packetTransportCloser); ok {
@@ -279,11 +289,10 @@ func (g *Gateway) run(ctx context.Context, tun readWriteCloser) error {
}
func (g *Gateway) copyGatewayToClient(ctx context.Context, tun io.Reader) error {
priorityPackets := make(chan []byte, 1024)
packets := make(chan []byte, 32768)
errCh := make(chan error, 1)
go func() {
errCh <- g.uploadGatewayPackets(ctx, priorityPackets, packets)
errCh <- g.uploadGatewayPackets(ctx, nil, packets)
}()
buffer := make([]byte, 65535)
@@ -307,25 +316,16 @@ func (g *Gateway) copyGatewayToClient(ctx context.Context, tun io.Reader) error
packet := append([]byte(nil), buffer[:n]...)
normalizeIPv4PacketChecksums(packet)
g.recordTunRead(packet)
if isTCPControlPacket(packet) {
select {
case priorityPackets <- packet:
default:
g.uploadQueueDrops.Add(1)
log.Printf("vpn gateway priority packet upload queue full; dropping packet: vpn_connection_id=%s", g.VPNConnectionID)
}
continue
}
select {
case packets <- packet:
default:
g.uploadQueueDrops.Add(1)
log.Printf("vpn gateway packet upload queue full; dropping packet: vpn_connection_id=%s", g.VPNConnectionID)
log.Printf("vpn gateway packet upload queue full; dropping packet: tunnel_id=%s", g.tunnelID())
}
}
}
func (g *Gateway) uploadGatewayPackets(ctx context.Context, priorityPackets <-chan []byte, packets <-chan []byte) error {
func (g *Gateway) uploadGatewayPackets(ctx context.Context, _ <-chan []byte, packets <-chan []byte) error {
batch := make([][]byte, 0, vpnGatewayBatchMaxPackets)
batchBytes := 0
timer := time.NewTimer(time.Hour)
@@ -341,7 +341,7 @@ func (g *Gateway) uploadGatewayPackets(ctx context.Context, priorityPackets <-ch
byteCount := packetBytesTotal(batch)
if err := g.Transport.SendGatewayPacketBatch(ctx, batch); err != nil {
g.uploadErrors.Add(1)
log.Printf("vpn gateway packet batch upload failed: vpn_connection_id=%s packets=%d error=%v", g.VPNConnectionID, len(batch), err)
log.Printf("vpn gateway packet batch upload failed: tunnel_id=%s packets=%d error=%v", g.tunnelID(), len(batch), err)
} else {
g.recordGatewayToClientBatch(packetCount, byteCount, batch[0])
}
@@ -366,50 +366,6 @@ func (g *Gateway) uploadGatewayPackets(ctx context.Context, priorityPackets <-ch
batchBytes += packetFrameSize
return true
}
flushPriority := func(packet []byte) {
pendingBatch := batch
pendingBatchBytes := batchBytes
batch = make([][]byte, 0, vpnGatewayBatchMaxPackets)
batchBytes = 0
if !addPacket(packet) {
batch = pendingBatch
batchBytes = pendingBatchBytes
return
}
deadline := time.Now().Add(vpnGatewayPriorityBatchWait)
for len(batch) < vpnGatewayBatchMaxPackets && batchBytes < vpnGatewayBatchMaxBytes {
wait := time.Until(deadline)
if wait <= 0 {
break
}
timer := time.NewTimer(wait)
select {
case next := <-priorityPackets:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
if !addPacket(next) {
flush()
_ = addPacket(next)
}
case <-timer.C:
flush()
return
}
}
flush()
if len(pendingBatch) > 0 {
batch = pendingBatch
batchBytes = pendingBatchBytes
if !timerActive {
timer.Reset(vpnGatewayBatchFlushTimeout)
timerActive = true
}
}
}
for {
if len(batch) == 0 && timerActive {
if !timer.Stop() {
@@ -421,17 +377,9 @@ func (g *Gateway) uploadGatewayPackets(ctx context.Context, priorityPackets <-ch
timerActive = false
}
select {
case packet := <-priorityPackets:
flushPriority(packet)
continue
default:
}
select {
case <-ctx.Done():
flush()
return ctx.Err()
case packet := <-priorityPackets:
flushPriority(packet)
case packet := <-packets:
if !addPacket(packet) {
continue
@@ -451,23 +399,11 @@ func (g *Gateway) uploadGatewayPackets(ctx context.Context, priorityPackets <-ch
}
}
func isTCPControlPacket(packet []byte) bool {
if len(packet) < 20 || packet[0]>>4 != 4 {
return false
}
ihl := int(packet[0]&0x0f) * 4
if ihl < 20 || len(packet) < ihl+20 || packet[9] != 6 {
return false
}
flags := packet[ihl+13]
return flags&0x17 != 0
}
func (g *Gateway) copyClientToGateway(ctx context.Context, tun io.Writer) error {
for {
packets, err := g.Transport.ReceiveGatewayPacketBatch(ctx, g.PollTimeout)
if err != nil {
log.Printf("vpn gateway packet download failed: vpn_connection_id=%s error=%v", g.VPNConnectionID, err)
log.Printf("vpn gateway packet download failed: tunnel_id=%s error=%v", g.tunnelID(), err)
select {
case <-ctx.Done():
return ctx.Err()
@@ -501,8 +437,8 @@ func (g *Gateway) recordClientToGatewayBatch(packetCount int, byteCount int, fir
g.mu.Unlock()
if next <= 5 {
log.Printf(
"vpn gateway client_to_gateway batch received: vpn_connection_id=%s batch=%d packets=%d bytes=%d first=%s",
g.VPNConnectionID,
"vpn gateway client_to_gateway batch received: tunnel_id=%s batch=%d packets=%d bytes=%d first=%s",
g.tunnelID(),
next,
packetCount,
byteCount,
@@ -522,8 +458,8 @@ func (g *Gateway) recordGatewayToClientBatch(packetCount int, byteCount int, fir
g.mu.Unlock()
if next <= 5 {
log.Printf(
"vpn gateway gateway_to_client batch uploaded: vpn_connection_id=%s batch=%d packets=%d bytes=%d first=%s",
g.VPNConnectionID,
"vpn gateway gateway_to_client batch uploaded: tunnel_id=%s batch=%d packets=%d bytes=%d first=%s",
g.tunnelID(),
next,
packetCount,
byteCount,
@@ -536,7 +472,7 @@ func (g *Gateway) recordTunWrite(packet []byte) {
next := g.tunWritePackets.Add(1)
g.tunWriteBytes.Add(uint64(len(packet)))
if next <= 5 {
log.Printf("vpn gateway packet written to tun: vpn_connection_id=%s packet=%d bytes=%d summary=%s", g.VPNConnectionID, next, len(packet), summarizePacket(packet))
log.Printf("vpn gateway packet written to tun: tunnel_id=%s packet=%d bytes=%d summary=%s", g.tunnelID(), next, len(packet), summarizePacket(packet))
}
}
@@ -544,7 +480,7 @@ func (g *Gateway) recordTunRead(packet []byte) {
next := g.tunReadPackets.Add(1)
g.tunReadBytes.Add(uint64(len(packet)))
if next <= 5 {
log.Printf("vpn gateway packet read from tun: vpn_connection_id=%s packet=%d bytes=%d summary=%s", g.VPNConnectionID, next, len(packet), summarizePacket(packet))
log.Printf("vpn gateway packet read from tun: tunnel_id=%s packet=%d bytes=%d summary=%s", g.tunnelID(), next, len(packet), summarizePacket(packet))
}
}
@@ -95,7 +95,7 @@ func TestGatewayRunClosesPacketTransportOnRuntimeError(t *testing.T) {
}
}
func TestGatewayNormalizeRejectsBackendPacketRelayFallback(t *testing.T) {
func TestGatewayNormalizeRequiresFabricPacketTransport(t *testing.T) {
gateway := &Gateway{
API: nil,
ClusterID: "cluster-1",
@@ -106,7 +106,7 @@ func TestGatewayNormalizeRejectsBackendPacketRelayFallback(t *testing.T) {
if err == nil {
t.Fatal("normalize succeeded without a fabric packet transport")
}
if got, want := err.Error(), "fabric packet transport is required; backend packet relay fallback is disabled"; got != want {
if got, want := err.Error(), "fabric packet transport is required"; got != want {
t.Fatalf("normalize error = %q, want %q", got, want)
}
}
@@ -120,6 +120,7 @@ func TestGatewaySnapshotReportsIPv4EgressServiceAdapter(t *testing.T) {
}
func TestGatewayUploadPrioritizesTCPControlPackets(t *testing.T) {
t.Skip("retired: base VPN gateway uploads opaque packet batches without TCP control prioritization")
transport := &recordingGatewayTransport{}
gateway := &Gateway{Transport: transport, VPNConnectionID: "vpn-1"}
priorityPackets := make(chan []byte, 1)
@@ -160,6 +161,7 @@ func TestGatewayUploadPrioritizesTCPControlPackets(t *testing.T) {
}
func TestGatewayUploadPreemptsPendingNormalBatchForTCPControlPackets(t *testing.T) {
t.Skip("retired: base VPN gateway preserves packet batch order instead of preempting by TCP flags")
transport := &recordingGatewayTransport{}
gateway := &Gateway{Transport: transport, VPNConnectionID: "vpn-1"}
priorityPackets := make(chan []byte, 1)
@@ -201,6 +203,7 @@ func TestGatewayUploadPreemptsPendingNormalBatchForTCPControlPackets(t *testing.
}
func TestGatewayUploadMicroBatchesTCPControlPackets(t *testing.T) {
t.Skip("retired: base VPN gateway no longer creates protocol-specific TCP control microbatches")
transport := &recordingGatewayTransport{}
gateway := &Gateway{Transport: transport, VPNConnectionID: "vpn-1"}
priorityPackets := make(chan []byte, 2)
@@ -239,18 +242,3 @@ func TestGatewayUploadMicroBatchesTCPControlPackets(t *testing.T) {
}
}
}
func TestIsTCPControlPacket(t *testing.T) {
packet := testIPv4TCPPacket([4]byte{192, 168, 200, 95}, [4]byte{10, 77, 0, 2}, 3389, 51000)
if isTCPControlPacket(packet) {
t.Fatal("packet without control flags was classified as control")
}
packet[33] = 0x12
if !isTCPControlPacket(packet) {
t.Fatal("tcp syn-ack was not classified as control")
}
packet[9] = 17
if isTCPControlPacket(packet) {
t.Fatal("udp packet was classified as tcp control")
}
}
@@ -0,0 +1,208 @@
package vpnruntime
import (
"fmt"
"sort"
"sync"
"time"
)
const (
FabricServiceStreamRegistrySchemaVersion = "rap.fabric_service_stream_registry.v1"
FabricServiceStreamStateOpen = "open"
FabricServiceStreamStateClosed = "closed"
FabricServiceStreamStateReset = "reset"
)
type FabricServiceStream struct {
TunnelID string `json:"tunnel_id"`
ServiceID string `json:"service_id"`
StreamID uint64 `json:"stream_id"`
TrafficClass string `json:"traffic_class"`
Direction string `json:"direction,omitempty"`
State string `json:"state"`
ServiceTunnel FabricServiceTunnel `json:"service_tunnel"`
OpenedAt time.Time `json:"opened_at"`
UpdatedAt time.Time `json:"updated_at"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type FabricServiceStreamRegistry struct {
mu sync.RWMutex
streams map[string]FabricServiceStream
}
func NewFabricServiceStreamRegistry() *FabricServiceStreamRegistry {
return &FabricServiceStreamRegistry{streams: map[string]FabricServiceStream{}}
}
func (r *FabricServiceStreamRegistry) Register(stream FabricServiceStream) FabricServiceStream {
if r == nil {
return FabricServiceStream{}
}
now := time.Now().UTC()
stream.ServiceTunnel = NormalizeServiceTunnel(stream.ServiceTunnel, stream.TunnelID)
stream.TunnelID = firstNonEmptyTunnelString(stream.TunnelID, stream.ServiceTunnel.TunnelID)
stream.ServiceID = firstNonEmptyTunnelString(stream.ServiceID, stream.ServiceTunnel.ServiceID)
stream.TrafficClass = normalizeFabricTrafficClass(stream.TrafficClass)
if stream.State == "" {
stream.State = FabricServiceStreamStateOpen
}
if stream.OpenedAt.IsZero() {
stream.OpenedAt = now
}
stream.UpdatedAt = now
r.mu.Lock()
defer r.mu.Unlock()
if r.streams == nil {
r.streams = map[string]FabricServiceStream{}
}
if existing, ok := r.streams[serviceStreamKey(stream.TunnelID, stream.StreamID)]; ok {
if !existing.OpenedAt.IsZero() {
stream.OpenedAt = existing.OpenedAt
}
}
r.streams[serviceStreamKey(stream.TunnelID, stream.StreamID)] = stream
return stream
}
func (r *FabricServiceStreamRegistry) MarkClosed(tunnelID string, streamID uint64) {
r.markState(tunnelID, streamID, FabricServiceStreamStateClosed)
}
func (r *FabricServiceStreamRegistry) MarkReset(tunnelID string, streamID uint64) {
r.markState(tunnelID, streamID, FabricServiceStreamStateReset)
}
func (r *FabricServiceStreamRegistry) StreamsForTunnel(tunnelID string) []FabricServiceStream {
if r == nil || tunnelID == "" {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]FabricServiceStream, 0)
for _, stream := range r.streams {
if stream.TunnelID == tunnelID {
out = append(out, cloneFabricServiceStream(stream))
}
}
sort.Slice(out, func(i, j int) bool { return out[i].StreamID < out[j].StreamID })
return out
}
func (r *FabricServiceStreamRegistry) Snapshot() map[string]any {
if r == nil {
return map[string]any{"schema_version": FabricServiceStreamRegistrySchemaVersion, "stream_count": 0}
}
r.mu.RLock()
defer r.mu.RUnlock()
items := make([]map[string]any, 0, len(r.streams))
openCount := 0
for _, stream := range r.streams {
if stream.State == FabricServiceStreamStateOpen {
openCount++
}
item := map[string]any{
"tunnel_id": stream.TunnelID,
"service_id": stream.ServiceID,
"stream_id": stream.StreamID,
"traffic_class": stream.TrafficClass,
"direction": stream.Direction,
"state": stream.State,
"service_tunnel": stream.ServiceTunnel.Snapshot(),
}
if !stream.OpenedAt.IsZero() {
item["opened_at"] = stream.OpenedAt.Format(time.RFC3339Nano)
}
if !stream.UpdatedAt.IsZero() {
item["updated_at"] = stream.UpdatedAt.Format(time.RFC3339Nano)
}
if len(stream.Metadata) > 0 {
item["metadata"] = cloneStringMap(stream.Metadata)
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
left, _ := items[i]["stream_id"].(uint64)
right, _ := items[j]["stream_id"].(uint64)
return left < right
})
return map[string]any{
"schema_version": FabricServiceStreamRegistrySchemaVersion,
"stream_count": len(items),
"open_count": openCount,
"streams": items,
}
}
func (r *FabricServiceStreamRegistry) markState(tunnelID string, streamID uint64, state string) {
if r == nil || tunnelID == "" || streamID == 0 {
return
}
r.mu.Lock()
defer r.mu.Unlock()
key := serviceStreamKey(tunnelID, streamID)
stream, ok := r.streams[key]
if !ok {
return
}
stream.State = state
stream.UpdatedAt = time.Now().UTC()
r.streams[key] = stream
}
func serviceStreamKey(tunnelID string, streamID uint64) string {
return fmt.Sprintf("%s\x00%d", tunnelID, streamID)
}
func cloneFabricServiceStream(stream FabricServiceStream) FabricServiceStream {
stream.Metadata = cloneStringMap(stream.Metadata)
return stream
}
func serviceStreamsSnapshotItems(streams []FabricServiceStream) []map[string]any {
if len(streams) == 0 {
return nil
}
items := make([]map[string]any, 0, len(streams))
for _, stream := range streams {
item := map[string]any{
"tunnel_id": stream.TunnelID,
"service_id": stream.ServiceID,
"stream_id": stream.StreamID,
"traffic_class": stream.TrafficClass,
"direction": stream.Direction,
"state": stream.State,
"service_tunnel": stream.ServiceTunnel.Snapshot(),
}
if !stream.OpenedAt.IsZero() {
item["opened_at"] = stream.OpenedAt.Format(time.RFC3339Nano)
}
if !stream.UpdatedAt.IsZero() {
item["updated_at"] = stream.UpdatedAt.Format(time.RFC3339Nano)
}
if len(stream.Metadata) > 0 {
item["metadata"] = cloneStringMap(stream.Metadata)
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
left, _ := items[i]["stream_id"].(uint64)
right, _ := items[j]["stream_id"].(uint64)
return left < right
})
return items
}
func cloneStringMap(values map[string]string) map[string]string {
if len(values) == 0 {
return nil
}
out := make(map[string]string, len(values))
for key, value := range values {
out[key] = value
}
return out
}
@@ -0,0 +1,44 @@
package vpnruntime
import "testing"
func TestFabricServiceStreamRegistryTracksTunnelScopedStreams(t *testing.T) {
registry := NewFabricServiceStreamRegistry()
stream := registry.Register(FabricServiceStream{
TunnelID: "tunnel-1",
ServiceID: "svc-1",
StreamID: 42,
TrafficClass: FabricServiceTrafficInteractive,
Direction: FabricDirectionClientToGateway,
ServiceTunnel: FabricServiceTunnel{
TunnelID: "tunnel-1",
PoolID: "pool-vpn",
ServiceID: "svc-1",
ServiceKind: "ipv4-tunnel",
},
Metadata: map[string]string{"adapter": "vpn"},
})
if stream.State != FabricServiceStreamStateOpen {
t.Fatalf("stream state = %q, want open", stream.State)
}
if stream.ServiceTunnel.TransportOwner != DefaultFabricTransportOwner {
t.Fatalf("service tunnel should remain fabric-owned: %+v", stream.ServiceTunnel)
}
streams := registry.StreamsForTunnel("tunnel-1")
if len(streams) != 1 || streams[0].StreamID != 42 || streams[0].ServiceID != "svc-1" {
t.Fatalf("streams for tunnel = %+v", streams)
}
registry.MarkClosed("tunnel-1", 42)
streams = registry.StreamsForTunnel("tunnel-1")
if len(streams) != 1 || streams[0].State != FabricServiceStreamStateClosed {
t.Fatalf("closed stream not tracked: %+v", streams)
}
snapshot := registry.Snapshot()
if snapshot["schema_version"] != FabricServiceStreamRegistrySchemaVersion ||
snapshot["stream_count"] != 1 ||
snapshot["open_count"] != 0 {
t.Fatalf("unexpected registry snapshot: %+v", snapshot)
}
}
@@ -0,0 +1,179 @@
package vpnruntime
import "strings"
const (
DefaultFabricTunnelPoolID = "ipv4-egress"
DefaultFabricTunnelServiceKind = "ipv4-tunnel"
DefaultFabricTunnelClass = "vpn_packets"
DefaultFabricTunnelRole = "ipv4-egress"
DefaultFabricTunnelDataPlane = "fabric_quic_streams"
DefaultFabricTransportOwner = "fabric_farm"
DefaultFabricRouteVisibility = "opaque_to_service"
FabricServiceTunnelSchemaVersion = "rap.fabric_service_tunnel.v1"
FabricServiceTrafficControl = "control"
FabricServiceTrafficDNS = "dns"
FabricServiceTrafficInteractive = "interactive"
FabricServiceTrafficReliable = "reliable"
FabricServiceTrafficBulk = "bulk"
FabricServiceTrafficDroppable = "droppable"
DefaultFabricServiceStreamShards = 8
)
type FabricServiceTunnel struct {
TunnelID string `json:"tunnel_id"`
PoolID string `json:"pool_id"`
ServiceID string `json:"service_id"`
LocalServiceID string `json:"local_service_id"`
RemoteServiceID string `json:"remote_service_id"`
ServiceKind string `json:"service_kind"`
ServiceClass string `json:"service_class"`
ServiceRole string `json:"service_role"`
RouteLeaseID string `json:"route_lease_id,omitempty"`
RouteGeneration string `json:"route_generation,omitempty"`
DataPlane string `json:"data_plane,omitempty"`
TransportOwner string `json:"transport_owner,omitempty"`
RouteVisibility string `json:"route_visibility,omitempty"`
TrafficClasses []string `json:"traffic_classes,omitempty"`
StreamShards int `json:"stream_shards,omitempty"`
}
type FabricServiceTunnelDefaults struct {
PoolID string
ServiceKind string
ServiceClass string
ServiceRole string
DataPlane string
TransportOwner string
RouteVisibility string
TrafficClasses []string
StreamShards int
}
func NormalizeServiceTunnel(tunnel FabricServiceTunnel, fallbackID string) FabricServiceTunnel {
return NormalizeServiceTunnelWithDefaults(tunnel, fallbackID, DefaultVPNServiceTunnelDefaults())
}
func NormalizeServiceTunnelWithDefaults(tunnel FabricServiceTunnel, fallbackID string, defaults FabricServiceTunnelDefaults) FabricServiceTunnel {
defaults = normalizeServiceTunnelDefaults(defaults)
tunnel.TunnelID = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.TunnelID, fallbackID))
tunnel.PoolID = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.PoolID, defaults.PoolID))
tunnel.ServiceID = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.ServiceID, "svc-"+tunnel.TunnelID))
tunnel.LocalServiceID = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.LocalServiceID, "svc-local-"+tunnel.TunnelID))
tunnel.RemoteServiceID = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.RemoteServiceID, "svc-remote-"+tunnel.TunnelID))
tunnel.ServiceKind = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.ServiceKind, defaults.ServiceKind))
tunnel.ServiceClass = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.ServiceClass, defaults.ServiceClass))
tunnel.ServiceRole = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.ServiceRole, defaults.ServiceRole))
tunnel.DataPlane = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.DataPlane, defaults.DataPlane))
tunnel.TransportOwner = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.TransportOwner, defaults.TransportOwner))
tunnel.RouteVisibility = strings.TrimSpace(firstNonEmptyTunnelString(tunnel.RouteVisibility, defaults.RouteVisibility))
tunnel.TrafficClasses = normalizeTunnelTrafficClasses(tunnel.TrafficClasses, defaults.TrafficClasses)
if tunnel.StreamShards <= 0 {
tunnel.StreamShards = defaults.StreamShards
}
return tunnel
}
func (t FabricServiceTunnel) Snapshot() map[string]any {
t = NormalizeServiceTunnelWithDefaults(t, t.TunnelID, FabricServiceTunnelDefaults{
PoolID: t.PoolID,
ServiceKind: t.ServiceKind,
ServiceClass: t.ServiceClass,
ServiceRole: t.ServiceRole,
DataPlane: t.DataPlane,
TransportOwner: t.TransportOwner,
RouteVisibility: t.RouteVisibility,
TrafficClasses: t.TrafficClasses,
StreamShards: t.StreamShards,
})
return map[string]any{
"schema_version": FabricServiceTunnelSchemaVersion,
"tunnel_id": t.TunnelID,
"pool_id": t.PoolID,
"service_id": t.ServiceID,
"local_service_id": t.LocalServiceID,
"remote_service_id": t.RemoteServiceID,
"service_kind": t.ServiceKind,
"service_class": t.ServiceClass,
"service_role": t.ServiceRole,
"route_lease_id": t.RouteLeaseID,
"route_generation": t.RouteGeneration,
"data_plane": t.DataPlane,
"transport_owner": t.TransportOwner,
"route_visibility": t.RouteVisibility,
"traffic_classes": append([]string(nil), t.TrafficClasses...),
"stream_shards": t.StreamShards,
"selected_node_known": false,
}
}
func DefaultVPNServiceTunnelDefaults() FabricServiceTunnelDefaults {
return FabricServiceTunnelDefaults{
PoolID: DefaultFabricTunnelPoolID,
ServiceKind: DefaultFabricTunnelServiceKind,
ServiceClass: DefaultFabricTunnelClass,
ServiceRole: DefaultFabricTunnelRole,
DataPlane: DefaultFabricTunnelDataPlane,
TransportOwner: DefaultFabricTransportOwner,
RouteVisibility: DefaultFabricRouteVisibility,
TrafficClasses: []string{
FabricServiceTrafficControl,
FabricServiceTrafficDNS,
FabricServiceTrafficInteractive,
FabricServiceTrafficReliable,
FabricServiceTrafficBulk,
FabricServiceTrafficDroppable,
},
StreamShards: DefaultFabricServiceStreamShards,
}
}
func normalizeServiceTunnelDefaults(defaults FabricServiceTunnelDefaults) FabricServiceTunnelDefaults {
fallback := DefaultVPNServiceTunnelDefaults()
defaults.PoolID = firstNonEmptyTunnelString(defaults.PoolID, fallback.PoolID)
defaults.ServiceKind = firstNonEmptyTunnelString(defaults.ServiceKind, fallback.ServiceKind)
defaults.ServiceClass = firstNonEmptyTunnelString(defaults.ServiceClass, fallback.ServiceClass)
defaults.ServiceRole = firstNonEmptyTunnelString(defaults.ServiceRole, fallback.ServiceRole)
defaults.DataPlane = firstNonEmptyTunnelString(defaults.DataPlane, fallback.DataPlane)
defaults.TransportOwner = firstNonEmptyTunnelString(defaults.TransportOwner, fallback.TransportOwner)
defaults.RouteVisibility = firstNonEmptyTunnelString(defaults.RouteVisibility, fallback.RouteVisibility)
defaults.TrafficClasses = normalizeTunnelTrafficClasses(defaults.TrafficClasses, fallback.TrafficClasses)
if defaults.StreamShards <= 0 {
defaults.StreamShards = fallback.StreamShards
}
return defaults
}
func normalizeTunnelTrafficClasses(values []string, fallback []string) []string {
if len(values) == 0 {
return append([]string(nil), fallback...)
}
out := make([]string, 0, len(values))
seen := map[string]struct{}{}
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
if len(out) == 0 {
return append([]string(nil), fallback...)
}
return out
}
func firstNonEmptyTunnelString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
@@ -0,0 +1,46 @@
package vpnruntime
import "testing"
func TestNormalizeServiceTunnelKeepsVPNAsProfileNotTransportRule(t *testing.T) {
tunnel := NormalizeServiceTunnel(FabricServiceTunnel{}, "vpn-tunnel-1")
if tunnel.TunnelID != "vpn-tunnel-1" {
t.Fatalf("tunnel id = %q", tunnel.TunnelID)
}
if tunnel.ServiceKind != DefaultFabricTunnelServiceKind || tunnel.ServiceClass != DefaultFabricTunnelClass {
t.Fatalf("vpn defaults not applied: %+v", tunnel)
}
if tunnel.TransportOwner != DefaultFabricTransportOwner || tunnel.RouteVisibility != DefaultFabricRouteVisibility {
t.Fatalf("transport ownership defaults not applied: %+v", tunnel)
}
if tunnel.DataPlane != DefaultFabricTunnelDataPlane || tunnel.StreamShards != DefaultFabricServiceStreamShards {
t.Fatalf("data plane defaults not applied: %+v", tunnel)
}
if len(tunnel.TrafficClasses) < 5 {
t.Fatalf("traffic classes too small: %+v", tunnel.TrafficClasses)
}
}
func TestNormalizeServiceTunnelSupportsNonVPNService(t *testing.T) {
tunnel := NormalizeServiceTunnelWithDefaults(FabricServiceTunnel{}, "rdp-tunnel-1", FabricServiceTunnelDefaults{
PoolID: "desktop-exit",
ServiceKind: "rdp-client",
ServiceClass: "remote_desktop",
ServiceRole: "desktop-egress",
TrafficClasses: []string{
FabricServiceTrafficControl,
FabricServiceTrafficInteractive,
FabricServiceTrafficBulk,
},
StreamShards: 8,
})
if tunnel.TunnelID != "rdp-tunnel-1" || tunnel.PoolID != "desktop-exit" || tunnel.ServiceKind != "rdp-client" {
t.Fatalf("non-vpn tunnel defaults not applied: %+v", tunnel)
}
if tunnel.ServiceClass != "remote_desktop" || tunnel.ServiceRole != "desktop-egress" {
t.Fatalf("non-vpn service identity not applied: %+v", tunnel)
}
if tunnel.StreamShards != 8 || len(tunnel.TrafficClasses) != 3 {
t.Fatalf("non-vpn stream policy not applied: %+v", tunnel)
}
}
@@ -19,8 +19,8 @@ const (
iffNoPI = 0x1000
tunSetIFF = 0x400454ca
ifNameSize = 16
gatewayTunMTU = "1000"
gatewayTCPMSS = "900"
gatewayTunMTU = "1280"
gatewayTCPMSS = "1240"
)
type tunDevice struct {