mirror of
https://github.com/v2fly/v2ray-core.git
synced 2026-10-12 02:24:01 +00:00
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:
@@ -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))
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 = ×tampCopy
|
||||
} 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
|
||||
}
|
||||
Reference in New Issue
Block a user