Add RRPIT - Rapid Reliable Packet Interactive Transport (#3658)

* Add rrpit(unreleased) transport

* rrpit(unreleased) transport: boost reconstruction for the front lane

* rrpit(unreleased) transport: avoid blocking delivery of packets with smux packet reordering

* rrpit(unreleased) transport: add dual channel with differential prioritization

* rrpit(unreleased) transport: add remoteMaxDataShardsPerLane

* rrpit(unreleased) transport: add remoteMaxDataShardsPerLane(fix)

* rrpit(unreleased) transport: add auto-reconnect for transport connection

* rrpit(unreleased) transport: add auto-reconnect for transport connection

* rrpit(unreleased) transport: update tests

* rrpit(unreleased) transport: limit frame size for framed packet write closer

* rrpit(unreleased) transport: remove packet length wrapper

* rrpit(unreleased) transport: update go mod

* rrpit(unreleased) transport: update dtls error propagation

* rrpit(unreleased) transport: update broken tests

* rrpit(unreleased) transport: fix deadlock when closing persistent channel

* rrpit(unreleased) transport: add server disconnection detection for persistent connection

* rrpit(unreleased) transport: fix broken lints

* rrpit(unreleased) transport: fix broken lints

* rrpit(unreleased) transport: fix broken lints

* rrpit(unreleased) transport: fix broken lints

* rrpit(unreleased) transport: fix broken lints
This commit is contained in:
Xiaokang Wang (Shelikhoo)
2026-03-31 17:21:48 +01:00
committed by GitHub
parent cf7577f650
commit d1e2c2391a
65 changed files with 16524 additions and 113 deletions
+13
View File
@@ -313,6 +313,19 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
}
} else if d.router != nil {
if route, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil {
if routeWithAttributes, ok := route.(interface{ GetSessionAttributes() map[string]string }); ok {
attrs := routeWithAttributes.GetSessionAttributes()
if len(attrs) > 0 {
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
for key, value := range attrs {
content.SetAttribute(key, value)
}
}
}
tag := route.GetOutboundTag()
if h := d.ohm.GetHandler(tag); h != nil {
newError("taking detour [", tag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
+4 -3
View File
@@ -18,9 +18,10 @@ import (
)
type Rule struct {
Tag string
Balancer *Balancer
Condition Condition
Tag string
Balancer *Balancer
Condition Condition
SetAttributes []*SetAttribute
}
func (r *Rule) GetTag() (string, error) {
+176 -94
View File
@@ -1,3 +1,9 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v5.28.1
// source: app/router/config.proto
package router
import (
@@ -75,6 +81,58 @@ func (DomainStrategy) EnumDescriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0}
}
type SetAttribute struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetAttribute) Reset() {
*x = SetAttribute{}
mi := &file_app_router_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetAttribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetAttribute) ProtoMessage() {}
func (x *SetAttribute) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetAttribute.ProtoReflect.Descriptor instead.
func (*SetAttribute) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0}
}
func (x *SetAttribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *SetAttribute) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
type RoutingRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to TargetTag:
@@ -115,12 +173,13 @@ type RoutingRule struct {
// source_cidr above will have no effect.
SourceGeoip []*routercommon.GeoIP `protobuf:"bytes,11,rep,name=source_geoip,json=sourceGeoip,proto3" json:"source_geoip,omitempty"`
// List of ports for source port matching.
SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"`
UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"`
InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"`
Attributes string `protobuf:"bytes,15,opt,name=attributes,proto3" json:"attributes,omitempty"`
DomainMatcher string `protobuf:"bytes,17,opt,name=domain_matcher,json=domainMatcher,proto3" json:"domain_matcher,omitempty"`
SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"`
UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"`
InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"`
Attributes string `protobuf:"bytes,15,opt,name=attributes,proto3" json:"attributes,omitempty"`
DomainMatcher string `protobuf:"bytes,17,opt,name=domain_matcher,json=domainMatcher,proto3" json:"domain_matcher,omitempty"`
SetAttribute []*SetAttribute `protobuf:"bytes,18,rep,name=set_attribute,json=setAttribute,proto3" json:"set_attribute,omitempty"`
// geo_domain instruct simplified config loader to load geo domain rule and fill in domain field.
GeoDomain []*routercommon.GeoSite `protobuf:"bytes,68001,rep,name=geo_domain,json=geoDomain,proto3" json:"geo_domain,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -129,7 +188,7 @@ type RoutingRule struct {
func (x *RoutingRule) Reset() {
*x = RoutingRule{}
mi := &file_app_router_config_proto_msgTypes[0]
mi := &file_app_router_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -141,7 +200,7 @@ func (x *RoutingRule) String() string {
func (*RoutingRule) ProtoMessage() {}
func (x *RoutingRule) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[0]
mi := &file_app_router_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -154,7 +213,7 @@ func (x *RoutingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead.
func (*RoutingRule) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0}
return file_app_router_config_proto_rawDescGZIP(), []int{1}
}
func (x *RoutingRule) GetTargetTag() isRoutingRule_TargetTag {
@@ -291,6 +350,13 @@ func (x *RoutingRule) GetDomainMatcher() string {
return ""
}
func (x *RoutingRule) GetSetAttribute() []*SetAttribute {
if x != nil {
return x.SetAttribute
}
return nil
}
func (x *RoutingRule) GetGeoDomain() []*routercommon.GeoSite {
if x != nil {
return x.GeoDomain
@@ -329,7 +395,7 @@ type BalancingRule struct {
func (x *BalancingRule) Reset() {
*x = BalancingRule{}
mi := &file_app_router_config_proto_msgTypes[1]
mi := &file_app_router_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -341,7 +407,7 @@ func (x *BalancingRule) String() string {
func (*BalancingRule) ProtoMessage() {}
func (x *BalancingRule) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[1]
mi := &file_app_router_config_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -354,7 +420,7 @@ func (x *BalancingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use BalancingRule.ProtoReflect.Descriptor instead.
func (*BalancingRule) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{1}
return file_app_router_config_proto_rawDescGZIP(), []int{2}
}
func (x *BalancingRule) GetTag() string {
@@ -403,7 +469,7 @@ type StrategyWeight struct {
func (x *StrategyWeight) Reset() {
*x = StrategyWeight{}
mi := &file_app_router_config_proto_msgTypes[2]
mi := &file_app_router_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -415,7 +481,7 @@ func (x *StrategyWeight) String() string {
func (*StrategyWeight) ProtoMessage() {}
func (x *StrategyWeight) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[2]
mi := &file_app_router_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -428,7 +494,7 @@ func (x *StrategyWeight) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyWeight.ProtoReflect.Descriptor instead.
func (*StrategyWeight) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{2}
return file_app_router_config_proto_rawDescGZIP(), []int{3}
}
func (x *StrategyWeight) GetRegexp() bool {
@@ -462,7 +528,7 @@ type StrategyRandomConfig struct {
func (x *StrategyRandomConfig) Reset() {
*x = StrategyRandomConfig{}
mi := &file_app_router_config_proto_msgTypes[3]
mi := &file_app_router_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -474,7 +540,7 @@ func (x *StrategyRandomConfig) String() string {
func (*StrategyRandomConfig) ProtoMessage() {}
func (x *StrategyRandomConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[3]
mi := &file_app_router_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -487,7 +553,7 @@ func (x *StrategyRandomConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyRandomConfig.ProtoReflect.Descriptor instead.
func (*StrategyRandomConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{3}
return file_app_router_config_proto_rawDescGZIP(), []int{4}
}
func (x *StrategyRandomConfig) GetObserverTag() string {
@@ -514,7 +580,7 @@ type StrategyLeastPingConfig struct {
func (x *StrategyLeastPingConfig) Reset() {
*x = StrategyLeastPingConfig{}
mi := &file_app_router_config_proto_msgTypes[4]
mi := &file_app_router_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -526,7 +592,7 @@ func (x *StrategyLeastPingConfig) String() string {
func (*StrategyLeastPingConfig) ProtoMessage() {}
func (x *StrategyLeastPingConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[4]
mi := &file_app_router_config_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -539,7 +605,7 @@ func (x *StrategyLeastPingConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyLeastPingConfig.ProtoReflect.Descriptor instead.
func (*StrategyLeastPingConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{4}
return file_app_router_config_proto_rawDescGZIP(), []int{5}
}
func (x *StrategyLeastPingConfig) GetObserverTag() string {
@@ -575,7 +641,7 @@ type StrategyLeastLoadConfig struct {
func (x *StrategyLeastLoadConfig) Reset() {
*x = StrategyLeastLoadConfig{}
mi := &file_app_router_config_proto_msgTypes[5]
mi := &file_app_router_config_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -587,7 +653,7 @@ func (x *StrategyLeastLoadConfig) String() string {
func (*StrategyLeastLoadConfig) ProtoMessage() {}
func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[5]
mi := &file_app_router_config_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -600,7 +666,7 @@ func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyLeastLoadConfig.ProtoReflect.Descriptor instead.
func (*StrategyLeastLoadConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{5}
return file_app_router_config_proto_rawDescGZIP(), []int{6}
}
func (x *StrategyLeastLoadConfig) GetCosts() []*StrategyWeight {
@@ -656,7 +722,7 @@ type Config struct {
func (x *Config) Reset() {
*x = Config{}
mi := &file_app_router_config_proto_msgTypes[6]
mi := &file_app_router_config_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -668,7 +734,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[6]
mi := &file_app_router_config_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -681,7 +747,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{6}
return file_app_router_config_proto_rawDescGZIP(), []int{7}
}
func (x *Config) GetDomainStrategy() DomainStrategy {
@@ -727,12 +793,13 @@ type SimplifiedRoutingRule struct {
// source_cidr above will have no effect.
SourceGeoip []*routercommon.GeoIP `protobuf:"bytes,11,rep,name=source_geoip,json=sourceGeoip,proto3" json:"source_geoip,omitempty"`
// List of ports for source port matching.
SourcePortList string `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"`
UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"`
InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"`
Attributes string `protobuf:"bytes,15,opt,name=attributes,proto3" json:"attributes,omitempty"`
DomainMatcher string `protobuf:"bytes,17,opt,name=domain_matcher,json=domainMatcher,proto3" json:"domain_matcher,omitempty"`
SourcePortList string `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"`
UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"`
InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"`
Attributes string `protobuf:"bytes,15,opt,name=attributes,proto3" json:"attributes,omitempty"`
DomainMatcher string `protobuf:"bytes,17,opt,name=domain_matcher,json=domainMatcher,proto3" json:"domain_matcher,omitempty"`
SetAttribute []*SetAttribute `protobuf:"bytes,18,rep,name=set_attribute,json=setAttribute,proto3" json:"set_attribute,omitempty"`
// geo_domain instruct simplified config loader to load geo domain rule and fill in domain field.
GeoDomain []*routercommon.GeoSite `protobuf:"bytes,68001,rep,name=geo_domain,json=geoDomain,proto3" json:"geo_domain,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -741,7 +808,7 @@ type SimplifiedRoutingRule struct {
func (x *SimplifiedRoutingRule) Reset() {
*x = SimplifiedRoutingRule{}
mi := &file_app_router_config_proto_msgTypes[7]
mi := &file_app_router_config_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -753,7 +820,7 @@ func (x *SimplifiedRoutingRule) String() string {
func (*SimplifiedRoutingRule) ProtoMessage() {}
func (x *SimplifiedRoutingRule) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[7]
mi := &file_app_router_config_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -766,7 +833,7 @@ func (x *SimplifiedRoutingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use SimplifiedRoutingRule.ProtoReflect.Descriptor instead.
func (*SimplifiedRoutingRule) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{7}
return file_app_router_config_proto_rawDescGZIP(), []int{8}
}
func (x *SimplifiedRoutingRule) GetTargetTag() isSimplifiedRoutingRule_TargetTag {
@@ -871,6 +938,13 @@ func (x *SimplifiedRoutingRule) GetDomainMatcher() string {
return ""
}
func (x *SimplifiedRoutingRule) GetSetAttribute() []*SetAttribute {
if x != nil {
return x.SetAttribute
}
return nil
}
func (x *SimplifiedRoutingRule) GetGeoDomain() []*routercommon.GeoSite {
if x != nil {
return x.GeoDomain
@@ -907,7 +981,7 @@ type SimplifiedConfig struct {
func (x *SimplifiedConfig) Reset() {
*x = SimplifiedConfig{}
mi := &file_app_router_config_proto_msgTypes[8]
mi := &file_app_router_config_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -919,7 +993,7 @@ func (x *SimplifiedConfig) String() string {
func (*SimplifiedConfig) ProtoMessage() {}
func (x *SimplifiedConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[8]
mi := &file_app_router_config_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -932,7 +1006,7 @@ func (x *SimplifiedConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use SimplifiedConfig.ProtoReflect.Descriptor instead.
func (*SimplifiedConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{8}
return file_app_router_config_proto_rawDescGZIP(), []int{9}
}
func (x *SimplifiedConfig) GetDomainStrategy() DomainStrategy {
@@ -960,7 +1034,10 @@ var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" +
"\n" +
"\x17app/router/config.proto\x12\x15v2ray.core.app.router\x1a\x19google/protobuf/any.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a common/protoext/extensions.proto\x1a$app/router/routercommon/common.proto\"\x80\b\n" +
"\x17app/router/config.proto\x12\x15v2ray.core.app.router\x1a\x19google/protobuf/any.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a common/protoext/extensions.proto\x1a$app/router/routercommon/common.proto\"6\n" +
"\fSetAttribute\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value\"\xca\b\n" +
"\vRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12B\n" +
@@ -985,7 +1062,8 @@ const file_app_router_config_proto_rawDesc = "" +
"\n" +
"attributes\x18\x0f \x01(\tR\n" +
"attributes\x12%\n" +
"\x0edomain_matcher\x18\x11 \x01(\tR\rdomainMatcher\x12L\n" +
"\x0edomain_matcher\x18\x11 \x01(\tR\rdomainMatcher\x12H\n" +
"\rset_attribute\x18\x12 \x03(\v2#.v2ray.core.app.router.SetAttributeR\fsetAttribute\x12L\n" +
"\n" +
"geo_domain\x18\xa1\x93\x04 \x03(\v2+.v2ray.core.app.router.routercommon.GeoSiteR\tgeoDomainB\f\n" +
"\n" +
@@ -1020,7 +1098,7 @@ const file_app_router_config_proto_rawDesc = "" +
"\x06Config\x12N\n" +
"\x0fdomain_strategy\x18\x01 \x01(\x0e2%.v2ray.core.app.router.DomainStrategyR\x0edomainStrategy\x126\n" +
"\x04rule\x18\x02 \x03(\v2\".v2ray.core.app.router.RoutingRuleR\x04rule\x12K\n" +
"\x0ebalancing_rule\x18\x03 \x03(\v2$.v2ray.core.app.router.BalancingRuleR\rbalancingRule\"\xab\x05\n" +
"\x0ebalancing_rule\x18\x03 \x03(\v2$.v2ray.core.app.router.BalancingRuleR\rbalancingRule\"\xf5\x05\n" +
"\x15SimplifiedRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12B\n" +
@@ -1039,7 +1117,8 @@ const file_app_router_config_proto_rawDesc = "" +
"\n" +
"attributes\x18\x0f \x01(\tR\n" +
"attributes\x12%\n" +
"\x0edomain_matcher\x18\x11 \x01(\tR\rdomainMatcher\x12L\n" +
"\x0edomain_matcher\x18\x11 \x01(\tR\rdomainMatcher\x12H\n" +
"\rset_attribute\x18\x12 \x03(\v2#.v2ray.core.app.router.SetAttributeR\fsetAttribute\x12L\n" +
"\n" +
"geo_domain\x18\xa1\x93\x04 \x03(\v2+.v2ray.core.app.router.routercommon.GeoSiteR\tgeoDomainB\f\n" +
"\n" +
@@ -1070,58 +1149,61 @@ func file_app_router_config_proto_rawDescGZIP() []byte {
}
var file_app_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_app_router_config_proto_goTypes = []any{
(DomainStrategy)(0), // 0: v2ray.core.app.router.DomainStrategy
(*RoutingRule)(nil), // 1: v2ray.core.app.router.RoutingRule
(*BalancingRule)(nil), // 2: v2ray.core.app.router.BalancingRule
(*StrategyWeight)(nil), // 3: v2ray.core.app.router.StrategyWeight
(*StrategyRandomConfig)(nil), // 4: v2ray.core.app.router.StrategyRandomConfig
(*StrategyLeastPingConfig)(nil), // 5: v2ray.core.app.router.StrategyLeastPingConfig
(*StrategyLeastLoadConfig)(nil), // 6: v2ray.core.app.router.StrategyLeastLoadConfig
(*Config)(nil), // 7: v2ray.core.app.router.Config
(*SimplifiedRoutingRule)(nil), // 8: v2ray.core.app.router.SimplifiedRoutingRule
(*SimplifiedConfig)(nil), // 9: v2ray.core.app.router.SimplifiedConfig
(*routercommon.Domain)(nil), // 10: v2ray.core.app.router.routercommon.Domain
(*routercommon.CIDR)(nil), // 11: v2ray.core.app.router.routercommon.CIDR
(*routercommon.GeoIP)(nil), // 12: v2ray.core.app.router.routercommon.GeoIP
(*net.PortRange)(nil), // 13: v2ray.core.common.net.PortRange
(*net.PortList)(nil), // 14: v2ray.core.common.net.PortList
(*net.NetworkList)(nil), // 15: v2ray.core.common.net.NetworkList
(net.Network)(0), // 16: v2ray.core.common.net.Network
(*routercommon.GeoSite)(nil), // 17: v2ray.core.app.router.routercommon.GeoSite
(*anypb.Any)(nil), // 18: google.protobuf.Any
(*SetAttribute)(nil), // 1: v2ray.core.app.router.SetAttribute
(*RoutingRule)(nil), // 2: v2ray.core.app.router.RoutingRule
(*BalancingRule)(nil), // 3: v2ray.core.app.router.BalancingRule
(*StrategyWeight)(nil), // 4: v2ray.core.app.router.StrategyWeight
(*StrategyRandomConfig)(nil), // 5: v2ray.core.app.router.StrategyRandomConfig
(*StrategyLeastPingConfig)(nil), // 6: v2ray.core.app.router.StrategyLeastPingConfig
(*StrategyLeastLoadConfig)(nil), // 7: v2ray.core.app.router.StrategyLeastLoadConfig
(*Config)(nil), // 8: v2ray.core.app.router.Config
(*SimplifiedRoutingRule)(nil), // 9: v2ray.core.app.router.SimplifiedRoutingRule
(*SimplifiedConfig)(nil), // 10: v2ray.core.app.router.SimplifiedConfig
(*routercommon.Domain)(nil), // 11: v2ray.core.app.router.routercommon.Domain
(*routercommon.CIDR)(nil), // 12: v2ray.core.app.router.routercommon.CIDR
(*routercommon.GeoIP)(nil), // 13: v2ray.core.app.router.routercommon.GeoIP
(*net.PortRange)(nil), // 14: v2ray.core.common.net.PortRange
(*net.PortList)(nil), // 15: v2ray.core.common.net.PortList
(*net.NetworkList)(nil), // 16: v2ray.core.common.net.NetworkList
(net.Network)(0), // 17: v2ray.core.common.net.Network
(*routercommon.GeoSite)(nil), // 18: v2ray.core.app.router.routercommon.GeoSite
(*anypb.Any)(nil), // 19: google.protobuf.Any
}
var file_app_router_config_proto_depIdxs = []int32{
10, // 0: v2ray.core.app.router.RoutingRule.domain:type_name -> v2ray.core.app.router.routercommon.Domain
11, // 1: v2ray.core.app.router.RoutingRule.cidr:type_name -> v2ray.core.app.router.routercommon.CIDR
12, // 2: v2ray.core.app.router.RoutingRule.geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
13, // 3: v2ray.core.app.router.RoutingRule.port_range:type_name -> v2ray.core.common.net.PortRange
14, // 4: v2ray.core.app.router.RoutingRule.port_list:type_name -> v2ray.core.common.net.PortList
15, // 5: v2ray.core.app.router.RoutingRule.network_list:type_name -> v2ray.core.common.net.NetworkList
16, // 6: v2ray.core.app.router.RoutingRule.networks:type_name -> v2ray.core.common.net.Network
11, // 7: v2ray.core.app.router.RoutingRule.source_cidr:type_name -> v2ray.core.app.router.routercommon.CIDR
12, // 8: v2ray.core.app.router.RoutingRule.source_geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
14, // 9: v2ray.core.app.router.RoutingRule.source_port_list:type_name -> v2ray.core.common.net.PortList
17, // 10: v2ray.core.app.router.RoutingRule.geo_domain:type_name -> v2ray.core.app.router.routercommon.GeoSite
18, // 11: v2ray.core.app.router.BalancingRule.strategy_settings:type_name -> google.protobuf.Any
3, // 12: v2ray.core.app.router.StrategyLeastLoadConfig.costs:type_name -> v2ray.core.app.router.StrategyWeight
0, // 13: v2ray.core.app.router.Config.domain_strategy:type_name -> v2ray.core.app.router.DomainStrategy
1, // 14: v2ray.core.app.router.Config.rule:type_name -> v2ray.core.app.router.RoutingRule
2, // 15: v2ray.core.app.router.Config.balancing_rule:type_name -> v2ray.core.app.router.BalancingRule
10, // 16: v2ray.core.app.router.SimplifiedRoutingRule.domain:type_name -> v2ray.core.app.router.routercommon.Domain
12, // 17: v2ray.core.app.router.SimplifiedRoutingRule.geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
15, // 18: v2ray.core.app.router.SimplifiedRoutingRule.networks:type_name -> v2ray.core.common.net.NetworkList
12, // 19: v2ray.core.app.router.SimplifiedRoutingRule.source_geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
17, // 20: v2ray.core.app.router.SimplifiedRoutingRule.geo_domain:type_name -> v2ray.core.app.router.routercommon.GeoSite
0, // 21: v2ray.core.app.router.SimplifiedConfig.domain_strategy:type_name -> v2ray.core.app.router.DomainStrategy
8, // 22: v2ray.core.app.router.SimplifiedConfig.rule:type_name -> v2ray.core.app.router.SimplifiedRoutingRule
2, // 23: v2ray.core.app.router.SimplifiedConfig.balancing_rule:type_name -> v2ray.core.app.router.BalancingRule
24, // [24:24] is the sub-list for method output_type
24, // [24:24] is the sub-list for method input_type
24, // [24:24] is the sub-list for extension type_name
24, // [24:24] is the sub-list for extension extendee
0, // [0:24] is the sub-list for field type_name
11, // 0: v2ray.core.app.router.RoutingRule.domain:type_name -> v2ray.core.app.router.routercommon.Domain
12, // 1: v2ray.core.app.router.RoutingRule.cidr:type_name -> v2ray.core.app.router.routercommon.CIDR
13, // 2: v2ray.core.app.router.RoutingRule.geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
14, // 3: v2ray.core.app.router.RoutingRule.port_range:type_name -> v2ray.core.common.net.PortRange
15, // 4: v2ray.core.app.router.RoutingRule.port_list:type_name -> v2ray.core.common.net.PortList
16, // 5: v2ray.core.app.router.RoutingRule.network_list:type_name -> v2ray.core.common.net.NetworkList
17, // 6: v2ray.core.app.router.RoutingRule.networks:type_name -> v2ray.core.common.net.Network
12, // 7: v2ray.core.app.router.RoutingRule.source_cidr:type_name -> v2ray.core.app.router.routercommon.CIDR
13, // 8: v2ray.core.app.router.RoutingRule.source_geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
15, // 9: v2ray.core.app.router.RoutingRule.source_port_list:type_name -> v2ray.core.common.net.PortList
1, // 10: v2ray.core.app.router.RoutingRule.set_attribute:type_name -> v2ray.core.app.router.SetAttribute
18, // 11: v2ray.core.app.router.RoutingRule.geo_domain:type_name -> v2ray.core.app.router.routercommon.GeoSite
19, // 12: v2ray.core.app.router.BalancingRule.strategy_settings:type_name -> google.protobuf.Any
4, // 13: v2ray.core.app.router.StrategyLeastLoadConfig.costs:type_name -> v2ray.core.app.router.StrategyWeight
0, // 14: v2ray.core.app.router.Config.domain_strategy:type_name -> v2ray.core.app.router.DomainStrategy
2, // 15: v2ray.core.app.router.Config.rule:type_name -> v2ray.core.app.router.RoutingRule
3, // 16: v2ray.core.app.router.Config.balancing_rule:type_name -> v2ray.core.app.router.BalancingRule
11, // 17: v2ray.core.app.router.SimplifiedRoutingRule.domain:type_name -> v2ray.core.app.router.routercommon.Domain
13, // 18: v2ray.core.app.router.SimplifiedRoutingRule.geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
16, // 19: v2ray.core.app.router.SimplifiedRoutingRule.networks:type_name -> v2ray.core.common.net.NetworkList
13, // 20: v2ray.core.app.router.SimplifiedRoutingRule.source_geoip:type_name -> v2ray.core.app.router.routercommon.GeoIP
1, // 21: v2ray.core.app.router.SimplifiedRoutingRule.set_attribute:type_name -> v2ray.core.app.router.SetAttribute
18, // 22: v2ray.core.app.router.SimplifiedRoutingRule.geo_domain:type_name -> v2ray.core.app.router.routercommon.GeoSite
0, // 23: v2ray.core.app.router.SimplifiedConfig.domain_strategy:type_name -> v2ray.core.app.router.DomainStrategy
9, // 24: v2ray.core.app.router.SimplifiedConfig.rule:type_name -> v2ray.core.app.router.SimplifiedRoutingRule
3, // 25: v2ray.core.app.router.SimplifiedConfig.balancing_rule:type_name -> v2ray.core.app.router.BalancingRule
26, // [26:26] is the sub-list for method output_type
26, // [26:26] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_app_router_config_proto_init() }
@@ -1129,11 +1211,11 @@ func file_app_router_config_proto_init() {
if File_app_router_config_proto != nil {
return
}
file_app_router_config_proto_msgTypes[0].OneofWrappers = []any{
file_app_router_config_proto_msgTypes[1].OneofWrappers = []any{
(*RoutingRule_Tag)(nil),
(*RoutingRule_BalancingTag)(nil),
}
file_app_router_config_proto_msgTypes[7].OneofWrappers = []any{
file_app_router_config_proto_msgTypes[8].OneofWrappers = []any{
(*SimplifiedRoutingRule_Tag)(nil),
(*SimplifiedRoutingRule_BalancingTag)(nil),
}
@@ -1143,7 +1225,7 @@ func file_app_router_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_config_proto_rawDesc), len(file_app_router_config_proto_rawDesc)),
NumEnums: 1,
NumMessages: 9,
NumMessages: 10,
NumExtensions: 0,
NumServices: 0,
},
+9 -1
View File
@@ -12,6 +12,10 @@ import "common/net/network.proto";
import "common/protoext/extensions.proto";
import "app/router/routercommon/common.proto";
message SetAttribute {
string key = 1;
string value = 2;
}
message RoutingRule {
oneof target_tag {
@@ -66,6 +70,8 @@ message RoutingRule {
string domain_matcher = 17;
repeated SetAttribute set_attribute = 18;
// geo_domain instruct simplified config loader to load geo domain rule and fill in domain field.
repeated v2ray.core.app.router.routercommon.GeoSite geo_domain = 68001;
}
@@ -178,6 +184,8 @@ message SimplifiedRoutingRule {
string domain_matcher = 17;
repeated SetAttribute set_attribute = 18;
// geo_domain instruct simplified config loader to load geo domain rule and fill in domain field.
repeated v2ray.core.app.router.routercommon.GeoSite geo_domain = 68001;
}
@@ -189,4 +197,4 @@ message SimplifiedConfig {
DomainStrategy domain_strategy = 1;
repeated SimplifiedRoutingRule rule = 2;
repeated BalancingRule balancing_rule = 3;
}
}
+30 -3
View File
@@ -29,6 +29,7 @@ type Route struct {
routing.Context
outboundGroupTags []string
outboundTag string
sessionAttributes map[string]string
}
// Init initializes the Router.
@@ -53,8 +54,9 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
Condition: cond,
Tag: rule.GetTag(),
SetAttributes: rule.GetSetAttribute(),
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
@@ -80,7 +82,20 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
if err != nil {
return nil, err
}
return &Route{Context: ctx, outboundTag: tag}, nil
route := &Route{Context: ctx, outboundTag: tag}
if len(rule.SetAttributes) > 0 {
route.sessionAttributes = make(map[string]string, len(rule.SetAttributes))
for _, attr := range rule.SetAttributes {
if attr == nil || attr.Key == "" {
continue
}
route.sessionAttributes[attr.Key] = attr.Value
}
if len(route.sessionAttributes) == 0 {
route.sessionAttributes = nil
}
}
return route, nil
}
func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) {
@@ -140,6 +155,17 @@ func (r *Route) GetOutboundTag() string {
return r.outboundTag
}
func (r *Route) GetSessionAttributes() map[string]string {
if r == nil || len(r.sessionAttributes) == 0 {
return nil
}
out := make(map[string]string, len(r.sessionAttributes))
for key, value := range r.sessionAttributes {
out[key] = value
}
return out
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
r := new(Router)
@@ -242,6 +268,7 @@ func init() {
rule.Networks = v.Networks.GetNetwork()
rule.Protocol = v.Protocol
rule.Attributes = v.Attributes
rule.SetAttribute = append(rule.SetAttribute, v.SetAttribute...)
rule.UserEmail = v.UserEmail
rule.InboundTag = v.InboundTag
rule.DomainMatcher = v.DomainMatcher
+44
View File
@@ -54,6 +54,50 @@ func TestSimpleRouter(t *testing.T) {
}
}
func TestRouteSetAttribute(t *testing.T) {
config := &Config{
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_Tag{
Tag: "test",
},
Networks: []net.Network{net.Network_TCP},
SetAttribute: []*SetAttribute{
{
Key: "rrpitSessionClass",
Value: "background",
},
},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
mockOhm := mocks.NewOutboundManager(mockCtl)
mockHs := mocks.NewOutboundHandlerSelector(mockCtl)
r := new(Router)
common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{
Manager: mockOhm,
HandlerSelector: mockHs,
}, nil))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
attrRoute, ok := route.(interface{ GetSessionAttributes() map[string]string })
if !ok {
t.Fatal("route does not expose session attributes")
}
attrs := attrRoute.GetSessionAttributes()
if attrs["rrpitSessionClass"] != "background" {
t.Fatalf("unexpected session attribute map: %#v", attrs)
}
}
func TestSimpleBalancer(t *testing.T) {
config := &Config{
Rule: []*RoutingRule{
@@ -1,4 +1,4 @@
package abstactOutbount
package abstactOutbound
import "github.com/v2fly/v2ray-core/v5/common/errors"
@@ -1,4 +1,4 @@
package abstactOutbount
package abstactOutbound
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v5.28.1
// source: config.proto
+5 -4
View File
@@ -18,10 +18,11 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/improbable-eng/grpc-web v0.15.0
github.com/jhump/protoreflect v1.18.0
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/miekg/dns v1.1.72
github.com/mustafaturan/bus v1.0.2
github.com/pelletier/go-toml v1.9.5
github.com/pion/dtls/v2 v2.2.12
github.com/pion/dtls/v3 v3.0.11
github.com/pion/stun/v3 v3.1.1
github.com/pion/transport/v2 v2.2.10
github.com/pires/go-proxyproto v0.11.0
@@ -36,6 +37,8 @@ require (
github.com/v2fly/struc v0.0.0-20241227015403-8e8fa1badfd6
github.com/vincent-petithory/dataurl v1.0.0
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432
github.com/xssnick/raptorq v1.3.0
github.com/xtaci/smux v1.5.24
go.starlark.net v0.0.0-20230612165344-9532f5667272
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
golang.org/x/crypto v0.49.0
@@ -71,10 +74,9 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/reedsolomon v1.11.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mustafaturan/monoton v1.0.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pion/dtls/v3 v3.0.11 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.7 // indirect
@@ -87,7 +89,6 @@ require (
github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xtaci/smux v1.5.24 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/text v0.35.0 // indirect
+2
View File
@@ -541,6 +541,8 @@ github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs=
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ=
github.com/xssnick/raptorq v1.3.0 h1:3GoaySKMg/i8rbjhIuqjxpTTO2l3Gs2/Gh7k3GAjvGo=
github.com/xssnick/raptorq v1.3.0/go.mod h1:kgEVVsZv2hP+IeV7C7985KIFsDdvYq2ARW234SBA9Q4=
github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
+2
View File
@@ -89,6 +89,8 @@ import (
_ "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransport"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/server"
@@ -0,0 +1,70 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "vmess",
"settings": {
"address": "127.0.0.1",
"port": 17893,
"uuid": "bcc71618-e552-42c2-a2a3-d4c17a9df764"
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 64
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-primary"
}
}
],
"lane": {
"shardSize": 640,
"maxDataShardsPerLane": 4,
"maxBufferedLanes": 64
},
"session": {
"oddChannelIds": true,
"maxRewindableTimestampNum": 512,
"maxRewindableControlMessageNum": 512
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 1,
"maxFrameSize": 512,
"maxReceiveBuffer": 1048576,
"maxStreamBuffer": 65536
}
}
}
}
],
"inbounds": [
{
"protocol": "socks",
"settings": {
"udpEnabled": false,
"address": "127.0.0.1",
"packetEncoding": "Packet"
},
"port": 17894,
"listen": "127.0.0.1"
}
]
}
@@ -0,0 +1,54 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "vmess",
"settings": {
"address": "127.0.0.1",
"port": 18103,
"uuid": "bcc71618-e552-42c2-a2a3-d4c17a9df764"
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"ipAddr": "127.0.0.1",
"port": 18103,
"password": "rrpit-test-defaults"
}
}
],
"session": {
"oddChannelIds": true
},
"sessionMgr": {
"timestampInterval": 5000000
}
}
}
}
],
"inbounds": [
{
"protocol": "socks",
"settings": {
"udpEnabled": false,
"address": "127.0.0.1",
"packetEncoding": "Packet"
},
"port": 18104,
"listen": "127.0.0.1"
}
]
}
@@ -0,0 +1,49 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "freedom"
}
],
"inbounds": [
{
"listen": "127.0.0.1",
"port": 18103,
"protocol": "vmess",
"settings": {
"users": [
"bcc71618-e552-42c2-a2a3-d4c17a9df764"
]
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"ipAddr": "127.0.0.1",
"port": 18103,
"password": "rrpit-test-defaults"
}
}
],
"session": {
"oddChannelIds": false
},
"sessionMgr": {
"timestampInterval": 5000000
}
}
}
}
]
}
@@ -0,0 +1,83 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "vmess",
"settings": {
"address": "127.0.0.1",
"port": 17903,
"uuid": "bcc71618-e552-42c2-a2a3-d4c17a9df764"
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 2
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-multichannel-primary"
}
},
{
"setting": {
"mtu": 1200,
"weight": 3,
"maxSendingSpeed": 4
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"ipAddr": "127.0.0.1",
"port": 17905,
"password": "rrpit-test-multichannel-secondary"
}
}
],
"lane": {
"shardSize": 640,
"maxDataShardsPerLane": 4,
"maxBufferedLanes": 64
},
"session": {
"oddChannelIds": true,
"maxRewindableTimestampNum": 512,
"maxRewindableControlMessageNum": 512
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 1,
"maxFrameSize": 512,
"maxReceiveBuffer": 1048576,
"maxStreamBuffer": 65536
}
}
}
}
],
"inbounds": [
{
"protocol": "socks",
"settings": {
"udpEnabled": false,
"address": "127.0.0.1",
"packetEncoding": "Packet"
},
"port": 17904,
"listen": "127.0.0.1"
}
]
}
@@ -0,0 +1,78 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "freedom"
}
],
"inbounds": [
{
"listen": "127.0.0.1",
"port": 17903,
"protocol": "vmess",
"settings": {
"users": [
"bcc71618-e552-42c2-a2a3-d4c17a9df764"
]
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 2
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-multichannel-primary"
}
},
{
"setting": {
"mtu": 1200,
"weight": 3,
"maxSendingSpeed": 4
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"ipAddr": "127.0.0.1",
"port": 17905,
"password": "rrpit-test-multichannel-secondary"
}
}
],
"lane": {
"shardSize": 640,
"maxDataShardsPerLane": 4,
"maxBufferedLanes": 64
},
"session": {
"oddChannelIds": false,
"maxRewindableTimestampNum": 512,
"maxRewindableControlMessageNum": 512
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 1,
"maxFrameSize": 512,
"maxReceiveBuffer": 1048576,
"maxStreamBuffer": 65536
}
}
}
}
]
}
@@ -0,0 +1,65 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "freedom"
}
],
"inbounds": [
{
"listen": "127.0.0.1",
"port": 17893,
"protocol": "vmess",
"settings": {
"users": [
"bcc71618-e552-42c2-a2a3-d4c17a9df764"
]
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 64
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-primary"
}
}
],
"lane": {
"shardSize": 640,
"maxDataShardsPerLane": 4,
"maxBufferedLanes": 64
},
"session": {
"oddChannelIds": false,
"maxRewindableTimestampNum": 512,
"maxRewindableControlMessageNum": 512
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 1,
"maxFrameSize": 512,
"maxReceiveBuffer": 1048576,
"maxStreamBuffer": 65536
}
}
}
}
]
}
@@ -0,0 +1,71 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "vmess",
"settings": {
"address": "127.0.0.1",
"port": 18003,
"uuid": "bcc71618-e552-42c2-a2a3-d4c17a9df764"
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 64
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-smallshard"
}
}
],
"lane": {
"shardSize": 256,
"maxDataShardsPerLane": 2,
"maxBufferedLanes": 32
},
"session": {
"oddChannelIds": true,
"maxRewindableTimestampNum": 128,
"maxRewindableControlMessageNum": 128
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 2,
"keepAliveDisabled": true,
"maxFrameSize": 192,
"maxReceiveBuffer": 524288,
"maxStreamBuffer": 32768
}
}
}
}
],
"inbounds": [
{
"protocol": "socks",
"settings": {
"udpEnabled": false,
"address": "127.0.0.1",
"packetEncoding": "Packet"
},
"port": 18004,
"listen": "127.0.0.1"
}
]
}
@@ -0,0 +1,66 @@
{
"log": {
"error": {
"level": "Debug",
"type": "Console"
},
"access": {
"type": "None"
}
},
"outbounds": [
{
"protocol": "freedom"
}
],
"inbounds": [
{
"listen": "127.0.0.1",
"port": 18003,
"protocol": "vmess",
"settings": {
"users": [
"bcc71618-e552-42c2-a2a3-d4c17a9df764"
]
},
"streamSettings": {
"transport": "rrpit",
"transportSettings": {
"channels": [
{
"setting": {
"mtu": 1200,
"weight": 1,
"maxSendingSpeed": 64
},
"channel": {
"@type": "types.v2fly.org/v2ray.core.transport.internet.rrpit.transport.DTLSUDPChannel",
"password": "rrpit-test-smallshard"
}
}
],
"lane": {
"shardSize": 256,
"maxDataShardsPerLane": 2,
"maxBufferedLanes": 32
},
"session": {
"oddChannelIds": false,
"maxRewindableTimestampNum": 128,
"maxRewindableControlMessageNum": 128
},
"sessionMgr": {
"timestampInterval": 5000000
},
"adaptor": {
"smuxVersion": 2,
"keepAliveDisabled": true,
"maxFrameSize": 192,
"maxReceiveBuffer": 524288,
"maxStreamBuffer": 32768
}
}
}
}
]
}
+71
View File
@@ -0,0 +1,71 @@
package scenarios
import (
"context"
"os"
"testing"
"time"
"github.com/v2fly/v2ray-core/v5/common"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
"github.com/v2fly/v2ray-core/v5/testing/servers/tcp"
_ "github.com/v2fly/v2ray-core/v5/main/distro/all"
)
func TestRRPITOverDTLSUDP(t *testing.T) {
runRRPITScenario(t, "config/rrpit_client.json", "config/rrpit_server.json", 17894)
}
func TestRRPITOverDTLSUDPMultiChannel(t *testing.T) {
runRRPITScenario(t, "config/rrpit_multichannel_client.json", "config/rrpit_multichannel_server.json", 17904)
}
func TestRRPITOverDTLSUDPSmallShard(t *testing.T) {
runRRPITScenario(t, "config/rrpit_smallshard_client.json", "config/rrpit_smallshard_server.json", 18004)
}
func TestRRPITOverDTLSUDPDefaultsAndExplicitAddress(t *testing.T) {
runRRPITScenario(t, "config/rrpit_defaults_client.json", "config/rrpit_defaults_server.json", 18104)
}
func runRRPITScenario(t *testing.T, clientConfigPath string, serverConfigPath string, socksPort v2net.Port) {
t.Helper()
tcpServer := tcp.Server{
MsgProcessor: xor,
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
coreInst, instMgr := NewInstanceManagerCoreInstance()
defer coreInst.Close()
common.Must(instMgr.AddInstance(
context.TODO(),
"rrpit_client",
common.Must2(os.ReadFile(clientConfigPath)).([]byte),
"jsonv5"))
common.Must(instMgr.AddInstance(
context.TODO(),
"rrpit_server",
common.Must2(os.ReadFile(serverConfigPath)).([]byte),
"jsonv5"))
common.Must(instMgr.StartInstance(context.TODO(), "rrpit_server"))
common.Must(instMgr.StartInstance(context.TODO(), "rrpit_client"))
defer func() {
common.Must(instMgr.StopInstance(context.TODO(), "rrpit_server"))
common.Must(instMgr.StopInstance(context.TODO(), "rrpit_client"))
common.Must(instMgr.UntrackInstance(context.TODO(), "rrpit_server"))
common.Must(instMgr.UntrackInstance(context.TODO(), "rrpit_client"))
coreInst.Close()
}()
if err := testTCPConnViaSocks(socksPort, dest.Port, 4096, 5*time.Second)(); err != nil {
t.Fatal(err)
}
}
+11 -2
View File
@@ -3,7 +3,8 @@ package dtls
import (
"context"
"github.com/pion/dtls/v2"
"github.com/pion/dtls/v3"
dtlsnet "github.com/pion/dtls/v3/pkg/net"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/common/environment"
@@ -39,7 +40,15 @@ func dialDTLS(ctx context.Context, dest net.Destination, streamSettings *interne
default:
return nil, newError("unknow dtls mode")
}
return dtls.Client(rawConn, config)
conn, err := dtls.Client(dtlsnet.PacketConnFromConn(rawConn), rawConn.RemoteAddr(), config)
if err != nil {
return nil, err
}
if err := conn.Handshake(); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
func dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) {
+24
View File
@@ -1,6 +1,8 @@
package dtls
import (
"github.com/pion/dtls/v3"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/transport/internet"
)
@@ -9,6 +11,28 @@ import (
const protocolName = "dtls"
type ClientIdentityProvider interface {
ClientIdentity() []byte
}
type connectionStateProvider interface {
ConnectionState() (dtls.State, bool)
}
func ClientIdentity(conn internet.Connection) []byte {
if provider, ok := conn.(ClientIdentityProvider); ok {
return append([]byte(nil), provider.ClientIdentity()...)
}
if provider, ok := conn.(connectionStateProvider); ok {
state, ready := provider.ConnectionState()
if !ready {
return nil
}
return append([]byte(nil), state.IdentityHint...)
}
return nil
}
func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config)
+94 -3
View File
@@ -2,12 +2,14 @@ package dtls
import (
"context"
"errors"
"io"
gonet "net"
"sync"
"time"
"github.com/pion/dtls/v2"
"github.com/pion/dtls/v3"
dtlsnet "github.com/pion/dtls/v3/pkg/net"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/common/buf"
@@ -54,6 +56,86 @@ func newDTLSServerConn(src net.Destination, parent *Listener) *dTLSConn {
type dTLSConnWrapped struct {
unencryptedConn *dTLSConn
dTLSConn *dtls.Conn
closeOnce sync.Once
closeErr error
}
func (c *dTLSConnWrapped) Read(b []byte) (int, error) {
if c == nil || c.dTLSConn == nil {
return 0, io.ErrClosedPipe
}
return c.dTLSConn.Read(b)
}
func (c *dTLSConnWrapped) Write(b []byte) (int, error) {
if c == nil || c.dTLSConn == nil {
return 0, io.ErrClosedPipe
}
return c.dTLSConn.Write(b)
}
func (c *dTLSConnWrapped) Close() error {
if c == nil {
return nil
}
c.closeOnce.Do(func() {
switch {
case c.dTLSConn != nil:
c.closeErr = c.dTLSConn.Close()
case c.unencryptedConn != nil:
c.closeErr = c.unencryptedConn.Close()
}
if errors.Is(c.closeErr, dtls.ErrConnClosed) {
c.closeErr = nil
}
})
return c.closeErr
}
func (c *dTLSConnWrapped) LocalAddr() gonet.Addr {
if c == nil || c.dTLSConn == nil {
return nil
}
return c.dTLSConn.LocalAddr()
}
func (c *dTLSConnWrapped) RemoteAddr() gonet.Addr {
if c == nil || c.dTLSConn == nil {
return nil
}
return c.dTLSConn.RemoteAddr()
}
func (c *dTLSConnWrapped) SetDeadline(t time.Time) error {
if c == nil || c.dTLSConn == nil {
return io.ErrClosedPipe
}
return c.dTLSConn.SetDeadline(t)
}
func (c *dTLSConnWrapped) SetReadDeadline(t time.Time) error {
if c == nil || c.dTLSConn == nil {
return io.ErrClosedPipe
}
return c.dTLSConn.SetReadDeadline(t)
}
func (c *dTLSConnWrapped) SetWriteDeadline(t time.Time) error {
if c == nil || c.dTLSConn == nil {
return io.ErrClosedPipe
}
return c.dTLSConn.SetWriteDeadline(t)
}
func (c *dTLSConnWrapped) ClientIdentity() []byte {
if c == nil || c.dTLSConn == nil {
return nil
}
state, ready := c.dTLSConn.ConnectionState()
if !ready {
return nil
}
return append([]byte(nil), state.IdentityHint...)
}
type dTLSConn struct {
@@ -161,7 +243,11 @@ func newDTLSConnWrapped(unencryptedConnection *dTLSConn, transportConfiguration
default:
newError("unknown dtls mode").WriteToLog()
}
dtlsConn, err := dtls.Server(unencryptedConnection, config)
dtlsConn, err := dtls.Server(
dtlsnet.PacketConnFromConn(unencryptedConnection),
unencryptedConnection.RemoteAddr(),
config,
)
if err != nil {
return nil, newError("unable to create dtls server conn").Base(err)
}
@@ -187,7 +273,12 @@ func (l *Listener) OnReceive(payload *buf.Buffer, src net.Destination) {
newError("unable to accept new dtls connection").Base(err).WriteToLog()
return
}
l.addConn(internet.Connection(conn.dTLSConn))
if err := conn.dTLSConn.Handshake(); err != nil {
newError("unable to complete dtls handshake").Base(err).WriteToLog()
_ = conn.Close()
return
}
l.addConn(internet.Connection(conn))
}()
}
conn.unencryptedConn.OnReceive(payload)
@@ -0,0 +1,39 @@
package dtls
import (
"testing"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
)
func TestDTLSConnWrappedCloseIsIdempotent(t *testing.T) {
parent := &Listener{
sessions: make(map[ConnectionID]*dTLSConnWrapped),
}
src := v2net.UDPDestination(v2net.LocalHostIP, 12345)
finishCalls := 0
wrapped := &dTLSConnWrapped{
unencryptedConn: &dTLSConn{
src: src,
parent: parent,
finish: func() {
finishCalls++
},
},
}
parent.sessions[ConnectionID{Remote: src.Address, Port: src.Port}] = wrapped
if err := wrapped.Close(); err != nil {
t.Fatal(err)
}
if err := wrapped.Close(); err != nil {
t.Fatal(err)
}
if finishCalls != 1 {
t.Fatalf("unexpected finish call count: %d", finishCalls)
}
if len(parent.sessions) != 0 {
t.Fatalf("expected session removal, got %d sessions", len(parent.sessions))
}
}
+155
View File
@@ -0,0 +1,155 @@
package dtls_test
import (
"bytes"
"context"
"io"
gonet "net"
"testing"
"time"
piondtls "github.com/pion/dtls/v3"
piondtlsnet "github.com/pion/dtls/v3/pkg/net"
"github.com/v2fly/v2ray-core/v5/common/environment"
"github.com/v2fly/v2ray-core/v5/common/environment/deferredpersistentstorage"
"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
"github.com/v2fly/v2ray-core/v5/common/environment/filesystemimpl"
"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
"github.com/v2fly/v2ray-core/v5/common/environment/transientstorageimpl"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
"github.com/v2fly/v2ray-core/v5/transport/internet"
transportdtls "github.com/v2fly/v2ray-core/v5/transport/internet/dtls"
)
func TestListenerExportsClientIdentity(t *testing.T) {
ctx := context.Background()
ctx = envctx.ContextWithEnvironment(ctx, newTransportEnvironment(t, ctx, "dtls"))
accepted := make(chan internet.Connection, 1)
listener, err := transportdtls.ListenDTLS(ctx, v2net.LocalHostIP, 0, &internet.MemoryStreamConfig{
ProtocolName: "dtls",
ProtocolSettings: &transportdtls.Config{
Mode: transportdtls.DTLSMode_PSK,
Psk: []byte("shared-secret"),
Mtu: 1200,
ReplayProtectionWindow: 64,
},
}, func(conn internet.Connection) {
accepted <- conn
})
if err != nil {
t.Fatal(err)
}
defer listener.Close()
serverAddr, ok := listener.Addr().(*gonet.UDPAddr)
if !ok {
t.Fatalf("unexpected listener addr type %T", listener.Addr())
}
rawConn, err := gonet.DialUDP("udp", nil, serverAddr)
if err != nil {
t.Fatal(err)
}
defer rawConn.Close()
identity := []byte("rrpit-client-identity")
clientConn, err := piondtls.Client(piondtlsnet.PacketConnFromConn(rawConn), rawConn.RemoteAddr(), &piondtls.Config{
MTU: 1200,
ReplayProtectionWindow: 64,
PSK: func([]byte) ([]byte, error) {
return []byte("shared-secret"), nil
},
PSKIdentityHint: identity,
CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256},
})
if err != nil {
t.Fatal(err)
}
defer clientConn.Close()
handshakeErr := make(chan error, 1)
go func() {
handshakeErr <- clientConn.Handshake()
}()
var serverConn internet.Connection
select {
case serverConn = <-accepted:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for accepted DTLS connection")
}
defer serverConn.Close()
if got := transportdtls.ClientIdentity(serverConn); !bytes.Equal(got, identity) {
t.Fatalf("unexpected client identity: got %q want %q", got, identity)
}
select {
case err := <-handshakeErr:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for client handshake")
}
serverErr := make(chan error, 1)
go func() {
buf := make([]byte, 4)
if _, err := io.ReadFull(serverConn, buf); err != nil {
serverErr <- err
return
}
if !bytes.Equal(buf, []byte("ping")) {
serverErr <- io.ErrUnexpectedEOF
return
}
_, err := serverConn.Write([]byte("pong"))
serverErr <- err
}()
if _, err := clientConn.Write([]byte("ping")); err != nil {
t.Fatal(err)
}
reply := make([]byte, 4)
if _, err := io.ReadFull(clientConn, reply); err != nil {
t.Fatal(err)
}
if !bytes.Equal(reply, []byte("pong")) {
t.Fatalf("unexpected reply: got %q want %q", reply, []byte("pong"))
}
select {
case err := <-serverErr:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for server echo")
}
}
func newTransportEnvironment(t *testing.T, ctx context.Context, scope string) environment.TransportEnvironment {
t.Helper()
defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
defaultFilesystemImpl := filesystemimpl.NewDefaultFileSystemDefaultImpl()
deferredPersistentStorageImpl := deferredpersistentstorage.NewDeferredPersistentStorage(ctx)
rootEnv := environment.NewRootEnvImpl(
ctx,
transientstorageimpl.NewScopedTransientStorageImpl(),
defaultNetworkImpl.Dialer(),
defaultNetworkImpl.Listener(),
defaultFilesystemImpl,
deferredPersistentStorageImpl,
)
proxyEnvironment := rootEnv.ProxyEnvironment(scope)
transportEnvironment, err := proxyEnvironment.NarrowScopeToTransport(scope)
if err != nil {
t.Fatal(err)
}
return transportEnvironment
}
@@ -0,0 +1,589 @@
package packetToStream
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/xtaci/smux"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
)
const (
adaptorFrameIDFieldSize = 8
adaptorStreamIDFieldSize = 4
adaptorStreamFrameSeqFieldSize = 8
adaptorSmuxCmdFieldSize = 1
adaptorSmuxVersionFieldSize = 1
adaptorHeaderSize = adaptorFrameIDFieldSize + adaptorStreamIDFieldSize + adaptorStreamFrameSeqFieldSize + adaptorSmuxCmdFieldSize + adaptorSmuxVersionFieldSize
smuxVersionFieldOffset = 0
smuxCmdFieldOffset = 1
smuxLengthFieldOffset = 2
smuxStreamIDFieldOffset = 4
smuxFrameHeaderSize = 8
)
var sessionPacketConnCloseDrainTimeout = 2 * time.Second
func MaxSmuxFrameSizeForMessage(maxMessageSize int) int {
maxFrameSize := maxMessageSize - adaptorHeaderSize - smuxFrameHeaderSize
if maxFrameSize <= 0 {
return 0
}
return maxFrameSize
}
// Adaptor consumes the rrpit receive callback and exposes an smux session on top.
type Adaptor struct {
smux *smux.Session
session *rrpitBidirectionalSession.BidirectionalSession
packetConn *sessionPacketConn
}
type adaptorFrame struct {
frameID uint64
streamID uint32
streamFrameSeq uint64
smuxCmd byte
smuxVersion byte
payload []byte
}
type sessionPacketConn struct {
session *rrpitBidirectionalSession.BidirectionalSession
mu sync.Mutex
// cond protects all mutable fields below.
cond *sync.Cond
readBuf bytes.Buffer
nextSendFrameID uint64
nextSendStreamFrameSeq map[uint32]uint64
nextExpectedStreamSeq map[uint32]uint64
readyFramesByStream map[uint32]map[uint64]*adaptorFrame
activeStreams []uint32
activeStreamSet map[uint32]bool
locallyKnownStreams map[uint32]bool
remoteSynEstablished map[uint32]bool
roundRobinIndex int
maxSerializedFrameBytes int
closeOnce sync.Once
closing bool
closed bool
closeErr error
localAddr net.Addr
remoteAddr net.Addr
}
func New(session *rrpitBidirectionalSession.BidirectionalSession, client bool, config *smux.Config) (*Adaptor, error) {
if session == nil {
return nil, fmt.Errorf("nil bidirectional session")
}
if session.Rx() == nil {
return nil, fmt.Errorf("nil rx session")
}
if config == nil {
config = smux.DefaultConfig()
}
maxSerializedFrameBytes, err := validateSmuxFrameSize(session, config)
if err != nil {
return nil, err
}
packetConn := newSessionPacketConn(session, maxSerializedFrameBytes)
session.Rx().OnMessage = packetConn.OnMessage
var smuxSession *smux.Session
if client {
smuxSession, err = smux.Client(packetConn, config)
} else {
smuxSession, err = smux.Server(packetConn, config)
}
if err != nil {
_ = packetConn.Close()
return nil, err
}
return &Adaptor{
smux: smuxSession,
session: session,
packetConn: packetConn,
}, nil
}
func NewClient(session *rrpitBidirectionalSession.BidirectionalSession, config *smux.Config) (*Adaptor, error) {
return New(session, true, config)
}
func NewServer(session *rrpitBidirectionalSession.BidirectionalSession, config *smux.Config) (*Adaptor, error) {
return New(session, false, config)
}
func (a *Adaptor) Session() *smux.Session {
if a == nil {
return nil
}
return a.smux
}
func (a *Adaptor) OpenStream() (*smux.Stream, error) {
if a == nil || a.smux == nil {
return nil, io.ErrClosedPipe
}
return a.smux.OpenStream()
}
func (a *Adaptor) AcceptStream() (*smux.Stream, error) {
if a == nil || a.smux == nil {
return nil, io.ErrClosedPipe
}
return a.smux.AcceptStream()
}
func (a *Adaptor) Close() error {
if a == nil {
return nil
}
var firstErr error
if a.smux != nil {
if err := a.smux.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if a.packetConn != nil {
if err := a.packetConn.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if a.session != nil {
if err := a.session.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func newSessionPacketConn(session *rrpitBidirectionalSession.BidirectionalSession, maxSerializedFrameBytes int) *sessionPacketConn {
conn := &sessionPacketConn{
session: session,
nextSendStreamFrameSeq: make(map[uint32]uint64),
nextExpectedStreamSeq: make(map[uint32]uint64),
readyFramesByStream: make(map[uint32]map[uint64]*adaptorFrame),
activeStreamSet: make(map[uint32]bool),
locallyKnownStreams: make(map[uint32]bool),
remoteSynEstablished: make(map[uint32]bool),
maxSerializedFrameBytes: maxSerializedFrameBytes,
localAddr: adaptorAddr("rrpit-local"),
remoteAddr: adaptorAddr("rrpit-remote"),
}
conn.cond = sync.NewCond(&conn.mu)
return conn
}
func (c *sessionPacketConn) OnMessage(data []byte) error {
frame, err := decodeAdaptorFrame(data)
if err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
if frame.streamFrameSeq < c.nextExpectedStreamSeq[frame.streamID] {
return nil
}
streamFrames := c.readyFramesByStream[frame.streamID]
if streamFrames == nil {
streamFrames = make(map[uint64]*adaptorFrame)
c.readyFramesByStream[frame.streamID] = streamFrames
}
if _, found := streamFrames[frame.streamFrameSeq]; found {
return nil
}
streamFrames[frame.streamFrameSeq] = frame
c.activateStreamLocked(frame.streamID)
c.deliverReadyFramesLocked()
c.cond.Broadcast()
return nil
}
func (c *sessionPacketConn) Read(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
for c.readBuf.Len() == 0 && !c.closed {
c.cond.Wait()
}
if c.readBuf.Len() == 0 && c.closed {
if c.closeErr != nil {
return 0, c.closeErr
}
return 0, io.EOF
}
n, err := c.readBuf.Read(p)
if c.readBuf.Len() == 0 {
c.cond.Broadcast()
}
return n, err
}
func (c *sessionPacketConn) Write(p []byte) (int, error) {
c.mu.Lock()
closed := c.closed
closing := c.closing
closeErr := c.closeErr
c.mu.Unlock()
if closed || closing {
if closeErr != nil {
return 0, closeErr
}
return 0, io.ErrClosedPipe
}
if len(p) == 0 {
return 0, nil
}
if len(p) > c.maxSerializedFrameBytes {
err := fmt.Errorf("serialized smux frame size %d exceeds rrpit adaptor budget %d", len(p), c.maxSerializedFrameBytes)
c.fail(err)
return 0, err
}
if c.session == nil {
return 0, io.ErrClosedPipe
}
smuxFrame, err := parseSmuxFrame(p)
if err != nil {
c.fail(err)
return 0, err
}
c.mu.Lock()
frameID := c.nextSendFrameID
c.nextSendFrameID++
streamFrameSeq := c.nextSendStreamFrameSeq[smuxFrame.streamID]
c.nextSendStreamFrameSeq[smuxFrame.streamID] = streamFrameSeq + 1
if smuxFrame.streamID != 0 {
c.locallyKnownStreams[smuxFrame.streamID] = true
}
c.mu.Unlock()
wire := encodeAdaptorFrame(&adaptorFrame{
frameID: frameID,
streamID: smuxFrame.streamID,
streamFrameSeq: streamFrameSeq,
smuxCmd: smuxFrame.cmd,
smuxVersion: smuxFrame.version,
payload: append([]byte(nil), p...),
})
if err := c.session.SendMessage(wire); err != nil {
c.fail(err)
return 0, err
}
return len(p), nil
}
func (c *sessionPacketConn) Close() error {
c.waitForBufferedPayloadBeforeClose()
c.closeWithError(nil)
return nil
}
func (c *sessionPacketConn) LocalAddr() net.Addr {
return c.localAddr
}
func (c *sessionPacketConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
func (c *sessionPacketConn) SetDeadline(time.Time) error {
return nil
}
func (c *sessionPacketConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *sessionPacketConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c *sessionPacketConn) fail(err error) {
if err == nil {
err = io.ErrClosedPipe
}
c.closeWithError(err)
}
func (c *sessionPacketConn) closeWithError(err error) {
c.closeOnce.Do(func() {
if err != nil {
c.closeErr = err
}
c.mu.Lock()
c.closing = true
c.closed = true
c.cond.Broadcast()
c.mu.Unlock()
})
}
func (c *sessionPacketConn) hasBufferedPayloadLocked() bool {
return c.readBuf.Len() > 0 || len(c.activeStreams) > 0
}
func (c *sessionPacketConn) waitForBufferedPayloadBeforeClose() {
if c == nil || sessionPacketConnCloseDrainTimeout <= 0 {
return
}
c.mu.Lock()
if c.closed || c.closing || !c.hasBufferedPayloadLocked() {
if !c.closed {
c.closing = true
}
c.mu.Unlock()
return
}
c.closing = true
timedOut := false
timer := time.AfterFunc(sessionPacketConnCloseDrainTimeout, func() {
c.mu.Lock()
timedOut = true
c.cond.Broadcast()
c.mu.Unlock()
})
for !c.closed && c.hasBufferedPayloadLocked() && !timedOut {
c.cond.Wait()
}
if !timer.Stop() && !timedOut {
timedOut = true
}
c.mu.Unlock()
}
func (c *sessionPacketConn) activateStreamLocked(streamID uint32) {
if c.activeStreamSet[streamID] {
return
}
c.activeStreams = append(c.activeStreams, streamID)
c.activeStreamSet[streamID] = true
}
func (c *sessionPacketConn) deactivateStreamAtLocked(index int) {
if index < 0 || index >= len(c.activeStreams) {
return
}
streamID := c.activeStreams[index]
c.activeStreams = append(c.activeStreams[:index], c.activeStreams[index+1:]...)
delete(c.activeStreamSet, streamID)
if c.roundRobinIndex > index {
c.roundRobinIndex--
}
if c.roundRobinIndex >= len(c.activeStreams) {
c.roundRobinIndex = 0
}
}
func (c *sessionPacketConn) deliverReadyFramesLocked() {
for {
if len(c.activeStreams) == 0 {
return
}
progressed := false
scans := len(c.activeStreams)
for i := 0; i < scans && len(c.activeStreams) > 0; i++ {
if c.roundRobinIndex >= len(c.activeStreams) {
c.roundRobinIndex = 0
}
streamID := c.activeStreams[c.roundRobinIndex]
frame, ok := c.nextDeliverableFrameLocked(streamID)
if !ok {
c.roundRobinIndex++
continue
}
_, _ = c.readBuf.Write(frame.payload)
delete(c.readyFramesByStream[streamID], frame.streamFrameSeq)
c.nextExpectedStreamSeq[streamID] = frame.streamFrameSeq + 1
if frame.smuxCmd == smuxCmdSYN {
c.remoteSynEstablished[streamID] = true
}
if len(c.readyFramesByStream[streamID]) == 0 {
delete(c.readyFramesByStream, streamID)
c.deactivateStreamAtLocked(c.roundRobinIndex)
} else {
c.roundRobinIndex++
}
progressed = true
}
if !progressed {
return
}
}
}
func (c *sessionPacketConn) nextDeliverableFrameLocked(streamID uint32) (*adaptorFrame, bool) {
streamFrames := c.readyFramesByStream[streamID]
if len(streamFrames) == 0 {
return nil, false
}
seq := c.nextExpectedStreamSeq[streamID]
frame, found := streamFrames[seq]
if !found {
return nil, false
}
if streamID == 0 {
return frame, true
}
if frame.smuxCmd == smuxCmdSYN {
return frame, true
}
if c.locallyKnownStreams[streamID] || c.remoteSynEstablished[streamID] {
return frame, true
}
return nil, false
}
func validateSmuxFrameSize(session *rrpitBidirectionalSession.BidirectionalSession, config *smux.Config) (int, error) {
if session == nil {
return 0, io.ErrClosedPipe
}
maxMessageSize, err := session.MaxMessageSize()
if err != nil {
return 0, err
}
maxSerializedFrameBytes := maxMessageSize - adaptorHeaderSize
if maxSerializedFrameBytes <= smuxFrameHeaderSize {
return 0, fmt.Errorf("rrpit max message size %d is too small for adaptor header %d and smux header %d", maxMessageSize, adaptorHeaderSize, smuxFrameHeaderSize)
}
if config.Version == 2 && maxSerializedFrameBytes < smuxFrameHeaderSize+smuxCommandUPDLength {
return 0, fmt.Errorf("rrpit max message size %d is too small for adaptor and smux control frames", maxMessageSize)
}
if config.MaxFrameSize+smuxFrameHeaderSize > maxSerializedFrameBytes {
return 0, fmt.Errorf("smux max frame size %d exceeds rrpit adaptor budget %d", config.MaxFrameSize, maxSerializedFrameBytes-smuxFrameHeaderSize)
}
return maxSerializedFrameBytes, nil
}
func encodeAdaptorFrame(frame *adaptorFrame) []byte {
wire := make([]byte, adaptorHeaderSize+len(frame.payload))
binary.BigEndian.PutUint64(wire[:adaptorFrameIDFieldSize], frame.frameID)
binary.BigEndian.PutUint32(wire[adaptorFrameIDFieldSize:adaptorFrameIDFieldSize+adaptorStreamIDFieldSize], frame.streamID)
binary.BigEndian.PutUint64(wire[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize:adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize], frame.streamFrameSeq)
wire[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize] = frame.smuxCmd
wire[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize+adaptorSmuxCmdFieldSize] = frame.smuxVersion
copy(wire[adaptorHeaderSize:], frame.payload)
return wire
}
func decodeAdaptorFrame(data []byte) (*adaptorFrame, error) {
if len(data) < adaptorHeaderSize+smuxFrameHeaderSize {
return nil, fmt.Errorf("rrpit adaptor frame too short: %d", len(data))
}
frame := &adaptorFrame{
frameID: binary.BigEndian.Uint64(data[:adaptorFrameIDFieldSize]),
streamID: binary.BigEndian.Uint32(data[adaptorFrameIDFieldSize : adaptorFrameIDFieldSize+adaptorStreamIDFieldSize]),
streamFrameSeq: binary.BigEndian.Uint64(data[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize : adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize]),
smuxCmd: data[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize],
smuxVersion: data[adaptorFrameIDFieldSize+adaptorStreamIDFieldSize+adaptorStreamFrameSeqFieldSize+adaptorSmuxCmdFieldSize],
payload: append([]byte(nil), data[adaptorHeaderSize:]...),
}
smuxFrame, err := parseSmuxFrame(frame.payload)
if err != nil {
return nil, err
}
if smuxFrame.streamID != frame.streamID || smuxFrame.cmd != frame.smuxCmd || smuxFrame.version != frame.smuxVersion {
return nil, fmt.Errorf("rrpit adaptor metadata does not match smux frame header")
}
return frame, nil
}
type smuxFrameMetadata struct {
version byte
cmd byte
streamID uint32
}
func parseSmuxFrame(data []byte) (*smuxFrameMetadata, error) {
if len(data) < smuxFrameHeaderSize {
return nil, fmt.Errorf("smux frame too short: %d", len(data))
}
version := data[smuxVersionFieldOffset]
cmd := data[smuxCmdFieldOffset]
length := int(binary.LittleEndian.Uint16(data[smuxLengthFieldOffset : smuxLengthFieldOffset+2]))
streamID := binary.LittleEndian.Uint32(data[smuxStreamIDFieldOffset : smuxStreamIDFieldOffset+4])
if len(data) != smuxFrameHeaderSize+length {
return nil, fmt.Errorf("smux frame length mismatch: header=%d payload=%d", length, len(data)-smuxFrameHeaderSize)
}
if err := validateSmuxCommand(version, cmd, length, streamID); err != nil {
return nil, err
}
return &smuxFrameMetadata{
version: version,
cmd: cmd,
streamID: streamID,
}, nil
}
func validateSmuxCommand(version byte, cmd byte, length int, streamID uint32) error {
switch cmd {
case smuxCmdSYN, smuxCmdFIN:
if length != 0 {
return fmt.Errorf("smux control frame %d must have empty payload", cmd)
}
if streamID == 0 {
return fmt.Errorf("smux control frame %d requires non-zero stream id", cmd)
}
case smuxCmdPSH:
if streamID == 0 {
return fmt.Errorf("smux psh frame requires non-zero stream id")
}
case smuxCmdNOP:
if length != 0 || streamID != 0 {
return fmt.Errorf("smux nop frame must have zero stream id and empty payload")
}
case smuxCmdUPD:
if version != 2 {
return fmt.Errorf("smux upd frame requires version 2")
}
if length != smuxCommandUPDLength {
return fmt.Errorf("smux upd frame must have %d-byte payload", smuxCommandUPDLength)
}
if streamID == 0 {
return fmt.Errorf("smux upd frame requires non-zero stream id")
}
default:
return fmt.Errorf("unsupported smux command %d", cmd)
}
return nil
}
type adaptorAddr string
func (a adaptorAddr) Network() string { return "rrpit" }
func (a adaptorAddr) String() string { return string(a) }
const (
smuxCmdSYN = byte(iota)
smuxCmdFIN
smuxCmdPSH
smuxCmdNOP
smuxCmdUPD
)
const smuxCommandUPDLength = 8
@@ -0,0 +1,839 @@
package packetToStream
import (
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/xtaci/smux"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
)
func TestAdaptorCarriesSmuxStreamsOverRRpit(t *testing.T) {
clientSession := mustNewPacketToStreamSession(t, true)
serverSession := mustNewPacketToStreamSession(t, false)
clientToServer := newPacketWire()
serverToClient := newPacketWire()
defer func() {
_ = clientToServer.Close()
_ = serverToClient.Close()
}()
clientRx, err := clientSession.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
serverRx, err := serverSession.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if _, err := clientSession.AttachTxChannel(clientToServer); err != nil {
t.Fatal(err)
}
if _, err := serverSession.AttachTxChannel(serverToClient); err != nil {
t.Fatal(err)
}
pumpErrs := make(chan error, 2)
go pumpPacketWire(clientToServer, serverRx, pumpErrs)
go pumpPacketWire(serverToClient, clientRx, pumpErrs)
smuxConfig := mustNewSmuxConfigForSession(t, clientSession, 2)
clientAdaptor, err := NewClient(clientSession, smuxConfig)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := clientAdaptor.Close(); err != nil {
t.Fatal(err)
}
}()
serverAdaptor, err := NewServer(serverSession, smuxConfig)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := serverAdaptor.Close(); err != nil {
t.Fatal(err)
}
}()
serverAccepted := make(chan *smux.Stream, 2)
serverAcceptErr := make(chan error, 1)
go func() {
defer close(serverAccepted)
for i := 0; i < 2; i++ {
stream, err := serverAdaptor.AcceptStream()
if err != nil {
serverAcceptErr <- err
return
}
serverAccepted <- stream
}
}()
clientStreamA, err := clientAdaptor.OpenStream()
if err != nil {
t.Fatal(err)
}
defer func() { _ = clientStreamA.Close() }()
clientStreamB, err := clientAdaptor.OpenStream()
if err != nil {
t.Fatal(err)
}
defer func() { _ = clientStreamB.Close() }()
serverStreamA := mustRecvSmuxStream(t, serverAccepted, serverAcceptErr)
defer func() { _ = serverStreamA.Close() }()
serverStreamB := mustRecvSmuxStream(t, serverAccepted, serverAcceptErr)
defer func() { _ = serverStreamB.Close() }()
payloadA := bytes.Repeat([]byte("a"), 1024)
payloadB := bytes.Repeat([]byte("b"), 1536)
if _, err := clientStreamA.Write(payloadA); err != nil {
t.Fatal(err)
}
if _, err := clientStreamB.Write(payloadB); err != nil {
t.Fatal(err)
}
serverReadA := make([]byte, len(payloadA))
serverReadB := make([]byte, len(payloadB))
if _, err := io.ReadFull(serverStreamA, serverReadA); err != nil {
t.Fatal(err)
}
if _, err := io.ReadFull(serverStreamB, serverReadB); err != nil {
t.Fatal(err)
}
if !bytes.Equal(serverReadA, payloadA) {
t.Fatal("payload A mismatch")
}
if !bytes.Equal(serverReadB, payloadB) {
t.Fatal("payload B mismatch")
}
replyA := []byte("reply-a")
replyB := []byte("reply-b-reply-b")
if _, err := serverStreamA.Write(replyA); err != nil {
t.Fatal(err)
}
if _, err := serverStreamB.Write(replyB); err != nil {
t.Fatal(err)
}
clientReadA := make([]byte, len(replyA))
clientReadB := make([]byte, len(replyB))
if _, err := io.ReadFull(clientStreamA, clientReadA); err != nil {
t.Fatal(err)
}
if _, err := io.ReadFull(clientStreamB, clientReadB); err != nil {
t.Fatal(err)
}
if !bytes.Equal(clientReadA, replyA) {
t.Fatal("reply A mismatch")
}
if !bytes.Equal(clientReadB, replyB) {
t.Fatal("reply B mismatch")
}
select {
case err := <-pumpErrs:
t.Fatal(err)
default:
}
}
func TestAdaptorRejectsSmuxFrameSizeAboveBudget(t *testing.T) {
session := mustNewPacketToStreamSession(t, true)
config := smux.DefaultConfig()
config.KeepAliveDisabled = true
config.MaxFrameSize = 256
if _, err := NewClient(session, config); err == nil {
t.Fatal("expected oversized smux frame config to be rejected")
}
}
func TestSessionPacketConnPreservesPerStreamOrder(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
conn.locallyKnownStreams[1] = true
frame0 := mustMarshalAdaptorFrameForTest(t, 0, 1, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("hello ")))
frame1 := mustMarshalAdaptorFrameForTest(t, 1, 1, 1, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("world")))
if err := conn.OnMessage(frame1); err != nil {
t.Fatal(err)
}
conn.mu.Lock()
if conn.readBuf.Len() != 0 {
conn.mu.Unlock()
t.Fatal("later same-stream frame should not be released before its predecessor")
}
conn.mu.Unlock()
if err := conn.OnMessage(frame0); err != nil {
t.Fatal(err)
}
want := append(decodePayloadForTest(t, frame0), decodePayloadForTest(t, frame1)...)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected reordered payload: %x", got)
}
}
func TestSessionPacketConnAllowsIndependentStreamProgress(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
conn.locallyKnownStreams[1] = true
conn.locallyKnownStreams[3] = true
blocked := mustMarshalAdaptorFrameForTest(t, 1, 1, 1, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("stream-a-late")))
unaffected := mustMarshalAdaptorFrameForTest(t, 2, 3, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 3, []byte("stream-b-now")))
if err := conn.OnMessage(blocked); err != nil {
t.Fatal(err)
}
conn.mu.Lock()
if conn.readBuf.Len() != 0 {
conn.mu.Unlock()
t.Fatal("blocked frame should not become readable early")
}
conn.mu.Unlock()
if err := conn.OnMessage(unaffected); err != nil {
t.Fatal(err)
}
want := decodePayloadForTest(t, unaffected)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected unrelated stream payload: %x", got)
}
}
func TestSessionPacketConnRemoteStreamWaitsForSyn(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
psh := mustMarshalAdaptorFrameForTest(t, 1, 11, 1, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 11, []byte("payload")))
if err := conn.OnMessage(psh); err != nil {
t.Fatal(err)
}
conn.mu.Lock()
if conn.readBuf.Len() != 0 {
conn.mu.Unlock()
t.Fatal("remote payload should wait for stream syn")
}
conn.mu.Unlock()
syn := mustMarshalAdaptorFrameForTest(t, 0, 11, 0, mustMarshalSmuxFrameForTest(t, smuxCmdSYN, 11, nil))
if err := conn.OnMessage(syn); err != nil {
t.Fatal(err)
}
want := append(decodePayloadForTest(t, syn), decodePayloadForTest(t, psh)...)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected remote stream sequence: %x", got)
}
}
func TestSessionPacketConnAllowsLocallyKnownStreamWithoutInboundSyn(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
conn.locallyKnownStreams[9] = true
frame := mustMarshalAdaptorFrameForTest(t, 0, 9, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 9, []byte("payload")))
if err := conn.OnMessage(frame); err != nil {
t.Fatal(err)
}
want := decodePayloadForTest(t, frame)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected locally-known stream payload: %x", got)
}
}
func TestSessionPacketConnDeliversNOPControlFrames(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
nop := mustMarshalAdaptorFrameForTest(t, 0, 0, 0, mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil))
if err := conn.OnMessage(nop); err != nil {
t.Fatal(err)
}
want := decodePayloadForTest(t, nop)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected nop payload: %x", got)
}
}
func TestSessionPacketConnIgnoresDuplicateAndOldFrames(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
conn.locallyKnownStreams[1] = true
frame0 := mustMarshalAdaptorFrameForTest(t, 0, 1, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("one")))
if err := conn.OnMessage(frame0); err != nil {
t.Fatal(err)
}
buf := make([]byte, len(decodePayloadForTest(t, frame0)))
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
if err := conn.OnMessage(frame0); err != nil {
t.Fatal(err)
}
conn.mu.Lock()
if conn.readBuf.Len() != 0 {
conn.mu.Unlock()
t.Fatal("duplicate old frame should be ignored")
}
conn.mu.Unlock()
frame1 := mustMarshalAdaptorFrameForTest(t, 1, 1, 1, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("two")))
if err := conn.OnMessage(frame1); err != nil {
t.Fatal(err)
}
if err := conn.OnMessage(frame1); err != nil {
t.Fatal(err)
}
want := decodePayloadForTest(t, frame1)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected duplicate handling payload: %x", got)
}
conn.mu.Lock()
if conn.readBuf.Len() != 0 {
conn.mu.Unlock()
t.Fatal("duplicate current frame should not be released twice")
}
conn.mu.Unlock()
}
func TestSessionPacketConnRoundRobinDeliveryAcrossStreams(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
conn.locallyKnownStreams[1] = true
conn.locallyKnownStreams[3] = true
frame10 := &adaptorFrame{frameID: 0, streamID: 1, streamFrameSeq: 0, smuxCmd: smuxCmdPSH, smuxVersion: 2, payload: mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("a0"))}
frame11 := &adaptorFrame{frameID: 1, streamID: 1, streamFrameSeq: 1, smuxCmd: smuxCmdPSH, smuxVersion: 2, payload: mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("a1"))}
frame30 := &adaptorFrame{frameID: 2, streamID: 3, streamFrameSeq: 0, smuxCmd: smuxCmdPSH, smuxVersion: 2, payload: mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 3, []byte("b0"))}
frame31 := &adaptorFrame{frameID: 3, streamID: 3, streamFrameSeq: 1, smuxCmd: smuxCmdPSH, smuxVersion: 2, payload: mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 3, []byte("b1"))}
conn.mu.Lock()
conn.readyFramesByStream[1] = map[uint64]*adaptorFrame{0: frame10, 1: frame11}
conn.readyFramesByStream[3] = map[uint64]*adaptorFrame{0: frame30, 1: frame31}
conn.activateStreamLocked(1)
conn.activateStreamLocked(3)
conn.deliverReadyFramesLocked()
conn.mu.Unlock()
want := append([]byte{}, frame10.payload...)
want = append(want, frame30.payload...)
want = append(want, frame11.payload...)
want = append(want, frame31.payload...)
got := make([]byte, len(want))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected round-robin order: %x", got)
}
}
func TestSessionPacketConnOnMessageIgnoresFramesAfterClose(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
if err := conn.Close(); err != nil {
t.Fatal(err)
}
frame := mustMarshalAdaptorFrameForTest(t, 0, 0, 0, mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil))
if err := conn.OnMessage(frame); err != nil {
t.Fatal(err)
}
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.readBuf.Len() != 0 {
t.Fatal("closed conn should ignore later frames")
}
}
func TestSessionPacketConnOnMessageRejectsMetadataMismatch(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
smuxFrame := mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("payload"))
badWire := encodeAdaptorFrame(&adaptorFrame{
frameID: 0,
streamID: 2,
streamFrameSeq: 0,
smuxCmd: smuxCmdPSH,
smuxVersion: 2,
payload: smuxFrame,
})
if err := conn.OnMessage(badWire); err == nil {
t.Fatal("expected metadata mismatch to be rejected")
}
}
func TestSessionPacketConnCloseWaitsForBufferedPayloadDrain(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
conn.locallyKnownStreams[1] = true
frame := mustMarshalAdaptorFrameForTest(t, 0, 1, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("hello")))
if err := conn.OnMessage(frame); err != nil {
t.Fatal(err)
}
closeDone := make(chan struct{})
go func() {
_ = conn.Close()
close(closeDone)
}()
select {
case <-closeDone:
t.Fatal("close returned before buffered payload drained")
case <-time.After(50 * time.Millisecond):
}
buf := make([]byte, len(decodePayloadForTest(t, frame)))
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, decodePayloadForTest(t, frame)) {
t.Fatalf("unexpected payload: %x", buf)
}
select {
case <-closeDone:
case <-time.After(500 * time.Millisecond):
t.Fatal("close did not finish after buffered payload drained")
}
if _, err := conn.Read(make([]byte, 1)); err != io.EOF {
t.Fatalf("expected EOF after graceful close, got %v", err)
}
}
func TestSessionPacketConnCloseRejectsWritesWhileDraining(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
conn.locallyKnownStreams[1] = true
frame := mustMarshalAdaptorFrameForTest(t, 0, 1, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("hello")))
if err := conn.OnMessage(frame); err != nil {
t.Fatal(err)
}
closeStarted := make(chan struct{})
closeDone := make(chan struct{})
go func() {
close(closeStarted)
_ = conn.Close()
close(closeDone)
}()
<-closeStarted
time.Sleep(20 * time.Millisecond)
if _, err := conn.Write(mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil)); err != io.ErrClosedPipe {
t.Fatalf("expected io.ErrClosedPipe while close is draining, got %v", err)
}
buf := make([]byte, len(decodePayloadForTest(t, frame)))
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
select {
case <-closeDone:
case <-time.After(500 * time.Millisecond):
t.Fatal("close did not finish after draining payload")
}
}
func TestSessionPacketConnCloseTimesOutWithUndeliverableFrames(t *testing.T) {
oldTimeout := sessionPacketConnCloseDrainTimeout
sessionPacketConnCloseDrainTimeout = 50 * time.Millisecond
defer func() {
sessionPacketConnCloseDrainTimeout = oldTimeout
}()
conn := newSessionPacketConn(nil, 1024)
frame := mustMarshalAdaptorFrameForTest(t, 0, 9, 0, mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 9, []byte("blocked")))
if err := conn.OnMessage(frame); err != nil {
t.Fatal(err)
}
closeDone := make(chan struct{})
go func() {
_ = conn.Close()
close(closeDone)
}()
select {
case <-closeDone:
t.Fatal("close returned before drain timeout for undeliverable frame")
case <-time.After(10 * time.Millisecond):
}
select {
case <-closeDone:
case <-time.After(500 * time.Millisecond):
t.Fatal("close did not time out for undeliverable frame")
}
if _, err := conn.Read(make([]byte, 1)); err != io.EOF {
t.Fatalf("expected EOF after close timeout, got %v", err)
}
}
func TestSessionPacketConnReadAndWriteReturnCloseError(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
wantErr := errors.New("boom")
conn.fail(wantErr)
if _, err := conn.Read(make([]byte, 1)); !errors.Is(err, wantErr) {
t.Fatalf("expected read error %v, got %v", wantErr, err)
}
if _, err := conn.Write(mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil)); !errors.Is(err, wantErr) {
t.Fatalf("expected write error %v, got %v", wantErr, err)
}
}
func TestSessionPacketConnWriteRejectsMalformedSmuxFrame(t *testing.T) {
conn, cleanup := newWritableSessionPacketConn(t)
defer cleanup()
if _, err := conn.Write([]byte{1, 2, 3}); err == nil {
t.Fatal("expected malformed smux frame write to fail")
}
if _, err := conn.Read(make([]byte, 1)); err == nil {
t.Fatal("expected malformed write to close conn with error")
}
}
func TestSessionPacketConnWriteFailsWithoutTransferChannel(t *testing.T) {
session := mustNewPacketToStreamSession(t, true)
defer func() { _ = session.Close() }()
conn := newSessionPacketConn(session, 1024)
frame := mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil)
firstErr := error(nil)
if _, err := conn.Write(frame); err == nil {
t.Fatal("expected missing transfer channel to fail")
} else {
firstErr = err
}
if _, err := conn.Read(make([]byte, 1)); !errors.Is(err, firstErr) && err.Error() != firstErr.Error() {
t.Fatalf("expected read to return write failure %v, got %v", firstErr, err)
}
}
func TestSessionPacketConnWriteTracksFrameAndStreamSequence(t *testing.T) {
conn, cleanup := newWritableSessionPacketConn(t)
defer cleanup()
frameA := mustMarshalSmuxFrameForTest(t, smuxCmdPSH, 1, []byte("a"))
frameB := mustMarshalSmuxFrameForTest(t, smuxCmdFIN, 1, nil)
frameNOP := mustMarshalSmuxFrameForTest(t, smuxCmdNOP, 0, nil)
if n, err := conn.Write(frameA); err != nil || n != len(frameA) {
t.Fatalf("unexpected first write result n=%d err=%v", n, err)
}
if n, err := conn.Write(frameB); err != nil || n != len(frameB) {
t.Fatalf("unexpected second write result n=%d err=%v", n, err)
}
if n, err := conn.Write(frameNOP); err != nil || n != len(frameNOP) {
t.Fatalf("unexpected nop write result n=%d err=%v", n, err)
}
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.nextSendFrameID != 3 {
t.Fatalf("unexpected nextSendFrameID %d", conn.nextSendFrameID)
}
if conn.nextSendStreamFrameSeq[1] != 2 {
t.Fatalf("unexpected stream frame seq for stream 1: %d", conn.nextSendStreamFrameSeq[1])
}
if conn.nextSendStreamFrameSeq[0] != 1 {
t.Fatalf("unexpected stream frame seq for control stream: %d", conn.nextSendStreamFrameSeq[0])
}
if !conn.locallyKnownStreams[1] {
t.Fatal("locally known stream should be recorded after write")
}
if conn.locallyKnownStreams[0] {
t.Fatal("control stream should not be tracked as locally known stream")
}
}
func TestSessionPacketConnLocalAddrAndDeadlines(t *testing.T) {
conn := newSessionPacketConn(nil, 1024)
defer func() {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}()
if got := conn.LocalAddr(); got == nil || got.Network() != "rrpit" || got.String() != "rrpit-local" {
t.Fatalf("unexpected local addr: %#v", got)
}
if got := conn.RemoteAddr(); got == nil || got.Network() != "rrpit" || got.String() != "rrpit-remote" {
t.Fatalf("unexpected remote addr: %#v", got)
}
if err := conn.SetDeadline(time.Now()); err != nil {
t.Fatal(err)
}
if err := conn.SetReadDeadline(time.Now()); err != nil {
t.Fatal(err)
}
if err := conn.SetWriteDeadline(time.Now()); err != nil {
t.Fatal(err)
}
}
type packetWire struct {
mu sync.Mutex
closed bool
ch chan []byte
}
func newPacketWire() *packetWire {
return &packetWire{
ch: make(chan []byte, 256),
}
}
func (w *packetWire) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return 0, io.ErrClosedPipe
}
wire := append([]byte(nil), p...)
w.ch <- wire
return len(p), nil
}
func (w *packetWire) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
w.closed = true
close(w.ch)
return nil
}
func pumpPacketWire(writer *packetWire, channel *rrpitMaterializedTransferChannel.ChannelRx, errs chan<- error) {
for wire := range writer.ch {
if err := channel.OnNewMessageArrived(wire); err != nil {
errs <- err
return
}
}
}
func mustNewPacketToStreamSession(t *testing.T, oddChannelIDs bool) *rrpitBidirectionalSession.BidirectionalSession {
t.Helper()
session, err := rrpitBidirectionalSession.New(rrpitBidirectionalSession.Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 128,
MaxBufferedLanes: 16,
OnMessage: func([]byte) error {
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 128,
MaxDataShardsPerLane: 4,
MaxBufferedLanes: 16,
MaxRewindableTimestampNum: 32,
MaxRewindableControlMessageNum: 32,
OddChannelIDs: oddChannelIDs,
},
TimestampInterval: 2 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
return session
}
func mustRecvSmuxStream(t *testing.T, streams <-chan *smux.Stream, errs <-chan error) *smux.Stream {
t.Helper()
select {
case stream := <-streams:
if stream == nil {
t.Fatal("nil smux stream")
}
return stream
case err := <-errs:
t.Fatal(err)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for smux stream")
}
return nil
}
func newWritableSessionPacketConn(t *testing.T) (*sessionPacketConn, func()) {
t.Helper()
session := mustNewPacketToStreamSession(t, true)
wire := newPacketWire()
if _, err := session.AttachTxChannel(wire); err != nil {
t.Fatal(err)
}
conn := newSessionPacketConn(session, 1024)
maxMessageSize, err := session.MaxMessageSize()
if err != nil {
t.Fatal(err)
}
conn.maxSerializedFrameBytes = maxMessageSize - adaptorHeaderSize
if conn.maxSerializedFrameBytes <= 0 {
t.Fatalf("unexpected max serialized frame bytes: %d", conn.maxSerializedFrameBytes)
}
return conn, func() {
_ = wire.Close()
_ = session.Close()
}
}
func mustNewSmuxConfigForSession(t *testing.T, session *rrpitBidirectionalSession.BidirectionalSession, version int) *smux.Config {
t.Helper()
maxMessageSize, err := session.MaxMessageSize()
if err != nil {
t.Fatal(err)
}
maxFrameSize := maxMessageSize - adaptorHeaderSize - smuxFrameHeaderSize
if maxFrameSize <= 0 {
t.Fatalf("rrpit message budget too small for smux frame: %d", maxMessageSize)
}
if maxFrameSize > 64 {
maxFrameSize = 64
}
config := smux.DefaultConfig()
config.Version = version
config.KeepAliveDisabled = true
config.MaxFrameSize = maxFrameSize
return config
}
func mustMarshalSmuxFrameForTest(t *testing.T, cmd byte, streamID uint32, payload []byte) []byte {
t.Helper()
if err := validateSmuxCommand(2, cmd, len(payload), streamID); err != nil {
t.Fatal(err)
}
frame := make([]byte, smuxFrameHeaderSize+len(payload))
frame[smuxVersionFieldOffset] = 2
frame[smuxCmdFieldOffset] = cmd
binary.LittleEndian.PutUint16(frame[smuxLengthFieldOffset:smuxLengthFieldOffset+2], uint16(len(payload)))
binary.LittleEndian.PutUint32(frame[smuxStreamIDFieldOffset:smuxStreamIDFieldOffset+4], streamID)
copy(frame[smuxFrameHeaderSize:], payload)
return frame
}
func mustMarshalAdaptorFrameForTest(t *testing.T, frameID uint64, streamID uint32, streamSeq uint64, smuxFrame []byte) []byte {
t.Helper()
meta, err := parseSmuxFrame(smuxFrame)
if err != nil {
t.Fatal(err)
}
if meta.streamID != streamID {
t.Fatalf("smux frame stream id %d does not match adaptor header %d", meta.streamID, streamID)
}
return encodeAdaptorFrame(&adaptorFrame{
frameID: frameID,
streamID: streamID,
streamFrameSeq: streamSeq,
smuxCmd: meta.cmd,
smuxVersion: meta.version,
payload: append([]byte(nil), smuxFrame...),
})
}
func decodePayloadForTest(t *testing.T, wire []byte) []byte {
t.Helper()
frame, err := decodeAdaptorFrame(wire)
if err != nil {
t.Fatal(err)
}
return frame.payload
}
@@ -0,0 +1,34 @@
package rriptMonoDirectionSession
import (
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferChannel"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferLane"
)
type SessionInstanceID [16]byte
type ControlMessage struct {
Session SessionControlMessage
FloodChannel SessionFloodChannelControlMessage
Lane SessionLaneControlMessage
Channel SessionChannelControlMessage
}
type SessionControlMessage struct {
InstanceID SessionInstanceID
}
type SessionFloodChannelControlMessage struct {
CurrentChannelID uint64
}
type SessionLaneControlMessage struct {
LaneACKTo int64
LenLaneControl uint16 `struc:"uint16,sizeof=LaneControl"`
LaneControl []rrpitTransferLane.TransferControl
}
type SessionChannelControlMessage struct {
LenChannelControl uint16 `struc:"uint16,sizeof=ChannelControl"`
ChannelControl []rrpitTransferChannel.ChannelControlMessage
}
@@ -0,0 +1,9 @@
package rriptMonoDirectionSession
import "github.com/v2fly/v2ray-core/v5/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}
@@ -0,0 +1,69 @@
package rriptMonoDirectionSession
const (
PacketKind_UNDEFINED uint8 = 0
PacketKind_InteractiveStreamData uint8 = 1
PacketKind_InteractiveStreamControl uint8 = 2
PacketKind_BackgroundStreamData uint8 = 3
PacketKind_BackgroundStreamControl uint8 = 4
PacketKind_InteractivePacketData uint8 = 5
PacketKind_InteractivePacketControl uint8 = 6
PacketKind_BackgroundPacketData uint8 = 7
PacketKind_BackgroundPacketControl uint8 = 8
PacketKind_DATA uint8 = PacketKind_InteractiveStreamData
PacketKind_CONTROL uint8 = PacketKind_InteractiveStreamControl
)
type SessionPacket struct {
PacketKind uint8
// followed by either a control packet content or data packet content
}
func PacketKindName(kind uint8) string {
switch kind {
case PacketKind_InteractiveStreamData:
return "interactive_stream_data"
case PacketKind_InteractiveStreamControl:
return "interactive_stream_control"
case PacketKind_BackgroundStreamData:
return "background_stream_data"
case PacketKind_BackgroundStreamControl:
return "background_stream_control"
case PacketKind_InteractivePacketData:
return "interactive_packet_data"
case PacketKind_InteractivePacketControl:
return "interactive_packet_control"
case PacketKind_BackgroundPacketData:
return "background_packet_data"
case PacketKind_BackgroundPacketControl:
return "background_packet_control"
default:
return "unknown"
}
}
func IsControlPacketKind(kind uint8) bool {
switch kind {
case PacketKind_InteractiveStreamControl,
PacketKind_BackgroundStreamControl,
PacketKind_InteractivePacketControl,
PacketKind_BackgroundPacketControl:
return true
default:
return false
}
}
func IsDataPacketKind(kind uint8) bool {
switch kind {
case PacketKind_InteractiveStreamData,
PacketKind_BackgroundStreamData,
PacketKind_InteractivePacketData,
PacketKind_BackgroundPacketData:
return true
default:
return false
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,445 @@
package rriptMonoDirectionSession
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
)
type directionalIntegrationConfig struct {
name string
channelCount int
shardSize int
maxDataShardsPerLane int
maxBufferedLanes int
oddChannelIDs bool
channelHistoryCapacity int
}
type directionalIntegrationHarness struct {
t *testing.T
tx *SessionTx
rx *SessionRx
writers []*recordingWriteCloser
rxChannels []*rrpitMaterializedTransferChannel.ChannelRx
txChannelIDs []uint64
nextRead []int
received [][]byte
timestamp uint64
maxDataShardsPerLane int
}
type directionalIntegrationFrame struct {
channelIndex int
raw []byte
packetKind uint8
data *sessionDataPacket
control *sessionControlPacket
}
func TestDirectionalSessionIntegrationMatrix(t *testing.T) {
configs := []directionalIntegrationConfig{
{
name: "single_channel_single_shard_odd",
channelCount: 1,
shardSize: 16,
maxDataShardsPerLane: 1,
maxBufferedLanes: 8,
oddChannelIDs: true,
channelHistoryCapacity: 16,
},
{
name: "single_channel_three_shards_even",
channelCount: 1,
shardSize: 24,
maxDataShardsPerLane: 3,
maxBufferedLanes: 8,
oddChannelIDs: false,
channelHistoryCapacity: 16,
},
{
name: "dual_channel_two_shards_odd",
channelCount: 2,
shardSize: 24,
maxDataShardsPerLane: 2,
maxBufferedLanes: 8,
oddChannelIDs: true,
channelHistoryCapacity: 16,
},
{
name: "triple_channel_three_shards_even",
channelCount: 3,
shardSize: 32,
maxDataShardsPerLane: 3,
maxBufferedLanes: 8,
oddChannelIDs: false,
channelHistoryCapacity: 16,
},
}
scenarios := []struct {
name string
run func(*testing.T, *directionalIntegrationHarness)
}{
{name: "ordered_no_loss", run: runDirectionalOrderedNoLoss},
{name: "reverse_delivery_no_loss", run: runDirectionalReverseDelivery},
{name: "drop_one_source_per_lane_repair_recovery", run: runDirectionalLossRecovery},
{name: "source_first_then_repair_and_duplicate_control", run: runDirectionalPartialControlAndDuplicates},
}
for _, config := range configs {
config := config
t.Run(config.name, func(t *testing.T) {
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
harness := newDirectionalIntegrationHarness(t, config)
scenario.run(t, harness)
})
}
})
}
}
func TestDirectionalSessionIntegrationBackpressureRelease(t *testing.T) {
harness := newDirectionalIntegrationHarness(t, directionalIntegrationConfig{
name: "backpressure_single_lane",
channelCount: 1,
shardSize: 16,
maxDataShardsPerLane: 1,
maxBufferedLanes: 1,
oddChannelIDs: true,
channelHistoryCapacity: 16,
})
first := []byte("bp-00")
second := []byte("bp-01")
harness.sendMessages(first)
harness.advanceTicks(1)
if err := harness.tx.SendMessage(second); err == nil {
t.Fatal("expected sender window to block a second lane before control acknowledgment")
}
harness.deliverFrames(harness.takePendingFrames())
harness.applyControl(harness.generateControl(), 1)
harness.assertSenderDrained()
if err := harness.tx.SendMessage(second); err != nil {
t.Fatalf("expected sender window to reopen after control acknowledgment, got %v", err)
}
harness.advanceTicks(1)
harness.deliverFrames(harness.takePendingFrames())
harness.applyControl(harness.generateControl(), 1)
harness.assertReceived([][]byte{first, second})
harness.assertSenderDrained()
}
func runDirectionalOrderedNoLoss(t *testing.T, harness *directionalIntegrationHarness) {
messages := makeDirectionalIntegrationMessages(harness.maxDataShardsPerLane * 3)
harness.sendMessages(messages...)
harness.advanceTicks(harness.expectedLaneCount(len(messages)))
frames := harness.takePendingFrames()
harness.deliverFrames(frames)
harness.applyControl(harness.generateControl(), 1)
harness.assertReceived(messages)
harness.assertSenderDrained()
}
func runDirectionalReverseDelivery(t *testing.T, harness *directionalIntegrationHarness) {
messages := makeDirectionalIntegrationMessages(harness.maxDataShardsPerLane * 3)
harness.sendMessages(messages...)
harness.advanceTicks(harness.expectedLaneCount(len(messages)))
frames := harness.takePendingFrames()
reverseDirectionalFrames(frames)
harness.deliverFrames(frames)
harness.applyControl(harness.generateControl(), 2)
harness.assertReceived(messages)
harness.assertSenderDrained()
}
func runDirectionalLossRecovery(t *testing.T, harness *directionalIntegrationHarness) {
messages := makeDirectionalIntegrationMessages(harness.maxDataShardsPerLane * 3)
harness.sendMessages(messages...)
harness.advanceTicks(harness.expectedLaneCount(len(messages)))
frames := harness.takePendingFrames()
deliver := make([]directionalIntegrationFrame, 0, len(frames))
droppedByLane := map[uint64]bool{}
for _, frame := range frames {
if frame.packetKind == PacketKind_DATA && frame.data != nil && frame.data.Transfer.TotalDataShards == 0 && !droppedByLane[frame.data.LaneID] {
droppedByLane[frame.data.LaneID] = true
continue
}
deliver = append(deliver, frame)
}
reverseDirectionalFrames(deliver)
harness.deliverFrames(deliver)
harness.applyControl(harness.generateControl(), 1)
harness.assertReceived(messages)
harness.assertSenderDrained()
}
func runDirectionalPartialControlAndDuplicates(t *testing.T, harness *directionalIntegrationHarness) {
messages := makeDirectionalIntegrationMessages(harness.maxDataShardsPerLane * 3)
harness.sendMessages(messages...)
harness.advanceTicks(harness.expectedLaneCount(len(messages)))
frames := harness.takePendingFrames()
sourceFrames := make([]directionalIntegrationFrame, 0, len(frames))
repairFrames := make([]directionalIntegrationFrame, 0, len(frames))
for _, frame := range frames {
if frame.packetKind == PacketKind_DATA && frame.data != nil && frame.data.Transfer.TotalDataShards == 0 {
sourceFrames = append(sourceFrames, frame)
continue
}
repairFrames = append(repairFrames, frame)
}
harness.deliverFrames(sourceFrames)
earlyControl := harness.generateControl()
lastLaneID := int64(harness.expectedLaneCount(len(messages)) - 1)
if earlyControl.Lane.LaneACKTo >= lastLaneID {
t.Fatalf("expected partial control to avoid acknowledging the final lane, got ack %d for last lane %d", earlyControl.Lane.LaneACKTo, lastLaneID)
}
harness.applyControl(earlyControl, 1)
duplicateRepairFrames := append([]directionalIntegrationFrame{}, repairFrames...)
if len(repairFrames) > 0 {
duplicateRepairFrames = append(duplicateRepairFrames, repairFrames[0])
}
if len(sourceFrames) > 0 {
duplicateRepairFrames = append(duplicateRepairFrames, sourceFrames[0])
}
harness.deliverFrames(duplicateRepairFrames)
harness.applyControl(harness.generateControl(), 2)
harness.assertReceived(messages)
harness.assertSenderDrained()
}
func newDirectionalIntegrationHarness(t *testing.T, config directionalIntegrationConfig) *directionalIntegrationHarness {
t.Helper()
harness := &directionalIntegrationHarness{
t: t,
maxDataShardsPerLane: config.maxDataShardsPerLane,
}
rx := mustNewSessionRx(t, SessionRxConfig{
LaneShardSize: config.shardSize,
MaxBufferedLanes: config.maxBufferedLanes,
OnMessage: func(data []byte) error {
harness.received = append(harness.received, append([]byte(nil), data...))
return nil
},
})
tx := mustNewSessionTx(t, SessionTxConfig{
LaneShardSize: config.shardSize,
MaxDataShardsPerLane: config.maxDataShardsPerLane,
MaxBufferedLanes: config.maxBufferedLanes,
MaxRewindableTimestampNum: config.channelHistoryCapacity,
MaxRewindableControlMessageNum: config.channelHistoryCapacity,
OddChannelIDs: config.oddChannelIDs,
})
harness.tx = tx
harness.rx = rx
harness.writers = make([]*recordingWriteCloser, 0, config.channelCount)
harness.rxChannels = make([]*rrpitMaterializedTransferChannel.ChannelRx, 0, config.channelCount)
harness.txChannelIDs = make([]uint64, 0, config.channelCount)
harness.nextRead = make([]int, config.channelCount)
for i := 0; i < config.channelCount; i++ {
writer := &recordingWriteCloser{}
channelID, err := tx.AttachTxChannel(writer)
if err != nil {
t.Fatal(err)
}
rxChannel, err := rx.AttachRxChannel(0)
if err != nil {
t.Fatal(err)
}
harness.writers = append(harness.writers, writer)
harness.rxChannels = append(harness.rxChannels, rxChannel)
harness.txChannelIDs = append(harness.txChannelIDs, channelID)
}
harness.learnChannelIDs()
return harness
}
func (h *directionalIntegrationHarness) learnChannelIDs() {
h.t.Helper()
if err := h.tx.FloodControlMessageToAllChannels(); err != nil {
h.t.Fatal(err)
}
frames := h.takePendingFrames()
if len(frames) != len(h.rxChannels) {
h.t.Fatalf("expected one control flood frame per attached channel, got %d", len(frames))
}
for _, frame := range frames {
if frame.packetKind != PacketKind_CONTROL || frame.control == nil {
h.t.Fatalf("expected flood learn phase to contain only control packets, got kind %d", frame.packetKind)
}
}
h.deliverFrames(frames)
for i, channel := range h.rxChannels {
if channel.ChannelID != h.txChannelIDs[i] {
h.t.Fatalf("expected rx channel %d to learn id %d, got %d", i, h.txChannelIDs[i], channel.ChannelID)
}
}
h.advanceTicks(1)
if pending := h.takePendingFrames(); len(pending) != 0 {
h.t.Fatalf("expected no pending frames after empty timestamp advance, got %d", len(pending))
}
}
func (h *directionalIntegrationHarness) sendMessages(messages ...[]byte) {
h.t.Helper()
for _, message := range messages {
if err := h.tx.SendMessage(message); err != nil {
h.t.Fatal(err)
}
}
}
func (h *directionalIntegrationHarness) advanceTicks(count int) {
h.t.Helper()
for i := 0; i < count; i++ {
h.timestamp++
if err := h.tx.OnNewTimestamp(h.timestamp); err != nil {
h.t.Fatal(err)
}
}
}
func (h *directionalIntegrationHarness) takePendingFrames() []directionalIntegrationFrame {
h.t.Helper()
frames := make([]directionalIntegrationFrame, 0)
for channelIndex, writer := range h.writers {
for h.nextRead[channelIndex] < len(writer.writes) {
raw := append([]byte(nil), writer.writes[h.nextRead[channelIndex]]...)
_, payload := splitMaterializedWire(h.t, raw)
if len(payload) == 0 {
h.t.Fatal("session payload is empty")
}
frame := directionalIntegrationFrame{
channelIndex: channelIndex,
raw: raw,
packetKind: payload[0],
}
switch frame.packetKind {
case PacketKind_DATA:
packet := mustUnmarshalSessionDataPacket(h.t, payload)
frame.data = &packet
case PacketKind_CONTROL:
packet := mustUnmarshalSessionControlPacket(h.t, payload)
frame.control = &packet
default:
h.t.Fatalf("unknown session packet kind %d", frame.packetKind)
}
frames = append(frames, frame)
h.nextRead[channelIndex] += 1
}
}
return frames
}
func (h *directionalIntegrationHarness) deliverFrames(frames []directionalIntegrationFrame) {
h.t.Helper()
for _, frame := range frames {
if err := h.rxChannels[frame.channelIndex].OnNewMessageArrived(frame.raw); err != nil {
h.t.Fatal(err)
}
}
}
func (h *directionalIntegrationHarness) generateControl() ControlMessage {
h.t.Helper()
currentChannelID := uint64(0)
for _, channel := range h.rxChannels {
if channel.ChannelID != 0 {
currentChannelID = channel.ChannelID
break
}
}
ctrl, err := h.rx.GenerateControlMessage(currentChannelID)
if err != nil {
h.t.Fatal(err)
}
if len(ctrl.Channel.ChannelControl) != len(h.rxChannels) {
h.t.Fatalf("expected channel control for all attached channels, got %d want %d", len(ctrl.Channel.ChannelControl), len(h.rxChannels))
}
return ctrl
}
func (h *directionalIntegrationHarness) applyControl(ctrl ControlMessage, copies int) {
h.t.Helper()
for i := 0; i < copies; i++ {
if err := h.tx.AcceptRemoteControlMessage(ctrl); err != nil {
h.t.Fatal(err)
}
}
}
func (h *directionalIntegrationHarness) assertReceived(expected [][]byte) {
h.t.Helper()
if diff := cmp.Diff(expected, h.received); diff != "" {
h.t.Fatalf("unexpected directional integration payloads (-want +got):\n%s", diff)
}
}
func (h *directionalIntegrationHarness) assertSenderDrained() {
h.t.Helper()
if len(h.tx.lanes) != 0 {
h.t.Fatalf("expected sender lanes to be drained, still have %d", len(h.tx.lanes))
}
}
func (h *directionalIntegrationHarness) expectedLaneCount(messageCount int) int {
return (messageCount + h.maxDataShardsPerLane - 1) / h.maxDataShardsPerLane
}
func reverseDirectionalFrames(frames []directionalIntegrationFrame) {
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
}
func makeDirectionalIntegrationMessages(count int) [][]byte {
messages := make([][]byte, 0, count)
for i := 0; i < count; i++ {
messages = append(messages, []byte(fmt.Sprintf("msg-%02d", i)))
}
return messages
}
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
package rrpit
// Rapid Reliable Packet Interactive Transport
/*
This transport is primarily created with agentic machine code generation workflow,
and requires extensive testing and tuning before generally ready to be used.
DO NOT attempt to run it unless you are a developer or explicitly asked to do so.
*/
@@ -0,0 +1,387 @@
package rrpitBidirectionalSession
import (
"bytes"
"errors"
"io"
"sync"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
)
type BidirectionalSession struct {
mu sync.Mutex
cond *sync.Cond
rx *rriptMonoDirectionSession.SessionRx
tx *rriptMonoDirectionSession.SessionTx
localSessionInstanceID rriptMonoDirectionSession.SessionInstanceID
TimestampInterval time.Duration
nextTimestamp uint64
lastControlPayload []byte
lastControlTimestamp uint64
managerHostedControlKeepaliveInterval int
sourcePacketActivity uint64
autoTickStop chan struct{}
autoTickDone chan struct{}
closeOnce sync.Once
closed bool
}
type Config struct {
Rx rriptMonoDirectionSession.SessionRxConfig
Tx rriptMonoDirectionSession.SessionTxConfig
LocalSessionInstanceID rriptMonoDirectionSession.SessionInstanceID
ValidateRemoteControl func(rriptMonoDirectionSession.ControlMessage) error
TimestampInterval time.Duration
ManagerHostedControlKeepaliveIntervalTicks int
}
const managerHostedControlKeepaliveIntervalTicks = 32
func New(config Config) (*BidirectionalSession, error) {
session := &BidirectionalSession{
TimestampInterval: config.TimestampInterval,
localSessionInstanceID: config.LocalSessionInstanceID,
}
session.cond = sync.NewCond(&session.mu)
tx, err := rriptMonoDirectionSession.NewSessionTx(config.Tx)
if err != nil {
return nil, err
}
session.tx = tx
rxConfig := config.Rx
userRemoteControlHandler := rxConfig.OnRemoteControlMsg
validateRemoteControl := config.ValidateRemoteControl
rxConfig.OnRemoteControlMsg = func(ctrl rriptMonoDirectionSession.ControlMessage) error {
if validateRemoteControl != nil {
if err := validateRemoteControl(ctrl); err != nil {
return err
}
}
session.mu.Lock()
if session.closed {
session.mu.Unlock()
return io.ErrClosedPipe
}
if err := tx.AcceptRemoteControlMessage(ctrl); err != nil {
session.mu.Unlock()
return err
}
if session.cond != nil {
session.cond.Broadcast()
}
session.mu.Unlock()
if userRemoteControlHandler != nil {
return userRemoteControlHandler(ctrl)
}
return nil
}
rx, err := rriptMonoDirectionSession.NewSessionRx(rxConfig)
if err != nil {
return nil, err
}
session.rx = rx
if config.ManagerHostedControlKeepaliveIntervalTicks <= 0 {
config.ManagerHostedControlKeepaliveIntervalTicks = managerHostedControlKeepaliveIntervalTicks
}
session.managerHostedControlKeepaliveInterval = config.ManagerHostedControlKeepaliveIntervalTicks
session.startAutoTick()
return session, nil
}
func (s *BidirectionalSession) SendMessage(data []byte) error {
if s == nil || s.tx == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
for {
if s.closed {
return io.ErrClosedPipe
}
err := s.tx.SendMessage(data)
if err == nil {
s.sourcePacketActivity += 1
return nil
}
if !errors.Is(err, rriptMonoDirectionSession.ErrTxLaneBufferFull) {
return err
}
if s.cond == nil {
return err
}
s.cond.Wait()
}
}
func (s *BidirectionalSession) OnNewTimestamp(timestamp uint64) error {
_, err := s.OnNewTimestampWithStats(timestamp)
return err
}
func (s *BidirectionalSession) OnNewTimestampWithStats(timestamp uint64) (rriptMonoDirectionSession.TickStats, error) {
if s == nil || s.tx == nil || s.rx == nil {
return rriptMonoDirectionSession.TickStats{}, nil
}
s.mu.Lock()
defer s.mu.Unlock()
if timestamp > s.nextTimestamp {
s.nextTimestamp = timestamp
}
return s.onNewTimestampLocked(timestamp)
}
func (s *BidirectionalSession) Close() error {
if s == nil {
return nil
}
s.closeOnce.Do(func() {
s.mu.Lock()
s.closed = true
if s.cond != nil {
s.cond.Broadcast()
}
s.mu.Unlock()
if s.autoTickStop != nil {
close(s.autoTickStop)
<-s.autoTickDone
}
})
return nil
}
func (s *BidirectionalSession) onNewTimestampLocked(timestamp uint64) (rriptMonoDirectionSession.TickStats, error) {
stats, err := s.tx.OnNewTimestampWithStats(timestamp)
if err != nil {
return stats, err
}
if s.tx.SendIgnoreQuota != nil {
ctrl, err := s.rx.GenerateStrippedControlMessage()
if err != nil {
return stats, err
}
ctrl = s.withLocalSessionControl(ctrl)
payload, err := rriptMonoDirectionSession.MarshalSessionControlPacket(s.tx.ControlPacketKind, ctrl)
if err != nil {
return stats, err
}
if !s.shouldSendManagerHostedControlLocked(timestamp, payload, stats) {
return stats, nil
}
stats.ControlPacketsGenerated += 1
if err := s.tx.SendIgnoreQuota(s.tx.ControlPacketKind, payload); err != nil {
return stats, err
}
stats.ControlPacketsSent += 1
s.lastControlPayload = append(s.lastControlPayload[:0], payload...)
s.lastControlTimestamp = timestamp
return stats, nil
}
stats.ControlPacketsGenerated += 1
if err := s.tx.FloodControlMessages(func(currentChannelID uint64) (rriptMonoDirectionSession.ControlMessage, error) {
ctrl, err := s.rx.GenerateControlMessage(currentChannelID)
if err != nil {
return rriptMonoDirectionSession.ControlMessage{}, err
}
return s.withLocalSessionControl(ctrl), nil
}); err != nil {
return stats, err
}
stats.ControlPacketsSent += uint32(s.tx.ChannelCount())
return stats, nil
}
func (s *BidirectionalSession) withLocalSessionControl(ctrl rriptMonoDirectionSession.ControlMessage) rriptMonoDirectionSession.ControlMessage {
ctrl.Session = rriptMonoDirectionSession.SessionControlMessage{
InstanceID: s.localSessionInstanceID,
}
return ctrl
}
func (s *BidirectionalSession) shouldSendManagerHostedControlLocked(
timestamp uint64,
payload []byte,
stats rriptMonoDirectionSession.TickStats,
) bool {
if len(s.lastControlPayload) == 0 {
return true
}
if !bytes.Equal(s.lastControlPayload, payload) {
return true
}
if stats.RepairPacketsGenerated > 0 || stats.RepairPacketsSent > 0 || stats.BlockedBySharedSendBudget {
return true
}
if s.lastControlTimestamp == 0 {
return true
}
return timestamp-s.lastControlTimestamp >= uint64(s.managerHostedControlKeepaliveIntervalTicks())
}
func (s *BidirectionalSession) managerHostedControlKeepaliveIntervalTicks() int {
if s == nil || s.cond == nil {
return managerHostedControlKeepaliveIntervalTicks
}
if s.tx == nil {
return managerHostedControlKeepaliveIntervalTicks
}
if s.managerHostedControlKeepaliveInterval == 0 {
return managerHostedControlKeepaliveIntervalTicks
}
return s.managerHostedControlKeepaliveInterval
}
func (s *BidirectionalSession) AttachTxChannel(closer io.WriteCloser) (channelID uint64, err error) {
return s.AttachTxChannelWithConfig(closer, rriptMonoDirectionSession.ChannelConfig{Weight: 1})
}
func (s *BidirectionalSession) ConsumeSourcePacketActivity() uint64 {
if s == nil {
return 0
}
s.mu.Lock()
defer s.mu.Unlock()
count := s.sourcePacketActivity
s.sourcePacketActivity = 0
return count
}
func (s *BidirectionalSession) SetDynamicRestrictSourceDataWhenOldestLaneStalled(enabled bool) {
if s == nil || s.tx == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.tx.SetDynamicRestrictSourceDataWhenOldestLaneStalled(enabled)
}
func (s *BidirectionalSession) RestrictSourceDataWhenOldestLaneStalledEnabled() bool {
if s == nil || s.tx == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
return s.tx.RestrictSourceDataWhenOldestLaneStalledEnabled()
}
func (s *BidirectionalSession) AttachTxChannelWithConfig(
closer io.WriteCloser,
config rriptMonoDirectionSession.ChannelConfig,
) (channelID uint64, err error) {
if s == nil || s.tx == nil || s.rx == nil {
return 0, nil
}
s.mu.Lock()
defer s.mu.Unlock()
channelID, err = s.tx.AttachTxChannelWithConfig(closer, config)
if err != nil {
return 0, err
}
return channelID, nil
}
func (s *BidirectionalSession) AttachRxChannel() (rxChannel *rrpitMaterializedTransferChannel.ChannelRx, err error) {
if s == nil || s.rx == nil {
return nil, nil
}
s.mu.Lock()
defer s.mu.Unlock()
rxChannel, err = s.rx.AttachRxChannel(0)
if err != nil {
return nil, err
}
return rxChannel, nil
}
func (s *BidirectionalSession) AttachChannel(closer io.WriteCloser) (channelID uint64, rxChannel *rrpitMaterializedTransferChannel.ChannelRx, err error) {
return s.AttachChannelWithConfig(closer, rriptMonoDirectionSession.ChannelConfig{Weight: 1})
}
func (s *BidirectionalSession) AttachChannelWithConfig(
closer io.WriteCloser,
config rriptMonoDirectionSession.ChannelConfig,
) (channelID uint64, rxChannel *rrpitMaterializedTransferChannel.ChannelRx, err error) {
if s == nil {
return 0, nil, nil
}
channelID, err = s.AttachTxChannelWithConfig(closer, config)
if err != nil {
return 0, nil, err
}
rxChannel, err = s.AttachRxChannel()
if err != nil {
return 0, nil, err
}
return channelID, rxChannel, nil
}
func (s *BidirectionalSession) Rx() *rriptMonoDirectionSession.SessionRx {
if s == nil {
return nil
}
return s.rx
}
func (s *BidirectionalSession) OnLogicalPacket(data []byte) error {
if s == nil || s.rx == nil {
return io.ErrClosedPipe
}
return s.rx.OnLogicalPacket(data)
}
func (s *BidirectionalSession) Tx() *rriptMonoDirectionSession.SessionTx {
if s == nil {
return nil
}
return s.tx
}
func (s *BidirectionalSession) MaxMessageSize() (int, error) {
if s == nil || s.tx == nil {
return 0, nil
}
s.mu.Lock()
defer s.mu.Unlock()
return s.tx.MaxMessageSize()
}
func (s *BidirectionalSession) startAutoTick() {
if s == nil || s.TimestampInterval <= 0 {
return
}
s.autoTickStop = make(chan struct{})
s.autoTickDone = make(chan struct{})
go func() {
ticker := time.NewTicker(s.TimestampInterval)
defer func() {
ticker.Stop()
close(s.autoTickDone)
}()
for {
select {
case <-ticker.C:
s.mu.Lock()
s.nextTimestamp += 1
_, err := s.onNewTimestampLocked(s.nextTimestamp)
s.mu.Unlock()
if err != nil {
return
}
case <-s.autoTickStop:
return
}
}
}()
}
@@ -0,0 +1,278 @@
package rrpitBidirectionalSession
import "testing"
type bidirectionalFuzzConfig struct {
maxSourceLossPerLane int
maxRepairLossPerLane int
maxControlLossPerSender int
maxLatencyTicks int
maxMessagesPerSide int
channelHistoryCapacity int
}
type bidirectionalFuzzSeed struct {
channelCountRaw uint8
shardSizeRaw uint8
maxDataShardsRaw uint8
sourceLossRaw uint8
repairLossRaw uint8
controlLossRaw uint8
latencyRaw uint8
aOddChannelIDs bool
bOddChannelIDs bool
schedule []byte
aPayload []byte
bPayload []byte
}
func FuzzBidirectionalSessionRoundTrip(f *testing.F) {
fuzzBidirectionalSessionRoundTrip(f, bidirectionalFuzzConfig{
maxSourceLossPerLane: 3,
maxRepairLossPerLane: 3,
maxControlLossPerSender: 3,
maxLatencyTicks: 4,
maxMessagesPerSide: 6,
channelHistoryCapacity: 32,
}, []bidirectionalFuzzSeed{
{
channelCountRaw: 1,
shardSizeRaw: 16,
maxDataShardsRaw: 1,
sourceLossRaw: 0,
repairLossRaw: 0,
controlLossRaw: 0,
latencyRaw: 0,
aOddChannelIDs: true,
bOddChannelIDs: false,
schedule: []byte{0, 1, 2, 3},
aPayload: []byte("alpha"),
bPayload: []byte("bravo"),
},
{
channelCountRaw: 2,
shardSizeRaw: 24,
maxDataShardsRaw: 2,
sourceLossRaw: 1,
repairLossRaw: 1,
controlLossRaw: 1,
latencyRaw: 2,
aOddChannelIDs: false,
bOddChannelIDs: true,
schedule: []byte{5, 4, 3, 2, 1},
aPayload: []byte("hello-world"),
bPayload: []byte("goodbye-moon"),
},
{
channelCountRaw: 3,
shardSizeRaw: 32,
maxDataShardsRaw: 3,
sourceLossRaw: 2,
repairLossRaw: 1,
controlLossRaw: 2,
latencyRaw: 3,
aOddChannelIDs: true,
bOddChannelIDs: false,
schedule: []byte{9, 7, 5, 3, 1, 0},
aPayload: []byte("abcdefghijk"),
bPayload: []byte("lmnopqrstuv"),
},
})
}
func FuzzBidirectionalSessionRoundTripHighLoss(f *testing.F) {
fuzzBidirectionalSessionRoundTrip(f, bidirectionalFuzzConfig{
maxSourceLossPerLane: 7,
maxRepairLossPerLane: 6,
maxControlLossPerSender: 6,
maxLatencyTicks: 8,
maxMessagesPerSide: 12,
channelHistoryCapacity: 128,
}, []bidirectionalFuzzSeed{
{
channelCountRaw: 1,
shardSizeRaw: 32,
maxDataShardsRaw: 3,
sourceLossRaw: 6,
repairLossRaw: 5,
controlLossRaw: 4,
latencyRaw: 7,
aOddChannelIDs: true,
bOddChannelIDs: false,
schedule: []byte{8, 6, 7, 5, 3, 0, 9},
aPayload: []byte("high-loss-alpha-seed"),
bPayload: []byte("high-loss-bravo-seed"),
},
{
channelCountRaw: 2,
shardSizeRaw: 30,
maxDataShardsRaw: 2,
sourceLossRaw: 7,
repairLossRaw: 6,
controlLossRaw: 5,
latencyRaw: 8,
aOddChannelIDs: false,
bOddChannelIDs: true,
schedule: []byte{1, 4, 1, 4, 2, 1, 3, 5, 8},
aPayload: []byte("the-quick-brown-fox-jumps"),
bPayload: []byte("over-the-lazy-dog-twice"),
},
{
channelCountRaw: 3,
shardSizeRaw: 28,
maxDataShardsRaw: 3,
sourceLossRaw: 5,
repairLossRaw: 6,
controlLossRaw: 6,
latencyRaw: 6,
aOddChannelIDs: true,
bOddChannelIDs: false,
schedule: []byte{2, 7, 1, 8, 2, 8, 1, 8},
aPayload: []byte("packets-may-drop-many-times-before-repair"),
bPayload: []byte("control-latency-and-loss-should-still-converge"),
},
})
}
func fuzzBidirectionalSessionRoundTrip(
f *testing.F,
config bidirectionalFuzzConfig,
seeds []bidirectionalFuzzSeed,
) {
for _, seed := range seeds {
f.Add(
seed.channelCountRaw,
seed.shardSizeRaw,
seed.maxDataShardsRaw,
seed.sourceLossRaw,
seed.repairLossRaw,
seed.controlLossRaw,
seed.latencyRaw,
seed.aOddChannelIDs,
seed.bOddChannelIDs,
seed.schedule,
seed.aPayload,
seed.bPayload,
)
}
f.Fuzz(func(
t *testing.T,
channelCountRaw uint8,
shardSizeRaw uint8,
maxDataShardsRaw uint8,
sourceLossRaw uint8,
repairLossRaw uint8,
controlLossRaw uint8,
latencyRaw uint8,
aOddChannelIDs bool,
bOddChannelIDs bool,
schedule []byte,
aPayload []byte,
bPayload []byte,
) {
channelCount := int(channelCountRaw%3) + 1
maxDataShardsPerLane := int(maxDataShardsRaw%3) + 1
shardSize := int(shardSizeRaw%25) + 8
profile := bidirectionalTickedNetworkProfile{
maxSourceLossPerLane: boundedFuzzValue(sourceLossRaw, config.maxSourceLossPerLane),
maxRepairLossPerLane: boundedFuzzValue(repairLossRaw, config.maxRepairLossPerLane),
maxControlLossPerSender: boundedFuzzValue(controlLossRaw, config.maxControlLossPerSender),
maxLatencyTicks: boundedFuzzValue(latencyRaw, config.maxLatencyTicks),
}
maxMessageLen := shardSize - 2
if maxMessageLen < 1 {
t.Skip()
}
aMessages := fuzzMessagesFromBytes(aPayload, maxMessageLen, config.maxMessagesPerSide, 'a')
bMessages := fuzzMessagesFromBytes(bPayload, maxMessageLen, config.maxMessagesPerSide, 'b')
maxBufferedLanes := max(
2,
max(
expectedLaneCountForConfig(len(aMessages), maxDataShardsPerLane),
expectedLaneCountForConfig(len(bMessages), maxDataShardsPerLane),
)+1,
)
h := newBidirectionalIntegrationHarness(t, bidirectionalIntegrationConfig{
name: "fuzz",
channelCount: channelCount,
shardSize: shardSize,
maxDataShardsPerLane: maxDataShardsPerLane,
maxBufferedLanes: maxBufferedLanes,
channelHistoryCapacity: config.channelHistoryCapacity,
aOddChannelIDs: aOddChannelIDs,
bOddChannelIDs: bOddChannelIDs,
})
h.bootstrapAndLearn()
h.sendMessages(h.a, aMessages...)
h.sendMessages(h.b, bMessages...)
settleTicks := h.settleTicksForProfile(profile)
h.runUntilExactDelivery(
bMessages,
aMessages,
newBidirectionalTickedNetwork(profile, schedule),
h.maxTicksForProfile(len(aMessages), len(bMessages), profile, settleTicks),
settleTicks,
)
h.assertReceived(h.a, bMessages)
h.assertReceived(h.b, aMessages)
h.assertControlCallbacksObserved()
})
}
func boundedFuzzValue(raw uint8, maxValue int) int {
if maxValue <= 0 {
return 0
}
return int(raw % uint8(maxValue+1))
}
func fuzzMessagesFromBytes(data []byte, maxMessageLen int, maxMessages int, fallback byte) [][]byte {
if maxMessageLen < 1 || maxMessages < 1 {
return [][]byte{{fallback}}
}
if len(data) == 0 {
return [][]byte{{fallback}}
}
messages := make([][]byte, 0, maxMessages)
cursor := 0
for cursor < len(data) && len(messages) < maxMessages {
size := int(data[cursor]%uint8(maxMessageLen)) + 1
cursor += 1
remaining := len(data) - cursor
if remaining <= 0 {
messages = append(messages, []byte{fallback})
continue
}
if size > remaining {
size = remaining
}
messages = append(messages, append([]byte(nil), data[cursor:cursor+size]...))
cursor += size
}
if len(messages) == 0 {
return [][]byte{{fallback}}
}
return messages
}
func rotateBidirectionalFrames(frames []bidirectionalIntegrationFrame, offset int) []bidirectionalIntegrationFrame {
if len(frames) == 0 || offset == 0 {
return frames
}
offset %= len(frames)
if offset < 0 {
offset += len(frames)
}
rotated := append([]bidirectionalIntegrationFrame{}, frames[offset:]...)
rotated = append(rotated, frames[:offset]...)
return rotated
}
@@ -0,0 +1,879 @@
package rrpitBidirectionalSession
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/lunixbochs/struc"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferLane"
)
type bidirectionalIntegrationConfig struct {
name string
channelCount int
shardSize int
maxDataShardsPerLane int
maxBufferedLanes int
channelHistoryCapacity int
aOddChannelIDs bool
bOddChannelIDs bool
}
type bidirectionalIntegrationHarness struct {
t *testing.T
config bidirectionalIntegrationConfig
timestamp uint64
a *bidirectionalIntegrationPeer
b *bidirectionalIntegrationPeer
}
type bidirectionalIntegrationPeer struct {
name string
session *BidirectionalSession
outboundWriters []*recordingWriteCloser
inboundChannels []*rrpitMaterializedTransferChannel.ChannelRx
txChannelIDs []uint64
nextRead []int
received [][]byte
seenControl []rriptMonoDirectionSession.ControlMessage
}
type bidirectionalIntegrationFrame struct {
from *bidirectionalIntegrationPeer
to *bidirectionalIntegrationPeer
channelIndex int
raw []byte
packetKind uint8
data *sessionDataPacketForIntegrationTest
control *sessionControlPacketForTest
}
type sessionDataPacketForIntegrationTest struct {
PacketKind uint8
LaneID uint64
Transfer rrpitTransferLane.TransferData
}
type bidirectionalTickedNetworkProfile struct {
maxSourceLossPerLane int
maxRepairLossPerLane int
maxControlLossPerSender int
maxLatencyTicks int
}
type bidirectionalScheduledFrame struct {
deliverAt uint64
frame bidirectionalIntegrationFrame
}
type bidirectionalTickedNetwork struct {
profile bidirectionalTickedNetworkProfile
schedule []byte
cursor int
sourceLossBudget map[string]int
repairLossBudget map[string]int
controlLossBudget map[string]int
pending []bidirectionalScheduledFrame
}
const materializedChannelSequenceFieldLengthForIntegration = 8
func TestBidirectionalSessionIntegrationMatrix(t *testing.T) {
configs := []bidirectionalIntegrationConfig{
{
name: "single_channel_single_shard",
channelCount: 1,
shardSize: 16,
maxDataShardsPerLane: 1,
maxBufferedLanes: 8,
channelHistoryCapacity: 16,
aOddChannelIDs: true,
bOddChannelIDs: false,
},
{
name: "dual_channel_dual_shard",
channelCount: 2,
shardSize: 24,
maxDataShardsPerLane: 2,
maxBufferedLanes: 8,
channelHistoryCapacity: 16,
aOddChannelIDs: false,
bOddChannelIDs: true,
},
{
name: "triple_channel_triple_shard",
channelCount: 3,
shardSize: 32,
maxDataShardsPerLane: 3,
maxBufferedLanes: 8,
channelHistoryCapacity: 16,
aOddChannelIDs: true,
bOddChannelIDs: false,
},
}
scenarios := []struct {
name string
run func(*testing.T, *bidirectionalIntegrationHarness)
}{
{name: "control_learning_and_hints", run: runBidirectionalControlLearningAndHints},
{name: "ordered_round_trip", run: runBidirectionalOrderedRoundTrip},
{name: "reverse_delivery_round_trip", run: runBidirectionalReverseRoundTrip},
{name: "loss_repair_and_duplicate_control", run: runBidirectionalLossRepairAndDuplicateControl},
{name: "eventual_delivery_with_loss_and_latency", run: runBidirectionalEventualDeliveryWithLossAndLatency},
}
for _, config := range configs {
config := config
t.Run(config.name, func(t *testing.T) {
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
h := newBidirectionalIntegrationHarness(t, config)
scenario.run(t, h)
})
}
})
}
}
func TestBidirectionalSessionIntegrationBackpressureRelease(t *testing.T) {
h := newBidirectionalIntegrationHarness(t, bidirectionalIntegrationConfig{
name: "backpressure_release",
channelCount: 1,
shardSize: 16,
maxDataShardsPerLane: 1,
maxBufferedLanes: 1,
channelHistoryCapacity: 16,
aOddChannelIDs: true,
bOddChannelIDs: false,
})
h.bootstrapAndLearn()
firstA := []byte("a-00")
firstB := []byte("b-00")
secondA := []byte("a-01")
secondB := []byte("b-01")
h.sendMessages(h.a, firstA)
h.sendMessages(h.b, firstB)
h.advanceBothTicks(1)
h.deliverFrames(h.takeAllPendingFrames())
aResult := make(chan error, 1)
bResult := make(chan error, 1)
go func() {
aResult <- h.a.session.SendMessage(secondA)
}()
go func() {
bResult <- h.b.session.SendMessage(secondB)
}()
select {
case err := <-aResult:
t.Fatalf("expected sender A to block before control acknowledgment, got %v", err)
case <-time.After(10 * time.Millisecond):
}
select {
case err := <-bResult:
t.Fatalf("expected sender B to block before control acknowledgment, got %v", err)
case <-time.After(10 * time.Millisecond):
}
h.advanceBothTicks(1)
h.deliverFrames(h.takeAllPendingFrames())
select {
case err := <-aResult:
if err != nil {
t.Fatalf("expected sender A to unblock successfully, got %v", err)
}
case <-time.After(200 * time.Millisecond):
t.Fatal("expected sender A to unblock after control acknowledgment")
}
select {
case err := <-bResult:
if err != nil {
t.Fatalf("expected sender B to unblock successfully, got %v", err)
}
case <-time.After(200 * time.Millisecond):
t.Fatal("expected sender B to unblock after control acknowledgment")
}
h.advanceBothTicks(1)
h.deliverFrames(h.takeAllPendingFrames())
h.advanceBothTicks(1)
h.deliverFrames(h.takeAllPendingFrames())
h.assertReceived(h.a, [][]byte{firstB, secondB})
h.assertReceived(h.b, [][]byte{firstA, secondA})
h.assertControlCallbacksObserved()
}
func runBidirectionalControlLearningAndHints(t *testing.T, h *bidirectionalIntegrationHarness) {
h.bootstrapAndLearn()
h.advanceBothTicks(1)
frames := h.takeAllPendingFrames()
if len(frames) != h.config.channelCount*2 {
t.Fatalf("expected one flooded control per direction/channel after bootstrap, got %d", len(frames))
}
for _, frame := range frames {
if frame.packetKind != rriptMonoDirectionSession.PacketKind_CONTROL || frame.control == nil {
t.Fatalf("expected control frame after empty tick, got kind %d", frame.packetKind)
}
if int(frame.control.Control.Channel.LenChannelControl) != h.config.channelCount {
t.Fatalf("expected %d channel controls after learning ids, got len field %d", h.config.channelCount, frame.control.Control.Channel.LenChannelControl)
}
}
reverseBidirectionalFrames(frames)
h.deliverFrames(frames)
h.assertControlCallbacksObserved()
}
func runBidirectionalOrderedRoundTrip(t *testing.T, h *bidirectionalIntegrationHarness) {
h.bootstrapAndLearn()
aToB := makeBidirectionalMessages("a", h.config.maxDataShardsPerLane*3+1)
bToA := makeBidirectionalMessages("b", h.config.maxDataShardsPerLane*2+1)
h.sendMessages(h.a, aToB...)
h.sendMessages(h.b, bToA...)
h.completeExchange(max(h.expectedLaneCount(len(aToB)), h.expectedLaneCount(len(bToA))), nil, nil)
h.assertReceived(h.a, bToA)
h.assertReceived(h.b, aToB)
h.assertControlCallbacksObserved()
}
func runBidirectionalReverseRoundTrip(t *testing.T, h *bidirectionalIntegrationHarness) {
h.bootstrapAndLearn()
aToB := makeBidirectionalMessages("ax", h.config.maxDataShardsPerLane*3+1)
bToA := makeBidirectionalMessages("bx", h.config.maxDataShardsPerLane*3)
h.sendMessages(h.a, aToB...)
h.sendMessages(h.b, bToA...)
h.completeExchange(
max(h.expectedLaneCount(len(aToB)), h.expectedLaneCount(len(bToA))),
func(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
reverseBidirectionalFrames(frames)
return frames
},
func(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
reverseBidirectionalFrames(frames)
return frames
},
)
h.assertReceived(h.a, bToA)
h.assertReceived(h.b, aToB)
h.assertControlCallbacksObserved()
}
func runBidirectionalLossRepairAndDuplicateControl(t *testing.T, h *bidirectionalIntegrationHarness) {
h.bootstrapAndLearn()
aToB := makeBidirectionalMessages("la", h.config.maxDataShardsPerLane*3)
bToA := makeBidirectionalMessages("lb", h.config.maxDataShardsPerLane*3)
h.sendMessages(h.a, aToB...)
h.sendMessages(h.b, bToA...)
h.completeExchange(
max(h.expectedLaneCount(len(aToB)), h.expectedLaneCount(len(bToA))),
func(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
frames = dropFirstSourceShardPerLane(frames)
frames = duplicateFirstRepairAndControlPerSender(frames)
reverseBidirectionalFrames(frames)
return frames
},
func(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
frames = duplicateFirstControlPerSender(frames)
reverseBidirectionalFrames(frames)
return frames
},
)
h.assertReceived(h.a, bToA)
h.assertReceived(h.b, aToB)
h.assertControlCallbacksObserved()
}
func runBidirectionalEventualDeliveryWithLossAndLatency(t *testing.T, h *bidirectionalIntegrationHarness) {
h.bootstrapAndLearn()
aToB := makeBidirectionalMessages("ea", h.config.maxDataShardsPerLane*3+2)
bToA := makeBidirectionalMessages("eb", h.config.maxDataShardsPerLane*3+1)
h.sendMessages(h.a, aToB...)
h.sendMessages(h.b, bToA...)
profile := bidirectionalTickedNetworkProfile{
maxSourceLossPerLane: 2,
maxRepairLossPerLane: 1,
maxControlLossPerSender: 2,
maxLatencyTicks: 3,
}
network := newBidirectionalTickedNetwork(profile, []byte{3, 1, 4, 1, 5, 9, 2, 6})
settleTicks := h.settleTicksForProfile(profile)
h.runUntilExactDelivery(
bToA,
aToB,
network,
h.maxTicksForProfile(len(aToB), len(bToA), profile, settleTicks),
settleTicks,
)
h.assertControlCallbacksObserved()
}
func newBidirectionalIntegrationHarness(t *testing.T, config bidirectionalIntegrationConfig) *bidirectionalIntegrationHarness {
t.Helper()
h := &bidirectionalIntegrationHarness{
t: t,
config: config,
}
h.a = h.newPeer("a", config.aOddChannelIDs)
h.b = h.newPeer("b", config.bOddChannelIDs)
h.a.nextRead = make([]int, config.channelCount)
h.b.nextRead = make([]int, config.channelCount)
for i := 0; i < config.channelCount; i++ {
aWriter := &recordingWriteCloser{}
aChannelID, err := h.a.session.AttachTxChannel(aWriter)
if err != nil {
t.Fatal(err)
}
bRx, err := h.b.session.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
bWriter := &recordingWriteCloser{}
bChannelID, err := h.b.session.AttachTxChannel(bWriter)
if err != nil {
t.Fatal(err)
}
aRx, err := h.a.session.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
h.a.outboundWriters = append(h.a.outboundWriters, aWriter)
h.a.txChannelIDs = append(h.a.txChannelIDs, aChannelID)
h.a.inboundChannels = append(h.a.inboundChannels, aRx)
h.b.outboundWriters = append(h.b.outboundWriters, bWriter)
h.b.txChannelIDs = append(h.b.txChannelIDs, bChannelID)
h.b.inboundChannels = append(h.b.inboundChannels, bRx)
}
return h
}
func (h *bidirectionalIntegrationHarness) newPeer(name string, oddChannelIDs bool) *bidirectionalIntegrationPeer {
peer := &bidirectionalIntegrationPeer{name: name}
peer.session = mustNewBidirectionalSession(h.t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: h.config.shardSize,
MaxBufferedLanes: h.config.maxBufferedLanes,
OnMessage: func(data []byte) error {
peer.received = append(peer.received, append([]byte(nil), data...))
return nil
},
OnRemoteControlMsg: func(ctrl rriptMonoDirectionSession.ControlMessage) error {
peer.seenControl = append(peer.seenControl, ctrl)
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: h.config.shardSize,
MaxDataShardsPerLane: h.config.maxDataShardsPerLane,
MaxBufferedLanes: h.config.maxBufferedLanes,
MaxRewindableTimestampNum: h.config.channelHistoryCapacity,
MaxRewindableControlMessageNum: h.config.channelHistoryCapacity,
OddChannelIDs: oddChannelIDs,
},
})
return peer
}
func (h *bidirectionalIntegrationHarness) bootstrapAndLearn() {
h.t.Helper()
h.advanceBothTicks(1)
frames := h.takeAllPendingFrames()
if len(frames) != h.config.channelCount*2 {
h.t.Fatalf("expected one control flood per direction/channel during bootstrap, got %d", len(frames))
}
for _, frame := range frames {
if frame.packetKind != rriptMonoDirectionSession.PacketKind_CONTROL || frame.control == nil {
h.t.Fatalf("expected bootstrap to emit only control frames, got kind %d", frame.packetKind)
}
}
h.deliverFrames(frames)
h.assertLearnedChannelIDs()
h.assertControlCallbacksObserved()
}
func (h *bidirectionalIntegrationHarness) completeExchange(
repairTicks int,
dataTransform func([]bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame,
ackTransform func([]bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame,
) {
h.t.Helper()
h.advanceBothTicks(repairTicks)
frames := h.takeAllPendingFrames()
if dataTransform != nil {
frames = dataTransform(frames)
}
h.deliverFrames(frames)
h.advanceBothTicks(1)
ackFrames := h.takeAllPendingFrames()
if ackTransform != nil {
ackFrames = ackTransform(ackFrames)
}
h.deliverFrames(ackFrames)
}
func (h *bidirectionalIntegrationHarness) sendMessages(peer *bidirectionalIntegrationPeer, messages ...[]byte) {
h.t.Helper()
for _, message := range messages {
if err := peer.session.SendMessage(message); err != nil {
h.t.Fatal(err)
}
}
}
func (h *bidirectionalIntegrationHarness) advanceBothTicks(count int) {
h.t.Helper()
for i := 0; i < count; i++ {
h.timestamp++
if err := h.a.session.OnNewTimestamp(h.timestamp); err != nil {
h.t.Fatal(err)
}
if err := h.b.session.OnNewTimestamp(h.timestamp); err != nil {
h.t.Fatal(err)
}
}
}
func (h *bidirectionalIntegrationHarness) takeAllPendingFrames() []bidirectionalIntegrationFrame {
h.t.Helper()
frames := h.takePendingFrames(h.a, h.b)
frames = append(frames, h.takePendingFrames(h.b, h.a)...)
return frames
}
func (h *bidirectionalIntegrationHarness) takePendingFrames(
from *bidirectionalIntegrationPeer,
to *bidirectionalIntegrationPeer,
) []bidirectionalIntegrationFrame {
h.t.Helper()
frames := make([]bidirectionalIntegrationFrame, 0)
for channelIndex, writer := range from.outboundWriters {
for from.nextRead[channelIndex] < len(writer.writes) {
raw := append([]byte(nil), writer.writes[from.nextRead[channelIndex]]...)
_, payload := splitMaterializedWireForIntegration(h.t, raw)
if len(payload) == 0 {
h.t.Fatal("empty materialized session payload")
}
frame := bidirectionalIntegrationFrame{
from: from,
to: to,
channelIndex: channelIndex,
raw: raw,
packetKind: payload[0],
}
switch payload[0] {
case rriptMonoDirectionSession.PacketKind_DATA:
packet := mustUnmarshalSessionDataPacketForIntegration(h.t, payload)
frame.data = &packet
case rriptMonoDirectionSession.PacketKind_CONTROL:
packet := mustUnmarshalSessionControlPacketForIntegration(h.t, payload)
frame.control = &packet
expectedChannelID := from.txChannelIDs[channelIndex]
if packet.Control.FloodChannel.CurrentChannelID != expectedChannelID {
h.t.Fatalf(
"expected flooded control on %s channel %d to carry id %d, got %d",
from.name,
channelIndex,
expectedChannelID,
packet.Control.FloodChannel.CurrentChannelID,
)
}
default:
h.t.Fatalf("unknown session packet kind %d", payload[0])
}
frames = append(frames, frame)
from.nextRead[channelIndex] += 1
}
}
return frames
}
func (h *bidirectionalIntegrationHarness) deliverFrames(frames []bidirectionalIntegrationFrame) {
h.t.Helper()
for _, frame := range frames {
if err := frame.to.inboundChannels[frame.channelIndex].OnNewMessageArrived(frame.raw); err != nil {
h.t.Fatal(err)
}
}
}
func (h *bidirectionalIntegrationHarness) assertLearnedChannelIDs() {
h.t.Helper()
h.assertPeerLearnedChannelIDs(h.a, h.b.txChannelIDs)
h.assertPeerLearnedChannelIDs(h.b, h.a.txChannelIDs)
}
func (h *bidirectionalIntegrationHarness) assertPeerLearnedChannelIDs(peer *bidirectionalIntegrationPeer, expected []uint64) {
h.t.Helper()
seen := map[uint64]bool{}
for i, channel := range peer.inboundChannels {
if channel.ChannelID != expected[i] {
h.t.Fatalf("expected %s inbound channel %d to learn id %d, got %d", peer.name, i, expected[i], channel.ChannelID)
}
if channel.ChannelID == 0 {
h.t.Fatalf("expected %s inbound channel %d to learn a non-zero id", peer.name, i)
}
if seen[channel.ChannelID] {
h.t.Fatalf("expected %s inbound learned ids to stay unique, got duplicate %d", peer.name, channel.ChannelID)
}
seen[channel.ChannelID] = true
}
}
func (h *bidirectionalIntegrationHarness) assertReceived(peer *bidirectionalIntegrationPeer, expected [][]byte) {
h.t.Helper()
if diff := cmp.Diff(expected, peer.received); diff != "" {
h.t.Fatalf("unexpected payloads received by %s (-want +got):\n%s", peer.name, diff)
}
}
func (h *bidirectionalIntegrationHarness) assertControlCallbacksObserved() {
h.t.Helper()
if len(h.a.seenControl) == 0 {
h.t.Fatal("expected peer A to observe remote control callbacks")
}
if len(h.b.seenControl) == 0 {
h.t.Fatal("expected peer B to observe remote control callbacks")
}
}
func (h *bidirectionalIntegrationHarness) runUntilExactDelivery(
expectedAtA [][]byte,
expectedAtB [][]byte,
network *bidirectionalTickedNetwork,
maxTicks int,
settleTicks int,
) {
h.t.Helper()
exactTicks := 0
for i := 0; i < maxTicks; i++ {
h.advanceBothTicks(1)
network.ingestFrames(h.takeAllPendingFrames(), h.timestamp)
h.deliverFrames(network.takeDueFrames(h.timestamp))
aExact := h.assertReceivedPrefix(h.a, expectedAtA)
bExact := h.assertReceivedPrefix(h.b, expectedAtB)
if aExact && bExact {
exactTicks += 1
if exactTicks >= settleTicks {
return
}
continue
}
exactTicks = 0
}
h.assertReceived(h.a, expectedAtA)
h.assertReceived(h.b, expectedAtB)
h.t.Fatalf("exact delivery did not stabilize within %d ticks", maxTicks)
}
func (h *bidirectionalIntegrationHarness) assertReceivedPrefix(
peer *bidirectionalIntegrationPeer,
expected [][]byte,
) bool {
h.t.Helper()
if len(peer.received) > len(expected) {
h.t.Fatalf(
"peer %s received too many payloads: got %d want at most %d",
peer.name,
len(peer.received),
len(expected),
)
}
for i := range peer.received {
if diff := cmp.Diff(expected[i], peer.received[i]); diff != "" {
h.t.Fatalf(
"peer %s received payload %d out of order or duplicated (-want +got):\n%s",
peer.name,
i,
diff,
)
}
}
return len(peer.received) == len(expected)
}
func (h *bidirectionalIntegrationHarness) settleTicksForProfile(profile bidirectionalTickedNetworkProfile) int {
return profile.maxLatencyTicks + profile.maxControlLossPerSender + 3
}
func (h *bidirectionalIntegrationHarness) maxTicksForProfile(
aMessageCount int,
bMessageCount int,
profile bidirectionalTickedNetworkProfile,
settleTicks int,
) int {
lanes := max(h.expectedLaneCount(aMessageCount), h.expectedLaneCount(bMessageCount))
perLaneRepairWork := h.config.maxDataShardsPerLane + profile.maxRepairLossPerLane + 2
return 8 +
2*profile.maxLatencyTicks +
2*profile.maxControlLossPerSender +
settleTicks +
lanes*(profile.maxSourceLossPerLane+perLaneRepairWork)
}
func (h *bidirectionalIntegrationHarness) expectedLaneCount(messageCount int) int {
return expectedLaneCountForConfig(messageCount, h.config.maxDataShardsPerLane)
}
func newBidirectionalTickedNetwork(
profile bidirectionalTickedNetworkProfile,
schedule []byte,
) *bidirectionalTickedNetwork {
clonedSchedule := append([]byte(nil), schedule...)
if len(clonedSchedule) == 0 {
clonedSchedule = []byte{0}
}
return &bidirectionalTickedNetwork{
profile: profile,
schedule: clonedSchedule,
sourceLossBudget: map[string]int{},
repairLossBudget: map[string]int{},
controlLossBudget: map[string]int{},
}
}
func (n *bidirectionalTickedNetwork) ingestFrames(frames []bidirectionalIntegrationFrame, currentTick uint64) {
for _, frame := range frames {
if n.shouldDrop(frame) {
continue
}
n.pending = append(n.pending, bidirectionalScheduledFrame{
deliverAt: currentTick + uint64(n.latencyForFrame()),
frame: frame,
})
}
}
func (n *bidirectionalTickedNetwork) takeDueFrames(currentTick uint64) []bidirectionalIntegrationFrame {
due := make([]bidirectionalIntegrationFrame, 0, len(n.pending))
pending := make([]bidirectionalScheduledFrame, 0, len(n.pending))
for _, scheduled := range n.pending {
if scheduled.deliverAt <= currentTick {
due = append(due, scheduled.frame)
continue
}
pending = append(pending, scheduled)
}
n.pending = pending
if len(due) > 1 && n.nextByte()&1 == 1 {
reverseBidirectionalFrames(due)
}
if len(due) > 1 {
due = rotateBidirectionalFrames(due, int(n.nextByte())%len(due))
}
return due
}
func (n *bidirectionalTickedNetwork) shouldDrop(frame bidirectionalIntegrationFrame) bool {
key, budgets, maxLoss := n.lossBudgetState(frame)
if maxLoss <= 0 {
return false
}
remaining, found := budgets[key]
if !found {
remaining = int(n.nextByte()) % (maxLoss + 1)
budgets[key] = remaining
}
if remaining == 0 {
return false
}
budgets[key] = remaining - 1
return true
}
func (n *bidirectionalTickedNetwork) lossBudgetState(
frame bidirectionalIntegrationFrame,
) (string, map[string]int, int) {
switch {
case frame.packetKind == rriptMonoDirectionSession.PacketKind_CONTROL && frame.control != nil:
return fmt.Sprintf("%s:%d:control", frame.from.name, frame.channelIndex), n.controlLossBudget, n.profile.maxControlLossPerSender
case frame.packetKind == rriptMonoDirectionSession.PacketKind_DATA &&
frame.data != nil &&
frame.data.Transfer.TotalDataShards == 0:
return fmt.Sprintf("%s:%d:source", frame.from.name, frame.data.LaneID), n.sourceLossBudget, n.profile.maxSourceLossPerLane
case frame.packetKind == rriptMonoDirectionSession.PacketKind_DATA && frame.data != nil:
return fmt.Sprintf("%s:%d:repair", frame.from.name, frame.data.LaneID), n.repairLossBudget, n.profile.maxRepairLossPerLane
default:
return "", nil, 0
}
}
func (n *bidirectionalTickedNetwork) latencyForFrame() int {
if n.profile.maxLatencyTicks <= 0 {
return 0
}
return int(n.nextByte()) % (n.profile.maxLatencyTicks + 1)
}
func (n *bidirectionalTickedNetwork) nextByte() byte {
if len(n.schedule) == 0 {
return 0
}
value := n.schedule[n.cursor%len(n.schedule)]
n.cursor += 1
return value
}
func splitMaterializedWireForIntegration(t *testing.T, wire []byte) (uint64, []byte) {
t.Helper()
if len(wire) < materializedChannelSequenceFieldLengthForIntegration {
t.Fatalf("materialized wire too short: %d", len(wire))
}
return binary.BigEndian.Uint64(wire[:materializedChannelSequenceFieldLengthForIntegration]), append([]byte(nil), wire[materializedChannelSequenceFieldLengthForIntegration:]...)
}
func mustUnmarshalSessionDataPacketForIntegration(t *testing.T, payload []byte) sessionDataPacketForIntegrationTest {
t.Helper()
var packet sessionDataPacketForIntegrationTest
if err := struc.Unpack(bytes.NewReader(payload), &packet); err != nil {
t.Fatalf("failed to unpack session data packet: %v", err)
}
return packet
}
func mustUnmarshalSessionControlPacketForIntegration(t *testing.T, payload []byte) sessionControlPacketForTest {
t.Helper()
var packet sessionControlPacketForTest
if err := struc.Unpack(bytes.NewReader(payload), &packet); err != nil {
t.Fatalf("failed to unpack session control packet: %v", err)
}
return packet
}
func reverseBidirectionalFrames(frames []bidirectionalIntegrationFrame) {
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
}
func dropFirstSourceShardPerLane(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
seen := map[string]bool{}
filtered := make([]bidirectionalIntegrationFrame, 0, len(frames))
for _, frame := range frames {
if frame.packetKind == rriptMonoDirectionSession.PacketKind_DATA && frame.data != nil && frame.data.Transfer.TotalDataShards == 0 {
key := fmt.Sprintf("%s:%d", frame.from.name, frame.data.LaneID)
if !seen[key] {
seen[key] = true
continue
}
}
filtered = append(filtered, frame)
}
return filtered
}
func duplicateFirstRepairAndControlPerSender(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
withDuplicates := append([]bidirectionalIntegrationFrame{}, frames...)
seenRepair := map[string]bool{}
seenControl := map[string]bool{}
for _, frame := range frames {
switch {
case frame.packetKind == rriptMonoDirectionSession.PacketKind_CONTROL && frame.control != nil && !seenControl[frame.from.name]:
seenControl[frame.from.name] = true
withDuplicates = append(withDuplicates, frame)
case frame.packetKind == rriptMonoDirectionSession.PacketKind_DATA &&
frame.data != nil &&
frame.data.Transfer.TotalDataShards != 0 &&
!seenRepair[frame.from.name]:
seenRepair[frame.from.name] = true
withDuplicates = append(withDuplicates, frame)
}
}
return withDuplicates
}
func duplicateFirstControlPerSender(frames []bidirectionalIntegrationFrame) []bidirectionalIntegrationFrame {
withDuplicates := append([]bidirectionalIntegrationFrame{}, frames...)
seen := map[string]bool{}
for _, frame := range frames {
if frame.packetKind != rriptMonoDirectionSession.PacketKind_CONTROL || frame.control == nil || seen[frame.from.name] {
continue
}
seen[frame.from.name] = true
withDuplicates = append(withDuplicates, frame)
}
return withDuplicates
}
func makeBidirectionalMessages(prefix string, count int) [][]byte {
messages := make([][]byte, 0, count)
for i := 0; i < count; i++ {
messages = append(messages, []byte(fmt.Sprintf("%s-%02d", prefix, i)))
}
return messages
}
func expectedLaneCountForConfig(messageCount int, maxDataShardsPerLane int) int {
return (messageCount + maxDataShardsPerLane - 1) / maxDataShardsPerLane
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
@@ -0,0 +1,435 @@
package rrpitBidirectionalSession
import (
"bytes"
"io"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/lunixbochs/struc"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
)
func TestBidirectionalSessionCoordinatesControlFeedback(t *testing.T) {
aToB := &recordingWriteCloser{}
bToA := &recordingWriteCloser{}
var receivedA [][]byte
var receivedB [][]byte
a := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func(data []byte) error {
receivedA = append(receivedA, append([]byte(nil), data...))
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 1,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
OddChannelIDs: true,
},
})
b := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func(data []byte) error {
receivedB = append(receivedB, append([]byte(nil), data...))
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 1,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
},
})
aTxChannelID, err := a.AttachTxChannel(aToB)
if err != nil {
t.Fatal(err)
}
aRxChannel, err := a.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
bTxChannelID, err := b.AttachTxChannel(bToA)
if err != nil {
t.Fatal(err)
}
bRxChannel, err := b.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if aTxChannelID != 1 || bTxChannelID != 2 {
t.Fatalf("expected tx channel ids 1 and 2, got %d and %d", aTxChannelID, bTxChannelID)
}
if aRxChannel.ChannelID != 0 || bRxChannel.ChannelID != 0 {
t.Fatalf("expected rx channels to start with unknown id 0, got %d and %d", aRxChannel.ChannelID, bRxChannel.ChannelID)
}
if err := a.SendMessage([]byte("hello")); err != nil {
t.Fatal(err)
}
if err := a.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
pumpWrites(t, aToB, bRxChannel)
if diff := cmp.Diff([][]byte{[]byte("hello")}, receivedB); diff != "" {
t.Fatalf("unexpected payloads received by B after first round (-want +got):\n%s", diff)
}
if bRxChannel.ChannelID != aTxChannelID {
t.Fatalf("expected B rx channel to learn A tx channel id %d, got %d", aTxChannelID, bRxChannel.ChannelID)
}
if err := b.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
pumpWrites(t, bToA, aRxChannel)
if aRxChannel.ChannelID != bTxChannelID {
t.Fatalf("expected A rx channel to learn B tx channel id %d, got %d", bTxChannelID, aRxChannel.ChannelID)
}
if err := a.SendMessage([]byte("world")); err != nil {
t.Fatalf("expected reverse control feedback to free A tx window, got %v", err)
}
if err := a.OnNewTimestamp(2); err != nil {
t.Fatal(err)
}
pumpWrites(t, aToB, bRxChannel)
if diff := cmp.Diff([][]byte{[]byte("hello"), []byte("world")}, receivedB); diff != "" {
t.Fatalf("unexpected payloads received by B after second round (-want +got):\n%s", diff)
}
if len(receivedA) != 0 {
t.Fatalf("expected no payloads delivered to A in this one-way test, got %d", len(receivedA))
}
}
func TestBidirectionalSessionAttachRxChannelLearnsRemoteIDs(t *testing.T) {
session := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func([]byte) error { return nil },
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 4,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
OddChannelIDs: true,
},
})
firstChannel, err := session.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
secondChannel, err := session.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if firstChannel.ChannelID != 0 || secondChannel.ChannelID != 0 {
t.Fatalf("expected rx channels to start with unknown ids, got %d and %d", firstChannel.ChannelID, secondChannel.ChannelID)
}
firstPayload, err := marshalControlPacket(rriptMonoDirectionSession.ControlMessage{
FloodChannel: rriptMonoDirectionSession.SessionFloodChannelControlMessage{CurrentChannelID: 11},
})
if err != nil {
t.Fatal(err)
}
secondPayload, err := marshalControlPacket(rriptMonoDirectionSession.ControlMessage{
FloodChannel: rriptMonoDirectionSession.SessionFloodChannelControlMessage{CurrentChannelID: 13},
})
if err != nil {
t.Fatal(err)
}
if err := firstChannel.OnNewMessageArrived(materializedWire(0, firstPayload)); err != nil {
t.Fatal(err)
}
if err := secondChannel.OnNewMessageArrived(materializedWire(0, secondPayload)); err != nil {
t.Fatal(err)
}
if firstChannel.ChannelID != 11 || secondChannel.ChannelID != 13 {
t.Fatalf("expected learned rx channel ids 11 and 13, got %d and %d", firstChannel.ChannelID, secondChannel.ChannelID)
}
}
func TestBidirectionalSessionControlPacketsCarryLocalSessionInstanceID(t *testing.T) {
writer := &recordingWriteCloser{}
want := rriptMonoDirectionSession.SessionInstanceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
session := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func([]byte) error { return nil },
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 4,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
OddChannelIDs: true,
},
LocalSessionInstanceID: want,
})
if _, err := session.AttachTxChannel(writer); err != nil {
t.Fatal(err)
}
if err := session.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
if len(writer.writes) == 0 {
t.Fatal("expected control packet write")
}
var packet sessionControlPacketForTest
if err := struc.Unpack(bytes.NewReader(writer.writes[0][8:]), &packet); err != nil {
t.Fatal(err)
}
if packet.Control.Session.InstanceID != want {
t.Fatalf("unexpected session instance id: got %x want %x", packet.Control.Session.InstanceID, want)
}
}
func TestBidirectionalSessionAutoTicks(t *testing.T) {
aToB := &recordingWriteCloser{}
bToA := &recordingWriteCloser{}
var receivedB [][]byte
a := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func([]byte) error { return nil },
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 1,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
OddChannelIDs: true,
},
TimestampInterval: time.Millisecond,
})
defer func() {
if err := a.Close(); err != nil {
t.Fatal(err)
}
}()
b := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func(data []byte) error {
receivedB = append(receivedB, append([]byte(nil), data...))
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 1,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
},
TimestampInterval: time.Millisecond,
})
defer func() {
if err := b.Close(); err != nil {
t.Fatal(err)
}
}()
if _, err := a.AttachTxChannel(aToB); err != nil {
t.Fatal(err)
}
aRxChannel, err := a.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if _, err := b.AttachTxChannel(bToA); err != nil {
t.Fatal(err)
}
bRxChannel, err := b.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if err := a.SendMessage([]byte("hello")); err != nil {
t.Fatal(err)
}
pumpWrites(t, aToB, bRxChannel)
deadline := time.Now().Add(250 * time.Millisecond)
secondResult := make(chan error, 1)
go func() {
secondResult <- a.SendMessage([]byte("world"))
}()
secondSent := false
for time.Now().Before(deadline) {
pumpWrites(t, bToA, aRxChannel)
pumpWrites(t, aToB, bRxChannel)
if !secondSent {
select {
case err := <-secondResult:
if err != nil {
t.Fatalf("expected blocked SendMessage to succeed once feedback arrives, got %v", err)
}
secondSent = true
default:
}
}
if secondSent && len(receivedB) == 2 {
break
}
time.Sleep(2 * time.Millisecond)
}
if !secondSent {
t.Fatal("expected background ticks to release tx backpressure")
}
if diff := cmp.Diff([][]byte{[]byte("hello"), []byte("world")}, receivedB); diff != "" {
t.Fatalf("unexpected payloads received by B (-want +got):\n%s", diff)
}
}
func TestBidirectionalSessionSuppressesDuplicateManagerHostedControl(t *testing.T) {
var sent [][]byte
session := mustNewBidirectionalSession(t, Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func([]byte) error { return nil },
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 1,
MaxRewindableTimestampNum: 4,
MaxRewindableControlMessageNum: 4,
DataPacketKind: rriptMonoDirectionSession.PacketKind_InteractiveStreamData,
ControlPacketKind: rriptMonoDirectionSession.PacketKind_InteractiveStreamControl,
SendIgnoreQuota: func(_ uint8, payload []byte) error {
sent = append(sent, append([]byte(nil), payload...))
return nil
},
},
ManagerHostedControlKeepaliveIntervalTicks: 2,
})
firstStats, err := session.OnNewTimestampWithStats(1)
if err != nil {
t.Fatal(err)
}
if firstStats.ControlPacketsSent != 1 || len(sent) != 1 {
t.Fatalf("expected first manager-hosted tick to send one control packet, stats=%+v sends=%d", firstStats, len(sent))
}
secondStats, err := session.OnNewTimestampWithStats(2)
if err != nil {
t.Fatal(err)
}
if secondStats.ControlPacketsSent != 0 || len(sent) != 1 {
t.Fatalf("expected duplicate control to be suppressed, stats=%+v sends=%d", secondStats, len(sent))
}
thirdStats, err := session.OnNewTimestampWithStats(3)
if err != nil {
t.Fatal(err)
}
if thirdStats.ControlPacketsSent != 1 || len(sent) != 2 {
t.Fatalf("expected keepalive control resend after suppression interval, stats=%+v sends=%d", thirdStats, len(sent))
}
}
type recordingWriteCloser struct {
writes [][]byte
readTo int
}
func (w *recordingWriteCloser) Write(p []byte) (int, error) {
w.writes = append(w.writes, append([]byte(nil), p...))
return len(p), nil
}
func (w *recordingWriteCloser) Close() error {
return nil
}
func pumpWrites(t *testing.T, writer *recordingWriteCloser, channel *rrpitMaterializedTransferChannel.ChannelRx) {
t.Helper()
for writer.readTo < len(writer.writes) {
if err := channel.OnNewMessageArrived(writer.writes[writer.readTo]); err != nil {
t.Fatal(err)
}
writer.readTo += 1
}
}
func materializedWire(seq uint64, payload []byte) []byte {
wire := make([]byte, 8+len(payload))
wire[0] = byte(seq >> 56)
wire[1] = byte(seq >> 48)
wire[2] = byte(seq >> 40)
wire[3] = byte(seq >> 32)
wire[4] = byte(seq >> 24)
wire[5] = byte(seq >> 16)
wire[6] = byte(seq >> 8)
wire[7] = byte(seq)
copy(wire[8:], payload)
return wire
}
func marshalControlPacket(ctrl rriptMonoDirectionSession.ControlMessage) ([]byte, error) {
buffer := bytes.NewBuffer(nil)
if err := struc.Pack(buffer, &sessionControlPacketForTest{
PacketKind: rriptMonoDirectionSession.PacketKind_CONTROL,
Control: ctrl,
}); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func mustNewBidirectionalSession(t *testing.T, config Config) *BidirectionalSession {
t.Helper()
session, err := New(config)
if err != nil {
t.Fatal(err)
}
return session
}
type sessionControlPacketForTest struct {
PacketKind uint8
Control rriptMonoDirectionSession.ControlMessage
}
var _ io.WriteCloser = (*recordingWriteCloser)(nil)
@@ -0,0 +1,270 @@
package rrpitBidirectionalSessionManager
import (
"io"
"sync"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitChannelManager"
)
type SessionName string
const (
InteractiveStream SessionName = "InteractiveStream"
BackgroundStream SessionName = "BackgroundStream"
)
type schedulerState int
const (
interactiveStreamPrimary schedulerState = iota
backgroundStreamPrimary
)
type Config struct {
ChannelManager *rrpitChannelManager.ChannelManager
BaseSessionConfig rrpitBidirectionalSession.Config
TimestampInterval time.Duration
InteractivePrimaryCancellationCounterLimit int
DynamicRestrictSourceDataWhenOldestLaneStalled bool
DynamicRestrictSourceDataWhenOldestLaneStalledTicks int
}
type ManagerTickStats struct {
InteractiveStream rriptMonoDirectionSession.TickStats
BackgroundStream rriptMonoDirectionSession.TickStats
}
type Manager struct {
mu sync.Mutex
channelManager *rrpitChannelManager.ChannelManager
sessions map[SessionName]*rrpitBidirectionalSession.BidirectionalSession
state schedulerState
interactivePrimaryCancellationCounter int
interactivePrimaryCancellationLimit int
dynamicRestrictSourceDataWhenOldestLaneStalled bool
dynamicRestrictSourceDataWhenOldestLaneStalledTicks int
backgroundDynamicRestrictTicksRemaining int
timestampInterval time.Duration
nextTimestamp uint64
autoTickStop chan struct{}
autoTickDone chan struct{}
closeOnce sync.Once
closed bool
}
func New(config Config) (*Manager, error) {
if config.ChannelManager == nil {
return nil, io.ErrClosedPipe
}
manager := &Manager{
channelManager: config.ChannelManager,
sessions: make(map[SessionName]*rrpitBidirectionalSession.BidirectionalSession),
state: interactiveStreamPrimary,
timestampInterval: config.TimestampInterval,
interactivePrimaryCancellationLimit: config.InteractivePrimaryCancellationCounterLimit,
dynamicRestrictSourceDataWhenOldestLaneStalled: config.DynamicRestrictSourceDataWhenOldestLaneStalled,
dynamicRestrictSourceDataWhenOldestLaneStalledTicks: config.DynamicRestrictSourceDataWhenOldestLaneStalledTicks,
}
interactiveConfig := config.BaseSessionConfig
interactiveConfig.TimestampInterval = 0
interactiveConfig.Rx.DataPacketKind = rriptMonoDirectionSession.PacketKind_InteractiveStreamData
interactiveConfig.Rx.ControlPacketKind = rriptMonoDirectionSession.PacketKind_InteractiveStreamControl
interactiveConfig.Tx.DataPacketKind = rriptMonoDirectionSession.PacketKind_InteractiveStreamData
interactiveConfig.Tx.ControlPacketKind = rriptMonoDirectionSession.PacketKind_InteractiveStreamControl
interactiveConfig.Tx.SendEnforced = config.ChannelManager.Send
interactiveConfig.Tx.SendIgnoreQuota = config.ChannelManager.SendIgnoreQuota
interactiveConfig.Tx.HasRemainingQuota = config.ChannelManager.HasRemainingQuota
interactive, err := rrpitBidirectionalSession.New(interactiveConfig)
if err != nil {
return nil, err
}
backgroundConfig := config.BaseSessionConfig
backgroundConfig.TimestampInterval = 0
backgroundConfig.Rx.DataPacketKind = rriptMonoDirectionSession.PacketKind_BackgroundStreamData
backgroundConfig.Rx.ControlPacketKind = rriptMonoDirectionSession.PacketKind_BackgroundStreamControl
backgroundConfig.Tx.DataPacketKind = rriptMonoDirectionSession.PacketKind_BackgroundStreamData
backgroundConfig.Tx.ControlPacketKind = rriptMonoDirectionSession.PacketKind_BackgroundStreamControl
backgroundConfig.Tx.SendEnforced = config.ChannelManager.Send
backgroundConfig.Tx.SendIgnoreQuota = config.ChannelManager.SendIgnoreQuota
backgroundConfig.Tx.HasRemainingQuota = config.ChannelManager.HasRemainingQuota
background, err := rrpitBidirectionalSession.New(backgroundConfig)
if err != nil {
_ = interactive.Close()
return nil, err
}
manager.sessions[InteractiveStream] = interactive
manager.sessions[BackgroundStream] = background
config.ChannelManager.RegisterListener(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, interactive.OnLogicalPacket)
config.ChannelManager.RegisterListener(rriptMonoDirectionSession.PacketKind_InteractiveStreamControl, interactive.OnLogicalPacket)
config.ChannelManager.RegisterListener(rriptMonoDirectionSession.PacketKind_BackgroundStreamData, background.OnLogicalPacket)
config.ChannelManager.RegisterListener(rriptMonoDirectionSession.PacketKind_BackgroundStreamControl, background.OnLogicalPacket)
manager.startAutoTick()
return manager, nil
}
func (m *Manager) Session(name SessionName) *rrpitBidirectionalSession.BidirectionalSession {
if m == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
return m.sessions[name]
}
func (m *Manager) ChannelManager() *rrpitChannelManager.ChannelManager {
if m == nil {
return nil
}
return m.channelManager
}
func (m *Manager) OnNewTimestamp(timestamp uint64) (ManagerTickStats, error) {
if m == nil {
return ManagerTickStats{}, nil
}
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return ManagerTickStats{}, io.ErrClosedPipe
}
if timestamp > m.nextTimestamp {
m.nextTimestamp = timestamp
}
m.channelManager.OnNewTimestamp(timestamp)
interactiveSourceActivity := m.sessions[InteractiveStream].ConsumeSourcePacketActivity()
backgroundDynamicRestrict := false
if m.dynamicRestrictSourceDataWhenOldestLaneStalled {
if interactiveSourceActivity > 0 {
m.backgroundDynamicRestrictTicksRemaining = m.dynamicRestrictSourceDataWhenOldestLaneStalledTicks
backgroundDynamicRestrict = true
} else if m.backgroundDynamicRestrictTicksRemaining > 0 {
m.backgroundDynamicRestrictTicksRemaining -= 1
backgroundDynamicRestrict = true
}
}
m.sessions[BackgroundStream].SetDynamicRestrictSourceDataWhenOldestLaneStalled(backgroundDynamicRestrict)
var stats ManagerTickStats
switch m.state {
case interactiveStreamPrimary:
interactive, err := m.sessions[InteractiveStream].OnNewTimestampWithStats(timestamp)
stats.InteractiveStream = interactive
if err != nil {
return stats, err
}
if interactive.RepairPacketsGenerated > 0 {
if m.interactivePrimaryCancellationCounter < m.interactivePrimaryCancellationLimit {
m.interactivePrimaryCancellationCounter += 1
m.state = interactiveStreamPrimary
return stats, nil
}
m.interactivePrimaryCancellationCounter = 0
m.state = backgroundStreamPrimary
return stats, nil
}
if m.channelManager.HasRemainingQuota() {
background, err := m.sessions[BackgroundStream].OnNewTimestampWithStats(timestamp)
stats.BackgroundStream = background
if err != nil {
return stats, err
}
}
m.interactivePrimaryCancellationCounter = 0
m.state = backgroundStreamPrimary
return stats, nil
case backgroundStreamPrimary:
background, err := m.sessions[BackgroundStream].OnNewTimestampWithStats(timestamp)
stats.BackgroundStream = background
if err != nil {
return stats, err
}
if m.channelManager.HasRemainingQuota() {
interactive, err := m.sessions[InteractiveStream].OnNewTimestampWithStats(timestamp)
stats.InteractiveStream = interactive
if err != nil {
return stats, err
}
}
m.state = interactiveStreamPrimary
return stats, nil
default:
return stats, nil
}
}
func (m *Manager) Close() error {
if m == nil {
return nil
}
var firstErr error
m.closeOnce.Do(func() {
// Wake any tick path blocked in the channel manager before trying to
// take m.mu; OnNewTimestamp holds m.mu while sending control packets.
if err := m.channelManager.Close(); err != nil && firstErr == nil {
firstErr = err
}
m.mu.Lock()
m.closed = true
sessions := make([]*rrpitBidirectionalSession.BidirectionalSession, 0, len(m.sessions))
for _, session := range m.sessions {
sessions = append(sessions, session)
}
m.sessions = nil
stop := m.autoTickStop
done := m.autoTickDone
m.mu.Unlock()
if stop != nil {
close(stop)
if done != nil {
<-done
}
}
for _, session := range sessions {
if err := session.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
})
return firstErr
}
func (m *Manager) startAutoTick() {
if m == nil || m.timestampInterval <= 0 {
return
}
m.autoTickStop = make(chan struct{})
m.autoTickDone = make(chan struct{})
go func() {
ticker := time.NewTicker(m.timestampInterval)
defer func() {
ticker.Stop()
close(m.autoTickDone)
}()
for {
select {
case <-ticker.C:
m.mu.Lock()
m.nextTimestamp += 1
timestamp := m.nextTimestamp
m.mu.Unlock()
if _, err := m.OnNewTimestamp(timestamp); err != nil {
return
}
case <-m.autoTickStop:
return
}
}
}()
}
@@ -0,0 +1,100 @@
package rrpitBidirectionalSessionManager
import (
"testing"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitChannelManager"
)
type recordingWriteCloser struct {
writes [][]byte
}
func (w *recordingWriteCloser) Write(p []byte) (int, error) {
w.writes = append(w.writes, append([]byte(nil), p...))
return len(p), nil
}
func (w *recordingWriteCloser) Close() error { return nil }
func TestManagerDynamicRestrictSourceDataWhenOldestLaneStalledTracksInteractiveActivity(t *testing.T) {
channelManager, err := rrpitChannelManager.New(rrpitChannelManager.Config{})
if err != nil {
t.Fatal(err)
}
defer channelManager.Close()
writer := &recordingWriteCloser{}
if _, err := channelManager.AttachChannelWithConfig(writer, rriptMonoDirectionSession.ChannelConfig{Weight: 1, MaxSendingSpeed: 8}); err != nil {
t.Fatal(err)
}
manager, err := New(Config{
ChannelManager: channelManager,
BaseSessionConfig: rrpitBidirectionalSession.Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 8,
OnMessage: func([]byte) error { return nil },
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 8,
MaxRewindableTimestampNum: 8,
MaxRewindableControlMessageNum: 8,
Reconstruction: rriptMonoDirectionSession.SessionTxReconstructionConfig{
InitialRepairShardRatio: 1,
SecondaryRepairShardRatio: 1,
TimeResendSecondaryRepairShard: 1,
StaleLaneFinalizedAgeThresholdTicks: 2,
StaleLaneProgressStallThresholdTicks: 2,
},
},
},
DynamicRestrictSourceDataWhenOldestLaneStalled: true,
DynamicRestrictSourceDataWhenOldestLaneStalledTicks: 2,
})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
background := manager.Session(BackgroundStream)
if background.RestrictSourceDataWhenOldestLaneStalledEnabled() {
t.Fatal("expected background restriction to start disabled")
}
if err := manager.Session(InteractiveStream).SendMessage([]byte("a")); err != nil {
t.Fatal(err)
}
if _, err := manager.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
if !background.RestrictSourceDataWhenOldestLaneStalledEnabled() {
t.Fatal("expected interactive source activity to enable dynamic background restriction")
}
if _, err := manager.OnNewTimestamp(2); err != nil {
t.Fatal(err)
}
if !background.RestrictSourceDataWhenOldestLaneStalledEnabled() {
t.Fatal("expected background restriction to stay enabled during retention window")
}
if _, err := manager.OnNewTimestamp(3); err != nil {
t.Fatal(err)
}
if !background.RestrictSourceDataWhenOldestLaneStalledEnabled() {
t.Fatal("expected background restriction to stay enabled for the second retained tick")
}
if _, err := manager.OnNewTimestamp(4); err != nil {
t.Fatal(err)
}
if background.RestrictSourceDataWhenOldestLaneStalledEnabled() {
t.Fatal("expected background restriction to clear after retained ticks expire")
}
}
@@ -0,0 +1,644 @@
package rrpitChannelManager
import (
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferChannel"
)
var ErrSharedSendQuotaReached = errors.New("rrpit shared send quota reached")
type (
Listener func(payload []byte) error
Sniffer func(payload []byte)
)
type Config struct {
OddChannelIDs bool
MaxRewindableTimestampNum int
MaxRewindableControlMessageNum int
}
type channelState struct {
config rriptMonoDirectionSession.ChannelConfig
status rriptMonoDirectionSession.ChannelRateControlStatus
tx *rrpitMaterializedTransferChannel.ChannelTx
rx *rrpitMaterializedTransferChannel.ChannelRx
}
type ChannelManager struct {
mu sync.Mutex
cond *sync.Cond
config Config
nextChannelID uint64
currentTimestamp uint64
channels []*channelState
listeners map[uint8][]Listener
sniffers map[uint8][]Sniffer
totalSent uint64
blockOnNoChannels bool
closed bool
}
func New(config Config) (*ChannelManager, error) {
manager := &ChannelManager{
config: config,
listeners: make(map[uint8][]Listener),
sniffers: make(map[uint8][]Sniffer),
}
manager.cond = sync.NewCond(&manager.mu)
return manager, nil
}
func (m *ChannelManager) RegisterListener(kind uint8, handler Listener) {
if m == nil || handler == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
m.listeners[kind] = append(m.listeners[kind], handler)
}
func (m *ChannelManager) AddSniffer(kind uint8, tap Sniffer) {
if m == nil || tap == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
m.sniffers[kind] = append(m.sniffers[kind], tap)
}
func (m *ChannelManager) AttachChannelWithConfig(
writer io.WriteCloser,
config rriptMonoDirectionSession.ChannelConfig,
) (int, error) {
if m == nil {
return 0, io.ErrClosedPipe
}
if writer == nil {
return 0, io.ErrClosedPipe
}
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return 0, io.ErrClosedPipe
}
if config.Weight == 0 {
config.Weight = 1
}
channelID := m.allocateChannelIDLocked()
tx, err := rrpitMaterializedTransferChannel.NewChannelTx(
channelID,
writer,
m.config.MaxRewindableTimestampNum,
m.config.MaxRewindableControlMessageNum,
)
if err != nil {
return 0, err
}
state := &channelState{config: config, tx: tx}
rx, err := rrpitMaterializedTransferChannel.NewChannelRx(0, func(data []byte) error {
return m.onLogicalPacket(state, data)
})
if err != nil {
return 0, err
}
state.rx = rx
m.channels = append(m.channels, state)
if m.cond != nil {
m.cond.Broadcast()
}
return len(m.channels) - 1, nil
}
func (m *ChannelManager) SetBlockOnNoChannels(enabled bool) {
if m == nil {
return
}
m.mu.Lock()
m.blockOnNoChannels = enabled
if m.cond != nil {
m.cond.Broadcast()
}
m.mu.Unlock()
}
func (m *ChannelManager) DetachChannel(channelIndex int) error {
if m == nil {
return io.ErrClosedPipe
}
m.mu.Lock()
if channelIndex < 0 || channelIndex >= len(m.channels) {
m.mu.Unlock()
return io.ErrClosedPipe
}
channel := m.channels[channelIndex]
m.channels[channelIndex] = nil
if m.cond != nil {
m.cond.Broadcast()
}
m.mu.Unlock()
if channel == nil {
return nil
}
var firstErr error
if channel.rx != nil {
if err := channel.rx.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if channel.tx != nil {
if err := channel.tx.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (m *ChannelManager) OnNewMessageArrived(channelIndex int, payload []byte) error {
m.mu.Lock()
if m.closed {
m.mu.Unlock()
return io.ErrClosedPipe
}
if channelIndex < 0 || channelIndex >= len(m.channels) || m.channels[channelIndex] == nil || m.channels[channelIndex].rx == nil {
m.mu.Unlock()
return io.ErrClosedPipe
}
rx := m.channels[channelIndex].rx
m.mu.Unlock()
return rx.OnNewMessageArrived(payload)
}
func (m *ChannelManager) OnNewTimestamp(timestamp uint64) {
if m == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
m.currentTimestamp = timestamp
for _, channel := range m.channels {
if channel == nil {
continue
}
if channel.status.TimestampLastSent != timestamp {
channel.status.PacketSentCurrentTimestamp = 0
channel.status.EnforcedPacketSentCurrentTimestamp = 0
}
}
}
func (m *ChannelManager) HasRemainingQuota() bool {
if m == nil {
return false
}
m.mu.Lock()
defer m.mu.Unlock()
return m.chooseEnforcedChannelLocked() >= 0
}
func (m *ChannelManager) Send(kind uint8, payload []byte) error {
if m == nil {
return io.ErrClosedPipe
}
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return io.ErrClosedPipe
}
channelIndex, err := m.waitForEnforcedChannelLocked()
if err != nil {
return err
}
return m.sendOnChannelLocked(channelIndex, kind, payload, true)
}
func (m *ChannelManager) SendIgnoreQuota(kind uint8, payload []byte) error {
if m == nil {
return io.ErrClosedPipe
}
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return io.ErrClosedPipe
}
if rriptMonoDirectionSession.IsControlPacketKind(kind) {
if err := m.waitForAnyTxChannelLocked(); err != nil {
return err
}
return m.sendFloodControlLocked(kind, payload)
}
channelIndex, err := m.waitForBypassChannelLocked()
if err != nil {
return err
}
return m.sendOnChannelLocked(channelIndex, kind, payload, false)
}
func (m *ChannelManager) Close() error {
if m == nil {
return nil
}
m.mu.Lock()
if m.closed {
m.mu.Unlock()
return nil
}
m.closed = true
if m.cond != nil {
m.cond.Broadcast()
}
channels := append([]*channelState(nil), m.channels...)
m.channels = nil
m.mu.Unlock()
var firstErr error
for _, channel := range channels {
if channel == nil {
continue
}
if channel.rx != nil {
if err := channel.rx.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if channel.tx != nil {
if err := channel.tx.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
}
return firstErr
}
func (m *ChannelManager) onLogicalPacket(channel *channelState, data []byte) error {
if len(data) == 0 {
return io.ErrUnexpectedEOF
}
kind := data[0]
m.emitSniffers(kind, data)
switch {
case rriptMonoDirectionSession.IsControlPacketKind(kind):
ctrl, err := rriptMonoDirectionSession.UnmarshalSessionControlPacket(data)
if err != nil {
return err
}
m.mu.Lock()
if channel != nil && channel.rx != nil && ctrl.FloodChannel.CurrentChannelID != 0 {
if m.hasAssignedChannelIDLocked(ctrl.FloodChannel.CurrentChannelID, channel) {
m.mu.Unlock()
return fmt.Errorf("duplicate rx channel id %d", ctrl.FloodChannel.CurrentChannelID)
}
if err := channel.rx.AssignChannelID(ctrl.FloodChannel.CurrentChannelID); err != nil {
m.mu.Unlock()
return err
}
}
for _, channelControl := range ctrl.Channel.ChannelControl {
if tx := m.txChannelByIDLocked(channelControl.ChannelID); tx != nil {
if err := tx.AcceptControlMessage(channelControl); err != nil {
m.mu.Unlock()
return err
}
}
}
ctrl.FloodChannel = rriptMonoDirectionSession.SessionFloodChannelControlMessage{}
ctrl.Channel = rriptMonoDirectionSession.SessionChannelControlMessage{}
listeners := append([]Listener(nil), m.listeners[kind]...)
m.mu.Unlock()
stripped, err := rriptMonoDirectionSession.MarshalSessionControlPacket(kind, ctrl)
if err != nil {
return err
}
return dispatchListeners(listeners, stripped)
case rriptMonoDirectionSession.IsDataPacketKind(kind):
m.mu.Lock()
listeners := append([]Listener(nil), m.listeners[kind]...)
m.mu.Unlock()
return dispatchListeners(listeners, append([]byte(nil), data...))
default:
return io.ErrUnexpectedEOF
}
}
func (m *ChannelManager) sendFloodControlLocked(kind uint8, payload []byte) error {
ctrl, err := rriptMonoDirectionSession.UnmarshalSessionControlPacket(payload)
if err != nil {
return err
}
sharedControl, err := m.buildSharedChannelControlLocked()
if err != nil {
return err
}
for index, channel := range m.channels {
if channel == nil || channel.tx == nil {
continue
}
cloned := ctrl
cloned.FloodChannel = rriptMonoDirectionSession.SessionFloodChannelControlMessage{
CurrentChannelID: channel.tx.ChannelID,
}
cloned.Channel = sharedControl
stamped, err := rriptMonoDirectionSession.MarshalSessionControlPacket(kind, cloned)
if err != nil {
return err
}
if err := channel.tx.SendDataMessage(stamped); err != nil {
return err
}
m.markChannelSentLocked(index, false)
m.emitSniffersLocked(kind, stamped)
}
return nil
}
func (m *ChannelManager) buildSharedChannelControlLocked() (rriptMonoDirectionSession.SessionChannelControlMessage, error) {
channelControl := rriptMonoDirectionSession.SessionChannelControlMessage{
ChannelControl: make([]rrpitTransferChannel.ChannelControlMessage, 0, len(m.channels)),
}
for _, channel := range m.channels {
if channel == nil || channel.rx == nil || channel.rx.ChannelID == 0 {
continue
}
ctrl, err := channel.rx.CreateControlMessage()
if err != nil {
return rriptMonoDirectionSession.SessionChannelControlMessage{}, err
}
channelControl.ChannelControl = append(channelControl.ChannelControl, *ctrl)
}
channelControl.LenChannelControl = uint16(len(channelControl.ChannelControl))
return channelControl, nil
}
func (m *ChannelManager) sendOnChannelLocked(channelIndex int, kind uint8, payload []byte, enforced bool) error {
if channelIndex < 0 || channelIndex >= len(m.channels) {
return io.ErrClosedPipe
}
channel := m.channels[channelIndex]
if channel == nil || channel.tx == nil {
return io.ErrClosedPipe
}
if err := channel.tx.SendDataMessage(payload); err != nil {
return err
}
m.markChannelSentLocked(channelIndex, enforced)
m.emitSniffersLocked(kind, payload)
return nil
}
func (m *ChannelManager) waitForEnforcedChannelLocked() (int, error) {
for {
if m.closed {
return -1, io.ErrClosedPipe
}
channelIndex := m.chooseEnforcedChannelLocked()
if channelIndex >= 0 {
return channelIndex, nil
}
if m.activeTxChannelCountLocked() == 0 {
if !m.blockOnNoChannels || m.cond == nil {
return -1, io.ErrClosedPipe
}
m.cond.Wait()
continue
}
return -1, ErrSharedSendQuotaReached
}
}
func (m *ChannelManager) waitForBypassChannelLocked() (int, error) {
for {
if m.closed {
return -1, io.ErrClosedPipe
}
channelIndex := m.chooseBypassChannelLocked()
if channelIndex >= 0 {
return channelIndex, nil
}
if !m.blockOnNoChannels || m.cond == nil {
return -1, io.ErrClosedPipe
}
m.cond.Wait()
}
}
func (m *ChannelManager) waitForAnyTxChannelLocked() error {
for {
if m.closed {
return io.ErrClosedPipe
}
if m.activeTxChannelCountLocked() > 0 {
return nil
}
if !m.blockOnNoChannels || m.cond == nil {
return io.ErrClosedPipe
}
m.cond.Wait()
}
}
func (m *ChannelManager) chooseEnforcedChannelLocked() int {
best := -1
for i := range m.channels {
channel := m.channels[i]
if channel == nil || channel.tx == nil || m.channelRateLimitedLocked(i) {
continue
}
if best == -1 || m.channelHasLessLoadLocked(i, best) {
best = i
}
}
return best
}
func (m *ChannelManager) chooseBypassChannelLocked() int {
best := -1
bestOversubscribe := 0.0
for i, channel := range m.channels {
if channel == nil || channel.tx == nil {
continue
}
if channel.config.MaxSendingSpeed <= 0 {
continue
}
oversubscribe := m.channelOversubscribePercentageLocked(i)
if best == -1 || oversubscribe < bestOversubscribe {
best = i
bestOversubscribe = oversubscribe
}
}
if best != -1 {
return best
}
for i, channel := range m.channels {
if channel != nil && channel.tx != nil {
return i
}
}
return -1
}
func (m *ChannelManager) activeTxChannelCountLocked() int {
count := 0
for _, channel := range m.channels {
if channel != nil && channel.tx != nil {
count += 1
}
}
return count
}
func (m *ChannelManager) channelOversubscribePercentageLocked(index int) float64 {
channel := m.channels[index]
if channel == nil || channel.config.MaxSendingSpeed <= 0 {
return 0
}
return float64(m.channelWindowSendCountLocked(index)) / float64(channel.config.MaxSendingSpeed)
}
func (m *ChannelManager) channelRateLimitedLocked(index int) bool {
channel := m.channels[index]
if channel == nil {
return true
}
maxSpeed := channel.config.MaxSendingSpeed
if maxSpeed <= 0 {
return false
}
status := channel.status
if status.TimestampLastSent != m.effectiveTimestampLocked() {
return false
}
return int(status.EnforcedPacketSentCurrentTimestamp) >= maxSpeed
}
func (m *ChannelManager) channelHasLessLoadLocked(candidateIndex int, currentBestIndex int) bool {
candidateWeight := m.channels[candidateIndex].config.Weight
if candidateWeight <= 0 {
candidateWeight = 1
}
bestWeight := m.channels[currentBestIndex].config.Weight
if bestWeight <= 0 {
bestWeight = 1
}
candidateSent := m.channelWindowSendCountLocked(candidateIndex)
bestSent := m.channelWindowSendCountLocked(currentBestIndex)
left := candidateSent * uint64(bestWeight)
right := bestSent * uint64(candidateWeight)
if left != right {
return left < right
}
return candidateIndex < currentBestIndex
}
func (m *ChannelManager) channelWindowSendCountLocked(index int) uint64 {
channel := m.channels[index]
if channel == nil {
return 0
}
if channel.status.TimestampLastSent != m.effectiveTimestampLocked() {
return 0
}
return channel.status.PacketSentCurrentTimestamp
}
func (m *ChannelManager) markChannelSentLocked(index int, enforced bool) {
channel := m.channels[index]
if channel == nil {
return
}
timestamp := m.effectiveTimestampLocked()
if channel.status.TimestampLastSent != timestamp {
channel.status.PacketSentCurrentTimestamp = 0
channel.status.EnforcedPacketSentCurrentTimestamp = 0
}
channel.status.TimestampLastSent = timestamp
channel.status.PacketSentCurrentTimestamp += 1
if enforced {
channel.status.EnforcedPacketSentCurrentTimestamp += 1
}
m.totalSent += 1
}
func (m *ChannelManager) effectiveTimestampLocked() uint64 {
if m.currentTimestamp == 0 {
m.currentTimestamp = uint64(time.Now().UnixNano())
}
return m.currentTimestamp
}
func (m *ChannelManager) txChannelByIDLocked(channelID uint64) *rrpitMaterializedTransferChannel.ChannelTx {
for _, channel := range m.channels {
if channel != nil && channel.tx != nil && channel.tx.ChannelID == channelID {
return channel.tx
}
}
return nil
}
func (m *ChannelManager) hasAssignedChannelIDLocked(channelID uint64, exclude *channelState) bool {
for _, channel := range m.channels {
if channel == nil || channel == exclude || channel.rx == nil {
continue
}
if channel.rx.ChannelID == channelID {
return true
}
}
return false
}
func (m *ChannelManager) allocateChannelIDLocked() uint64 {
if m.nextChannelID == 0 {
if m.config.OddChannelIDs {
m.nextChannelID = 1
} else {
m.nextChannelID = 2
}
}
channelID := m.nextChannelID
m.nextChannelID += 2
return channelID
}
func (m *ChannelManager) emitSniffers(kind uint8, payload []byte) {
m.mu.Lock()
defer m.mu.Unlock()
m.emitSniffersLocked(kind, payload)
}
func (m *ChannelManager) emitSniffersLocked(kind uint8, payload []byte) {
taps := append([]Sniffer(nil), m.sniffers[kind]...)
if len(taps) == 0 {
return
}
cloned := append([]byte(nil), payload...)
for _, tap := range taps {
tap := tap
go tap(cloned)
}
}
func dispatchListeners(listeners []Listener, payload []byte) error {
for _, listener := range listeners {
if listener == nil {
continue
}
if err := listener(append([]byte(nil), payload...)); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,234 @@
package rrpitChannelManager
import (
"io"
"sync"
"testing"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
)
type recordingWriteCloser struct {
mu sync.Mutex
writes [][]byte
}
func (w *recordingWriteCloser) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
w.writes = append(w.writes, append([]byte(nil), p...))
return len(p), nil
}
func (w *recordingWriteCloser) Close() error { return nil }
func (w *recordingWriteCloser) snapshot() [][]byte {
w.mu.Lock()
defer w.mu.Unlock()
out := make([][]byte, len(w.writes))
for i, write := range w.writes {
out[i] = append([]byte(nil), write...)
}
return out
}
func TestChannelManagerFloodsControlAndStripsSharedFieldsOnReceive(t *testing.T) {
manager, err := New(Config{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
firstWriter := &recordingWriteCloser{}
secondWriter := &recordingWriteCloser{}
firstChannel, err := manager.AttachChannelWithConfig(firstWriter, rriptMonoDirectionSession.ChannelConfig{Weight: 1})
if err != nil {
t.Fatal(err)
}
if _, err := manager.AttachChannelWithConfig(secondWriter, rriptMonoDirectionSession.ChannelConfig{Weight: 1}); err != nil {
t.Fatal(err)
}
var seen [][]byte
manager.RegisterListener(rriptMonoDirectionSession.PacketKind_InteractiveStreamControl, func(payload []byte) error {
seen = append(seen, append([]byte(nil), payload...))
return nil
})
payload, err := rriptMonoDirectionSession.MarshalSessionControlPacket(
rriptMonoDirectionSession.PacketKind_InteractiveStreamControl,
rriptMonoDirectionSession.ControlMessage{},
)
if err != nil {
t.Fatal(err)
}
if err := manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamControl, payload); err != nil {
t.Fatal(err)
}
firstWrites := firstWriter.snapshot()
secondWrites := secondWriter.snapshot()
if len(firstWrites) != 1 || len(secondWrites) != 1 {
t.Fatalf("expected one flooded control write per channel, got %d and %d", len(firstWrites), len(secondWrites))
}
firstCtrl, err := rriptMonoDirectionSession.UnmarshalSessionControlPacket(firstWrites[0][8:])
if err != nil {
t.Fatal(err)
}
secondCtrl, err := rriptMonoDirectionSession.UnmarshalSessionControlPacket(secondWrites[0][8:])
if err != nil {
t.Fatal(err)
}
if firstCtrl.FloodChannel.CurrentChannelID == 0 || secondCtrl.FloodChannel.CurrentChannelID == 0 {
t.Fatal("expected flooded control to stamp non-zero channel ids")
}
if firstCtrl.FloodChannel.CurrentChannelID == secondCtrl.FloodChannel.CurrentChannelID {
t.Fatal("expected flooded control clones to stamp distinct egress channel ids")
}
if err := manager.OnNewMessageArrived(firstChannel, firstWrites[0]); err != nil {
t.Fatal(err)
}
if len(seen) != 1 {
t.Fatalf("expected one stripped control dispatch, got %d", len(seen))
}
strippedCtrl, err := rriptMonoDirectionSession.UnmarshalSessionControlPacket(seen[0])
if err != nil {
t.Fatal(err)
}
if strippedCtrl.FloodChannel.CurrentChannelID != 0 {
t.Fatalf("expected stripped flood channel id 0, got %d", strippedCtrl.FloodChannel.CurrentChannelID)
}
if len(strippedCtrl.Channel.ChannelControl) != 0 || strippedCtrl.Channel.LenChannelControl != 0 {
t.Fatalf("expected stripped channel control, got len field %d and %d entries", strippedCtrl.Channel.LenChannelControl, len(strippedCtrl.Channel.ChannelControl))
}
}
func TestChannelManagerBypassSourceSelectionUsesLowestOversubscribe(t *testing.T) {
manager, err := New(Config{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
firstWriter := &recordingWriteCloser{}
secondWriter := &recordingWriteCloser{}
if _, err := manager.AttachChannelWithConfig(firstWriter, rriptMonoDirectionSession.ChannelConfig{Weight: 1, MaxSendingSpeed: 1}); err != nil {
t.Fatal(err)
}
if _, err := manager.AttachChannelWithConfig(secondWriter, rriptMonoDirectionSession.ChannelConfig{Weight: 1, MaxSendingSpeed: 10}); err != nil {
t.Fatal(err)
}
manager.OnNewTimestamp(1)
if err := manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x01}); err != nil {
t.Fatal(err)
}
if err := manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x02}); err != nil {
t.Fatal(err)
}
if len(firstWriter.snapshot()) != 1 {
t.Fatalf("expected first channel to carry only the first bypass source packet, got %d writes", len(firstWriter.snapshot()))
}
if len(secondWriter.snapshot()) != 1 {
t.Fatalf("expected second channel to carry the second bypass source packet, got %d writes", len(secondWriter.snapshot()))
}
}
func TestChannelManagerBypassTrafficDoesNotConsumeEnforcedQuota(t *testing.T) {
manager, err := New(Config{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
writer := &recordingWriteCloser{}
if _, err := manager.AttachChannelWithConfig(writer, rriptMonoDirectionSession.ChannelConfig{Weight: 1, MaxSendingSpeed: 1}); err != nil {
t.Fatal(err)
}
manager.OnNewTimestamp(1)
if !manager.HasRemainingQuota() {
t.Fatal("expected enforced quota to be available at start of tick")
}
if err := manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x01}); err != nil {
t.Fatal(err)
}
if !manager.HasRemainingQuota() {
t.Fatal("expected bypass traffic to leave enforced quota available")
}
if err := manager.Send(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x02}); err != nil {
t.Fatalf("expected one enforced send after bypass traffic, got %v", err)
}
if manager.HasRemainingQuota() {
t.Fatal("expected enforced quota to be exhausted after one enforced send")
}
}
func TestChannelManagerDetachRemovesChannelFromSelection(t *testing.T) {
manager, err := New(Config{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
writer := &recordingWriteCloser{}
channelIndex, err := manager.AttachChannelWithConfig(writer, rriptMonoDirectionSession.ChannelConfig{Weight: 1, MaxSendingSpeed: 1})
if err != nil {
t.Fatal(err)
}
if !manager.HasRemainingQuota() {
t.Fatal("expected quota with attached channel")
}
if err := manager.DetachChannel(channelIndex); err != nil {
t.Fatal(err)
}
if manager.HasRemainingQuota() {
t.Fatal("expected no remaining quota after detaching the only channel")
}
if err := manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x01}); err == nil {
t.Fatal("expected send to fail with no attached channels")
} else if err != io.ErrClosedPipe {
t.Fatalf("expected io.ErrClosedPipe after detach, got %v", err)
}
}
func TestChannelManagerWaitsForChannelWhenConfigured(t *testing.T) {
manager, err := New(Config{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
manager.SetBlockOnNoChannels(true)
done := make(chan error, 1)
go func() {
done <- manager.SendIgnoreQuota(rriptMonoDirectionSession.PacketKind_InteractiveStreamData, []byte{rriptMonoDirectionSession.PacketKind_InteractiveStreamData, 0x01})
}()
select {
case err := <-done:
t.Fatalf("expected send to wait for a channel, got early result %v", err)
case <-time.After(20 * time.Millisecond):
}
writer := &recordingWriteCloser{}
if _, err := manager.AttachChannelWithConfig(writer, rriptMonoDirectionSession.ChannelConfig{Weight: 1}); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(200 * time.Millisecond):
t.Fatal("timed out waiting for send to complete after channel attach")
}
if len(writer.snapshot()) != 1 {
t.Fatalf("expected one write after attach, got %d", len(writer.snapshot()))
}
}
@@ -0,0 +1,857 @@
package rrpitDebuger
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"sync"
"time"
"unsafe"
"github.com/lunixbochs/struc"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferChannel"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferLane"
)
const (
materializedChannelSequenceFieldLength = 8
defaultPacketLogBaseName = "rrpit-packets"
defaultPacketLogMaxFileSizeBytes = 1 << 20
defaultPacketLogMaxFiles = 8
packetLogScannerBufferSize = 8 << 20
)
type PacketDirection string
const (
PacketDirectionTx PacketDirection = "tx"
PacketDirectionRx PacketDirection = "rx"
)
type DiagnoseOutput struct {
GeneratedAt time.Time `json:"generated_at"`
Session BidirectionalSessionSnapshot `json:"session"`
PacketLog PacketLogManifest `json:"packet_log"`
}
type BidirectionalSessionSnapshot struct {
TimestampInterval string `json:"timestamp_interval"`
Tx *SessionTxSnapshot `json:"tx,omitempty"`
Rx *SessionRxSnapshot `json:"rx,omitempty"`
}
type SessionTxSnapshot struct {
OddChannelIDs bool `json:"odd_channel_ids"`
CurrentTimestamp uint64 `json:"current_timestamp"`
NextChannelID uint64 `json:"next_channel_id"`
LaneShardSize int `json:"lane_shard_size"`
MaxDataShardsPerLane int `json:"max_data_shards_per_lane"`
MaxBufferedLanes int `json:"max_buffered_lanes"`
FirstLaneID int64 `json:"first_lane_id"`
Lanes []TxLaneSnapshot `json:"lanes"`
Channels []TxChannelSnapshot `json:"channels"`
}
type TxLaneSnapshot struct {
LaneID uint64 `json:"lane_id"`
DataShards uint32 `json:"data_shards"`
TotalDataShards uint32 `json:"total_data_shards"`
Finalized bool `json:"finalized"`
PeerSeenChunks uint16 `json:"peer_seen_chunks"`
PeerReconstructed bool `json:"peer_reconstructed"`
RepairPackets uint32 `json:"repair_packets"`
InitialRepairPacketsPending uint32 `json:"initial_repair_packets_pending"`
SecondaryRepairPacketsPending uint32 `json:"secondary_repair_packets_pending"`
NextSecondaryRepairTimestamp uint64 `json:"next_secondary_repair_timestamp"`
CreatedAtTimestamp uint64 `json:"created_at_timestamp"`
FinalizedAtTimestamp uint64 `json:"finalized_at_timestamp"`
LastProgressTimestamp uint64 `json:"last_progress_timestamp"`
TransferLane TransferLaneTxSnapshot `json:"transfer_lane"`
}
type TransferLaneTxSnapshot struct {
ShardSize int `json:"shard_size"`
MaxDataShards int `json:"max_data_shards"`
TotalDataShards uint32 `json:"total_data_shards"`
SeenShardCount int `json:"seen_shard_count"`
PeerSeenChunks uint16 `json:"peer_seen_chunks"`
NextSeq uint32 `json:"next_seq"`
HasEncoder bool `json:"has_encoder"`
}
type TxChannelSnapshot struct {
ChannelID uint64 `json:"channel_id"`
NextSeq uint64 `json:"next_seq"`
WriterType string `json:"writer_type,omitempty"`
Config rriptMonoDirectionSession.ChannelConfig `json:"config"`
Status rriptMonoDirectionSession.ChannelRateControlStatus `json:"status"`
SentPacketHistory []SentPacketSnapshot `json:"sent_packet_history"`
ControlHistory []rrpitTransferChannel.ChannelControlMessage `json:"control_history"`
RemoteLastSeenSenderTimestamp *uint64 `json:"remote_last_seen_sender_timestamp,omitempty"`
RemoteLastSeenSenderTimestampError string `json:"remote_last_seen_sender_timestamp_error,omitempty"`
}
type SentPacketSnapshot struct {
Seq uint64 `json:"seq"`
Timestamp uint64 `json:"timestamp"`
}
type SessionRxSnapshot struct {
LaneShardSize int `json:"lane_shard_size"`
MaxBufferedLanes int `json:"max_buffered_lanes"`
FirstLaneID int64 `json:"first_lane_id"`
Lanes []RxLaneSnapshot `json:"lanes"`
Channels []RxChannelSnapshot `json:"channels"`
}
type RxLaneSnapshot struct {
LaneID uint64 `json:"lane_id"`
Ready bool `json:"ready"`
NextDeliverIndex int `json:"next_deliver_index"`
ReconstructedCount int `json:"reconstructed_count"`
ReconstructedLens []int `json:"reconstructed_lengths"`
TransferLane TransferLaneRxSnapshot `json:"transfer_lane"`
}
type TransferLaneRxSnapshot struct {
ShardSize int `json:"shard_size"`
TotalDataShards uint32 `json:"total_data_shards"`
SeenShardCount uint32 `json:"seen_shard_count"`
SeenDataShardCount int `json:"seen_data_shard_count"`
HasDecoder bool `json:"has_decoder"`
Completed bool `json:"completed"`
}
type RxChannelSnapshot struct {
ChannelID uint64 `json:"channel_id"`
TotalPacketsReceived uint64 `json:"total_packets_received"`
LastPacketSeqReceived uint64 `json:"last_packet_seq_received"`
}
type PacketLog struct {
Index int `json:"index"`
ObservedAt time.Time `json:"observed_at"`
Peer string `json:"peer"`
Direction PacketDirection `json:"direction"`
ChannelIndex int `json:"channel_index"`
WireSequence uint64 `json:"wire_sequence"`
WireSize int `json:"wire_size"`
PayloadSize int `json:"payload_size"`
PacketKind uint8 `json:"packet_kind"`
PacketKindName string `json:"packet_kind_name"`
LaneID *uint64 `json:"lane_id,omitempty"`
Transfer *rrpitTransferLane.TransferData `json:"transfer,omitempty"`
Control *rriptMonoDirectionSession.ControlMessage `json:"control,omitempty"`
RawWireHex string `json:"raw_wire_hex"`
RawPayloadHex string `json:"raw_payload_hex,omitempty"`
DecodeError string `json:"decode_error,omitempty"`
TransportError string `json:"transport_error,omitempty"`
}
type PacketRecorderConfig struct {
Directory string
BaseName string
MaxFileSizeBytes int64
MaxFiles int
}
type PacketLogManifest struct {
Directory string `json:"directory"`
Files []string `json:"files"`
MaxFileSizeBytes int64 `json:"max_file_size_bytes"`
MaxFiles int `json:"max_files"`
TotalPacketsLogged int `json:"total_packets_logged"`
DroppedLogFiles int `json:"dropped_log_files"`
WriteErrors []string `json:"write_errors,omitempty"`
}
type PacketRecorder struct {
mu sync.Mutex
config PacketRecorderConfig
runID string
nextIdx int
currentFileIndex int
currentFileSize int64
currentFile *os.File
files []string
droppedLogFiles int
writeErrors []string
}
type debugSessionDataPacket struct {
PacketKind uint8
LaneID uint64
Transfer rrpitTransferLane.TransferData
}
type debugSessionControlPacket struct {
PacketKind uint8
Control rriptMonoDirectionSession.ControlMessage
}
type packetRecordingWriteCloser struct {
recorder *PacketRecorder
peer string
channelIndex int
io.WriteCloser
}
type FailureLogger interface {
Helper()
Failed() bool
Logf(format string, args ...any)
}
func NewPacketRecorder(config PacketRecorderConfig) (*PacketRecorder, error) {
if config.Directory == "" {
return nil, fmt.Errorf("packet recorder directory is required")
}
if config.BaseName == "" {
config.BaseName = defaultPacketLogBaseName
}
if config.MaxFileSizeBytes <= 0 {
config.MaxFileSizeBytes = defaultPacketLogMaxFileSizeBytes
}
if config.MaxFiles <= 0 {
config.MaxFiles = defaultPacketLogMaxFiles
}
absDirectory, err := filepath.Abs(config.Directory)
if err != nil {
return nil, err
}
if err := os.MkdirAll(absDirectory, 0o755); err != nil {
return nil, err
}
config.Directory = absDirectory
recorder := &PacketRecorder{
config: config,
runID: time.Now().UTC().Format("20060102T150405.000000000Z07"),
}
if err := recorder.rotateLocked(); err != nil {
return nil, err
}
return recorder, nil
}
func (r *PacketRecorder) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
return r.closeCurrentFileLocked()
}
func (r *PacketRecorder) WrapWriter(peer string, channelIndex int, writer io.WriteCloser) io.WriteCloser {
if r == nil || writer == nil {
return writer
}
return &packetRecordingWriteCloser{
recorder: r,
peer: peer,
channelIndex: channelIndex,
WriteCloser: writer,
}
}
func (r *PacketRecorder) RecordInbound(peer string, channelIndex int, wire []byte) {
if r == nil {
return
}
r.recordPacket(peer, PacketDirectionRx, channelIndex, wire, nil)
}
func (r *PacketRecorder) Manifest() PacketLogManifest {
if r == nil {
return PacketLogManifest{}
}
r.mu.Lock()
defer r.mu.Unlock()
manifest := PacketLogManifest{
Directory: r.config.Directory,
Files: append([]string(nil), r.files...),
MaxFileSizeBytes: r.config.MaxFileSizeBytes,
MaxFiles: r.config.MaxFiles,
TotalPacketsLogged: r.nextIdx,
DroppedLogFiles: r.droppedLogFiles,
}
if len(r.writeErrors) > 0 {
manifest.WriteErrors = append([]string(nil), r.writeErrors...)
}
return manifest
}
func (r *PacketRecorder) ReadAllPackets() ([]PacketLog, error) {
if r == nil {
return nil, nil
}
manifest := r.Manifest()
packets := make([]PacketLog, 0, manifest.TotalPacketsLogged)
for _, path := range manifest.Files {
file, err := os.Open(path)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 64*1024), packetLogScannerBufferSize)
lineNumber := 0
for scanner.Scan() {
lineNumber += 1
var packet PacketLog
if err := json.Unmarshal(scanner.Bytes(), &packet); err != nil {
_ = file.Close()
return nil, fmt.Errorf("decode packet log %s line %d: %w", path, lineNumber, err)
}
packets = append(packets, packet)
}
if err := scanner.Err(); err != nil {
_ = file.Close()
return nil, fmt.Errorf("read packet log %s: %w", path, err)
}
if err := file.Close(); err != nil {
return nil, err
}
}
return packets, nil
}
func DiagnoseBidirectionalSession(
session *rrpitBidirectionalSession.BidirectionalSession,
recorder *PacketRecorder,
) (*DiagnoseOutput, error) {
if session == nil {
return nil, fmt.Errorf("nil bidirectional session")
}
output := &DiagnoseOutput{
GeneratedAt: time.Now().UTC(),
Session: BidirectionalSessionSnapshot{
TimestampInterval: session.TimestampInterval.String(),
},
}
output.Session.Tx = snapshotSessionTx(session.Tx())
output.Session.Rx = snapshotSessionRx(session.Rx())
if recorder != nil {
output.PacketLog = recorder.Manifest()
}
return output, nil
}
func (o *DiagnoseOutput) MarshalIndented() ([]byte, error) {
if o == nil {
return []byte("null"), nil
}
return json.MarshalIndent(o, "", " ")
}
func (o *DiagnoseOutput) String() string {
data, err := o.MarshalIndented()
if err != nil {
return fmt.Sprintf("failed to marshal diagnose output: %v", err)
}
return string(data)
}
func WriteDiagnoseOutput(
writer io.Writer,
session *rrpitBidirectionalSession.BidirectionalSession,
recorder *PacketRecorder,
) error {
if writer == nil {
return fmt.Errorf("nil writer")
}
output, err := DiagnoseBidirectionalSession(session, recorder)
if err != nil {
return err
}
data, err := output.MarshalIndented()
if err != nil {
return err
}
_, err = writer.Write(data)
return err
}
func LogBidirectionalSessionOnFailure(
logger FailureLogger,
label string,
session *rrpitBidirectionalSession.BidirectionalSession,
recorder *PacketRecorder,
) {
if logger == nil {
return
}
logger.Helper()
if !logger.Failed() {
return
}
output, err := DiagnoseBidirectionalSession(session, recorder)
if err != nil {
logger.Logf("%s: failed to capture rrpit diagnose output: %v", label, err)
return
}
logger.Logf("%s:\n%s", label, output.String())
}
func (w *packetRecordingWriteCloser) Write(p []byte) (int, error) {
written, err := w.WriteCloser.Write(p)
if written > 0 {
var transportErr error
switch {
case err != nil:
transportErr = err
case written != len(p):
transportErr = io.ErrShortWrite
}
w.recorder.recordPacket(w.peer, PacketDirectionTx, w.channelIndex, p[:written], transportErr)
}
return written, err
}
func (r *PacketRecorder) recordPacket(
peer string,
direction PacketDirection,
channelIndex int,
wire []byte,
transportErr error,
) {
clonedWire := append([]byte(nil), wire...)
packet := PacketLog{
ObservedAt: time.Now().UTC(),
Peer: peer,
Direction: direction,
ChannelIndex: channelIndex,
WireSize: len(clonedWire),
RawWireHex: hex.EncodeToString(clonedWire),
}
if transportErr != nil {
packet.TransportError = transportErr.Error()
}
if len(clonedWire) < materializedChannelSequenceFieldLength {
packet.DecodeError = "materialized channel message too short"
r.writePacket(packet)
return
}
packet.WireSequence = binary.BigEndian.Uint64(clonedWire[:materializedChannelSequenceFieldLength])
payload := append([]byte(nil), clonedWire[materializedChannelSequenceFieldLength:]...)
packet.PayloadSize = len(payload)
packet.RawPayloadHex = hex.EncodeToString(payload)
if len(payload) == 0 {
packet.DecodeError = "empty session payload"
r.writePacket(packet)
return
}
packet.PacketKind = payload[0]
packet.PacketKindName = packetKindName(payload[0])
switch payload[0] {
case rriptMonoDirectionSession.PacketKind_DATA:
var decoded debugSessionDataPacket
if err := struc.Unpack(bytes.NewReader(payload), &decoded); err != nil {
packet.DecodeError = err.Error()
break
}
laneID := decoded.LaneID
packet.LaneID = &laneID
transfer := decoded.Transfer
transfer.Data = append([]byte(nil), transfer.Data...)
packet.Transfer = &transfer
case rriptMonoDirectionSession.PacketKind_CONTROL:
var decoded debugSessionControlPacket
if err := struc.Unpack(bytes.NewReader(payload), &decoded); err != nil {
packet.DecodeError = err.Error()
break
}
packet.Control = cloneControlMessage(decoded.Control)
default:
packet.DecodeError = fmt.Sprintf("unknown session packet kind %d", payload[0])
}
r.writePacket(packet)
}
func (r *PacketRecorder) writePacket(packet PacketLog) {
r.mu.Lock()
defer r.mu.Unlock()
packet.Index = r.nextIdx
r.nextIdx += 1
if r.currentFile == nil {
if err := r.rotateLocked(); err != nil {
r.writeErrors = append(r.writeErrors, err.Error())
return
}
}
line, err := json.Marshal(packet)
if err != nil {
r.writeErrors = append(r.writeErrors, err.Error())
return
}
line = append(line, '\n')
if r.currentFileSize > 0 && r.currentFileSize+int64(len(line)) > r.config.MaxFileSizeBytes {
if err := r.rotateLocked(); err != nil {
r.writeErrors = append(r.writeErrors, err.Error())
return
}
}
written, err := r.currentFile.Write(line)
r.currentFileSize += int64(written)
if err != nil {
r.writeErrors = append(r.writeErrors, err.Error())
return
}
if written != len(line) {
r.writeErrors = append(r.writeErrors, io.ErrShortWrite.Error())
}
}
func (r *PacketRecorder) rotateLocked() error {
if r == nil {
return nil
}
if err := r.closeCurrentFileLocked(); err != nil {
return err
}
filePath := filepath.Join(
r.config.Directory,
fmt.Sprintf("%s-%s-%06d.jsonl", r.config.BaseName, r.runID, r.currentFileIndex),
)
r.currentFileIndex += 1
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
r.currentFile = file
r.currentFileSize = 0
r.files = append(r.files, filePath)
for len(r.files) > r.config.MaxFiles {
oldest := r.files[0]
r.files = r.files[1:]
if err := os.Remove(oldest); err != nil && !os.IsNotExist(err) {
r.writeErrors = append(r.writeErrors, err.Error())
continue
}
r.droppedLogFiles += 1
}
return nil
}
func (r *PacketRecorder) closeCurrentFileLocked() error {
if r == nil || r.currentFile == nil {
return nil
}
err := r.currentFile.Close()
r.currentFile = nil
r.currentFileSize = 0
return err
}
func clonePacketLog(packet PacketLog) PacketLog {
cloned := packet
if packet.Transfer != nil {
transfer := *packet.Transfer
transfer.Data = append([]byte(nil), transfer.Data...)
cloned.Transfer = &transfer
}
if packet.Control != nil {
cloned.Control = cloneControlMessage(*packet.Control)
}
if packet.LaneID != nil {
laneID := *packet.LaneID
cloned.LaneID = &laneID
}
return cloned
}
func cloneControlMessage(ctrl rriptMonoDirectionSession.ControlMessage) *rriptMonoDirectionSession.ControlMessage {
cloned := ctrl
cloned.Lane.LaneControl = append([]rrpitTransferLane.TransferControl(nil), ctrl.Lane.LaneControl...)
cloned.Channel.ChannelControl = append([]rrpitTransferChannel.ChannelControlMessage(nil), ctrl.Channel.ChannelControl...)
return &cloned
}
func packetKindName(kind uint8) string {
return rriptMonoDirectionSession.PacketKindName(kind)
}
func snapshotSessionTx(tx *rriptMonoDirectionSession.SessionTx) *SessionTxSnapshot {
if tx == nil {
return nil
}
root := forceValue(reflect.ValueOf(tx).Elem())
txLanes := forceValue(root.FieldByName("txLanes"))
txChannels := forceValue(root.FieldByName("txChannels"))
channelsConfig := forceValue(root.FieldByName("txChannelsConfig"))
snapshot := &SessionTxSnapshot{
OddChannelIDs: root.FieldByName("OddChannelIDs").Bool(),
CurrentTimestamp: root.FieldByName("currentTimestamp").Uint(),
NextChannelID: txChannels.FieldByName("nextChannelID").Uint(),
LaneShardSize: int(txLanes.FieldByName("laneShardSize").Int()),
MaxDataShardsPerLane: int(txLanes.FieldByName("maxDataShardsPerLane").Int()),
MaxBufferedLanes: int(txLanes.FieldByName("maxBufferedLanes").Int()),
FirstLaneID: txLanes.FieldByName("firstLaneID").Int(),
Lanes: make([]TxLaneSnapshot, 0, forceValue(txLanes.FieldByName("lanes")).Len()),
Channels: make([]TxChannelSnapshot, 0, channelsConfig.Len()),
}
lanes := forceValue(txLanes.FieldByName("lanes"))
for i := 0; i < lanes.Len(); i++ {
laneValue := forceValue(lanes.Index(i))
if laneValue.IsNil() {
continue
}
snapshot.Lanes = append(snapshot.Lanes, snapshotTxLane(laneValue))
}
for i := 0; i < channelsConfig.Len(); i++ {
snapshot.Channels = append(snapshot.Channels, snapshotTxChannel(forceValue(channelsConfig.Index(i))))
}
return snapshot
}
func snapshotSessionRx(rx *rriptMonoDirectionSession.SessionRx) *SessionRxSnapshot {
if rx == nil {
return nil
}
root := forceValue(reflect.ValueOf(rx).Elem())
rxLanes := forceValue(root.FieldByName("rxLanes"))
rxChannels := forceValue(root.FieldByName("rxChannels"))
snapshot := &SessionRxSnapshot{
LaneShardSize: int(rxLanes.FieldByName("laneShardSize").Int()),
MaxBufferedLanes: int(rxLanes.FieldByName("maxBufferedLanes").Int()),
FirstLaneID: rxLanes.FieldByName("firstLaneID").Int(),
Lanes: make([]RxLaneSnapshot, 0, forceValue(rxLanes.FieldByName("lanes")).Len()),
Channels: make([]RxChannelSnapshot, 0, forceValue(rxChannels.FieldByName("channels")).Len()),
}
lanes := forceValue(rxLanes.FieldByName("lanes"))
for i := 0; i < lanes.Len(); i++ {
laneValue := forceValue(lanes.Index(i))
if laneValue.IsNil() {
continue
}
snapshot.Lanes = append(snapshot.Lanes, snapshotRxLane(laneValue))
}
channels := forceValue(rxChannels.FieldByName("channels"))
for i := 0; i < channels.Len(); i++ {
channelValue := forceValue(channels.Index(i))
if channelValue.IsNil() {
continue
}
snapshot.Channels = append(snapshot.Channels, snapshotRxChannel(channelValue))
}
return snapshot
}
func snapshotTxLane(lanePtr reflect.Value) TxLaneSnapshot {
lane := forceValue(lanePtr.Elem())
transferLane := forceValue(lane.FieldByName("TransferLane"))
snapshot := TxLaneSnapshot{
LaneID: lane.FieldByName("LaneID").Uint(),
DataShards: uint32(lane.FieldByName("DataShards").Uint()),
TotalDataShards: uint32(lane.FieldByName("TotalDataShards").Uint()),
Finalized: lane.FieldByName("Finalized").Bool(),
PeerSeenChunks: uint16(lane.FieldByName("PeerSeenChunks").Uint()),
PeerReconstructed: lane.FieldByName("PeerReconstructed").Bool(),
RepairPackets: uint32(lane.FieldByName("RepairPackets").Uint()),
InitialRepairPacketsPending: uint32(lane.FieldByName("InitialRepairPacketsPending").Uint()),
SecondaryRepairPacketsPending: uint32(lane.FieldByName("SecondaryRepairPacketsPending").Uint()),
NextSecondaryRepairTimestamp: lane.FieldByName("NextSecondaryRepairTimestamp").Uint(),
CreatedAtTimestamp: lane.FieldByName("CreatedAtTimestamp").Uint(),
FinalizedAtTimestamp: lane.FieldByName("FinalizedAtTimestamp").Uint(),
LastProgressTimestamp: lane.FieldByName("LastProgressTimestamp").Uint(),
}
if !transferLane.IsNil() {
snapshot.TransferLane = snapshotTransferLaneTx(transferLane)
}
return snapshot
}
func snapshotTransferLaneTx(transferLanePtr reflect.Value) TransferLaneTxSnapshot {
transferLane := forceValue(transferLanePtr.Elem())
seenShards := forceValue(transferLane.FieldByName("seenShards"))
return TransferLaneTxSnapshot{
ShardSize: int(transferLane.FieldByName("ShardSize").Int()),
MaxDataShards: int(transferLane.FieldByName("MaxDataShards").Int()),
TotalDataShards: uint32(transferLane.FieldByName("TotalDataShards").Uint()),
SeenShardCount: seenShards.Len(),
PeerSeenChunks: uint16(transferLane.FieldByName("peerSeenChunks").Uint()),
NextSeq: uint32(transferLane.FieldByName("nextSeq").Uint()),
HasEncoder: !transferLane.FieldByName("txCodesState").IsNil(),
}
}
func snapshotTxChannel(channelStatus reflect.Value) TxChannelSnapshot {
config := forceInterface[rriptMonoDirectionSession.ChannelConfig](channelStatus.FieldByName("Config"))
status := forceInterface[rriptMonoDirectionSession.ChannelRateControlStatus](channelStatus.FieldByName("Status"))
snapshot := TxChannelSnapshot{
Config: config,
Status: status,
}
materializedValue := forceValue(channelStatus.FieldByName("MaterializeChannel"))
if materializedValue.IsNil() {
return snapshot
}
materialized := forceInterface[*rrpitMaterializedTransferChannel.ChannelTx](materializedValue)
snapshot.ChannelID = materialized.ChannelID
snapshot.NextSeq = materialized.NextSeq
snapshot.WriterType = fmt.Sprintf("%T", materialized.WriteCloser)
channelTx := forceValue(materializedValue.Elem().FieldByName("ChannelTx"))
snapshot.SentPacketHistory = snapshotSentPacketHistory(channelTx.FieldByName("sentPacketHistory"))
snapshot.ControlHistory = snapshotControlHistory(channelTx.FieldByName("controlHistory"))
if timestamp, err := materialized.RemoteLastSeenMessageSenderTimestamp(); err == nil {
timestampCopy := timestamp
snapshot.RemoteLastSeenSenderTimestamp = &timestampCopy
} else {
snapshot.RemoteLastSeenSenderTimestampError = err.Error()
}
return snapshot
}
func snapshotSentPacketHistory(history reflect.Value) []SentPacketSnapshot {
items := snapshotRingBuffer(history)
result := make([]SentPacketSnapshot, 0, len(items))
for _, item := range items {
item = forceValue(item)
result = append(result, SentPacketSnapshot{
Seq: item.FieldByName("seq").Uint(),
Timestamp: item.FieldByName("timestamp").Uint(),
})
}
return result
}
func snapshotControlHistory(history reflect.Value) []rrpitTransferChannel.ChannelControlMessage {
items := snapshotRingBuffer(history)
result := make([]rrpitTransferChannel.ChannelControlMessage, 0, len(items))
for _, item := range items {
result = append(result, forceInterface[rrpitTransferChannel.ChannelControlMessage](item))
}
return result
}
func snapshotRxLane(lanePtr reflect.Value) RxLaneSnapshot {
lane := forceValue(lanePtr.Elem())
transferLane := forceValue(lane.FieldByName("TransferLane"))
reconstructed := forceValue(lane.FieldByName("Reconstructed"))
snapshot := RxLaneSnapshot{
LaneID: lane.FieldByName("LaneID").Uint(),
Ready: lane.FieldByName("Ready").Bool(),
NextDeliverIndex: int(lane.FieldByName("NextDeliverIndex").Int()),
ReconstructedCount: reconstructed.Len(),
ReconstructedLens: make([]int, 0, reconstructed.Len()),
}
for i := 0; i < reconstructed.Len(); i++ {
reconstruction := forceInterface[rrpitTransferLane.ReconstructionData](reconstructed.Index(i))
snapshot.ReconstructedLens = append(snapshot.ReconstructedLens, len(reconstruction.Data))
}
if !transferLane.IsNil() {
snapshot.TransferLane = snapshotTransferLaneRx(transferLane)
}
return snapshot
}
func snapshotTransferLaneRx(transferLanePtr reflect.Value) TransferLaneRxSnapshot {
transferLane := forceValue(transferLanePtr.Elem())
seenDataShards := forceValue(transferLane.FieldByName("seenDataShards"))
return TransferLaneRxSnapshot{
ShardSize: int(transferLane.FieldByName("ShardSize").Int()),
TotalDataShards: uint32(transferLane.FieldByName("TotalDataShards").Uint()),
SeenShardCount: uint32(transferLane.FieldByName("seenShardCount").Uint()),
SeenDataShardCount: seenDataShards.Len(),
HasDecoder: !transferLane.FieldByName("rxCodesState").IsNil(),
Completed: transferLane.FieldByName("completed").Bool(),
}
}
func snapshotRxChannel(channelPtr reflect.Value) RxChannelSnapshot {
channel := forceValue(channelPtr.Elem())
materializedValue := forceValue(channel.FieldByName("MaterializeChannel"))
if materializedValue.IsNil() {
return RxChannelSnapshot{}
}
materialized := forceInterface[*rrpitMaterializedTransferChannel.ChannelRx](materializedValue)
return RxChannelSnapshot{
ChannelID: materialized.ChannelID,
TotalPacketsReceived: materialized.TotalPacketsReceived,
LastPacketSeqReceived: materialized.LastPacketSeqReceived,
}
}
func snapshotRingBuffer(history reflect.Value) []reflect.Value {
history = forceValue(history)
values := forceValue(history.FieldByName("values"))
if values.Len() == 0 {
return nil
}
start := int(history.FieldByName("start").Int())
size := int(history.FieldByName("size").Int())
items := make([]reflect.Value, 0, size)
for i := 0; i < size; i++ {
index := (start + i) % values.Len()
items = append(items, forceValue(values.Index(index)))
}
return items
}
func forceValue(value reflect.Value) reflect.Value {
if !value.IsValid() {
return value
}
if value.CanInterface() {
return value
}
if value.CanAddr() {
return reflect.NewAt(value.Type(), unsafe.Pointer(value.UnsafeAddr())).Elem()
}
return value
}
func forceInterface[T any](value reflect.Value) T {
return forceValue(value).Interface().(T)
}
@@ -0,0 +1,263 @@
package rrpitDebuger
import (
"encoding/binary"
"io"
"os"
"strings"
"testing"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitMaterializedTransferChannel"
)
func TestDiagnoseBidirectionalSessionCapturesPacketsAndState(t *testing.T) {
logDir := t.TempDir()
recorder, err := NewPacketRecorder(PacketRecorderConfig{
Directory: logDir,
MaxFileSizeBytes: 128,
MaxFiles: 16,
})
if err != nil {
t.Fatal(err)
}
defer func() {
if err := recorder.Close(); err != nil {
t.Fatal(err)
}
}()
var receivedA [][]byte
var receivedB [][]byte
a := mustNewDebugBidirectionalSession(t, rrpitBidirectionalSession.Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func(data []byte) error {
receivedA = append(receivedA, append([]byte(nil), data...))
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 4,
MaxRewindableTimestampNum: 8,
MaxRewindableControlMessageNum: 8,
OddChannelIDs: true,
},
})
b := mustNewDebugBidirectionalSession(t, rrpitBidirectionalSession.Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: 16,
MaxBufferedLanes: 4,
OnMessage: func(data []byte) error {
receivedB = append(receivedB, append([]byte(nil), data...))
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: 16,
MaxDataShardsPerLane: 1,
MaxBufferedLanes: 4,
MaxRewindableTimestampNum: 8,
MaxRewindableControlMessageNum: 8,
},
})
aToB := &debugWriteCloser{}
bToA := &debugWriteCloser{}
aTxChannelID, err := a.AttachTxChannel(recorder.WrapWriter("a", 0, aToB))
if err != nil {
t.Fatal(err)
}
aRxChannel, err := a.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
bTxChannelID, err := b.AttachTxChannel(recorder.WrapWriter("b", 0, bToA))
if err != nil {
t.Fatal(err)
}
bRxChannel, err := b.AttachRxChannel()
if err != nil {
t.Fatal(err)
}
if err := a.SendMessage([]byte("hello")); err != nil {
t.Fatal(err)
}
if err := a.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
pumpDebugTraffic(t, recorder, "b", 0, aToB, bRxChannel)
if len(receivedB) != 1 || string(receivedB[0]) != "hello" {
t.Fatalf("unexpected payloads delivered to B: %q", receivedB)
}
if err := b.OnNewTimestamp(1); err != nil {
t.Fatal(err)
}
pumpDebugTraffic(t, recorder, "a", 0, bToA, aRxChannel)
output, err := DiagnoseBidirectionalSession(a, recorder)
if err != nil {
t.Fatal(err)
}
if output.Session.Tx == nil || output.Session.Rx == nil {
t.Fatal("expected tx and rx snapshots to be present")
}
if output.PacketLog.Directory != logDir {
t.Fatalf("expected packet log directory %q, got %q", logDir, output.PacketLog.Directory)
}
if len(output.PacketLog.Files) == 0 {
t.Fatal("expected packet log manifest to include files")
}
if len(output.PacketLog.Files) < 2 {
t.Fatalf("expected log rotation to create multiple files, got %d", len(output.PacketLog.Files))
}
if output.Session.Tx.Channels[0].ChannelID != aTxChannelID {
t.Fatalf("expected tx channel id %d, got %d", aTxChannelID, output.Session.Tx.Channels[0].ChannelID)
}
if output.Session.Rx.Channels[0].ChannelID != bTxChannelID {
t.Fatalf("expected rx channel to learn remote id %d, got %d", bTxChannelID, output.Session.Rx.Channels[0].ChannelID)
}
packets, err := recorder.ReadAllPackets()
if err != nil {
t.Fatal(err)
}
if len(packets) != output.PacketLog.TotalPacketsLogged {
t.Fatalf("expected %d retained packets, got %d", output.PacketLog.TotalPacketsLogged, len(packets))
}
var sawTx bool
var sawRx bool
for _, packet := range packets {
if packet.Direction == PacketDirectionTx {
sawTx = true
}
if packet.Direction == PacketDirectionRx {
sawRx = true
}
}
if !sawTx || !sawRx {
t.Fatalf("expected both tx and rx packet logs, got tx=%v rx=%v", sawTx, sawRx)
}
if !strings.Contains(output.String(), "\"packet_log\"") {
t.Fatal("expected formatted output to include packet_log section")
}
if len(receivedA) != 0 {
t.Fatalf("expected no payloads delivered to A in this flow, got %d", len(receivedA))
}
}
func TestPacketRecorderRotatesAndDropsOldFiles(t *testing.T) {
logDir := t.TempDir()
recorder, err := NewPacketRecorder(PacketRecorderConfig{
Directory: logDir,
MaxFileSizeBytes: 128,
MaxFiles: 2,
})
if err != nil {
t.Fatal(err)
}
defer func() {
if err := recorder.Close(); err != nil {
t.Fatal(err)
}
}()
for i := 0; i < 32; i++ {
wire := make([]byte, 40)
binary.BigEndian.PutUint64(wire[:8], uint64(i))
for j := 8; j < len(wire); j++ {
wire[j] = byte(i + j)
}
recorder.RecordInbound("peer", i%2, wire)
}
manifest := recorder.Manifest()
if manifest.MaxFiles != 2 {
t.Fatalf("expected max files 2, got %d", manifest.MaxFiles)
}
if len(manifest.Files) != 2 {
t.Fatalf("expected exactly 2 retained files, got %d", len(manifest.Files))
}
if manifest.DroppedLogFiles == 0 {
t.Fatal("expected old log files to be dropped after rotation")
}
entries, err := os.ReadDir(logDir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatalf("expected exactly 2 log files on disk, got %d", len(entries))
}
packets, err := recorder.ReadAllPackets()
if err != nil {
t.Fatal(err)
}
if len(packets) == 0 {
t.Fatal("expected retained packet logs to be readable")
}
if len(packets) >= manifest.TotalPacketsLogged {
t.Fatalf("expected some packets to be trimmed after rotation, got %d retained of %d total", len(packets), manifest.TotalPacketsLogged)
}
}
type debugWriteCloser struct {
writes [][]byte
readTo int
}
func (w *debugWriteCloser) Write(p []byte) (int, error) {
w.writes = append(w.writes, append([]byte(nil), p...))
return len(p), nil
}
func (w *debugWriteCloser) Close() error {
return nil
}
func pumpDebugTraffic(
t *testing.T,
recorder *PacketRecorder,
peer string,
channelIndex int,
writer *debugWriteCloser,
channel *rrpitMaterializedTransferChannel.ChannelRx,
) {
t.Helper()
for writer.readTo < len(writer.writes) {
wire := writer.writes[writer.readTo]
recorder.RecordInbound(peer, channelIndex, wire)
if err := channel.OnNewMessageArrived(wire); err != nil {
t.Fatal(err)
}
writer.readTo += 1
}
}
func mustNewDebugBidirectionalSession(
t *testing.T,
config rrpitBidirectionalSession.Config,
) *rrpitBidirectionalSession.BidirectionalSession {
t.Helper()
session, err := rrpitBidirectionalSession.New(config)
if err != nil {
t.Fatal(err)
}
return session
}
var _ io.WriteCloser = (*debugWriteCloser)(nil)
@@ -0,0 +1,9 @@
package rrpitMaterializedTransferChannel
import "github.com/v2fly/v2ray-core/v5/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}
@@ -0,0 +1,93 @@
package rrpitMaterializedTransferChannel
import (
"encoding/binary"
"io"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferChannel"
)
const channelSequenceFieldLength = 8
type ChannelRx struct {
rrpitTransferChannel.ChannelRx
OnNewDataMessage func(data []byte) error
closed bool
}
func NewChannelRx(channelID uint64, onNewDataMessage func(data []byte) error) (*ChannelRx, error) {
if onNewDataMessage == nil {
return nil, newError("nil OnNewDataMessage callback")
}
return &ChannelRx{
ChannelRx: *rrpitTransferChannel.NewChannelRx(channelID),
OnNewDataMessage: onNewDataMessage,
}, nil
}
func (rx *ChannelRx) OnNewMessageArrived(message []byte) (err error) {
if rx.closed {
return newError("channel receiver closed")
}
if len(message) < channelSequenceFieldLength {
return newError("materialized channel message too short")
}
dataMessage := rrpitTransferChannel.ChannelDataMessage{
ChannelSeq: binary.BigEndian.Uint64(message[:channelSequenceFieldLength]),
Data: append([]byte(nil), message[channelSequenceFieldLength:]...),
}
if err := rx.ProcessMessageReceived(dataMessage); err != nil {
return err
}
return rx.OnNewDataMessage(dataMessage.Data)
}
func (rx *ChannelRx) AssignChannelID(channelID uint64) error {
return rx.ChannelRx.AssignChannelID(channelID)
}
func (rx *ChannelRx) Close() error {
rx.closed = true
return nil
}
type ChannelTx struct {
rrpitTransferChannel.ChannelTx
io.WriteCloser
}
func NewChannelTx(channelID uint64, writer io.WriteCloser, maxRewindableTimestampNum int, maxRewindableControlMessageNum int) (*ChannelTx, error) {
if writer == nil {
return nil, newError("nil writer")
}
channelTx, err := rrpitTransferChannel.NewChannelTx(channelID, maxRewindableTimestampNum, maxRewindableControlMessageNum)
if err != nil {
return nil, err
}
return &ChannelTx{
ChannelTx: *channelTx,
WriteCloser: writer,
}, nil
}
func (tx *ChannelTx) SendDataMessage(data []byte) error {
payload := append([]byte(nil), data...)
wireMessage := make([]byte, channelSequenceFieldLength+len(payload))
binary.BigEndian.PutUint64(wireMessage[:channelSequenceFieldLength], tx.NextSeq)
copy(wireMessage[channelSequenceFieldLength:], payload)
written, err := tx.Write(wireMessage)
if err != nil {
return err
}
if written != len(wireMessage) {
return io.ErrShortWrite
}
if _, err := tx.CreateDataMessage(payload, uint64(time.Now().UnixNano())); err != nil {
return err
}
return nil
}
@@ -0,0 +1,272 @@
package rrpitMaterializedTransferChannel
import (
"encoding/binary"
"errors"
"io"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransferChannel"
)
func TestNewChannelConstructors(t *testing.T) {
if _, err := NewChannelRx(1, nil); err == nil {
t.Fatal("expected nil callback error")
}
rx, err := NewChannelRx(2, func([]byte) error { return nil })
if err != nil {
t.Fatal(err)
}
if rx.ChannelID != 2 {
t.Fatalf("expected rx channel id 2, got %d", rx.ChannelID)
}
if _, err := NewChannelTx(3, nil, 1, 1); err == nil {
t.Fatal("expected nil writer error")
}
if _, err := NewChannelTx(3, &recordingWriteCloser{}, -1, 1); err == nil {
t.Fatal("expected invalid timestamp history size error")
}
tx, err := NewChannelTx(4, &recordingWriteCloser{}, 2, 3)
if err != nil {
t.Fatal(err)
}
if tx.ChannelID != 4 {
t.Fatalf("expected tx channel id 4, got %d", tx.ChannelID)
}
}
func TestChannelTxSendDataMessageWritesWireFormatAndClose(t *testing.T) {
writer := &recordingWriteCloser{}
tx := mustNewChannelTx(t, 10, writer, 4, 4)
payload := []byte("hello")
if err := tx.SendDataMessage(payload); err != nil {
t.Fatal(err)
}
payload[0] = 'j'
if err := tx.SendDataMessage([]byte("world")); err != nil {
t.Fatal(err)
}
if tx.NextSeq != 2 {
t.Fatalf("expected next seq 2, got %d", tx.NextSeq)
}
if len(writer.writes) != 2 {
t.Fatalf("expected 2 writes, got %d", len(writer.writes))
}
firstSeq, firstPayload := parseWireMessage(t, writer.writes[0])
if firstSeq != 0 {
t.Fatalf("expected first wire seq 0, got %d", firstSeq)
}
if string(firstPayload) != "hello" {
t.Fatalf("expected first payload hello, got %q", string(firstPayload))
}
secondSeq, secondPayload := parseWireMessage(t, writer.writes[1])
if secondSeq != 1 {
t.Fatalf("expected second wire seq 1, got %d", secondSeq)
}
if string(secondPayload) != "world" {
t.Fatalf("expected second payload world, got %q", string(secondPayload))
}
if err := tx.Close(); err != nil {
t.Fatal(err)
}
if writer.closeCalls != 1 {
t.Fatalf("expected one close call, got %d", writer.closeCalls)
}
}
func TestChannelTxSendDataMessageWriteFailures(t *testing.T) {
t.Run("writer error", func(t *testing.T) {
writer := &recordingWriteCloser{writeErr: errors.New("write failed")}
tx := mustNewChannelTx(t, 20, writer, 2, 2)
if err := tx.SendDataMessage([]byte("payload")); err == nil || err.Error() != "write failed" {
t.Fatalf("expected write failed error, got %v", err)
}
if tx.NextSeq != 0 {
t.Fatalf("expected next seq to remain 0 after write failure, got %d", tx.NextSeq)
}
})
t.Run("short write", func(t *testing.T) {
writer := &recordingWriteCloser{shortWrite: 3}
tx := mustNewChannelTx(t, 21, writer, 2, 2)
if err := tx.SendDataMessage([]byte("payload")); !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("expected io.ErrShortWrite, got %v", err)
}
if tx.NextSeq != 0 {
t.Fatalf("expected next seq to remain 0 after short write, got %d", tx.NextSeq)
}
})
}
func TestChannelRxOnNewMessageArrivedAndControl(t *testing.T) {
var received [][]byte
rx := mustNewChannelRx(t, 30, func(data []byte) error {
received = append(received, append([]byte(nil), data...))
return nil
})
if err := rx.OnNewMessageArrived(materializedMessage(2, []byte("gamma"))); err != nil {
t.Fatal(err)
}
if err := rx.OnNewMessageArrived(materializedMessage(1, []byte("beta"))); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff([][]byte{[]byte("gamma"), []byte("beta")}, received); diff != "" {
t.Fatalf("unexpected received payloads (-want +got):\n%s", diff)
}
if rx.TotalPacketsReceived != 2 {
t.Fatalf("expected total packets received 2, got %d", rx.TotalPacketsReceived)
}
if rx.LastPacketSeqReceived != 2 {
t.Fatalf("expected last packet seq received 2, got %d", rx.LastPacketSeqReceived)
}
control, err := rx.CreateControlMessage()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(&rrpitTransferChannel.ChannelControlMessage{
ChannelID: 30,
TotalPacketReceived: 2,
LastSequenceNumberReceived: 2,
}, control); diff != "" {
t.Fatalf("unexpected control message (-want +got):\n%s", diff)
}
if err := rx.Close(); err != nil {
t.Fatal(err)
}
if err := rx.OnNewMessageArrived(materializedMessage(3, []byte("late"))); err == nil {
t.Fatal("expected closed receiver error")
}
}
func TestChannelRxOnNewMessageArrivedErrors(t *testing.T) {
t.Run("short message", func(t *testing.T) {
rx := mustNewChannelRx(t, 40, func([]byte) error { return nil })
if err := rx.OnNewMessageArrived([]byte{1, 2, 3}); err == nil {
t.Fatal("expected short message error")
}
})
t.Run("callback error", func(t *testing.T) {
callbackErr := errors.New("callback failed")
rx := mustNewChannelRx(t, 41, func([]byte) error { return callbackErr })
err := rx.OnNewMessageArrived(materializedMessage(5, []byte("boom")))
if !errors.Is(err, callbackErr) {
t.Fatalf("expected callback error, got %v", err)
}
if rx.TotalPacketsReceived != 1 || rx.LastPacketSeqReceived != 5 {
t.Fatalf("expected receiver stats to reflect delivered frame, got total=%d last=%d", rx.TotalPacketsReceived, rx.LastPacketSeqReceived)
}
})
}
func TestMaterializedChannelRoundTrip(t *testing.T) {
writer := &recordingWriteCloser{}
tx := mustNewChannelTx(t, 50, writer, 4, 4)
var received [][]byte
rx := mustNewChannelRx(t, 50, func(data []byte) error {
received = append(received, append([]byte(nil), data...))
return nil
})
if err := tx.SendDataMessage([]byte("one")); err != nil {
t.Fatal(err)
}
if err := tx.SendDataMessage(nil); err != nil {
t.Fatal(err)
}
for _, wire := range writer.writes {
if err := rx.OnNewMessageArrived(wire); err != nil {
t.Fatal(err)
}
}
if diff := cmp.Diff([][]byte{[]byte("one"), []byte(nil)}, received); diff != "" {
t.Fatalf("unexpected round-trip payloads (-want +got):\n%s", diff)
}
control, err := rx.CreateControlMessage()
if err != nil {
t.Fatal(err)
}
if control.TotalPacketReceived != 2 || control.LastSequenceNumberReceived != 1 {
t.Fatalf("unexpected round-trip control message: %+v", control)
}
}
type recordingWriteCloser struct {
writes [][]byte
writeErr error
shortWrite int
closeErr error
closeCalls int
}
func (w *recordingWriteCloser) Write(p []byte) (int, error) {
if w.writeErr != nil {
return 0, w.writeErr
}
if w.shortWrite > 0 && w.shortWrite < len(p) {
w.writes = append(w.writes, append([]byte(nil), p[:w.shortWrite]...))
return w.shortWrite, nil
}
w.writes = append(w.writes, append([]byte(nil), p...))
return len(p), nil
}
func (w *recordingWriteCloser) Close() error {
w.closeCalls++
return w.closeErr
}
func materializedMessage(seq uint64, payload []byte) []byte {
wire := make([]byte, channelSequenceFieldLength+len(payload))
binary.BigEndian.PutUint64(wire[:channelSequenceFieldLength], seq)
copy(wire[channelSequenceFieldLength:], payload)
return wire
}
func parseWireMessage(t *testing.T, wire []byte) (uint64, []byte) {
t.Helper()
if len(wire) < channelSequenceFieldLength {
t.Fatalf("wire message too short: %d", len(wire))
}
return binary.BigEndian.Uint64(wire[:channelSequenceFieldLength]), append([]byte(nil), wire[channelSequenceFieldLength:]...)
}
func mustNewChannelRx(t *testing.T, channelID uint64, onNewDataMessage func([]byte) error) *ChannelRx {
t.Helper()
rx, err := NewChannelRx(channelID, onNewDataMessage)
if err != nil {
t.Fatal(err)
}
return rx
}
func mustNewChannelTx(t *testing.T, channelID uint64, writer io.WriteCloser, maxTimestampHistory int, maxControlHistory int) *ChannelTx {
t.Helper()
tx, err := NewChannelTx(channelID, writer, maxTimestampHistory, maxControlHistory)
if err != nil {
t.Fatal(err)
}
return tx
}
@@ -0,0 +1,287 @@
package rrpitTransferChannel
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
type ringBuffer[T any] struct {
values []T
start int
size int
}
func newRingBuffer[T any](capacity int) ringBuffer[T] {
if capacity <= 0 {
return ringBuffer[T]{}
}
return ringBuffer[T]{
values: make([]T, capacity),
}
}
func (rb *ringBuffer[T]) Cap() int {
return len(rb.values)
}
func (rb *ringBuffer[T]) Len() int {
return rb.size
}
func (rb *ringBuffer[T]) Append(value T) {
if len(rb.values) == 0 {
return
}
if rb.size < len(rb.values) {
rb.values[(rb.start+rb.size)%len(rb.values)] = value
rb.size++
return
}
rb.values[rb.start] = value
rb.start = (rb.start + 1) % len(rb.values)
}
func (rb *ringBuffer[T]) At(index int) T {
return rb.values[(rb.start+index)%len(rb.values)]
}
func (rb *ringBuffer[T]) Snapshot() []T {
items := make([]T, rb.size)
for i := 0; i < rb.size; i++ {
items[i] = rb.At(i)
}
return items
}
func (rb *ringBuffer[T]) Range(fn func(T) bool) {
for i := 0; i < rb.size; i++ {
if !fn(rb.At(i)) {
return
}
}
}
type sentPacketSnapshot struct {
seq uint64
timestamp uint64
}
type ChannelRx struct {
ChannelID uint64
TotalPacketsReceived uint64
LastPacketSeqReceived uint64
}
func NewChannelRx(channelID uint64) *ChannelRx {
return &ChannelRx{ChannelID: channelID}
}
func (cr *ChannelRx) AssignChannelID(channelID uint64) error {
if channelID == 0 {
return newError("invalid channel id")
}
if cr.ChannelID == 0 {
cr.ChannelID = channelID
return nil
}
if cr.ChannelID != channelID {
return newError("channel id already assigned")
}
return nil
}
func (cr *ChannelRx) ProcessMessageReceived(msg ChannelDataMessage) error {
cr.TotalPacketsReceived++
if cr.TotalPacketsReceived == 1 || msg.ChannelSeq > cr.LastPacketSeqReceived {
cr.LastPacketSeqReceived = msg.ChannelSeq
}
return nil
}
func (cr *ChannelRx) CreateControlMessage() (*ChannelControlMessage, error) {
return &ChannelControlMessage{
ChannelID: cr.ChannelID,
TotalPacketReceived: cr.TotalPacketsReceived,
LastSequenceNumberReceived: cr.LastPacketSeqReceived,
}, nil
}
type ChannelTx struct {
ChannelID uint64
NextSeq uint64
maxRewindableTimestampNum int
// use a ring buffer to store packetSeq <-> time stamp look up
maxRewindableControlMessageNum int
// use a ring buffer to store the last few control message received
sentPacketHistory ringBuffer[sentPacketSnapshot]
controlHistory ringBuffer[ChannelControlMessage]
}
func NewChannelTx(channelID uint64, maxRewindableTimestampNum int, maxRewindableControlMessageNum int) (*ChannelTx, error) {
if maxRewindableTimestampNum < 0 {
return nil, newError("invalid max rewindable timestamp number")
}
if maxRewindableControlMessageNum < 0 {
return nil, newError("invalid max rewindable control message number")
}
tx := &ChannelTx{
ChannelID: channelID,
maxRewindableTimestampNum: maxRewindableTimestampNum,
maxRewindableControlMessageNum: maxRewindableControlMessageNum,
}
tx.sentPacketHistory = newRingBuffer[sentPacketSnapshot](maxRewindableTimestampNum)
tx.controlHistory = newRingBuffer[ChannelControlMessage](maxRewindableControlMessageNum)
return tx, nil
}
func (ct *ChannelTx) CreateDataMessage(data []byte, timestamp uint64) (*ChannelDataMessage, error) {
payload := append([]byte(nil), data...)
msg := &ChannelDataMessage{
ChannelSeq: ct.NextSeq,
Data: payload,
}
ct.sentPacketHistory.Append(sentPacketSnapshot{
seq: ct.NextSeq,
timestamp: timestamp,
})
ct.NextSeq++
return msg, nil
}
func (ct *ChannelTx) AcceptControlMessage(ctrl ChannelControlMessage) error {
if ctrl.ChannelID != ct.ChannelID {
return newError("channel id mismatch")
}
ct.controlHistory.Append(ctrl)
return nil
}
func (ct *ChannelTx) RemoteLastSeenMessageSenderTimestamp() (uint64, error) {
bestControl, ok := ct.latestUsableControl()
if !ok {
return 0, newError("remote last seen timestamp unavailable")
}
timestamp, ok := ct.timestampForSeq(bestControl.LastSequenceNumberReceived)
if !ok {
return 0, newError("remote last seen timestamp unavailable")
}
return timestamp, nil
}
func (ct *ChannelTx) RemotePacketLossSinceTimestamp(since uint64) (sent, lost uint64, err error) {
if ct.sentPacketHistory.Cap() == 0 || ct.sentPacketHistory.Len() == 0 {
return 0, 0, newError("sent packet rewind history unavailable")
}
firstIdx, rewindable, ok := ct.firstPacketIndexSince(since)
if !rewindable {
return 0, 0, newError("timestamp outside rewind window")
}
if !ok {
return 0, 0, nil
}
firstSeq := ct.sentPacketHistory.At(firstIdx).seq
endControl, ok := ct.latestUsableControl()
if !ok {
return 0, 0, newError("remote control history unavailable")
}
if endControl.LastSequenceNumberReceived < firstSeq {
return 0, 0, nil
}
for i := firstIdx; i < ct.sentPacketHistory.Len(); i++ {
snapshot := ct.sentPacketHistory.At(i)
if snapshot.seq > endControl.LastSequenceNumberReceived {
break
}
sent++
}
startReceived := uint64(0)
if startControl, ok := ct.bestControlBeforeSeq(firstSeq); ok {
startReceived = startControl.TotalPacketReceived
}
if endControl.TotalPacketReceived < startReceived {
return 0, 0, newError("remote control history regressed")
}
received := endControl.TotalPacketReceived - startReceived
if received >= sent {
return sent, 0, nil
}
return sent, sent - received, nil
}
func (ct *ChannelTx) latestUsableControl() (ChannelControlMessage, bool) {
var (
best ChannelControlMessage
found bool
)
ct.controlHistory.Range(func(ctrl ChannelControlMessage) bool {
if ctrl.TotalPacketReceived == 0 {
return true
}
if _, ok := ct.timestampForSeq(ctrl.LastSequenceNumberReceived); !ok {
return true
}
if !found || ctrl.LastSequenceNumberReceived > best.LastSequenceNumberReceived {
best = ctrl
found = true
}
return true
})
return best, found
}
func (ct *ChannelTx) bestControlBeforeSeq(seq uint64) (ChannelControlMessage, bool) {
var (
best ChannelControlMessage
found bool
)
ct.controlHistory.Range(func(ctrl ChannelControlMessage) bool {
if ctrl.TotalPacketReceived == 0 || ctrl.LastSequenceNumberReceived >= seq {
return true
}
if !found || ctrl.LastSequenceNumberReceived > best.LastSequenceNumberReceived {
best = ctrl
found = true
}
return true
})
return best, found
}
func (ct *ChannelTx) timestampForSeq(seq uint64) (uint64, bool) {
var (
timestamp uint64
found bool
)
ct.sentPacketHistory.Range(func(snapshot sentPacketSnapshot) bool {
if snapshot.seq == seq {
timestamp = snapshot.timestamp
found = true
return false
}
return true
})
return timestamp, found
}
func (ct *ChannelTx) firstPacketIndexSince(since uint64) (idx int, rewindable bool, ok bool) {
if ct.sentPacketHistory.Len() == 0 {
return 0, false, false
}
oldest := ct.sentPacketHistory.At(0)
if oldest.seq > 0 && since < oldest.timestamp {
return 0, false, false
}
for i := 0; i < ct.sentPacketHistory.Len(); i++ {
snapshot := ct.sentPacketHistory.At(i)
if snapshot.timestamp >= since {
return i, true, true
}
}
return 0, true, false
}
@@ -0,0 +1,349 @@
package rrpitTransferChannel
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestNewChannelConstructors(t *testing.T) {
rx := NewChannelRx(7)
if rx.ChannelID != 7 {
t.Fatalf("expected rx channel id 7, got %d", rx.ChannelID)
}
if rx.TotalPacketsReceived != 0 || rx.LastPacketSeqReceived != 0 {
t.Fatal("expected rx counters to start at zero")
}
if _, err := NewChannelTx(9, -1, 1); err == nil {
t.Fatal("expected invalid timestamp history size error")
}
if _, err := NewChannelTx(9, 1, -1); err == nil {
t.Fatal("expected invalid control history size error")
}
tx, err := NewChannelTx(9, 3, 2)
if err != nil {
t.Fatal(err)
}
if tx.ChannelID != 9 {
t.Fatalf("expected tx channel id 9, got %d", tx.ChannelID)
}
if tx.sentPacketHistory.Cap() != 3 {
t.Fatalf("expected sent history capacity 3, got %d", tx.sentPacketHistory.Cap())
}
if tx.controlHistory.Cap() != 2 {
t.Fatalf("expected control history capacity 2, got %d", tx.controlHistory.Cap())
}
}
func TestChannelRxProcessMessageReceivedAndControl(t *testing.T) {
rx := NewChannelRx(11)
for _, msg := range []ChannelDataMessage{
{ChannelSeq: 2, Data: []byte("c")},
{ChannelSeq: 1, Data: []byte("b")},
{ChannelSeq: 5, Data: []byte("f")},
} {
if err := rx.ProcessMessageReceived(msg); err != nil {
t.Fatal(err)
}
}
if rx.TotalPacketsReceived != 3 {
t.Fatalf("expected total packets received 3, got %d", rx.TotalPacketsReceived)
}
if rx.LastPacketSeqReceived != 5 {
t.Fatalf("expected last packet seq received 5, got %d", rx.LastPacketSeqReceived)
}
ctrl, err := rx.CreateControlMessage()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(&ChannelControlMessage{
ChannelID: 11,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 5,
}, ctrl); diff != "" {
t.Fatalf("unexpected control message (-want +got):\n%s", diff)
}
}
func TestChannelRxAssignChannelID(t *testing.T) {
rx := NewChannelRx(0)
if err := rx.AssignChannelID(0); err == nil {
t.Fatal("expected invalid channel id error")
}
if err := rx.AssignChannelID(17); err != nil {
t.Fatal(err)
}
if rx.ChannelID != 17 {
t.Fatalf("expected learned channel id 17, got %d", rx.ChannelID)
}
if err := rx.AssignChannelID(17); err != nil {
t.Fatal(err)
}
if err := rx.AssignChannelID(18); err == nil {
t.Fatal("expected conflicting channel id assignment error")
}
}
func TestChannelTxCreateDataMessageAndTimestampHistory(t *testing.T) {
tx := mustNewChannelTx(t, 21, 2, 1)
firstPayload := []byte("alpha")
firstMessage := mustCreateDataMessage(t, tx, firstPayload, 100)
firstPayload[0] = 'z'
if firstMessage.ChannelSeq != 0 {
t.Fatalf("expected first message seq 0, got %d", firstMessage.ChannelSeq)
}
if string(firstMessage.Data) != "alpha" {
t.Fatalf("expected message payload copy, got %q", string(firstMessage.Data))
}
secondMessage := mustCreateDataMessage(t, tx, []byte("beta"), 200)
thirdMessage := mustCreateDataMessage(t, tx, []byte("gamma"), 300)
if secondMessage.ChannelSeq != 1 || thirdMessage.ChannelSeq != 2 {
t.Fatalf("unexpected message sequences: second=%d third=%d", secondMessage.ChannelSeq, thirdMessage.ChannelSeq)
}
if tx.NextSeq != 3 {
t.Fatalf("expected next seq to be 3, got %d", tx.NextSeq)
}
assertSentPacketHistory(t, tx.sentPacketHistory.Snapshot(), []sentPacketSnapshot{
{seq: 1, timestamp: 200},
{seq: 2, timestamp: 300},
})
}
func assertSentPacketHistory(t *testing.T, got []sentPacketSnapshot, want []sentPacketSnapshot) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("unexpected sent packet history length: got %d want %d", len(got), len(want))
}
for i := range want {
if got[i].seq != want[i].seq || got[i].timestamp != want[i].timestamp {
t.Fatalf("unexpected sent packet history at %d: got %+v want %+v", i, got[i], want[i])
}
}
}
func TestChannelTxAcceptControlMessageAndRemoteLastSeenTimestamp(t *testing.T) {
tx := mustNewChannelTx(t, 31, 4, 2)
mustCreateDataMessage(t, tx, []byte("one"), 10)
mustCreateDataMessage(t, tx, []byte("two"), 20)
mustCreateDataMessage(t, tx, []byte("three"), 30)
if _, err := tx.RemoteLastSeenMessageSenderTimestamp(); err == nil {
t.Fatal("expected remote last seen timestamp query to fail without controls")
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 99,
TotalPacketReceived: 1,
LastSequenceNumberReceived: 0,
}); err == nil {
t.Fatal("expected channel id mismatch error")
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 31,
TotalPacketReceived: 2,
LastSequenceNumberReceived: 1,
}); err != nil {
t.Fatal(err)
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 31,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 2,
}); err != nil {
t.Fatal(err)
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 31,
TotalPacketReceived: 1,
LastSequenceNumberReceived: 0,
}); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff([]ChannelControlMessage{
{
ChannelID: 31,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 2,
},
{
ChannelID: 31,
TotalPacketReceived: 1,
LastSequenceNumberReceived: 0,
},
}, tx.controlHistory.Snapshot()); diff != "" {
t.Fatalf("unexpected control history (-want +got):\n%s", diff)
}
timestamp, err := tx.RemoteLastSeenMessageSenderTimestamp()
if err != nil {
t.Fatal(err)
}
if timestamp != 30 {
t.Fatalf("expected remote last seen timestamp 30, got %d", timestamp)
}
}
func TestChannelTxHistoryUsesRingBufferOrdering(t *testing.T) {
tx := mustNewChannelTx(t, 61, 2, 2)
mustCreateDataMessage(t, tx, []byte("one"), 10)
mustCreateDataMessage(t, tx, []byte("two"), 20)
mustCreateDataMessage(t, tx, []byte("three"), 30)
assertSentPacketHistory(t, tx.sentPacketHistory.Snapshot(), []sentPacketSnapshot{
{seq: 1, timestamp: 20},
{seq: 2, timestamp: 30},
})
for _, ctrl := range []ChannelControlMessage{
{
ChannelID: 61,
TotalPacketReceived: 1,
LastSequenceNumberReceived: 0,
},
{
ChannelID: 61,
TotalPacketReceived: 2,
LastSequenceNumberReceived: 1,
},
{
ChannelID: 61,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 2,
},
} {
if err := tx.AcceptControlMessage(ctrl); err != nil {
t.Fatal(err)
}
}
if diff := cmp.Diff([]ChannelControlMessage{
{
ChannelID: 61,
TotalPacketReceived: 2,
LastSequenceNumberReceived: 1,
},
{
ChannelID: 61,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 2,
},
}, tx.controlHistory.Snapshot()); diff != "" {
t.Fatalf("unexpected control ring contents (-want +got):\n%s", diff)
}
}
func TestChannelTxRemotePacketLossSinceTimestamp(t *testing.T) {
tx := mustNewChannelTx(t, 41, 8, 4)
for i, timestamp := range []uint64{10, 20, 30, 40, 50} {
payload := []byte{byte('a' + i)}
mustCreateDataMessage(t, tx, payload, timestamp)
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 41,
TotalPacketReceived: 2,
LastSequenceNumberReceived: 1,
}); err != nil {
t.Fatal(err)
}
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 41,
TotalPacketReceived: 4,
LastSequenceNumberReceived: 4,
}); err != nil {
t.Fatal(err)
}
sent, lost, err := tx.RemotePacketLossSinceTimestamp(30)
if err != nil {
t.Fatal(err)
}
if sent != 3 || lost != 1 {
t.Fatalf("expected sent=3 lost=1 since timestamp 30, got sent=%d lost=%d", sent, lost)
}
sent, lost, err = tx.RemotePacketLossSinceTimestamp(10)
if err != nil {
t.Fatal(err)
}
if sent != 5 || lost != 1 {
t.Fatalf("expected sent=5 lost=1 since timestamp 10, got sent=%d lost=%d", sent, lost)
}
sent, lost, err = tx.RemotePacketLossSinceTimestamp(60)
if err != nil {
t.Fatal(err)
}
if sent != 0 || lost != 0 {
t.Fatalf("expected empty window since timestamp 60, got sent=%d lost=%d", sent, lost)
}
}
func TestChannelTxRemotePacketLossErrors(t *testing.T) {
t.Run("no rewind history", func(t *testing.T) {
tx := mustNewChannelTx(t, 51, 0, 2)
mustCreateDataMessage(t, tx, []byte("one"), 10)
if _, _, err := tx.RemotePacketLossSinceTimestamp(10); err == nil {
t.Fatal("expected missing rewind history error")
}
})
t.Run("no control history", func(t *testing.T) {
tx := mustNewChannelTx(t, 52, 2, 2)
mustCreateDataMessage(t, tx, []byte("one"), 10)
if _, _, err := tx.RemotePacketLossSinceTimestamp(10); err == nil {
t.Fatal("expected missing control history error")
}
})
t.Run("timestamp outside rewind window", func(t *testing.T) {
tx := mustNewChannelTx(t, 53, 2, 2)
mustCreateDataMessage(t, tx, []byte("one"), 10)
mustCreateDataMessage(t, tx, []byte("two"), 20)
mustCreateDataMessage(t, tx, []byte("three"), 30)
if err := tx.AcceptControlMessage(ChannelControlMessage{
ChannelID: 53,
TotalPacketReceived: 3,
LastSequenceNumberReceived: 2,
}); err != nil {
t.Fatal(err)
}
if _, _, err := tx.RemotePacketLossSinceTimestamp(5); err == nil {
t.Fatal("expected rewind window error")
}
})
}
func mustNewChannelTx(t *testing.T, channelID uint64, maxTimestampHistory int, maxControlHistory int) *ChannelTx {
t.Helper()
tx, err := NewChannelTx(channelID, maxTimestampHistory, maxControlHistory)
if err != nil {
t.Fatal(err)
}
return tx
}
func mustCreateDataMessage(t *testing.T, tx *ChannelTx, data []byte, timestamp uint64) *ChannelDataMessage {
t.Helper()
msg, err := tx.CreateDataMessage(data, timestamp)
if err != nil {
t.Fatal(err)
}
return msg
}
@@ -0,0 +1,9 @@
package rrpitTransferChannel
import "github.com/v2fly/v2ray-core/v5/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}
@@ -0,0 +1,12 @@
package rrpitTransferChannel
type ChannelDataMessage struct {
ChannelSeq uint64
Data []byte
}
type ChannelControlMessage struct {
ChannelID uint64
TotalPacketReceived uint64
LastSequenceNumberReceived uint64
}
@@ -0,0 +1,9 @@
package rrpitTransferLane
import "github.com/v2fly/v2ray-core/v5/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}
@@ -0,0 +1,446 @@
package rrpitTransferLane
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
import (
"bytes"
stderrors "errors"
"math"
"github.com/lunixbochs/struc"
"github.com/xssnick/raptorq"
commonerrors "github.com/v2fly/v2ray-core/v5/common/errors"
)
const reconstructionLengthFieldSize = 2
var ErrNotEnoughSymbolsToReconstruct = stderrors.New("not enough symbols to reconstruct")
const (
SeenChunksCompletionSentinel = math.MaxUint16
maxReportedSeenChunks = SeenChunksCompletionSentinel - 1
)
func IsNotEnoughSymbolsToReconstruct(err error) bool {
return commonerrors.Cause(err) == ErrNotEnoughSymbolsToReconstruct
}
type TransferLaneRx struct {
ShardSize int
RemoteMaxDataShards int
TotalDataShards uint32
rxCodesState *raptorq.Decoder
seenDataShards []ReconstructionData
seenRepairShards map[uint32]struct{}
seenShardCount uint32
completed bool
}
func NewTransferLaneRx(shardSize int, remoteMaxDataShards int) (*TransferLaneRx, error) {
if err := validateTransferLaneConfig(shardSize); err != nil {
return nil, err
}
if remoteMaxDataShards < 0 {
return nil, newError("invalid remote max data shards")
}
return &TransferLaneRx{
ShardSize: shardSize,
RemoteMaxDataShards: remoteMaxDataShards,
seenDataShards: make([]ReconstructionData, 0),
seenRepairShards: make(map[uint32]struct{}),
}, nil
}
func (lr *TransferLaneRx) AddTransferData(data TransferData) (done bool, err error) {
if err := validateTransferLaneConfig(lr.ShardSize); err != nil {
return false, err
}
if data.LengthOfData != uint16(len(data.Data)) {
return false, newError("invalid transfer data length")
}
if data.TotalDataShards != 0 {
if err := lr.ensureDecoder(data.TotalDataShards); err != nil {
return false, err
}
}
if lr.TotalDataShards != 0 && data.Seq >= lr.TotalDataShards {
if len(data.Data) != lr.ShardSize {
return false, newError("invalid reconstruction symbol size")
}
if lr.rxCodesState == nil {
if hasAllShards(lr.seenDataShards, lr.TotalDataShards) {
return true, nil
}
return false, newError("reconstruction decoder not initialized")
}
if lr.seenRepairShards == nil {
lr.seenRepairShards = make(map[uint32]struct{})
}
if _, found := lr.seenRepairShards[data.Seq]; found {
return lr.seenShardCount >= lr.TotalDataShards, nil
}
canTryDecode, err := lr.rxCodesState.AddSymbol(data.Seq, data.Data)
if err != nil {
return false, newError("failed to add reconstruction symbol: ", err)
}
lr.seenRepairShards[data.Seq] = struct{}{}
lr.seenShardCount += 1
return lr.seenShardCount >= lr.TotalDataShards || canTryDecode, nil
}
shard := ReconstructionData{
LengthOfData: data.LengthOfData,
Data: append([]byte(nil), data.Data...),
}
if err := validateReconstructionData(lr.ShardSize, shard); err != nil {
return false, err
}
if err := lr.storeDataShard(data.Seq, shard); err != nil {
return false, err
}
if lr.TotalDataShards == 0 && lr.RemoteMaxDataShards > 0 && hasAllShards(lr.seenDataShards, uint32(lr.RemoteMaxDataShards)) {
lr.TotalDataShards = uint32(lr.RemoteMaxDataShards)
return true, nil
}
if lr.rxCodesState != nil {
symbol, err := encodeReconstructionData(lr.ShardSize, shard)
if err != nil {
return false, err
}
canTryDecode, err := lr.rxCodesState.AddSymbol(data.Seq, symbol)
if err != nil {
return false, newError("failed to add data shard symbol: ", err)
}
return lr.seenShardCount == lr.TotalDataShards || canTryDecode, nil
}
return lr.TotalDataShards != 0 && lr.seenShardCount == lr.TotalDataShards, nil
}
func (lr *TransferLaneRx) GenerateControl() (TransferControl, error) {
if lr.completed {
return TransferControl{SeenChunks: SeenChunksCompletionSentinel}, nil
}
seenChunks := lr.seenShardCount
if seenChunks > uint32(maxReportedSeenChunks) {
seenChunks = uint32(maxReportedSeenChunks)
}
return TransferControl{SeenChunks: uint16(seenChunks)}, nil
}
func (lr *TransferLaneRx) Reconstruct() ([]ReconstructionData, error) {
if lr.TotalDataShards == 0 {
return nil, newError("total data shards unknown")
}
if hasAllShards(lr.seenDataShards, lr.TotalDataShards) {
lr.completed = true
return cloneReconstructionDataSlice(lr.seenDataShards[:int(lr.TotalDataShards)]), nil
}
if lr.rxCodesState == nil {
return nil, newError("reconstruction decoder not initialized")
}
ok, payload, err := lr.rxCodesState.Decode()
if err != nil {
return nil, newError("failed to decode reconstruction data: ", err)
}
if !ok {
return nil, newError("not enough symbols to reconstruct").Base(ErrNotEnoughSymbolsToReconstruct)
}
reconstructed, err := decodeReconstructionPayload(lr.ShardSize, lr.TotalDataShards, payload)
if err != nil {
return nil, err
}
lr.seenDataShards = reconstructed
lr.seenShardCount = lr.TotalDataShards
lr.completed = true
return cloneReconstructionDataSlice(reconstructed), nil
}
type TransferLaneTx struct {
ShardSize int
MaxDataShards int
TotalDataShards uint32
txCodesState *raptorq.Encoder
seenShards []ReconstructionData
peerSeenChunks uint16
nextSeq uint32
}
func NewTransferLaneTx(shardSize int, maxDataShards int) (*TransferLaneTx, error) {
if err := validateTransferLaneConfig(shardSize); err != nil {
return nil, err
}
if maxDataShards < 0 {
return nil, newError("invalid max data shards")
}
lane := &TransferLaneTx{
ShardSize: shardSize,
MaxDataShards: maxDataShards,
}
if maxDataShards > 0 {
lane.seenShards = make([]ReconstructionData, 0, maxDataShards)
}
return lane, nil
}
func (lr *TransferLaneTx) AddData(data []byte) (transferData *TransferData, err error) {
if err := validateTransferLaneConfig(lr.ShardSize); err != nil {
return nil, err
}
if lr.TotalDataShards != 0 {
return nil, newError("finalized transfer lane")
}
if lr.MaxDataShards > 0 && lr.nextSeq >= uint32(lr.MaxDataShards) {
return nil, newError("max data shards reached")
}
if len(data) == 0 || len(data) > int(^uint16(0)) {
return nil, newError("invalid data length")
}
if len(data)+reconstructionLengthFieldSize > lr.ShardSize {
return nil, newError("data shard too large for configured shard size")
}
copied := append([]byte(nil), data...)
lr.seenShards = append(lr.seenShards, ReconstructionData{
Data: copied,
LengthOfData: uint16(len(copied)),
})
thisSeq := lr.nextSeq
lr.nextSeq += 1
return &TransferData{Seq: thisSeq, Data: copied, LengthOfData: uint16(len(copied))}, nil
}
func (lr *TransferLaneTx) CreateReconstructionTransmissionData() (TransferData, error) {
if err := validateTransferLaneConfig(lr.ShardSize); err != nil {
return TransferData{}, err
}
if lr.nextSeq == 0 {
return TransferData{}, newError("no data shards available for reconstruction")
}
if lr.txCodesState == nil {
totalDataShards := lr.TotalDataShards
if totalDataShards == 0 {
totalDataShards = lr.nextSeq
}
payloadSize, err := reconstructionPayloadSize(totalDataShards, lr.ShardSize)
if err != nil {
return TransferData{}, err
}
bufferBuilder := bytes.NewBuffer(make([]byte, 0, payloadSize))
for _, shard := range lr.seenShards {
symbol, err := encodeReconstructionData(lr.ShardSize, shard)
if err != nil {
return TransferData{}, err
}
bufferBuilder.Write(symbol)
}
rpQ := raptorq.NewRaptorQ(uint32(lr.ShardSize))
encoder, err := rpQ.CreateEncoder(bufferBuilder.Bytes())
if err != nil {
return TransferData{}, newError("failed to create reconstruction encoder: ", err)
}
if encoder.BaseSymbolsNum() != totalDataShards {
return TransferData{}, newError("unexpected number of base symbols")
}
lr.TotalDataShards = totalDataShards
lr.txCodesState = encoder
if lr.nextSeq < lr.TotalDataShards {
lr.nextSeq = lr.TotalDataShards
}
}
seq := lr.nextSeq
symbol := lr.txCodesState.GenSymbol(seq)
lr.nextSeq += 1
return TransferData{
TotalDataShards: lr.TotalDataShards,
Seq: seq,
LengthOfData: uint16(len(symbol)),
Data: symbol,
}, nil
}
func (lr *TransferLaneTx) AcceptControlData(control TransferControl) error {
if control.SeenChunks > lr.peerSeenChunks {
lr.peerSeenChunks = control.SeenChunks
}
return nil
}
func (lr *TransferLaneRx) ensureDecoder(totalDataShards uint32) error {
if err := validateTransferLaneConfig(lr.ShardSize); err != nil {
return err
}
if totalDataShards == 0 {
return newError("invalid total data shards")
}
if lr.TotalDataShards != 0 {
if lr.TotalDataShards != totalDataShards {
return newError("mismatched total data shards")
}
return nil
}
if exceedsMaxInt(totalDataShards) {
return newError("total data shards exceed supported slice size")
}
if uint32(len(lr.seenDataShards)) > totalDataShards {
return newError("received source shard exceeds announced total data shards")
}
payloadSize, err := reconstructionPayloadSize(totalDataShards, lr.ShardSize)
if err != nil {
return err
}
decoder, err := raptorq.NewRaptorQ(uint32(lr.ShardSize)).CreateDecoder(uint32(payloadSize))
if err != nil {
return newError("failed to create reconstruction decoder: ", err)
}
lr.TotalDataShards = totalDataShards
lr.rxCodesState = decoder
for i, shard := range lr.seenDataShards {
if shard.Data == nil {
continue
}
symbol, err := encodeReconstructionData(lr.ShardSize, shard)
if err != nil {
return err
}
if _, err := lr.rxCodesState.AddSymbol(uint32(i), symbol); err != nil {
return newError("failed to add cached data shard symbol: ", err)
}
}
return nil
}
func (lr *TransferLaneRx) storeDataShard(seq uint32, shard ReconstructionData) error {
if lr.TotalDataShards != 0 && seq >= lr.TotalDataShards {
return newError("source shard sequence out of range")
}
if exceedsMaxInt(seq) {
return newError("source shard sequence exceeds supported slice size")
}
index := int(seq)
if index >= len(lr.seenDataShards) {
extended := make([]ReconstructionData, index+1)
copy(extended, lr.seenDataShards)
lr.seenDataShards = extended
}
if existing := lr.seenDataShards[index]; existing.Data != nil {
if existing.LengthOfData != shard.LengthOfData || !bytes.Equal(existing.Data, shard.Data) {
return newError("conflicting source shard data")
}
return nil
}
lr.seenDataShards[index] = shard
lr.seenShardCount += 1
return nil
}
func validateTransferLaneConfig(shardSize int) error {
if shardSize <= reconstructionLengthFieldSize {
return newError("invalid shard size")
}
if shardSize > int(^uint16(0)) {
return newError("shard size exceeds transfer packet capacity")
}
return nil
}
func validateReconstructionData(shardSize int, shard ReconstructionData) error {
if shard.LengthOfData != uint16(len(shard.Data)) {
return newError("invalid reconstruction data length")
}
if shard.LengthOfData == 0 {
return newError("invalid reconstruction data length")
}
if int(shard.LengthOfData)+reconstructionLengthFieldSize > shardSize {
return newError("reconstruction data shard too large")
}
return nil
}
func reconstructionPayloadSize(totalDataShards uint32, shardSize int) (int, error) {
if totalDataShards == 0 {
return 0, newError("invalid total data shards")
}
if err := validateTransferLaneConfig(shardSize); err != nil {
return 0, err
}
payloadSize := uint64(totalDataShards) * uint64(shardSize)
if payloadSize > uint64(^uint32(0)) || payloadSize > uint64(int(^uint(0)>>1)) {
return 0, newError("reconstruction payload too large")
}
return int(payloadSize), nil
}
func encodeReconstructionData(shardSize int, shard ReconstructionData) ([]byte, error) {
if err := validateReconstructionData(shardSize, shard); err != nil {
return nil, err
}
buffer := bytes.NewBuffer(make([]byte, 0, shardSize))
if err := struc.Pack(buffer, &shard); err != nil {
return nil, newError("failed to pack reconstruction data: ", err)
}
if buffer.Len() > shardSize {
return nil, newError("packed reconstruction data exceeds shard size")
}
symbol := make([]byte, shardSize)
copy(symbol, buffer.Bytes())
return symbol, nil
}
func decodeReconstructionPayload(shardSize int, totalDataShards uint32, payload []byte) ([]ReconstructionData, error) {
expectedSize, err := reconstructionPayloadSize(totalDataShards, shardSize)
if err != nil {
return nil, err
}
if len(payload) != expectedSize {
return nil, newError("invalid reconstructed payload size")
}
reconstructed := make([]ReconstructionData, 0, totalDataShards)
for offset := 0; offset < len(payload); offset += shardSize {
var shard ReconstructionData
if err := struc.Unpack(bytes.NewReader(payload[offset:offset+shardSize]), &shard); err != nil {
return nil, newError("failed to unpack reconstructed shard: ", err)
}
if err := validateReconstructionData(shardSize, shard); err != nil {
return nil, err
}
reconstructed = append(reconstructed, shard)
}
return reconstructed, nil
}
func hasAllShards(shards []ReconstructionData, totalDataShards uint32) bool {
if uint32(len(shards)) < totalDataShards {
return false
}
for i := 0; i < int(totalDataShards); i++ {
if shards[i].Data == nil {
return false
}
}
return true
}
func cloneReconstructionDataSlice(shards []ReconstructionData) []ReconstructionData {
cloned := make([]ReconstructionData, len(shards))
for i, shard := range shards {
cloned[i] = ReconstructionData{
LengthOfData: shard.LengthOfData,
Data: append([]byte(nil), shard.Data...),
}
}
return cloned
}
func exceedsMaxInt(v uint32) bool {
return uint64(v) > uint64(^uint(0)>>1)
}
@@ -0,0 +1,659 @@
package rrpitTransferLane
import (
"math"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestNewTransferLaneConstructors(t *testing.T) {
t.Run("rx validates shard size", func(t *testing.T) {
if _, err := NewTransferLaneRx(reconstructionLengthFieldSize, 0); err == nil {
t.Fatal("expected invalid shard size error")
}
rx, err := NewTransferLaneRx(16, 3)
if err != nil {
t.Fatal(err)
}
if rx.ShardSize != 16 {
t.Fatalf("expected shard size 16, got %d", rx.ShardSize)
}
if rx.RemoteMaxDataShards != 3 {
t.Fatalf("expected remote max data shards 3, got %d", rx.RemoteMaxDataShards)
}
if rx.seenDataShards == nil {
t.Fatal("expected seenDataShards to be initialized")
}
})
t.Run("tx validates config", func(t *testing.T) {
if _, err := NewTransferLaneTx(reconstructionLengthFieldSize, 1); err == nil {
t.Fatal("expected invalid shard size error")
}
if _, err := NewTransferLaneTx(16, -1); err == nil {
t.Fatal("expected invalid max data shards error")
}
tx, err := NewTransferLaneTx(16, 3)
if err != nil {
t.Fatal(err)
}
if tx.ShardSize != 16 {
t.Fatalf("expected shard size 16, got %d", tx.ShardSize)
}
if tx.MaxDataShards != 3 {
t.Fatalf("expected max data shards 3, got %d", tx.MaxDataShards)
}
if cap(tx.seenShards) != 3 {
t.Fatalf("expected preallocated seenShards capacity 3, got %d", cap(tx.seenShards))
}
})
}
func TestTransferLaneTxAddDataAndLimits(t *testing.T) {
tx := mustNewTransferLaneTx(t, 8, 2)
firstInput := []byte("hello")
firstPacket := mustAddData(t, tx, firstInput)
firstInput[0] = 'j'
if firstPacket.Seq != 0 {
t.Fatalf("expected first packet seq 0, got %d", firstPacket.Seq)
}
if firstPacket.TotalDataShards != 0 {
t.Fatalf("expected data packet to omit total data shards, got %d", firstPacket.TotalDataShards)
}
if string(firstPacket.Data) != "hello" {
t.Fatalf("expected packet data to be copied, got %q", string(firstPacket.Data))
}
secondPacket := mustAddData(t, tx, []byte("bye"))
if secondPacket.Seq != 1 {
t.Fatalf("expected second packet seq 1, got %d", secondPacket.Seq)
}
if _, err := tx.AddData([]byte("x")); err == nil {
t.Fatal("expected max data shards reached error")
}
tx = mustNewTransferLaneTx(t, 8, 0)
if _, err := tx.AddData(nil); err == nil {
t.Fatal("expected invalid data length error for empty payload")
}
if _, err := tx.AddData([]byte("1234567")); err == nil {
t.Fatal("expected data shard too large error")
}
tx = mustNewTransferLaneTx(t, 8, 0)
mustAddData(t, tx, []byte("ok"))
if _, err := tx.CreateReconstructionTransmissionData(); err != nil {
t.Fatal(err)
}
if _, err := tx.AddData([]byte("no")); err == nil {
t.Fatal("expected finalized transfer lane error")
}
}
func TestTransferLaneRxSourceShardDedupAndControl(t *testing.T) {
rx := mustNewTransferLaneRx(t, 16)
done, err := rx.AddTransferData(TransferData{
Seq: 1,
LengthOfData: uint16(len("beta")),
Data: []byte("beta"),
})
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion before total shard count is known")
}
done, err = rx.AddTransferData(TransferData{
Seq: 0,
LengthOfData: uint16(len("alpha")),
Data: []byte("alpha"),
})
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion before total shard count is known")
}
control, err := rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != 2 {
t.Fatalf("expected SeenChunks 2, got %d", control.SeenChunks)
}
done, err = rx.AddTransferData(TransferData{
Seq: 1,
LengthOfData: uint16(len("beta")),
Data: []byte("beta"),
})
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect duplicate packet to report completion")
}
if rx.seenShardCount != 2 {
t.Fatalf("expected duplicate packet to preserve seen shard count, got %d", rx.seenShardCount)
}
if _, err := rx.Reconstruct(); err == nil {
t.Fatal("expected reconstruct to fail before total shard count is known")
}
if _, err := rx.AddTransferData(TransferData{
Seq: 1,
LengthOfData: uint16(len("BETA")),
Data: []byte("BETA"),
}); err == nil {
t.Fatal("expected conflicting duplicate packet to fail")
}
}
func TestTransferLaneRxGenerateControlCapsAtMaxReportedValue(t *testing.T) {
rx := &TransferLaneRx{
ShardSize: 16,
seenDataShards: make([]ReconstructionData, 0),
seenShardCount: uint32(math.MaxUint16) + 1,
}
control, err := rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != maxReportedSeenChunks {
t.Fatalf("expected SeenChunks %d, got %d", maxReportedSeenChunks, control.SeenChunks)
}
}
func TestTransferLaneDirectReconstructWithAllSourceShards(t *testing.T) {
payloads := [][]byte{
[]byte("alpha"),
[]byte("beta"),
[]byte("gamma"),
}
tx := mustNewTransferLaneTx(t, 24, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
rx := mustNewTransferLaneRx(t, 24)
for _, packet := range sourcePackets {
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion before repair packet announces total shards")
}
}
repair := mustCreateRepairPacket(t, tx)
if repair.TotalDataShards != uint32(len(payloads)) {
t.Fatalf("expected TotalDataShards %d, got %d", len(payloads), repair.TotalDataShards)
}
if repair.Seq != uint32(len(payloads)) {
t.Fatalf("expected first repair seq %d, got %d", len(payloads), repair.Seq)
}
if len(repair.Data) != tx.ShardSize {
t.Fatalf("expected repair symbol size %d, got %d", tx.ShardSize, len(repair.Data))
}
done, err := rx.AddTransferData(repair)
if err != nil {
t.Fatal(err)
}
if !done {
t.Fatal("expected all source shards plus repair announcement to complete lane")
}
want := reconstructionDataFromPayloads(payloads)
got, err := rx.Reconstruct()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected reconstruction (-want +got):\n%s", diff)
}
got[0].Data[0] = 'z'
gotAgain, err := rx.Reconstruct()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(want, gotAgain); diff != "" {
t.Fatalf("expected reconstruct to return cloned data (-want +got):\n%s", diff)
}
}
func TestTransferLaneDirectReconstructWithAllSourceShardsRemoteMaxConfigured(t *testing.T) {
payloads := [][]byte{
[]byte("alpha"),
[]byte("beta"),
[]byte("gamma"),
}
tx := mustNewTransferLaneTx(t, 24, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
rx := mustNewTransferLaneRxWithRemoteMax(t, 24, len(payloads))
for i, packet := range sourcePackets {
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if i < len(sourcePackets)-1 && done {
t.Fatal("did not expect early completion before final source shard")
}
if i == len(sourcePackets)-1 && !done {
t.Fatal("expected completion after final source shard when remote max is configured")
}
}
got, err := rx.Reconstruct()
if err != nil {
t.Fatal(err)
}
want := reconstructionDataFromPayloads(payloads)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected reconstruction (-want +got):\n%s", diff)
}
}
func TestTransferLaneDirectReconstructWithAllSourceShardsOutOfOrderRemoteMaxConfigured(t *testing.T) {
payloads := [][]byte{
[]byte("alpha"),
[]byte("beta"),
[]byte("gamma"),
}
tx := mustNewTransferLaneTx(t, 24, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
rx := mustNewTransferLaneRxWithRemoteMax(t, 24, len(payloads))
for _, index := range []int{2, 0} {
done, err := rx.AddTransferData(sourcePackets[index])
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion before all source shards exist")
}
}
done, err := rx.AddTransferData(sourcePackets[1])
if err != nil {
t.Fatal(err)
}
if !done {
t.Fatal("expected completion once the full contiguous source shard set exists")
}
}
func TestTransferLaneIgnoresLateRepairAfterRemoteMaxSourceCompletion(t *testing.T) {
payloads := [][]byte{
[]byte("alpha"),
[]byte("beta"),
[]byte("gamma"),
}
tx := mustNewTransferLaneTx(t, 24, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
repair := mustCreateRepairPacket(t, tx)
rx := mustNewTransferLaneRxWithRemoteMax(t, 24, len(payloads))
for _, packet := range sourcePackets {
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if packet.Seq == uint32(len(payloads)-1) && !done {
t.Fatal("expected source-only completion before repair arrives")
}
}
done, err := rx.AddTransferData(repair)
if err != nil {
t.Fatal(err)
}
if !done {
t.Fatal("expected completed lane to stay complete when late repair arrives")
}
got, err := rx.Reconstruct()
if err != nil {
t.Fatal(err)
}
want := reconstructionDataFromPayloads(payloads)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected reconstruction (-want +got):\n%s", diff)
}
}
func TestTransferLaneReconstructWithRepairSymbolsAfterLoss(t *testing.T) {
payloads := [][]byte{
[]byte("first"),
[]byte("second"),
[]byte("third"),
[]byte("fourth"),
[]byte("fifth"),
}
tx := mustNewTransferLaneTx(t, 32, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
repairPackets := make([]TransferData, 0, 12)
for i := 0; i < 12; i++ {
repairPackets = append(repairPackets, mustCreateRepairPacket(t, tx))
}
rx := mustNewTransferLaneRx(t, 32)
for i, packet := range sourcePackets {
if i == 1 || i == 4 {
continue
}
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion from partial source data")
}
}
want := reconstructionDataFromPayloads(payloads)
var (
got []ReconstructionData
lastErr error
)
for _, packet := range repairPackets {
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if !done {
continue
}
got, err = rx.Reconstruct()
if err == nil {
break
}
lastErr = err
}
if got == nil {
t.Fatalf("failed to reconstruct after repair symbols: %v", lastErr)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected reconstruction (-want +got):\n%s", diff)
}
control, err := rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != SeenChunksCompletionSentinel {
t.Fatalf("expected completion sentinel %d after reconstruction, got %d", SeenChunksCompletionSentinel, control.SeenChunks)
}
}
func TestTransferLaneRxGenerateControlUsesCompletionSentinelAfterDirectReconstruct(t *testing.T) {
rx := mustNewTransferLaneRx(t, 16)
if _, err := rx.AddTransferData(TransferData{
TotalDataShards: 1,
Seq: 0,
LengthOfData: uint16(len("done")),
Data: []byte("done"),
}); err != nil {
t.Fatal(err)
}
if _, err := rx.Reconstruct(); err != nil {
t.Fatal(err)
}
control, err := rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != SeenChunksCompletionSentinel {
t.Fatalf("expected completion sentinel %d, got %d", SeenChunksCompletionSentinel, control.SeenChunks)
}
}
func TestTransferLaneRxDoesNotEarlyCompleteShortLaneWithRemoteMaxConfigured(t *testing.T) {
tx := mustNewTransferLaneTx(t, 16, 4)
packet := mustAddData(t, tx, []byte("done"))
rx := mustNewTransferLaneRxWithRemoteMax(t, 16, 4)
done, err := rx.AddTransferData(packet)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect short lane to complete before total shard count is known")
}
}
func TestTransferLaneRxGenerateControlCountsUniqueRepairSymbols(t *testing.T) {
payloads := [][]byte{
[]byte("first"),
[]byte("second"),
[]byte("third"),
}
tx := mustNewTransferLaneTx(t, 32, len(payloads))
sourcePackets := addPayloads(t, tx, payloads)
repair := mustCreateRepairPacket(t, tx)
rx := mustNewTransferLaneRx(t, 32)
done, err := rx.AddTransferData(sourcePackets[0])
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion from one source shard")
}
done, err = rx.AddTransferData(repair)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect completion from one repair shard")
}
control, err := rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != 2 {
t.Fatalf("expected SeenChunks 2 after one data + one repair, got %d", control.SeenChunks)
}
done, err = rx.AddTransferData(repair)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("did not expect duplicate repair shard to complete lane")
}
control, err = rx.GenerateControl()
if err != nil {
t.Fatal(err)
}
if control.SeenChunks != 2 {
t.Fatalf("expected duplicate repair shard not to change SeenChunks, got %d", control.SeenChunks)
}
}
func TestTransferLaneTxAcceptControlData(t *testing.T) {
tx := mustNewTransferLaneTx(t, 16, 2)
addPayloads(t, tx, [][]byte{
[]byte("one"),
[]byte("two"),
})
firstRepair := mustCreateRepairPacket(t, tx)
if firstRepair.Seq != 2 {
t.Fatalf("expected first repair seq 2, got %d", firstRepair.Seq)
}
if err := tx.AcceptControlData(TransferControl{SeenChunks: 3}); err != nil {
t.Fatal(err)
}
if tx.peerSeenChunks != 3 {
t.Fatalf("expected sender to store peer seen chunks 3, got %d", tx.peerSeenChunks)
}
secondRepair := mustCreateRepairPacket(t, tx)
if secondRepair.Seq != 3 {
t.Fatalf("expected second repair seq 3 after oversized control report, got %d", secondRepair.Seq)
}
if err := tx.AcceptControlData(TransferControl{SeenChunks: 1}); err != nil {
t.Fatal(err)
}
if tx.peerSeenChunks != 3 {
t.Fatalf("expected peer seen chunk count to remain at max value 3, got %d", tx.peerSeenChunks)
}
if err := tx.AcceptControlData(TransferControl{SeenChunks: 2}); err != nil {
t.Fatal(err)
}
if tx.peerSeenChunks != 3 {
t.Fatalf("expected peer seen chunk count to remain at max value 3, got %d", tx.peerSeenChunks)
}
thirdRepair := mustCreateRepairPacket(t, tx)
if thirdRepair.Seq != 4 {
t.Fatalf("expected third repair seq 4 after control updates, got %d", thirdRepair.Seq)
}
tx2 := mustNewTransferLaneTx(t, 16, 2)
if err := tx2.AcceptControlData(TransferControl{SeenChunks: 4}); err != nil {
t.Fatal(err)
}
if tx2.peerSeenChunks != 4 {
t.Fatalf("expected peer seen chunk count 4 before finalization, got %d", tx2.peerSeenChunks)
}
}
func TestTransferLaneRxValidationErrors(t *testing.T) {
t.Run("invalid transfer data length", func(t *testing.T) {
rx := mustNewTransferLaneRx(t, 16)
if _, err := rx.AddTransferData(TransferData{
Seq: 0,
LengthOfData: 4,
Data: []byte("abc"),
}); err == nil {
t.Fatal("expected invalid transfer data length error")
}
})
t.Run("invalid repair symbol size", func(t *testing.T) {
rx := mustNewTransferLaneRx(t, 16)
if _, err := rx.AddTransferData(TransferData{
TotalDataShards: 1,
Seq: 1,
LengthOfData: 3,
Data: []byte("abc"),
}); err == nil {
t.Fatal("expected invalid reconstruction symbol size error")
}
})
t.Run("mismatched total shard count", func(t *testing.T) {
rx := mustNewTransferLaneRx(t, 16)
if _, err := rx.AddTransferData(TransferData{
TotalDataShards: 2,
Seq: 2,
LengthOfData: 16,
Data: make([]byte, 16),
}); err != nil {
t.Fatal(err)
}
if _, err := rx.AddTransferData(TransferData{
TotalDataShards: 3,
Seq: 3,
LengthOfData: 16,
Data: make([]byte, 16),
}); err == nil {
t.Fatal("expected mismatched total data shards error")
}
})
}
func mustNewTransferLaneRx(t *testing.T, shardSize int) *TransferLaneRx {
return mustNewTransferLaneRxWithRemoteMax(t, shardSize, 0)
}
func mustNewTransferLaneRxWithRemoteMax(t *testing.T, shardSize int, remoteMaxDataShards int) *TransferLaneRx {
t.Helper()
rx, err := NewTransferLaneRx(shardSize, remoteMaxDataShards)
if err != nil {
t.Fatal(err)
}
return rx
}
func mustNewTransferLaneTx(t *testing.T, shardSize int, maxDataShards int) *TransferLaneTx {
t.Helper()
tx, err := NewTransferLaneTx(shardSize, maxDataShards)
if err != nil {
t.Fatal(err)
}
return tx
}
func mustAddData(t *testing.T, tx *TransferLaneTx, payload []byte) TransferData {
t.Helper()
packet, err := tx.AddData(payload)
if err != nil {
t.Fatal(err)
}
return *packet
}
func addPayloads(t *testing.T, tx *TransferLaneTx, payloads [][]byte) []TransferData {
t.Helper()
packets := make([]TransferData, 0, len(payloads))
for _, payload := range payloads {
packets = append(packets, mustAddData(t, tx, payload))
}
return packets
}
func mustCreateRepairPacket(t *testing.T, tx *TransferLaneTx) TransferData {
t.Helper()
packet, err := tx.CreateReconstructionTransmissionData()
if err != nil {
t.Fatal(err)
}
return packet
}
func reconstructionDataFromPayloads(payloads [][]byte) []ReconstructionData {
reconstructed := make([]ReconstructionData, 0, len(payloads))
for _, payload := range payloads {
cp := append([]byte(nil), payload...)
reconstructed = append(reconstructed, ReconstructionData{
LengthOfData: uint16(len(cp)),
Data: cp,
})
}
return reconstructed
}
@@ -0,0 +1,24 @@
package rrpitTransferLane
const ReconstructionLengthFieldSize = reconstructionLengthFieldSize
type ReconstructionData struct {
LengthOfData uint16 `struc:"uint16,sizeof=Data"`
Data []byte
// For the data feed into raptorQ FEC, this data is always padded to shard size
}
type TransferData struct {
// This value shows the number of data shards in this lane,
// since we don't delay the transfer of payload, this value is 0 for data packet
TotalDataShards uint32
// this is the sequence of this packet, first TotalDataShards packets are data packet
// rest of them are reconstruction packet. The value of TotalDataShards is not present in every packet.
Seq uint32
LengthOfData uint16 `struc:"uint16,sizeof=Data"`
Data []byte
}
type TransferControl struct {
// tell sender how many chunk the receiver have seen, includes both data and reconstruction shards
SeenChunks uint16
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,169 @@
syntax = "proto3";
package v2ray.core.transport.internet.rrpit.transport;
option csharp_namespace = "V2Ray.Core.Transport.Internet.Rrpit.Transport";
option go_package = "github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitTransport";
option java_package = "com.v2ray.core.transport.internet.rrpit.transport";
option java_multiple_files = true;
import "google/protobuf/any.proto";
import "common/protoext/extensions.proto";
message DTLSUDPChannel {
bytes ip = 1;
uint32 port = 2;
string password = 3;
// If positive, consider this DTLS channel dead when no inbound rrpit message bytes arrive for this duration.
int64 no_incoming_message_timeout = 4;
string ip_addr = 68000 [(v2ray.core.common.protoext.field_opt).convert_time_parse_ip = "ip"];
}
message Channel {
// Underlying channel transport settings, for example DTLSUDPChannel.
ChannelSetting setting = 1;
google.protobuf.Any channel = 2;
}
message ChannelSetting {
// Maximum transmission unit of the underlying channel, in bytes.
uint32 mtu = 1;
// Relative scheduling weight of this channel. Zero means the default weight.
int32 weight = 2;
// Maximum packets this channel may send in a single rrpit timestamp window.
int32 max_sending_speed = 3;
}
message LaneSetting {
// Size of each shard carried inside a transfer lane, in bytes.
int32 shard_size = 1;
// Maximum number of source data shards before a lane is finalized.
int32 max_data_shards_per_lane = 2;
// Maximum number of in-flight / buffered lanes kept by tx and rx state.
int32 max_buffered_lanes = 3;
// Remote sender's maximum number of source data shards per lane. Zero disables the heuristic.
int32 remote_max_data_shards_per_lane = 4;
}
message SessionReconstructionSetting {
// the fraction of repair share (compare to data shards) to send immediately after finishing sending the data.
float initial_repair_shard_ratio = 1;
// when there is still sending capacity in channels, send repair shards for each lane depending on this weight,
// and number of repair shards still missing(known from control message)
repeated float lane_repair_weight = 2;
// when we received acknowledgement from remote, the fraction of repair shard to send compared to data shards missing
float secondary_repair_shard_ratio = 3;
// the number of tickets before we send the secondary repair shard again
int32 time_resend_secondary_repair_shard = 4;
// Number of ticks a finalized oldest lane may stay incomplete before stale-lane boost activates.
int32 stale_lane_finalized_age_threshold_ticks = 5;
// Number of ticks the oldest finalized lane may make no progress before stale-lane boost activates.
int32 stale_lane_progress_stall_threshold_ticks = 6;
// Minimum number of repair shards in a normal secondary resend burst.
int32 secondary_repair_min_burst = 7;
bool always_restrict_source_data_when_oldest_lane_stalled = 8;
}
message SessionManagerSetting {
// Tick interval, int64 values of time.Duration.
int64 timestamp_interval = 1;
int32 interactive_primary_cancellation_counter_limit = 2;
int32 manager_hosted_control_keepalive_interval_ticks = 3;
bool dynamic_restrict_source_data_when_oldest_lane_stalled = 4;
int32 dynamic_restrict_source_data_when_oldest_lane_stalled_ticks = 5;
}
message SessionSetting {
// Whether locally assigned tx channel ids should use odd parity.
bool odd_channel_ids = 1;
// Rewind window for tx-side packet timestamp history.
int32 max_rewindable_timestamp_num = 2;
// Rewind window for tx-side remote control history.
int32 max_rewindable_control_message_num = 3;
SessionReconstructionSetting reconstruction = 5;
}
message EngineeringSetting {
// If non-empty, rrpit debug snapshots and packet logs are written here.
string debugger_dir = 1;
// Maximum bytes per rotated debugger packet log file.
int64 debugger_max_file_size_bytes = 2;
// Maximum number of rotated debugger packet log files retained.
int32 debugger_max_files = 3;
}
message AdaptorSetting {
// SMUX protocol version. Zero means the smux default.
int32 smux_version = 1;
// Disable smux keep-alive probes.
bool keep_alive_disabled = 2;
// Keep-alive interval, int64 values of time.Duration. Zero means the smux default.
int64 keep_alive_interval = 3;
// Keep-alive timeout, int64 values of time.Duration. Zero means the smux default.
int64 keep_alive_timeout = 4;
// Maximum smux frame size in bytes. Zero means the smux default.
int32 max_frame_size = 5;
// Maximum total smux receive buffer in bytes. Zero means the smux default.
int32 max_receive_buffer = 6;
// Maximum per-stream smux receive buffer in bytes. Zero means the smux default.
int32 max_stream_buffer = 7;
}
message ConnectionPersistenceSetting {
// How long to retain a logical rrpit session after all DTLS channels are gone. Zero closes immediately.
int64 disconnected_session_retention = 1;
// Client reconnect retry interval while the logical session is retained. Zero means use the transport default.
int64 reconnect_retry_interval = 2;
// Keep the client rrpit transport session open even when there are no proxied streams.
bool keep_transport_session_without_streams = 3;
// Idle timeout when keep_transport_session_without_streams is false. Zero means use the transport default.
int64 idle_timeout = 4;
// If positive, discard the local cached rrpit session when no remote control packet is received within this duration.
int64 remote_control_inactivity_timeout = 5;
}
message Config {
option (v2ray.core.common.protoext.message_opt).type = "transport";
option (v2ray.core.common.protoext.message_opt).short_name = "rrpit";
option (v2ray.core.common.protoext.message_opt).allow_restricted_mode_load = false;
repeated Channel channels = 1;
LaneSetting lane = 2;
SessionSetting session = 3;
EngineeringSetting engineering = 4;
AdaptorSetting adaptor = 5;
SessionManagerSetting session_mgr = 6;
ConnectionPersistenceSetting persistence = 7;
}
@@ -0,0 +1,641 @@
//go:build !confonly
// +build !confonly
package rrpitTransport
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
gonet "net"
"sync"
"time"
piondtls "github.com/pion/dtls/v3"
piondtlsnet "github.com/pion/dtls/v3/pkg/net"
core "github.com/v2fly/v2ray-core/v5"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/common/environment"
"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
v2session "github.com/v2fly/v2ray-core/v5/common/session"
"github.com/v2fly/v2ray-core/v5/features/extension/storage"
"github.com/v2fly/v2ray-core/v5/transport/internet"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSessionManager"
)
const (
rrpitTransportConnectionStateKey = "rrpit-transport-connection-state"
rrpitClientSessionIdleTimeout = 5 * time.Second
rrpitSessionClassAttributeKey = "rrpitSessionClass"
rrpitReconnectAttemptTimeout = 5 * time.Second
)
var errRemoteSessionRestarted = errors.New("rrpit remote session restarted")
type transportConnectionState struct {
scopedSessionMap map[string]*persistentClientSession
scopedSessionAccess sync.Mutex
closed bool
}
func (*transportConnectionState) IsTransientStorageLifecycleReceiver() {}
func (s *transportConnectionState) Close() error {
s.scopedSessionAccess.Lock()
s.closed = true
sessions := make([]*persistentClientSession, 0, len(s.scopedSessionMap))
for _, session := range s.scopedSessionMap {
sessions = append(sessions, session)
}
s.scopedSessionMap = nil
s.scopedSessionAccess.Unlock()
var firstErr error
for _, session := range sessions {
if err := session.Close(); err != nil && firstErr == nil && err != io.ErrClosedPipe {
firstErr = err
}
}
return firstErr
}
func (s *transportConnectionState) getOrCreateSession(
ctx context.Context,
dest v2net.Destination,
streamSettings *internet.MemoryStreamConfig,
) (*persistentClientSession, error) {
key := rrpitTransportSessionKey(dest)
s.scopedSessionAccess.Lock()
if s.closed {
s.scopedSessionAccess.Unlock()
return nil, io.ErrClosedPipe
}
if existing := s.scopedSessionMap[key]; existing != nil && !existing.IsClosed() {
s.scopedSessionAccess.Unlock()
return existing, nil
}
s.scopedSessionAccess.Unlock()
var session *persistentClientSession
created, err := newPersistentClientSession(ctx, dest, streamSettings, func() {
s.removeSession(key, session)
})
if err != nil {
return nil, err
}
session = created
s.scopedSessionAccess.Lock()
defer s.scopedSessionAccess.Unlock()
if s.closed {
_ = session.Close()
return nil, io.ErrClosedPipe
}
if s.scopedSessionMap == nil {
s.scopedSessionMap = make(map[string]*persistentClientSession)
}
if existing := s.scopedSessionMap[key]; existing != nil && !existing.IsClosed() {
_ = session.Close()
return existing, nil
}
s.scopedSessionMap[key] = session
return session, nil
}
func (s *transportConnectionState) removeSession(key string, session *persistentClientSession) {
if s == nil {
return
}
s.scopedSessionAccess.Lock()
defer s.scopedSessionAccess.Unlock()
if s.scopedSessionMap == nil {
return
}
if current := s.scopedSessionMap[key]; current == session {
delete(s.scopedSessionMap, key)
}
}
type persistentClientSession struct {
owner *transportSession
openStream func(rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error)
closeSession func() error
idleTimeout time.Duration
keepTransportSessionWithoutStreams bool
baseCtx context.Context
resolvedChannels []resolvedChannel
streamSettings *internet.MemoryStreamConfig
reconnectRetryInterval time.Duration
remoteControlInactivityTimeout time.Duration
reconnectStop chan struct{}
reconnectDone chan struct{}
mu sync.Mutex
activeStreams int
idleTimer *time.Timer
remoteControlTimer *time.Timer
remoteControlTimerSeq uint64
closed bool
expectedRemoteSessionInstanceID rriptMonoDirectionSession.SessionInstanceID
remoteSessionInstanceSet bool
invalidateOnce sync.Once
}
func (s *persistentClientSession) IsClosed() bool {
if s == nil {
return true
}
s.mu.Lock()
defer s.mu.Unlock()
return s.closed
}
func (s *persistentClientSession) OpenConnection(ctx context.Context) (internet.Connection, error) {
if s == nil {
return nil, io.ErrClosedPipe
}
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil, io.ErrClosedPipe
}
if s.idleTimer != nil {
s.idleTimer.Stop()
s.idleTimer = nil
}
s.activeStreams++
s.mu.Unlock()
stream, err := s.openStream(sessionClassFromContext(ctx))
if err != nil {
s.releaseStream()
return nil, err
}
conn := &ownedConn{
Conn: stream,
owner: s.owner,
onClose: s.releaseStream,
ctx: ctx,
done: make(chan struct{}),
}
if ctx != nil {
if err := ctx.Err(); err != nil {
_ = conn.Close()
return nil, err
}
if done := ctx.Done(); done != nil {
go func() {
select {
case <-done:
_ = conn.Close()
case <-conn.done:
}
}()
}
}
return conn, nil
}
func (s *persistentClientSession) Close() error {
if s == nil {
return nil
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return io.ErrClosedPipe
}
s.closed = true
if s.idleTimer != nil {
s.idleTimer.Stop()
s.idleTimer = nil
}
s.stopRemoteControlTimerLocked()
s.mu.Unlock()
s.stopReconnectLoop()
if s.closeSession != nil {
return s.closeSession()
}
return nil
}
func (s *persistentClientSession) releaseStream() {
if s == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if s.activeStreams > 0 {
s.activeStreams--
}
if s.closed || s.keepTransportSessionWithoutStreams || s.activeStreams != 0 || s.idleTimeout <= 0 || s.idleTimer != nil {
return
}
s.idleTimer = time.AfterFunc(s.idleTimeout, func() {
s.mu.Lock()
if s.closed || s.activeStreams != 0 {
s.idleTimer = nil
s.mu.Unlock()
return
}
s.idleTimer = nil
s.mu.Unlock()
_ = s.Close()
})
}
func (s *persistentClientSession) stopReconnectLoop() {
if s == nil {
return
}
s.mu.Lock()
stop := s.reconnectStop
done := s.reconnectDone
s.reconnectStop = nil
s.reconnectDone = nil
s.mu.Unlock()
if stop != nil {
close(stop)
if done != nil {
<-done
}
}
}
func rrpitTransportSessionKey(dest v2net.Destination) string {
return dest.String()
}
func getRRPITTransportConnectionState(ctx context.Context) (*transportConnectionState, error) {
transportEnvironment := envctx.EnvironmentFromContext(ctx).(environment.TransportEnvironment)
state, err := transportEnvironment.TransientStorage().Get(ctx, rrpitTransportConnectionStateKey)
if err != nil {
state = &transportConnectionState{}
if putErr := transportEnvironment.TransientStorage().Put(ctx, rrpitTransportConnectionStateKey, state); putErr != nil {
return nil, putErr
}
state, err = transportEnvironment.TransientStorage().Get(ctx, rrpitTransportConnectionStateKey)
if err != nil {
return nil, err
}
}
typed, ok := state.(*transportConnectionState)
if !ok {
return nil, fmt.Errorf("invalid rrpit transport connection state %T", state)
}
return typed, nil
}
func newPersistentClientSession(
ctx context.Context,
dest v2net.Destination,
streamSettings *internet.MemoryStreamConfig,
onClose func(),
) (*persistentClientSession, error) {
config, ok := streamSettings.ProtocolSettings.(*Config)
if !ok {
return nil, fmt.Errorf("invalid rrpit transport config")
}
persistence := buildConnectionPersistencePolicy(config)
channels, err := resolveDialChannels(dest, config)
if err != nil {
return nil, err
}
id, err := newSessionID()
if err != nil {
return nil, err
}
baseCtx := ctx
if baseCtx == nil {
baseCtx = context.Background()
}
session := &persistentClientSession{
idleTimeout: persistence.IdleTimeout,
keepTransportSessionWithoutStreams: persistence.KeepTransportSessionWithoutStreams,
baseCtx: baseCtx,
resolvedChannels: append([]resolvedChannel(nil), channels...),
streamSettings: streamSettings,
reconnectRetryInterval: persistence.ReconnectRetryInterval,
remoteControlInactivityTimeout: persistence.RemoteControlInactivityTimeout,
}
owner, err := newTransportSession("client", id, config, true, func() {
session.mu.Lock()
session.closed = true
if session.idleTimer != nil {
session.idleTimer.Stop()
session.idleTimer = nil
}
session.stopRemoteControlTimerLocked()
session.mu.Unlock()
session.stopReconnectLoop()
if onClose != nil {
onClose()
}
}, func(remoteSessionInstanceID rriptMonoDirectionSession.SessionInstanceID) error {
if session == nil {
return nil
}
return session.handleRemoteSessionInstance(remoteSessionInstanceID)
})
if err != nil {
return nil, err
}
session.owner = owner
session.mu.Lock()
session.armRemoteControlTimerLocked()
session.mu.Unlock()
for index := range channels {
if err := session.connectChannelSlot(ctx, index); err != nil {
_ = owner.Close()
return nil, err
}
}
session.openStream = owner.OpenStreamByClass
session.closeSession = owner.Close
session.startReconnectLoop(id)
return session, nil
}
func (s *persistentClientSession) startReconnectLoop(sessionID transportSessionID) {
if s == nil || s.reconnectRetryInterval <= 0 {
return
}
s.mu.Lock()
if s.reconnectStop != nil || s.closed {
s.mu.Unlock()
return
}
s.reconnectStop = make(chan struct{})
s.reconnectDone = make(chan struct{})
stop := s.reconnectStop
done := s.reconnectDone
s.mu.Unlock()
go func() {
ticker := time.NewTicker(s.reconnectRetryInterval)
defer func() {
ticker.Stop()
close(done)
}()
for {
select {
case <-ticker.C:
if s.IsClosed() || s.owner == nil {
return
}
missing := s.owner.MissingChannelSlots()
for _, slotIndex := range missing {
if s.IsClosed() {
return
}
attemptCtx, cancel := context.WithTimeout(s.baseCtx, rrpitReconnectAttemptTimeout)
_ = s.connectChannelSlotWithID(attemptCtx, slotIndex, sessionID)
cancel()
}
case <-stop:
return
}
}
}()
}
func (s *persistentClientSession) connectChannelSlot(ctx context.Context, slotIndex int) error {
if s == nil || s.owner == nil {
return io.ErrClosedPipe
}
return s.connectChannelSlotWithID(ctx, slotIndex, s.owner.id)
}
func (s *persistentClientSession) connectChannelSlotWithID(ctx context.Context, slotIndex int, sessionID transportSessionID) error {
if s == nil || s.owner == nil || slotIndex < 0 || slotIndex >= len(s.resolvedChannels) {
return io.ErrClosedPipe
}
channel := s.resolvedChannels[slotIndex]
rawConn, err := internet.DialSystem(ctx, v2net.UDPDestination(channel.address, channel.port), s.streamSettings.SocketSettings)
if err != nil {
return err
}
dtlsConn, err := piondtls.Client(
piondtlsnet.PacketConnFromConn(rawConn),
rawConn.RemoteAddr(),
makePionDTLSConfig(channel, sessionID),
)
if err != nil {
_ = rawConn.Close()
return err
}
if err := dtlsConn.Handshake(); err != nil {
_ = dtlsConn.Close()
return err
}
if err := s.owner.attachChannel(dtlsConn, slotIndex, rrpitChannelConfig(channel.transport), channelReadIdleTimeout(channel)); err != nil {
_ = dtlsConn.Close()
return err
}
return nil
}
func sessionClassFromContext(ctx context.Context) rrpitBidirectionalSessionManager.SessionName {
if ctx == nil {
return rrpitBidirectionalSessionManager.InteractiveStream
}
content := v2session.ContentFromContext(ctx)
if content == nil {
return rrpitBidirectionalSessionManager.InteractiveStream
}
switch content.Attribute(rrpitSessionClassAttributeKey) {
case "background":
return rrpitBidirectionalSessionManager.BackgroundStream
default:
return rrpitBidirectionalSessionManager.InteractiveStream
}
}
func (s *persistentClientSession) handleRemoteSessionInstance(remoteSessionInstanceID rriptMonoDirectionSession.SessionInstanceID) error {
if s == nil {
return io.ErrClosedPipe
}
var expected rriptMonoDirectionSession.SessionInstanceID
shouldInvalidate := false
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return io.ErrClosedPipe
}
s.armRemoteControlTimerLocked()
if !s.remoteSessionInstanceSet {
s.expectedRemoteSessionInstanceID = remoteSessionInstanceID
s.remoteSessionInstanceSet = true
s.mu.Unlock()
return nil
}
expected = s.expectedRemoteSessionInstanceID
if expected != remoteSessionInstanceID {
shouldInvalidate = true
s.closed = true
if s.idleTimer != nil {
s.idleTimer.Stop()
s.idleTimer = nil
}
s.stopRemoteControlTimerLocked()
}
s.mu.Unlock()
if !shouldInvalidate {
return nil
}
go s.invalidateRemoteSession(expected, remoteSessionInstanceID)
return errRemoteSessionRestarted
}
func (s *persistentClientSession) armRemoteControlTimerLocked() {
if s == nil || s.remoteControlInactivityTimeout <= 0 {
return
}
s.remoteControlTimerSeq++
seq := s.remoteControlTimerSeq
if s.remoteControlTimer != nil {
s.remoteControlTimer.Stop()
}
s.remoteControlTimer = time.AfterFunc(s.remoteControlInactivityTimeout, func() {
s.handleRemoteControlInactivityTimeout(seq)
})
}
func (s *persistentClientSession) stopRemoteControlTimerLocked() {
if s == nil {
return
}
s.remoteControlTimerSeq++
if s.remoteControlTimer != nil {
s.remoteControlTimer.Stop()
s.remoteControlTimer = nil
}
}
func (s *persistentClientSession) handleRemoteControlInactivityTimeout(seq uint64) {
if s == nil {
return
}
s.mu.Lock()
if s.closed || s.remoteControlInactivityTimeout <= 0 || seq != s.remoteControlTimerSeq {
s.mu.Unlock()
return
}
s.closed = true
if s.idleTimer != nil {
s.idleTimer.Stop()
s.idleTimer = nil
}
s.stopRemoteControlTimerLocked()
timeout := s.remoteControlInactivityTimeout
s.mu.Unlock()
s.invalidateRemoteControlSilence(timeout)
}
func (s *persistentClientSession) invalidateRemoteSession(expected rriptMonoDirectionSession.SessionInstanceID, received rriptMonoDirectionSession.SessionInstanceID) {
if s == nil {
return
}
s.invalidateOnce.Do(func() {
newError(
"rrpit client detected remote session restart: expected remote instance ",
hex.EncodeToString(expected[:]),
", received ",
hex.EncodeToString(received[:]),
).AtWarning().WriteToLog()
s.stopReconnectLoop()
if s.closeSession != nil {
_ = s.closeSession()
}
})
}
func (s *persistentClientSession) invalidateRemoteControlSilence(timeout time.Duration) {
if s == nil {
return
}
s.invalidateOnce.Do(func() {
newError(
"rrpit client discarded cached session after no remote control packet for ",
timeout,
).AtWarning().WriteToLog()
s.stopReconnectLoop()
if s.closeSession != nil {
_ = s.closeSession()
}
})
}
func Dial(ctx context.Context, dest v2net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) {
detachedContext := core.ToBackgroundDetachedContext(ctx)
state, err := getRRPITTransportConnectionState(detachedContext)
if err != nil {
return nil, err
}
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
session, err := state.getOrCreateSession(detachedContext, dest, streamSettings)
if err != nil {
return nil, err
}
conn, err := session.OpenConnection(ctx)
if err == nil {
return conn, nil
}
lastErr = err
_ = session.Close()
}
if lastErr == nil {
lastErr = io.ErrClosedPipe
}
return nil, lastErr
}
var _ storage.TransientStorageLifecycleReceiver = (*transportConnectionState)(nil)
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}
@@ -0,0 +1,112 @@
package rrpitTransport
import (
"errors"
"testing"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
)
func TestPersistentClientSessionKeepsSessionOnMatchingRemoteInstanceID(t *testing.T) {
closeCalled := make(chan struct{}, 1)
session := &persistentClientSession{
closeSession: func() error {
closeCalled <- struct{}{}
return nil
},
}
remoteInstanceID := rriptMonoDirectionSession.SessionInstanceID{1, 2, 3}
if err := session.handleRemoteSessionInstance(remoteInstanceID); err != nil {
t.Fatal(err)
}
if err := session.handleRemoteSessionInstance(remoteInstanceID); err != nil {
t.Fatal(err)
}
select {
case <-closeCalled:
t.Fatal("did not expect session close on matching remote instance id")
case <-time.After(20 * time.Millisecond):
}
}
func TestPersistentClientSessionInvalidatesOnRemoteInstanceIDMismatch(t *testing.T) {
state := &transportConnectionState{
scopedSessionMap: map[string]*persistentClientSession{},
}
key := "rrpit-test"
closeDone := make(chan struct{}, 1)
session := &persistentClientSession{}
session.closeSession = func() error {
state.removeSession(key, session)
closeDone <- struct{}{}
return nil
}
state.scopedSessionMap[key] = session
if err := session.handleRemoteSessionInstance(rriptMonoDirectionSession.SessionInstanceID{1}); err != nil {
t.Fatal(err)
}
err := session.handleRemoteSessionInstance(rriptMonoDirectionSession.SessionInstanceID{2})
if !errors.Is(err, errRemoteSessionRestarted) {
t.Fatalf("unexpected mismatch error: %v", err)
}
select {
case <-closeDone:
case <-time.After(200 * time.Millisecond):
t.Fatal("timed out waiting for stale session close")
}
if !session.IsClosed() {
t.Fatal("expected session to be closed after remote instance mismatch")
}
state.scopedSessionAccess.Lock()
_, ok := state.scopedSessionMap[key]
state.scopedSessionAccess.Unlock()
if ok {
t.Fatal("expected stale session to be removed from cache")
}
}
func TestPersistentClientSessionInvalidatesOnRemoteControlInactivity(t *testing.T) {
state := &transportConnectionState{
scopedSessionMap: map[string]*persistentClientSession{},
}
key := "rrpit-timeout"
closeDone := make(chan struct{}, 1)
session := &persistentClientSession{
remoteControlInactivityTimeout: 20 * time.Millisecond,
}
session.closeSession = func() error {
state.removeSession(key, session)
closeDone <- struct{}{}
return nil
}
state.scopedSessionMap[key] = session
session.mu.Lock()
session.armRemoteControlTimerLocked()
session.mu.Unlock()
select {
case <-closeDone:
case <-time.After(200 * time.Millisecond):
t.Fatal("timed out waiting for remote control inactivity invalidation")
}
if !session.IsClosed() {
t.Fatal("expected session to be closed after remote control inactivity")
}
state.scopedSessionAccess.Lock()
_, ok := state.scopedSessionMap[key]
state.scopedSessionAccess.Unlock()
if ok {
t.Fatal("expected stale session to be removed from cache after inactivity timeout")
}
}
@@ -0,0 +1,183 @@
//go:build !confonly
// +build !confonly
package rrpitTransport
import (
"context"
"fmt"
"io"
gonet "net"
"sync"
"github.com/v2fly/v2ray-core/v5/common"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
"github.com/v2fly/v2ray-core/v5/transport/internet"
transportdtls "github.com/v2fly/v2ray-core/v5/transport/internet/dtls"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSessionManager"
)
type Listener struct {
config *Config
handler internet.ConnHandler
channels []resolvedChannel
mu sync.Mutex
listeners []internet.Listener
sessions map[transportSessionID]*transportSession
closed bool
}
func (l *Listener) Close() error {
if l == nil {
return nil
}
l.mu.Lock()
if l.closed {
l.mu.Unlock()
return nil
}
l.closed = true
listeners := append([]internet.Listener(nil), l.listeners...)
l.listeners = nil
sessions := make([]*transportSession, 0, len(l.sessions))
for _, session := range l.sessions {
sessions = append(sessions, session)
}
l.sessions = nil
l.mu.Unlock()
var firstErr error
for _, listener := range listeners {
if err := listener.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
for _, session := range sessions {
if err := session.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (l *Listener) Addr() gonet.Addr {
if l == nil {
return nil
}
l.mu.Lock()
defer l.mu.Unlock()
if len(l.listeners) == 0 {
return nil
}
return l.listeners[0].Addr()
}
func (l *Listener) handleAcceptedChannel(conn gonet.Conn, channelIndex int) {
go l.serveAcceptedChannel(conn, channelIndex)
}
func (l *Listener) serveAcceptedChannel(conn gonet.Conn, channelIndex int) {
sessionID, err := readTransportSessionID(conn)
if err != nil {
_ = conn.Close()
return
}
owner, err := l.getOrCreateSession(sessionID)
if err != nil {
_ = conn.Close()
return
}
if err := owner.attachChannel(conn, channelIndex, rrpitChannelConfig(l.channels[channelIndex].transport), channelReadIdleTimeout(l.channels[channelIndex])); err != nil {
_ = owner.Close()
}
}
func (l *Listener) getOrCreateSession(sessionID transportSessionID) (*transportSession, error) {
l.mu.Lock()
defer l.mu.Unlock()
if l.closed {
return nil, io.ErrClosedPipe
}
if existing := l.sessions[sessionID]; existing != nil {
return existing, nil
}
owner, err := newTransportSession("server", sessionID, l.config, false, func() {
l.mu.Lock()
defer l.mu.Unlock()
if l.sessions != nil {
delete(l.sessions, sessionID)
}
}, nil)
if err != nil {
return nil, err
}
if l.sessions == nil {
l.sessions = map[transportSessionID]*transportSession{}
}
l.sessions[sessionID] = owner
go l.acceptStreams(owner, rrpitBidirectionalSessionManager.InteractiveStream)
go l.acceptStreams(owner, rrpitBidirectionalSessionManager.BackgroundStream)
return owner, nil
}
func (l *Listener) acceptStreams(owner *transportSession, sessionClass rrpitBidirectionalSessionManager.SessionName) {
for {
stream, err := owner.AcceptStream(sessionClass)
if err != nil {
_ = owner.Close()
return
}
l.handler(&ownedConn{Conn: stream, owner: owner})
}
}
func Listen(ctx context.Context, address v2net.Address, port v2net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
config, ok := streamSettings.ProtocolSettings.(*Config)
if !ok {
return nil, fmt.Errorf("invalid rrpit transport config")
}
channels, err := resolveListenChannels(address, port, config)
if err != nil {
return nil, err
}
listener := &Listener{
config: config,
handler: handler,
channels: channels,
sessions: map[transportSessionID]*transportSession{},
}
for index, channel := range channels {
index := index
dtlsListener, err := transportdtls.ListenDTLS(
ctx,
channel.address,
channel.port,
makeDTLSStreamSettings(channel, streamSettings.SocketSettings),
func(conn internet.Connection) {
listener.handleAcceptedChannel(conn, index)
},
)
if err != nil {
_ = listener.Close()
return nil, err
}
listener.listeners = append(listener.listeners, dtlsListener)
}
return listener, nil
}
func init() {
common.Must(internet.RegisterTransportListener(protocolName, Listen))
}
@@ -0,0 +1,46 @@
//go:build !confonly
// +build !confonly
package rrpitTransport
import "time"
const defaultReconnectRetryInterval = time.Second
type connectionPersistencePolicy struct {
DisconnectedSessionRetention time.Duration
ReconnectRetryInterval time.Duration
KeepTransportSessionWithoutStreams bool
IdleTimeout time.Duration
RemoteControlInactivityTimeout time.Duration
}
func buildConnectionPersistencePolicy(config *Config) connectionPersistencePolicy {
var persistence *ConnectionPersistenceSetting
if config != nil {
persistence = config.GetPersistence()
}
policy := connectionPersistencePolicy{
DisconnectedSessionRetention: time.Duration(persistence.GetDisconnectedSessionRetention()),
ReconnectRetryInterval: time.Duration(persistence.GetReconnectRetryInterval()),
KeepTransportSessionWithoutStreams: persistence.GetKeepTransportSessionWithoutStreams(),
IdleTimeout: time.Duration(persistence.GetIdleTimeout()),
RemoteControlInactivityTimeout: time.Duration(persistence.GetRemoteControlInactivityTimeout()),
}
if policy.DisconnectedSessionRetention < 0 {
policy.DisconnectedSessionRetention = 0
}
if policy.ReconnectRetryInterval < 0 {
policy.ReconnectRetryInterval = 0
}
if policy.DisconnectedSessionRetention > 0 && policy.ReconnectRetryInterval == 0 {
policy.ReconnectRetryInterval = defaultReconnectRetryInterval
}
if policy.IdleTimeout <= 0 {
policy.IdleTimeout = rrpitClientSessionIdleTimeout
}
if policy.RemoteControlInactivityTimeout < 0 {
policy.RemoteControlInactivityTimeout = 0
}
return policy
}
@@ -0,0 +1,14 @@
package rrpitTransport
import (
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/transport/internet"
)
const protocolName = "rrpit"
func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config)
}))
}
@@ -0,0 +1,933 @@
//go:build !confonly
// +build !confonly
package rrpitTransport
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
gonet "net"
"os"
"path/filepath"
"sync"
"time"
piondtls "github.com/pion/dtls/v3"
"github.com/xtaci/smux"
commonerrors "github.com/v2fly/v2ray-core/v5/common/errors"
v2net "github.com/v2fly/v2ray-core/v5/common/net"
"github.com/v2fly/v2ray-core/v5/common/serial"
"github.com/v2fly/v2ray-core/v5/transport/internet"
transportdtls "github.com/v2fly/v2ray-core/v5/transport/internet/dtls"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/packetToStream"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSessionManager"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitChannelManager"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitDebuger"
)
const (
defaultDTLSMTU = 1200
defaultDTLSReplayWindow = 128
transportMaxPacketSize = 8 << 20
transportIdentityVersion = byte(1)
)
var (
transportIdentityMagic = [4]byte{'R', 'R', 'P', 'T'}
transportHandshakeMagic = transportIdentityMagic
)
type errPathObjHolder struct{}
func newError(values ...interface{}) *commonerrors.Error {
return commonerrors.New(values...).WithPathObj(errPathObjHolder{})
}
type transportSessionID [16]byte
type resolvedChannel struct {
transport *ChannelSetting
dtls *DTLSUDPChannel
address v2net.Address
port v2net.Port
}
type ownedConn struct {
gonet.Conn
owner *transportSession
onClose func()
closeOnce sync.Once
ctx context.Context
done chan struct{}
}
type transportChannelSlot struct {
config rriptMonoDirectionSession.ChannelConfig
conn gonet.Conn
managerChannelIndex int
readIdleTimeout time.Duration
}
type transportSession struct {
id transportSessionID
role string
localSessionInstanceID rriptMonoDirectionSession.SessionInstanceID
session *rrpitBidirectionalSession.BidirectionalSession
adaptor *packetToStream.Adaptor
sessionManager *rrpitBidirectionalSessionManager.Manager
backgroundAdaptor *packetToStream.Adaptor
recorder *rrpitDebuger.PacketRecorder
onClose func()
persistence connectionPersistencePolicy
mu sync.Mutex
slots []transportChannelSlot
localAddr gonet.Addr
remoteAddr gonet.Addr
disconnected bool
disconnectedTimer *time.Timer
closeOnce sync.Once
closeErr error
closed bool
}
func newTransportSession(
role string,
id transportSessionID,
config *Config,
client bool,
onClose func(),
onRemoteSessionInstance func(rriptMonoDirectionSession.SessionInstanceID) error,
) (*transportSession, error) {
persistence := buildConnectionPersistencePolicy(config)
channelManager, err := rrpitChannelManager.New(buildChannelManagerConfig(config))
if err != nil {
return nil, err
}
channelManager.SetBlockOnNoChannels(persistence.DisconnectedSessionRetention > 0)
recorder, err := newPacketRecorder(config, role, id)
if err != nil {
_ = channelManager.Close()
return nil, err
}
localSessionInstanceID, err := newSessionInstanceID()
if err != nil {
if recorder != nil {
_ = recorder.Close()
}
_ = channelManager.Close()
return nil, err
}
sessionManagerConfig := buildBidirectionalSessionManagerConfig(config, channelManager)
sessionManagerConfig.BaseSessionConfig.LocalSessionInstanceID = localSessionInstanceID
if onRemoteSessionInstance != nil {
sessionManagerConfig.BaseSessionConfig.ValidateRemoteControl = func(ctrl rriptMonoDirectionSession.ControlMessage) error {
return onRemoteSessionInstance(ctrl.Session.InstanceID)
}
}
sessionManager, err := rrpitBidirectionalSessionManager.New(sessionManagerConfig)
if err != nil {
if recorder != nil {
_ = recorder.Close()
}
_ = channelManager.Close()
return nil, err
}
session := sessionManager.Session(rrpitBidirectionalSessionManager.InteractiveStream)
backgroundSession := sessionManager.Session(rrpitBidirectionalSessionManager.BackgroundStream)
maxMessageSize, err := session.MaxMessageSize()
if err != nil {
if recorder != nil {
_ = recorder.Close()
}
_ = sessionManager.Close()
return nil, err
}
smuxConfig, err := buildSmuxConfig(config.GetAdaptor(), maxMessageSize)
if err != nil {
if recorder != nil {
_ = recorder.Close()
}
_ = sessionManager.Close()
return nil, err
}
adaptor, err := packetToStream.New(session, client, smuxConfig)
if err != nil {
if recorder != nil {
_ = recorder.Close()
}
_ = sessionManager.Close()
return nil, err
}
backgroundAdaptor, err := packetToStream.New(backgroundSession, client, smuxConfig)
if err != nil {
_ = adaptor.Close()
if recorder != nil {
_ = recorder.Close()
}
_ = sessionManager.Close()
return nil, err
}
return &transportSession{
id: id,
role: role,
localSessionInstanceID: localSessionInstanceID,
session: session,
adaptor: adaptor,
sessionManager: sessionManager,
backgroundAdaptor: backgroundAdaptor,
recorder: recorder,
onClose: onClose,
persistence: persistence,
slots: make([]transportChannelSlot, len(config.GetChannels())),
}, nil
}
func (s *transportSession) attachChannel(conn gonet.Conn, channelSlot int, config rriptMonoDirectionSession.ChannelConfig, readIdleTimeout time.Duration) error {
if s == nil || s.sessionManager == nil {
return io.ErrClosedPipe
}
writer := io.WriteCloser(conn)
if s.recorder != nil {
writer = s.recorder.WrapWriter(s.role, channelSlot, writer)
}
managerChannelIndex, err := s.sessionManager.ChannelManager().AttachChannelWithConfig(writer, config)
if err != nil {
_ = conn.Close()
return err
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
_ = s.sessionManager.ChannelManager().DetachChannel(managerChannelIndex)
_ = conn.Close()
return io.ErrClosedPipe
}
for len(s.slots) <= channelSlot {
s.slots = append(s.slots, transportChannelSlot{})
}
oldConn := s.slots[channelSlot].conn
oldManagerChannelIndex := s.slots[channelSlot].managerChannelIndex
s.slots[channelSlot] = transportChannelSlot{
config: config,
conn: conn,
managerChannelIndex: managerChannelIndex,
readIdleTimeout: readIdleTimeout,
}
if s.localAddr == nil {
s.localAddr = conn.LocalAddr()
}
if s.remoteAddr == nil {
s.remoteAddr = conn.RemoteAddr()
}
s.cancelDisconnectedTimerLocked()
s.mu.Unlock()
if oldConn != nil {
_ = oldConn.Close()
_ = s.sessionManager.ChannelManager().DetachChannel(oldManagerChannelIndex)
}
go s.readChannel(conn, channelSlot, managerChannelIndex, readIdleTimeout)
return nil
}
func (s *transportSession) readChannel(conn gonet.Conn, channelSlot int, managerChannelIndex int, readIdleTimeout time.Duration) {
packetBuf := make([]byte, transportMaxPacketSize)
for {
payload, err := readChannelPacket(conn, readIdleTimeout, packetBuf)
if err != nil {
s.handleChannelReadFailure(channelSlot, managerChannelIndex, conn, channelReadFailureMessage("failed to read channel packet", err), err)
return
}
if s.recorder != nil {
s.recorder.RecordInbound(s.role, channelSlot, payload)
}
if err := s.sessionManager.ChannelManager().OnNewMessageArrived(managerChannelIndex, payload); err != nil {
if !s.isCurrentChannel(channelSlot, managerChannelIndex, conn) {
_ = conn.Close()
return
}
s.logChannelFailure(channelSlot, conn, "failed to handle inbound rrpit packet", err)
_ = s.Close()
return
}
}
}
func (s *transportSession) logChannelFailure(channelIndex int, conn gonet.Conn, message string, err error) {
if s == nil || err == nil {
return
}
var localAddr, remoteAddr interface{}
if conn != nil {
localAddr = conn.LocalAddr()
remoteAddr = conn.RemoteAddr()
}
newError(
"rrpit ", s.role, " channel ", channelIndex, ": ", message,
", local=", localAddr, ", remote=", remoteAddr,
).Base(err).AtWarning().WriteToLog()
}
func (s *transportSession) isCurrentChannel(channelSlot int, managerChannelIndex int, conn gonet.Conn) bool {
if s == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
return s.isCurrentChannelLocked(channelSlot, managerChannelIndex, conn)
}
func (s *transportSession) isCurrentChannelLocked(channelSlot int, managerChannelIndex int, conn gonet.Conn) bool {
if s.closed || channelSlot < 0 || channelSlot >= len(s.slots) {
return false
}
slot := s.slots[channelSlot]
return slot.conn == conn && slot.managerChannelIndex == managerChannelIndex
}
func (s *transportSession) handleChannelReadFailure(channelSlot int, managerChannelIndex int, conn gonet.Conn, message string, err error) {
if s == nil {
return
}
s.logChannelFailure(channelSlot, conn, message, err)
s.mu.Lock()
if !s.isCurrentChannelLocked(channelSlot, managerChannelIndex, conn) {
s.mu.Unlock()
_ = conn.Close()
return
}
s.slots[channelSlot].conn = nil
s.slots[channelSlot].managerChannelIndex = 0
closeImmediately := s.activeChannelCountLocked() == 0 && s.scheduleDisconnectedTimerLocked()
s.mu.Unlock()
_ = s.sessionManager.ChannelManager().DetachChannel(managerChannelIndex)
_ = conn.Close()
if closeImmediately {
_ = s.Close()
}
}
func (s *transportSession) activeChannelCountLocked() int {
count := 0
for _, slot := range s.slots {
if slot.conn != nil {
count += 1
}
}
return count
}
func (s *transportSession) cancelDisconnectedTimerLocked() {
s.disconnected = false
if s.disconnectedTimer != nil {
s.disconnectedTimer.Stop()
s.disconnectedTimer = nil
}
}
func (s *transportSession) scheduleDisconnectedTimerLocked() bool {
s.disconnected = true
if s.disconnectedTimer != nil {
s.disconnectedTimer.Stop()
s.disconnectedTimer = nil
}
if s.persistence.DisconnectedSessionRetention <= 0 {
return true
}
s.disconnectedTimer = time.AfterFunc(s.persistence.DisconnectedSessionRetention, func() {
_ = s.Close()
})
return false
}
func (s *transportSession) MissingChannelSlots() []int {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
missing := make([]int, 0, len(s.slots))
for index, slot := range s.slots {
if slot.conn == nil {
missing = append(missing, index)
}
}
return missing
}
func (s *transportSession) Close() error {
if s == nil {
return nil
}
s.closeOnce.Do(func() {
var channels []gonet.Conn
s.mu.Lock()
s.closed = true
s.cancelDisconnectedTimerLocked()
for index := range s.slots {
if s.slots[index].conn != nil {
channels = append(channels, s.slots[index].conn)
s.slots[index].conn = nil
}
}
s.mu.Unlock()
if s.recorder != nil {
s.closeErr = firstNonNil(s.closeErr, s.writeDiagnose())
}
for _, conn := range channels {
s.closeErr = firstNonNil(s.closeErr, conn.Close())
}
// Stop the session manager before closing adaptors so its auto-tick loop
// can't hold the bidirectional session lock while adaptor shutdown calls
// back into session.Close().
if s.sessionManager != nil {
s.closeErr = firstNonNil(s.closeErr, s.sessionManager.Close())
} else if s.session != nil {
s.closeErr = firstNonNil(s.closeErr, s.session.Close())
}
if s.adaptor != nil {
s.closeErr = firstNonNil(s.closeErr, s.adaptor.Close())
}
if s.backgroundAdaptor != nil {
s.closeErr = firstNonNil(s.closeErr, s.backgroundAdaptor.Close())
}
if s.recorder != nil {
s.closeErr = firstNonNil(s.closeErr, s.recorder.Close())
}
if s.onClose != nil {
s.onClose()
}
})
return s.closeErr
}
func (s *transportSession) writeDiagnose() error {
if s == nil || s.recorder == nil || s.session == nil {
return nil
}
manifest := s.recorder.Manifest()
if manifest.Directory == "" {
return nil
}
file, err := os.Create(filepath.Join(manifest.Directory, "session.json"))
if err != nil {
return err
}
defer file.Close()
return rrpitDebuger.WriteDiagnoseOutput(file, s.session, s.recorder)
}
func (s *transportSession) OpenStream() (gonet.Conn, error) {
return s.OpenStreamByClass(rrpitBidirectionalSessionManager.InteractiveStream)
}
func (s *transportSession) OpenStreamByClass(sessionClass rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error) {
if s == nil || s.adaptor == nil {
return nil, io.ErrClosedPipe
}
adaptor := s.adaptor
if sessionClass == rrpitBidirectionalSessionManager.BackgroundStream {
newError("opening background stream connection").AtDebug().WriteToLog()
adaptor = s.backgroundAdaptor
}
if adaptor == nil {
return nil, io.ErrClosedPipe
}
stream, err := adaptor.OpenStream()
if err != nil {
return nil, err
}
return gonet.Conn(stream), nil
}
func (s *transportSession) AcceptStream(sessionClass rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error) {
if s == nil {
return nil, io.ErrClosedPipe
}
adaptor := s.adaptor
if sessionClass == rrpitBidirectionalSessionManager.BackgroundStream {
adaptor = s.backgroundAdaptor
}
if adaptor == nil {
return nil, io.ErrClosedPipe
}
stream, err := adaptor.AcceptStream()
if err != nil {
return nil, err
}
return gonet.Conn(stream), nil
}
func (c *ownedConn) GetConnectionContext() context.Context {
if c == nil {
return nil
}
return c.ctx
}
func (c *ownedConn) Close() error {
if c == nil {
return nil
}
defer c.closeOnce.Do(func() {
if c.onClose != nil {
c.onClose()
}
if c.done != nil {
close(c.done)
}
})
var err error
if c.Conn != nil {
err = c.Conn.Close()
} else if c.owner != nil {
err = c.owner.Close()
}
return err
}
func (c *ownedConn) LocalAddr() gonet.Addr {
if c == nil {
return nil
}
if c.owner != nil {
c.owner.mu.Lock()
addr := c.owner.localAddr
c.owner.mu.Unlock()
if addr != nil {
return addr
}
}
if c.Conn != nil {
return c.Conn.LocalAddr()
}
return nil
}
func (c *ownedConn) RemoteAddr() gonet.Addr {
if c == nil {
return nil
}
if c.owner != nil {
c.owner.mu.Lock()
addr := c.owner.remoteAddr
c.owner.mu.Unlock()
if addr != nil {
return addr
}
}
if c.Conn != nil {
return c.Conn.RemoteAddr()
}
return nil
}
func buildBidirectionalSessionConfig(config *Config) rrpitBidirectionalSession.Config {
var lane *LaneSetting
if config != nil {
lane = config.GetLane()
}
var session *SessionSetting
if config != nil {
session = config.GetSession()
}
var reconstruction *SessionReconstructionSetting
if session != nil {
reconstruction = session.GetReconstruction()
}
return rrpitBidirectionalSession.Config{
Rx: rriptMonoDirectionSession.SessionRxConfig{
LaneShardSize: int(lane.GetShardSize()),
MaxBufferedLanes: int(lane.GetMaxBufferedLanes()),
RemoteMaxDataShardsPerLane: int(lane.GetRemoteMaxDataShardsPerLane()),
OnMessage: func([]byte) error {
return nil
},
},
Tx: rriptMonoDirectionSession.SessionTxConfig{
LaneShardSize: int(lane.GetShardSize()),
MaxDataShardsPerLane: int(lane.GetMaxDataShardsPerLane()),
MaxBufferedLanes: int(lane.GetMaxBufferedLanes()),
MaxRewindableTimestampNum: int(session.GetMaxRewindableTimestampNum()),
MaxRewindableControlMessageNum: int(session.GetMaxRewindableControlMessageNum()),
OddChannelIDs: session.GetOddChannelIds(),
Reconstruction: rriptMonoDirectionSession.SessionTxReconstructionConfig{
InitialRepairShardRatio: float64(reconstruction.GetInitialRepairShardRatio()),
LaneRepairWeight: float32SliceToFloat64Slice(reconstruction.GetLaneRepairWeight()),
SecondaryRepairShardRatio: float64(reconstruction.GetSecondaryRepairShardRatio()),
TimeResendSecondaryRepairShard: int(reconstruction.GetTimeResendSecondaryRepairShard()),
StaleLaneFinalizedAgeThresholdTicks: int(reconstruction.GetStaleLaneFinalizedAgeThresholdTicks()),
StaleLaneProgressStallThresholdTicks: int(reconstruction.GetStaleLaneProgressStallThresholdTicks()),
SecondaryRepairMinBurst: int(reconstruction.GetSecondaryRepairMinBurst()),
AlwaysRestrictSourceDataWhenOldestLaneStalled: reconstruction.GetAlwaysRestrictSourceDataWhenOldestLaneStalled(),
},
},
TimestampInterval: 0,
}
}
func buildChannelManagerConfig(config *Config) rrpitChannelManager.Config {
var session *SessionSetting
if config != nil {
session = config.GetSession()
}
return rrpitChannelManager.Config{
OddChannelIDs: session.GetOddChannelIds(),
MaxRewindableTimestampNum: int(session.GetMaxRewindableTimestampNum()),
MaxRewindableControlMessageNum: int(session.GetMaxRewindableControlMessageNum()),
}
}
func buildBidirectionalSessionManagerConfig(
config *Config,
channelManager *rrpitChannelManager.ChannelManager,
) rrpitBidirectionalSessionManager.Config {
var sessionMgr *SessionManagerSetting
if config != nil {
sessionMgr = config.GetSessionMgr()
}
baseSessionConfig := buildBidirectionalSessionConfig(config)
baseSessionConfig.ManagerHostedControlKeepaliveIntervalTicks = int(sessionMgr.GetManagerHostedControlKeepaliveIntervalTicks())
return rrpitBidirectionalSessionManager.Config{
ChannelManager: channelManager,
BaseSessionConfig: baseSessionConfig,
TimestampInterval: time.Duration(sessionMgr.GetTimestampInterval()),
InteractivePrimaryCancellationCounterLimit: int(sessionMgr.GetInteractivePrimaryCancellationCounterLimit()),
DynamicRestrictSourceDataWhenOldestLaneStalled: sessionMgr.GetDynamicRestrictSourceDataWhenOldestLaneStalled(),
DynamicRestrictSourceDataWhenOldestLaneStalledTicks: int(sessionMgr.GetDynamicRestrictSourceDataWhenOldestLaneStalledTicks()),
}
}
func float32SliceToFloat64Slice(values []float32) []float64 {
if len(values) == 0 {
return nil
}
converted := make([]float64, len(values))
for i, value := range values {
converted[i] = float64(value)
}
return converted
}
func buildSmuxConfig(config *AdaptorSetting, maxMessageSize int) (*smux.Config, error) {
smuxConfig := smux.DefaultConfig()
if config == nil {
if maxFrameSize := packetToStream.MaxSmuxFrameSizeForMessage(maxMessageSize); maxFrameSize > 0 && smuxConfig.MaxFrameSize > maxFrameSize {
smuxConfig.MaxFrameSize = maxFrameSize
}
return smuxConfig, nil
}
if version := config.GetSmuxVersion(); version != 0 {
smuxConfig.Version = int(version)
}
smuxConfig.KeepAliveDisabled = config.GetKeepAliveDisabled()
if interval := config.GetKeepAliveInterval(); interval != 0 {
smuxConfig.KeepAliveInterval = time.Duration(interval)
}
if timeout := config.GetKeepAliveTimeout(); timeout != 0 {
smuxConfig.KeepAliveTimeout = time.Duration(timeout)
}
if frameSize := config.GetMaxFrameSize(); frameSize != 0 {
smuxConfig.MaxFrameSize = int(frameSize)
} else if maxFrameSize := packetToStream.MaxSmuxFrameSizeForMessage(maxMessageSize); maxFrameSize > 0 && smuxConfig.MaxFrameSize > maxFrameSize {
smuxConfig.MaxFrameSize = maxFrameSize
}
if receiveBuffer := config.GetMaxReceiveBuffer(); receiveBuffer != 0 {
smuxConfig.MaxReceiveBuffer = int(receiveBuffer)
}
if streamBuffer := config.GetMaxStreamBuffer(); streamBuffer != 0 {
smuxConfig.MaxStreamBuffer = int(streamBuffer)
}
if err := smux.VerifyConfig(smuxConfig); err != nil {
return nil, err
}
return smuxConfig, nil
}
func newPacketRecorder(config *Config, role string, id transportSessionID) (*rrpitDebuger.PacketRecorder, error) {
if config == nil || config.GetEngineering() == nil || config.GetEngineering().GetDebuggerDir() == "" {
return nil, nil
}
return rrpitDebuger.NewPacketRecorder(rrpitDebuger.PacketRecorderConfig{
Directory: filepath.Join(config.GetEngineering().GetDebuggerDir(), role+"-"+hex.EncodeToString(id[:])),
MaxFileSizeBytes: config.GetEngineering().GetDebuggerMaxFileSizeBytes(),
MaxFiles: int(config.GetEngineering().GetDebuggerMaxFiles()),
})
}
func resolveDialChannels(dest v2net.Destination, config *Config) ([]resolvedChannel, error) {
return resolveChannels(func(index int, channel *DTLSUDPChannel) (v2net.Address, v2net.Port, error) {
address := dest.Address
port := dest.Port
if resolved := channelAddress(channel); resolved != nil {
address = resolved
}
if channel.GetPort() != 0 {
port = v2net.Port(channel.GetPort())
}
if address == nil {
return nil, 0, fmt.Errorf("rrpit channel %d missing remote address", index)
}
if port == 0 {
return nil, 0, fmt.Errorf("rrpit channel %d missing remote port", index)
}
return address, port, nil
}, config)
}
func resolveListenChannels(address v2net.Address, port v2net.Port, config *Config) ([]resolvedChannel, error) {
return resolveChannels(func(index int, channel *DTLSUDPChannel) (v2net.Address, v2net.Port, error) {
resolvedAddress := address
resolvedPort := port
if resolved := channelAddress(channel); resolved != nil {
resolvedAddress = resolved
}
if channel.GetPort() != 0 {
resolvedPort = v2net.Port(channel.GetPort())
}
if resolvedAddress == nil {
return nil, 0, fmt.Errorf("rrpit channel %d missing listen address", index)
}
if resolvedPort == 0 {
return nil, 0, fmt.Errorf("rrpit channel %d missing listen port", index)
}
return resolvedAddress, resolvedPort, nil
}, config)
}
func resolveChannels(
resolve func(index int, channel *DTLSUDPChannel) (v2net.Address, v2net.Port, error),
config *Config,
) ([]resolvedChannel, error) {
if config == nil || len(config.GetChannels()) == 0 {
return nil, fmt.Errorf("rrpit requires at least one channel")
}
resolved := make([]resolvedChannel, 0, len(config.GetChannels()))
for index, channel := range config.GetChannels() {
if channel == nil || channel.GetChannel() == nil {
return nil, fmt.Errorf("rrpit channel %d missing channel settings", index)
}
channelSetting := channel.GetSetting()
if channelSetting == nil {
channelSetting = &ChannelSetting{}
}
message, err := serial.GetInstanceOf(channel.GetChannel())
if err != nil {
return nil, err
}
dtlsChannel, ok := message.(*DTLSUDPChannel)
if !ok {
return nil, fmt.Errorf("rrpit channel %d uses unsupported transport %T", index, message)
}
address, resolvedPort, err := resolve(index, dtlsChannel)
if err != nil {
return nil, err
}
resolved = append(resolved, resolvedChannel{
transport: channelSetting,
dtls: dtlsChannel,
address: address,
port: resolvedPort,
})
}
return resolved, nil
}
func channelAddress(channel *DTLSUDPChannel) v2net.Address {
if channel == nil {
return nil
}
if len(channel.GetIp()) > 0 {
return v2net.IPAddress(channel.GetIp())
}
if channel.GetIpAddr() != "" {
return v2net.ParseAddress(channel.GetIpAddr())
}
return nil
}
func rrpitChannelConfig(config *ChannelSetting) rriptMonoDirectionSession.ChannelConfig {
if config == nil {
config = &ChannelSetting{}
}
return rriptMonoDirectionSession.ChannelConfig{
Weight: int(config.GetWeight()),
MaxSendingSpeed: int(config.GetMaxSendingSpeed()),
}
}
func channelReadIdleTimeout(config resolvedChannel) time.Duration {
if config.dtls == nil {
return 0
}
timeout := time.Duration(config.dtls.GetNoIncomingMessageTimeout())
if timeout <= 0 {
return 0
}
return timeout
}
func applyChannelReadDeadline(conn gonet.Conn, timeout time.Duration) error {
if conn == nil {
return io.ErrClosedPipe
}
if timeout <= 0 {
return conn.SetReadDeadline(time.Time{})
}
return conn.SetReadDeadline(time.Now().Add(timeout))
}
func readChannelPacket(conn gonet.Conn, timeout time.Duration, buffer []byte) ([]byte, error) {
if conn == nil {
return nil, io.ErrClosedPipe
}
if len(buffer) == 0 {
return nil, io.ErrShortBuffer
}
if err := applyChannelReadDeadline(conn, timeout); err != nil {
return nil, err
}
n, err := conn.Read(buffer)
if err != nil {
return nil, err
}
return append([]byte(nil), buffer[:n]...), nil
}
func channelReadFailureMessage(base string, err error) string {
if err == nil {
return base
}
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
return "channel considered dead after no incoming message within configured timeout"
}
return base
}
func makeTransportDTLSConfig(config resolvedChannel) *transportdtls.Config {
mtu := config.transport.GetMtu()
if mtu == 0 {
mtu = defaultDTLSMTU
}
return &transportdtls.Config{
Mode: transportdtls.DTLSMode_PSK,
Psk: []byte(config.dtls.GetPassword()),
Mtu: mtu,
ReplayProtectionWindow: defaultDTLSReplayWindow,
}
}
func makePionDTLSConfig(config resolvedChannel, sessionID transportSessionID) *piondtls.Config {
mtu := config.transport.GetMtu()
if mtu == 0 {
mtu = defaultDTLSMTU
}
return &piondtls.Config{
MTU: int(mtu),
ReplayProtectionWindow: defaultDTLSReplayWindow,
PSK: func([]byte) ([]byte, error) {
return []byte(config.dtls.GetPassword()), nil
},
PSKIdentityHint: encodeTransportSessionIdentity(sessionID),
CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256},
}
}
func newSessionID() (transportSessionID, error) {
var id transportSessionID
_, err := rand.Read(id[:])
return id, err
}
func newSessionInstanceID() (rriptMonoDirectionSession.SessionInstanceID, error) {
var id rriptMonoDirectionSession.SessionInstanceID
_, err := rand.Read(id[:])
return id, err
}
func encodeTransportSessionIdentity(id transportSessionID) []byte {
identity := make([]byte, 0, len(transportIdentityMagic)+1+len(id))
identity = append(identity, transportIdentityMagic[:]...)
identity = append(identity, transportIdentityVersion)
identity = append(identity, id[:]...)
return identity
}
func readTransportSessionID(conn gonet.Conn) (transportSessionID, error) {
var id transportSessionID
identity := transportdtls.ClientIdentity(conn)
if len(identity) == 0 {
return id, fmt.Errorf("rrpit missing dtls client identity")
}
if len(identity) != len(transportIdentityMagic)+1+len(id) {
return id, fmt.Errorf("rrpit invalid dtls client identity length")
}
for i, b := range transportIdentityMagic {
if identity[i] != b {
return id, fmt.Errorf("rrpit invalid dtls client identity magic")
}
}
if identity[len(transportIdentityMagic)] != transportIdentityVersion {
return id, fmt.Errorf("rrpit unsupported dtls client identity version")
}
copy(id[:], identity[len(transportIdentityMagic)+1:])
return id, nil
}
func makeDTLSStreamSettings(config resolvedChannel, socketSettings *internet.SocketConfig) *internet.MemoryStreamConfig {
return &internet.MemoryStreamConfig{
ProtocolName: "dtls",
ProtocolSettings: makeTransportDTLSConfig(config),
SocketSettings: socketSettings,
}
}
func firstNonNil(current error, next error) error {
if current != nil {
return current
}
return next
}
@@ -0,0 +1,525 @@
package rrpitTransport
import (
"bytes"
"context"
"io"
gonet "net"
"sync"
"testing"
"time"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rriptMonoDirectionSession"
"github.com/v2fly/v2ray-core/v5/transport/internet/rrpit/rrpitBidirectionalSessionManager"
)
type testClientIdentityConn struct {
gonet.Conn
identity []byte
}
func (c *testClientIdentityConn) ClientIdentity() []byte {
return append([]byte(nil), c.identity...)
}
func TestTransportSessionIdentityRoundTrip(t *testing.T) {
want := transportSessionID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
conn := &testClientIdentityConn{identity: encodeTransportSessionIdentity(want)}
got, err := readTransportSessionID(conn)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("unexpected session id: got %x want %x", got, want)
}
}
func TestTransportSessionIdentityRejectsInvalidIdentity(t *testing.T) {
tests := []struct {
name string
identity []byte
}{
{name: "empty"},
{name: "short", identity: []byte("rrpit")},
{name: "bad magic", identity: append([]byte("WRNG"), append([]byte{transportIdentityVersion}, bytes.Repeat([]byte{1}, len(transportSessionID{}))...)...)},
{name: "bad version", identity: append(append([]byte(nil), transportIdentityMagic[:]...), append([]byte{transportIdentityVersion + 1}, bytes.Repeat([]byte{2}, len(transportSessionID{}))...)...)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
conn := &testClientIdentityConn{identity: test.identity}
if _, err := readTransportSessionID(conn); err == nil {
t.Fatal("expected error")
}
})
}
}
func TestBuildBidirectionalSessionConfigIncludesReconstructionSettings(t *testing.T) {
config := buildBidirectionalSessionConfig(&Config{
Lane: &LaneSetting{
ShardSize: 1400,
MaxDataShardsPerLane: 6,
MaxBufferedLanes: 8,
RemoteMaxDataShardsPerLane: 5,
},
Session: &SessionSetting{
OddChannelIds: true,
MaxRewindableTimestampNum: 9,
MaxRewindableControlMessageNum: 10,
Reconstruction: &SessionReconstructionSetting{
InitialRepairShardRatio: 1.25,
LaneRepairWeight: []float32{0.5, 0.25},
SecondaryRepairShardRatio: 0.75,
TimeResendSecondaryRepairShard: 3,
StaleLaneFinalizedAgeThresholdTicks: 7,
StaleLaneProgressStallThresholdTicks: 8,
SecondaryRepairMinBurst: 2,
AlwaysRestrictSourceDataWhenOldestLaneStalled: true,
},
},
SessionMgr: &SessionManagerSetting{
TimestampInterval: 11,
},
})
want := rriptMonoDirectionSession.SessionTxReconstructionConfig{
InitialRepairShardRatio: 1.25,
LaneRepairWeight: []float64{0.5, 0.25},
SecondaryRepairShardRatio: 0.75,
TimeResendSecondaryRepairShard: 3,
StaleLaneFinalizedAgeThresholdTicks: 7,
StaleLaneProgressStallThresholdTicks: 8,
SecondaryRepairMinBurst: 2,
AlwaysRestrictSourceDataWhenOldestLaneStalled: true,
}
got := config.Tx.Reconstruction
if got.InitialRepairShardRatio != want.InitialRepairShardRatio ||
got.SecondaryRepairShardRatio != want.SecondaryRepairShardRatio ||
got.TimeResendSecondaryRepairShard != want.TimeResendSecondaryRepairShard ||
got.StaleLaneFinalizedAgeThresholdTicks != want.StaleLaneFinalizedAgeThresholdTicks ||
got.StaleLaneProgressStallThresholdTicks != want.StaleLaneProgressStallThresholdTicks ||
got.SecondaryRepairMinBurst != want.SecondaryRepairMinBurst ||
len(got.LaneRepairWeight) != len(want.LaneRepairWeight) {
t.Fatalf("unexpected reconstruction config: %+v", got)
}
for i, weight := range want.LaneRepairWeight {
if got.LaneRepairWeight[i] != weight {
t.Fatalf("unexpected lane repair weight[%d]: got %v want %v", i, got.LaneRepairWeight[i], weight)
}
}
if config.Rx.RemoteMaxDataShardsPerLane != 5 {
t.Fatalf("unexpected remote max data shards per lane: got %d want 5", config.Rx.RemoteMaxDataShardsPerLane)
}
}
func TestBuildSmuxConfigClampsDefaultFrameSizeToMessageBudget(t *testing.T) {
config, err := buildSmuxConfig(&AdaptorSetting{}, 1198)
if err != nil {
t.Fatal(err)
}
if got, want := config.MaxFrameSize, 1168; got != want {
t.Fatalf("unexpected default smux max frame size: got %d want %d", got, want)
}
explicit, err := buildSmuxConfig(&AdaptorSetting{MaxFrameSize: 2048}, 1198)
if err != nil {
t.Fatal(err)
}
if explicit.MaxFrameSize != 2048 {
t.Fatalf("unexpected explicit smux max frame size: got %d want 2048", explicit.MaxFrameSize)
}
}
func TestBuildConnectionPersistencePolicy(t *testing.T) {
defaults := buildConnectionPersistencePolicy(&Config{})
if defaults.DisconnectedSessionRetention != 0 {
t.Fatalf("unexpected default disconnected retention: %v", defaults.DisconnectedSessionRetention)
}
if defaults.ReconnectRetryInterval != 0 {
t.Fatalf("unexpected default reconnect retry interval: %v", defaults.ReconnectRetryInterval)
}
if defaults.IdleTimeout != rrpitClientSessionIdleTimeout {
t.Fatalf("unexpected default idle timeout: %v", defaults.IdleTimeout)
}
if defaults.RemoteControlInactivityTimeout != 0 {
t.Fatalf("unexpected default remote control inactivity timeout: %v", defaults.RemoteControlInactivityTimeout)
}
if defaults.KeepTransportSessionWithoutStreams {
t.Fatal("did not expect keep-without-streams default to be enabled")
}
policy := buildConnectionPersistencePolicy(&Config{
Persistence: &ConnectionPersistenceSetting{
DisconnectedSessionRetention: int64(3 * time.Second),
ReconnectRetryInterval: int64(250 * time.Millisecond),
KeepTransportSessionWithoutStreams: true,
IdleTimeout: int64(7 * time.Second),
RemoteControlInactivityTimeout: int64(9 * time.Second),
},
})
if policy.DisconnectedSessionRetention != 3*time.Second {
t.Fatalf("unexpected disconnected retention: %v", policy.DisconnectedSessionRetention)
}
if policy.ReconnectRetryInterval != 250*time.Millisecond {
t.Fatalf("unexpected reconnect retry interval: %v", policy.ReconnectRetryInterval)
}
if policy.IdleTimeout != 7*time.Second {
t.Fatalf("unexpected idle timeout: %v", policy.IdleTimeout)
}
if policy.RemoteControlInactivityTimeout != 9*time.Second {
t.Fatalf("unexpected remote control inactivity timeout: %v", policy.RemoteControlInactivityTimeout)
}
if !policy.KeepTransportSessionWithoutStreams {
t.Fatal("expected keep-without-streams to be enabled")
}
}
func TestChannelReadIdleTimeout(t *testing.T) {
if got := channelReadIdleTimeout(resolvedChannel{}); got != 0 {
t.Fatalf("unexpected zero-config timeout: %v", got)
}
if got := channelReadIdleTimeout(resolvedChannel{dtls: &DTLSUDPChannel{NoIncomingMessageTimeout: int64(250 * time.Millisecond)}}); got != 250*time.Millisecond {
t.Fatalf("unexpected configured timeout: %v", got)
}
if got := channelReadIdleTimeout(resolvedChannel{dtls: &DTLSUDPChannel{NoIncomingMessageTimeout: -1}}); got != 0 {
t.Fatalf("unexpected negative timeout handling: %v", got)
}
}
func TestChannelReadFailureMessageUsesTimeoutWording(t *testing.T) {
timeoutErr := &timeoutOnlyError{}
if got := channelReadFailureMessage("failed to read channel packet", timeoutErr); got != "channel considered dead after no incoming message within configured timeout" {
t.Fatalf("unexpected timeout failure message: %q", got)
}
if got := channelReadFailureMessage("failed to read channel packet", io.EOF); got != "failed to read channel packet" {
t.Fatalf("unexpected non-timeout failure message: %q", got)
}
}
func TestReadChannelPacketPreservesPacketBoundaries(t *testing.T) {
conn := &testPacketConn{
packets: [][]byte{
[]byte("first"),
[]byte("second"),
},
}
buffer := make([]byte, 16)
first, err := readChannelPacket(conn, 50*time.Millisecond, buffer)
if err != nil {
t.Fatal(err)
}
second, err := readChannelPacket(conn, 50*time.Millisecond, buffer)
if err != nil {
t.Fatal(err)
}
if string(first) != "first" {
t.Fatalf("unexpected first packet: %q", string(first))
}
if string(second) != "second" {
t.Fatalf("unexpected second packet: %q", string(second))
}
if conn.readDeadline.IsZero() {
t.Fatal("expected read deadline to be set")
}
buffer[0] = 'x'
if string(first) != "first" {
t.Fatalf("first packet was not copied: %q", string(first))
}
}
func TestPersistentClientSessionKeepsTransportAliveUntilIdle(t *testing.T) {
clientConn, serverConn := gonet.Pipe()
defer serverConn.Close()
var (
closeCalls int
closeMu sync.Mutex
)
session := &persistentClientSession{
owner: &transportSession{
localAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10001},
remoteAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10002},
},
openStream: func(rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error) {
return clientConn, nil
},
closeSession: func() error {
closeMu.Lock()
closeCalls++
closeMu.Unlock()
return nil
},
idleTimeout: 20 * time.Millisecond,
}
conn, err := session.OpenConnection(context.Background())
if err != nil {
t.Fatal(err)
}
if conn.LocalAddr().String() != "127.0.0.1:10001" {
t.Fatalf("unexpected local addr: %v", conn.LocalAddr())
}
if conn.RemoteAddr().String() != "127.0.0.1:10002" {
t.Fatalf("unexpected remote addr: %v", conn.RemoteAddr())
}
if err := conn.Close(); err != nil && err != io.ErrClosedPipe {
t.Fatal(err)
}
time.Sleep(5 * time.Millisecond)
closeMu.Lock()
if closeCalls != 0 {
closeMu.Unlock()
t.Fatalf("transport closed too early: %d", closeCalls)
}
closeMu.Unlock()
time.Sleep(40 * time.Millisecond)
closeMu.Lock()
defer closeMu.Unlock()
if closeCalls != 1 {
t.Fatalf("unexpected transport close count: %d", closeCalls)
}
}
func TestPersistentClientSessionOpenConnectionTracksContext(t *testing.T) {
clientConn, serverConn := gonet.Pipe()
defer serverConn.Close()
var (
closeCalls int
closeMu sync.Mutex
)
session := &persistentClientSession{
owner: &transportSession{
localAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10011},
remoteAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10012},
},
openStream: func(rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error) {
return clientConn, nil
},
closeSession: func() error {
closeMu.Lock()
closeCalls++
closeMu.Unlock()
return nil
},
idleTimeout: 20 * time.Millisecond,
}
ctx, cancel := context.WithCancel(context.Background())
conn, err := session.OpenConnection(ctx)
if err != nil {
t.Fatal(err)
}
getter, ok := conn.(interface{ GetConnectionContext() context.Context })
if !ok {
t.Fatal("connection does not expose connection context")
}
if getter.GetConnectionContext() != ctx {
t.Fatal("unexpected connection context")
}
cancel()
time.Sleep(5 * time.Millisecond)
closeMu.Lock()
if closeCalls != 0 {
closeMu.Unlock()
t.Fatalf("transport closed too early: %d", closeCalls)
}
closeMu.Unlock()
time.Sleep(40 * time.Millisecond)
closeMu.Lock()
defer closeMu.Unlock()
if closeCalls != 1 {
t.Fatalf("unexpected transport close count after context cancel: %d", closeCalls)
}
}
func TestPersistentClientSessionCanStayOpenWithoutStreams(t *testing.T) {
clientConn, serverConn := gonet.Pipe()
defer serverConn.Close()
var (
closeCalls int
closeMu sync.Mutex
)
session := &persistentClientSession{
owner: &transportSession{
localAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10021},
remoteAddr: &gonet.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 10022},
},
openStream: func(rrpitBidirectionalSessionManager.SessionName) (gonet.Conn, error) {
return clientConn, nil
},
closeSession: func() error {
closeMu.Lock()
closeCalls++
closeMu.Unlock()
return nil
},
idleTimeout: 20 * time.Millisecond,
keepTransportSessionWithoutStreams: true,
}
conn, err := session.OpenConnection(context.Background())
if err != nil {
t.Fatal(err)
}
if err := conn.Close(); err != nil && err != io.ErrClosedPipe {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond)
closeMu.Lock()
defer closeMu.Unlock()
if closeCalls != 0 {
t.Fatalf("expected transport to stay open without streams, got close count %d", closeCalls)
}
}
func TestTransportSessionDisconnectedRetentionDelaysClose(t *testing.T) {
var (
closeCalls int
closeMu sync.Mutex
)
session := &transportSession{
persistence: connectionPersistencePolicy{
DisconnectedSessionRetention: 20 * time.Millisecond,
},
onClose: func() {
closeMu.Lock()
closeCalls++
closeMu.Unlock()
},
}
session.mu.Lock()
closeImmediately := session.scheduleDisconnectedTimerLocked()
session.mu.Unlock()
if closeImmediately {
t.Fatal("expected disconnect retention to delay close")
}
time.Sleep(5 * time.Millisecond)
closeMu.Lock()
if closeCalls != 0 {
closeMu.Unlock()
t.Fatalf("transport closed too early: %d", closeCalls)
}
closeMu.Unlock()
time.Sleep(40 * time.Millisecond)
closeMu.Lock()
defer closeMu.Unlock()
if closeCalls != 1 {
t.Fatalf("unexpected transport close count after retention expiry: %d", closeCalls)
}
}
func TestTransportSessionCloseDoesNotDeadlockWithAutoTickAndNoChannels(t *testing.T) {
session, err := newTransportSession(
"server",
transportSessionID{},
&Config{
SessionMgr: &SessionManagerSetting{
TimestampInterval: int64(time.Millisecond),
},
Persistence: &ConnectionPersistenceSetting{
DisconnectedSessionRetention: int64(time.Second),
},
},
false,
nil,
nil,
)
if err != nil {
t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
closeDone := make(chan error, 1)
go func() {
closeDone <- session.Close()
}()
select {
case err := <-closeDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(500 * time.Millisecond):
t.Fatal("transport session close timed out")
}
}
type timeoutOnlyError struct{}
func (*timeoutOnlyError) Error() string { return "timeout" }
func (*timeoutOnlyError) Timeout() bool { return true }
type testPacketConn struct {
gonet.Conn
packets [][]byte
readErr error
readDeadline time.Time
}
func (c *testPacketConn) Read(b []byte) (int, error) {
if c.readErr != nil {
return 0, c.readErr
}
if len(c.packets) == 0 {
return 0, io.EOF
}
packet := c.packets[0]
c.packets = c.packets[1:]
if len(b) < len(packet) {
return 0, io.ErrShortBuffer
}
copy(b, packet)
return len(packet), nil
}
func (c *testPacketConn) Write(b []byte) (int, error) {
return len(b), nil
}
func (c *testPacketConn) Close() error {
return nil
}
func (c *testPacketConn) LocalAddr() gonet.Addr {
return nil
}
func (c *testPacketConn) RemoteAddr() gonet.Addr {
return nil
}
func (c *testPacketConn) SetDeadline(t time.Time) error {
c.readDeadline = t
return nil
}
func (c *testPacketConn) SetReadDeadline(t time.Time) error {
c.readDeadline = t
return nil
}
func (c *testPacketConn) SetWriteDeadline(time.Time) error {
return nil
}