From 958eb9ea8f1c157e7b4b48eb6aaf5c14d8c15ae2 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Sun, 3 May 2026 04:40:46 +0800 Subject: [PATCH] Direct/Freedom outbound: Add `blockDelay` to `finalRules` (30~90s by default) (#6060) Document: https://xtls.github.io/config/outbounds/freedom.html#finalruleobject https://github.com/XTLS/Xray-core/pull/6058#issuecomment-4363489838 https://github.com/XTLS/Xray-core/pull/6058#issuecomment-4363522836 https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4363675060 https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4364587508 --- infra/conf/freedom.go | 16 +++-- infra/conf/freedom_test.go | 7 +- proxy/freedom/config.pb.go | 129 ++++++++++++++++++++++++++++--------- proxy/freedom/config.proto | 6 ++ proxy/freedom/freedom.go | 64 ++++++++++++++---- 5 files changed, 173 insertions(+), 49 deletions(-) diff --git a/infra/conf/freedom.go b/infra/conf/freedom.go index c53be8eb..1d2020e3 100644 --- a/infra/conf/freedom.go +++ b/infra/conf/freedom.go @@ -44,10 +44,11 @@ type Noise struct { } type FreedomFinalRuleConfig struct { - Action string `json:"action"` - Network *NetworkList `json:"network"` - Port *PortList `json:"port"` - IP *StringList `json:"ip"` + Action string `json:"action"` + Network *NetworkList `json:"network"` + Port *PortList `json:"port"` + IP *StringList `json:"ip"` + BlockDelay *Int32Range `json:"blockDelay"` } // Build implements Buildable @@ -276,5 +277,12 @@ func (c *FreedomFinalRuleConfig) Build() (*freedom.FinalRuleConfig, error) { rule.Ip = rules } + if c.BlockDelay != nil { + rule.BlockDelay = &freedom.Range{ + Min: uint64(c.BlockDelay.From), + Max: uint64(c.BlockDelay.To), + } + } + return rule, nil } diff --git a/infra/conf/freedom_test.go b/infra/conf/freedom_test.go index da60b1d1..db7c4b30 100644 --- a/infra/conf/freedom_test.go +++ b/infra/conf/freedom_test.go @@ -45,7 +45,8 @@ func TestFreedomConfig(t *testing.T) { "action": "block", "network": "tcp,udp", "port": "53,443", - "ip": ["10.0.0.0/8", "2001:db8::/32"] + "ip": ["10.0.0.0/8", "2001:db8::/32"], + "blockDelay": "30-60" }, { "action": "allow", "network": ["udp"] @@ -85,6 +86,10 @@ func TestFreedomConfig(t *testing.T) { }, }, }, + BlockDelay: &freedom.Range{ + Min: 30, + Max: 60, + }, }, { Action: freedom.RuleAction_Allow, diff --git a/proxy/freedom/config.pb.go b/proxy/freedom/config.pb.go index e48c7ba7..9e58587a 100644 --- a/proxy/freedom/config.pb.go +++ b/proxy/freedom/config.pb.go @@ -299,19 +299,72 @@ func (x *Noise) GetApplyTo() string { return "" } +type Range struct { + state protoimpl.MessageState `protogen:"open.v1"` + Min uint64 `protobuf:"varint,1,opt,name=min,proto3" json:"min,omitempty"` + Max uint64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Range) Reset() { + *x = Range{} + mi := &file_proxy_freedom_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Range) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Range) ProtoMessage() {} + +func (x *Range) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_config_proto_msgTypes[3] + 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 Range.ProtoReflect.Descriptor instead. +func (*Range) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Range) GetMin() uint64 { + if x != nil { + return x.Min + } + return 0 +} + +func (x *Range) GetMax() uint64 { + if x != nil { + return x.Max + } + return 0 +} + type FinalRuleConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.freedom.RuleAction" json:"action,omitempty"` Networks []net.Network `protobuf:"varint,2,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` PortList *net.PortList `protobuf:"bytes,3,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` Ip []*geodata.IPRule `protobuf:"bytes,4,rep,name=ip,proto3" json:"ip,omitempty"` + BlockDelay *Range `protobuf:"bytes,5,opt,name=block_delay,json=blockDelay,proto3" json:"block_delay,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FinalRuleConfig) Reset() { *x = FinalRuleConfig{} - mi := &file_proxy_freedom_config_proto_msgTypes[3] + mi := &file_proxy_freedom_config_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -323,7 +376,7 @@ func (x *FinalRuleConfig) String() string { func (*FinalRuleConfig) ProtoMessage() {} func (x *FinalRuleConfig) ProtoReflect() protoreflect.Message { - mi := &file_proxy_freedom_config_proto_msgTypes[3] + mi := &file_proxy_freedom_config_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -336,7 +389,7 @@ func (x *FinalRuleConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalRuleConfig.ProtoReflect.Descriptor instead. func (*FinalRuleConfig) Descriptor() ([]byte, []int) { - return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3} + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{4} } func (x *FinalRuleConfig) GetAction() RuleAction { @@ -367,6 +420,13 @@ func (x *FinalRuleConfig) GetIp() []*geodata.IPRule { return nil } +func (x *FinalRuleConfig) GetBlockDelay() *Range { + if x != nil { + return x.BlockDelay + } + return nil +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` @@ -382,7 +442,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} - mi := &file_proxy_freedom_config_proto_msgTypes[4] + mi := &file_proxy_freedom_config_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -394,7 +454,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_proxy_freedom_config_proto_msgTypes[4] + mi := &file_proxy_freedom_config_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -407,7 +467,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_proxy_freedom_config_proto_rawDescGZIP(), []int{4} + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{5} } func (x *Config) GetDomainStrategy() internet.DomainStrategy { @@ -486,12 +546,17 @@ const file_proxy_freedom_config_proto_rawDesc = "" + "\tdelay_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" + "\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\n" + - "\bapply_to\x18\x06 \x01(\tR\aapplyTo\"\xe4\x01\n" + + "\bapply_to\x18\x06 \x01(\tR\aapplyTo\"+\n" + + "\x05Range\x12\x10\n" + + "\x03min\x18\x01 \x01(\x04R\x03min\x12\x10\n" + + "\x03max\x18\x02 \x01(\x04R\x03max\"\xa0\x02\n" + "\x0fFinalRuleConfig\x126\n" + "\x06action\x18\x01 \x01(\x0e2\x1e.xray.proxy.freedom.RuleActionR\x06action\x124\n" + "\bnetworks\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x126\n" + "\tport_list\x18\x03 \x01(\v2\x19.xray.common.net.PortListR\bportList\x12+\n" + - "\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\"\xaf\x03\n" + + "\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x12:\n" + + "\vblock_delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\n" + + "blockDelay\"\xaf\x03\n" + "\x06Config\x12P\n" + "\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" + "\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" + @@ -521,36 +586,38 @@ func file_proxy_freedom_config_proto_rawDescGZIP() []byte { } var file_proxy_freedom_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_proxy_freedom_config_proto_goTypes = []any{ (RuleAction)(0), // 0: xray.proxy.freedom.RuleAction (*DestinationOverride)(nil), // 1: xray.proxy.freedom.DestinationOverride (*Fragment)(nil), // 2: xray.proxy.freedom.Fragment (*Noise)(nil), // 3: xray.proxy.freedom.Noise - (*FinalRuleConfig)(nil), // 4: xray.proxy.freedom.FinalRuleConfig - (*Config)(nil), // 5: xray.proxy.freedom.Config - (*protocol.ServerEndpoint)(nil), // 6: xray.common.protocol.ServerEndpoint - (net.Network)(0), // 7: xray.common.net.Network - (*net.PortList)(nil), // 8: xray.common.net.PortList - (*geodata.IPRule)(nil), // 9: xray.common.geodata.IPRule - (internet.DomainStrategy)(0), // 10: xray.transport.internet.DomainStrategy + (*Range)(nil), // 4: xray.proxy.freedom.Range + (*FinalRuleConfig)(nil), // 5: xray.proxy.freedom.FinalRuleConfig + (*Config)(nil), // 6: xray.proxy.freedom.Config + (*protocol.ServerEndpoint)(nil), // 7: xray.common.protocol.ServerEndpoint + (net.Network)(0), // 8: xray.common.net.Network + (*net.PortList)(nil), // 9: xray.common.net.PortList + (*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule + (internet.DomainStrategy)(0), // 11: xray.transport.internet.DomainStrategy } var file_proxy_freedom_config_proto_depIdxs = []int32{ - 6, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint + 7, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint 0, // 1: xray.proxy.freedom.FinalRuleConfig.action:type_name -> xray.proxy.freedom.RuleAction - 7, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network - 8, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList - 9, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule - 10, // 5: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy - 1, // 6: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride - 2, // 7: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment - 3, // 8: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise - 4, // 9: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network + 9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList + 10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule + 4, // 5: xray.proxy.freedom.FinalRuleConfig.block_delay:type_name -> xray.proxy.freedom.Range + 11, // 6: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy + 1, // 7: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride + 2, // 8: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment + 3, // 9: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise + 5, // 10: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_proxy_freedom_config_proto_init() } @@ -564,7 +631,7 @@ func file_proxy_freedom_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)), NumEnums: 1, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/proxy/freedom/config.proto b/proxy/freedom/config.proto index 86242adb..bd27584c 100644 --- a/proxy/freedom/config.proto +++ b/proxy/freedom/config.proto @@ -36,6 +36,11 @@ message Noise { string apply_to = 6; } +message Range { + uint64 min = 1; + uint64 max = 2; +} + enum RuleAction { Allow = 0; Block = 1; @@ -46,6 +51,7 @@ message FinalRuleConfig { repeated xray.common.net.Network networks = 2; xray.common.net.PortList port_list = 3; repeated xray.common.geodata.IPRule ip = 4; + Range block_delay = 5; } message Config { diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index 8be8dbe0..4cd48b80 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -89,10 +89,11 @@ func init() { } type FinalRule struct { - action RuleAction - network [8]bool - port net.MemoryPortList - ip geodata.IPMatcher + action RuleAction + network [8]bool + port net.MemoryPortList + ip geodata.IPMatcher + blockDelay *Range } // Handler handles Freedom connections. @@ -104,7 +105,8 @@ type Handler struct { func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) { rule := &FinalRule{ - action: config.GetAction(), + action: config.GetAction(), + blockDelay: config.GetBlockDelay(), } if len(config.Networks) == 0 { @@ -176,7 +178,7 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule { } func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool { - if dialDest.Network != net.Network_TCP || !dialDest.Address.Family().IsDomain() { + if !dialDest.Address.Family().IsDomain() { return false } if len(h.finalRules) > 0 { @@ -191,14 +193,21 @@ func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, return false } -func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction { +func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule { for _, rule := range h.finalRules { if rule.Apply(network, address, port) { - return rule.action + return rule } } if defaultRule != nil && defaultRule.Apply(network, address, port) { - return defaultRule.action + return defaultRule + } + return nil +} + +func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction { + if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil { + return rule.action } return RuleAction_Allow } @@ -223,6 +232,20 @@ func (h *Handler) policy() policy.Session { return p } +func (h *Handler) blockDelay(rule *FinalRule) time.Duration { + min := uint64(30) + max := uint64(90) + if rule.blockDelay != nil { + min = rule.blockDelay.Min + max = rule.blockDelay.Max + } + abs := max - min + if max < min { + abs = min - max + } + return time.Duration(min+uint64(dice.Roll(int(abs+1)))) * time.Second +} + func isValidAddress(addr *net.IPOrDomain) bool { if addr == nil { return false @@ -269,6 +292,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte var conn stat.Connection var blockedDest *net.Destination + var blockedRule *FinalRule err := retry.ExponentialBackoff(5, 100).On(func() error { dialDest := destination if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() { @@ -290,7 +314,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } errors.LogInfo(ctx, "dialing to ", dialDest) } - } else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { + } else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules addrs, err := net.DefaultResolver.LookupIPAddr(ctx, dialDest.Address.Domain()) if err != nil { errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain()) @@ -301,8 +325,9 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } } } - if dialDest.Network == net.Network_TCP && h.applyFinalRules(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule) == RuleAction_Block { + if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block { blockedDest = &dialDest + blockedRule = rule return nil } @@ -318,11 +343,24 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return errors.New("failed to open connection to ", destination).Base(err) } if blockedDest != nil { - return errors.New("blocked target: ", *blockedDest).AtInfo() + delay := h.blockDelay(blockedRule) + errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay) + timer := time.AfterFunc(delay, func() { + common.Interrupt(input) + common.Interrupt(output) + errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest) + }) + defer timer.Stop() + defer common.Close(output) + if err := buf.Copy(input, buf.Discard); err != nil { + return nil + } + return nil } + // TODO: SRV/TXT // if remoteDest := net.DestinationFromAddr(conn.RemoteAddr()); h.applyFinalRules(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule) == RuleAction_Block { // conn.Close() - // return errors.New("blocked target: ", remoteDest).AtInfo() + // return blackhole(remoteDest) // } if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 { version := byte(h.config.ProxyProtocol)