рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user