From c23bbe5871b93cde9ba3bc33d3e5e2db746db4c0 Mon Sep 17 00:00:00 2001 From: SunBK201 Date: Sun, 30 Nov 2025 22:26:24 +0800 Subject: [PATCH] feat: introduce tcp desync --- openwrt/files/luci/model/cbi/ua3f.lua | 6 +- openwrt/files/luci/model/cbi/ua3f/fields.lua | 20 ++ openwrt/files/ua3f.init | 14 +- openwrt/files/ua3f.uci | 1 + openwrt/po/zh_cn/ua3f.po | 23 +- src/internal/config/config.go | 25 ++ src/internal/netfilter/firewall.go | 1 + src/internal/netfilter/frame.go | 300 +++++++++++++++++++ src/internal/netfilter/packet.go | 51 ++++ src/internal/server/desync/desync_linux.go | 99 ++++++ src/internal/server/desync/desync_others.go | 30 ++ src/internal/server/desync/iptables.go | 77 +++++ src/internal/server/desync/nftables.go | 66 ++++ src/main.go | 64 ++-- 14 files changed, 750 insertions(+), 27 deletions(-) create mode 100644 src/internal/netfilter/frame.go create mode 100644 src/internal/server/desync/desync_linux.go create mode 100644 src/internal/server/desync/desync_others.go create mode 100644 src/internal/server/desync/iptables.go create mode 100644 src/internal/server/desync/nftables.go diff --git a/openwrt/files/luci/model/cbi/ua3f.lua b/openwrt/files/luci/model/cbi/ua3f.lua index ae75fd7..2dfb4a2 100644 --- a/openwrt/files/luci/model/cbi/ua3f.lua +++ b/openwrt/files/luci/model/cbi/ua3f.lua @@ -23,9 +23,10 @@ function create_sections(map) sections.general = map:section(NamedSection, "main", "ua3f", translate("General")) sections.general:tab("general", translate("Settings")) sections.general:tab("rewrite", translate("Rewrite Rules")) + sections.general:tab("desync", translate("Desync Settings")) + sections.general:tab("others", translate("Others Settings")) sections.general:tab("stats", translate("Statistics")) sections.general:tab("log", translate("Log")) - sections.general:tab("others", translate("Others Settings")) return sections end @@ -35,8 +36,9 @@ local sections = create_sections(ua3f) fields.add_status_fields(sections.status) fields.add_general_fields(sections.general) fields.add_rewrite_fields(sections.general) +fields.add_desync_fields(sections.general) +fields.add_others_fields(sections.general) fields.add_stats_fields(sections.general) fields.add_log_fields(sections.general) -fields.add_others_fields(sections.general) return ua3f diff --git a/openwrt/files/luci/model/cbi/ua3f/fields.lua b/openwrt/files/luci/model/cbi/ua3f/fields.lua index 1fff022..9cfe08d 100644 --- a/openwrt/files/luci/model/cbi/ua3f/fields.lua +++ b/openwrt/files/luci/model/cbi/ua3f/fields.lua @@ -203,6 +203,26 @@ function M.add_log_fields(section) return log end +function M.add_desync_fields(section) + -- Enable TCP Desync + local desync_enabled = section:taboption("desync", Flag, "desync_enabled", translate("Enable TCP Desync")) + desync_enabled.description = translate("Enable TCP Desynchronization to evade DPI") + + -- CT Byte Setting + local ct_byte = section:taboption("desync", Value, "desync_ct_bytes", translate("Desync Bytes")) + ct_byte.placeholder = "1500" + ct_byte.datatype = "uinteger" + ct_byte.description = translate("Number of bytes for fragmented random emission") + ct_byte:depends("desync_enabled", "1") + + -- CT Packets Setting + local ct_packets = section:taboption("desync", Value, "desync_ct_packets", translate("Desync Packets")) + ct_packets.placeholder = "8" + ct_packets.datatype = "uinteger" + ct_packets.description = translate("Number of packets for fragmented random emission") + ct_packets:depends("desync_enabled", "1") +end + -- Others Tab Fields function M.add_others_fields(section) -- TTL Setting diff --git a/openwrt/files/ua3f.init b/openwrt/files/ua3f.init index c152017..25167c5 100755 --- a/openwrt/files/ua3f.init +++ b/openwrt/files/ua3f.init @@ -33,6 +33,13 @@ start_service() { config_get_bool del_tcpts "main" "del_tcpts" 0 config_get_bool set_tcp_init_window "main" "set_tcp_init_window" 0 + local desync_enabled desync_ct_bytes desync_ct_packets + config_get_bool desync_enabled "main" "desync_enabled" 0 + if [ "$desync_enabled" -eq "1" ]; then + config_get desync_ct_bytes "main" "desync_ct_bytes" "1500" + config_get desync_ct_packets "main" "desync_ct_packets" "8" + fi + procd_open_instance "$NAME" procd_set_param command "$PROG" procd_append_param command -m "$server_mode" @@ -48,6 +55,9 @@ start_service() { procd_append_param env UA3F_IPID="$set_ipid" procd_append_param env UA3F_TCPTS="$del_tcpts" procd_append_param env UA3F_TCP_INIT_WINDOW="$set_tcp_init_window" + procd_append_param env UA3F_DESYNC="$desync_enabled" + procd_append_param env UA3F_DESYNC_BYTES="$desync_ct_bytes" + procd_append_param env UA3F_DESYNC_PACKETS="$desync_ct_packets" procd_set_param respawn procd_set_param stdout 1 @@ -59,8 +69,8 @@ start_service() { } reload_service() { - stop - start + stop + start } service_triggers() { diff --git a/openwrt/files/ua3f.uci b/openwrt/files/ua3f.uci index 8d2f355..be387d5 100644 --- a/openwrt/files/ua3f.uci +++ b/openwrt/files/ua3f.uci @@ -15,4 +15,5 @@ config 'ua3f' 'main' option del_tcpts '0' option set_ipid '0' option set_tcp_init_window '0' + option desync_enabled '0' option rewrite_rules '[{"enabled":false,"type":"DEST-PORT","action":"DIRECT","match_value":"443","rewrite_header":"User-Agent","rewrite_value":"","description":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"MicroMessenger Client","rewrite_header":"User-Agent","rewrite_value":"","description":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"Bilibili Freedoooooom\/MarkII","rewrite_header":"User-Agent","rewrite_value":"","description":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"Valve\/Steam HTTP Client 1.0","rewrite_header":"User-Agent","rewrite_value":"","description":""},{"enabled":true,"type":"KEYWORD","action":"REPLACE","match_value":"Mac","rewrite_header":"User-Agent","description":"","rewrite_value":"FFF"},{"enabled":true,"type":"REGEX","action":"REPLACE","match_value":"(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)","rewrite_header":"User-Agent","description":"","rewrite_value":"FFF"},{"enabled":true,"type":"FINAL","action":"REPLACE","match_value":"","rewrite_header":"User-Agent","description":"Default Fallback Rule","rewrite_value":"FFF"}]' \ No newline at end of file diff --git a/openwrt/po/zh_cn/ua3f.po b/openwrt/po/zh_cn/ua3f.po index 02b62d0..eea7274 100644 --- a/openwrt/po/zh_cn/ua3f.po +++ b/openwrt/po/zh_cn/ua3f.po @@ -273,4 +273,25 @@ msgid "Issue Report" msgstr "问题反馈" msgid "Log Management" -msgstr "日志管理" \ No newline at end of file +msgstr "日志管理" + +msgid "Desync Settings" +msgstr "Desync 设置" + +msgid "Enable TCP Desync" +msgstr "启用 TCP 分片乱序发射" + +msgid "Desync Bytes" +msgstr "Desync 字节" + +msgid "Desync Packets" +msgstr "Desync 数据包" + +msgid "Number of bytes for fragmented random emission" +msgstr "乱序发射的字节数,谨慎设置过大" + +msgid "Number of packets for fragmented random emission" +msgstr "乱序发射的数据包数,谨慎设置过大" + +msgid "Enable TCP Desynchronization to evade DPI" +msgstr "启用 TCP 分片乱序发射,可以用于规避 DPI 检测" \ No newline at end of file diff --git a/src/internal/config/config.go b/src/internal/config/config.go index 7dc3255..6a4f458 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -41,6 +41,13 @@ type Config struct { SetIPID bool DelTCPTimestamp bool SetTCPInitialWindow bool + TCPDesync TCPDesyncConfig +} + +type TCPDesyncConfig struct { + Enabled bool + Bytes uint32 + Packets uint32 } func Parse() (*Config, bool) { @@ -102,6 +109,24 @@ func Parse() (*Config, bool) { cfg.SetTCPInitialWindow = true } + if os.Getenv("UA3F_DESYNC") == "1" { + cfg.TCPDesync.Enabled = true + if val := os.Getenv("UA3F_DESYNC_BYTES"); val != "" { + var bytes uint32 + _, err := fmt.Sscanf(val, "%d", &bytes) + if err == nil { + cfg.TCPDesync.Bytes = bytes + } + } + if val := os.Getenv("UA3F_DESYNC_PACKETS"); val != "" { + var packets uint32 + _, err := fmt.Sscanf(val, "%d", &packets) + if err == nil { + cfg.TCPDesync.Packets = packets + } + } + } + // Parse other options from -o flag opts := strings.Split(others, ",") for _, opt := range opts { diff --git a/src/internal/netfilter/firewall.go b/src/internal/netfilter/firewall.go index 88cf3f6..61d9e59 100644 --- a/src/internal/netfilter/firewall.go +++ b/src/internal/netfilter/firewall.go @@ -27,6 +27,7 @@ const ( SKIP_PORTS = "22,51080,51090" FAKEIP_RANGE = "198.18.0.0/16,198.18.0.1/15,28.0.0.1/8" HELPER_QUEUE = 10301 + DESYNC_QUEUE = 10901 SO_MARK = 0xc9 ) diff --git a/src/internal/netfilter/frame.go b/src/internal/netfilter/frame.go new file mode 100644 index 0000000..e3d8b21 --- /dev/null +++ b/src/internal/netfilter/frame.go @@ -0,0 +1,300 @@ +package netfilter + +import ( + "crypto/rand" + "fmt" + "log/slog" + "math/big" + + nfq "github.com/florianl/go-nfqueue/v2" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" +) + +type Frame struct { + A *nfq.Attribute + Ethernet *layers.Ethernet + NetworkLayer gopacket.NetworkLayer + TCP *layers.TCP + SrcAddr string + DstAddr string + IsIPv6 bool +} + +type FragmentConfig struct { + Enable bool + FragmentSize int + OutOfOrder bool + MinFragments int // 0 auto calculate + FirstFragmentSize int // 0 means random 1-5 bytes +} + +// NewFrame creates a Ethernet frame from the given nfqueue attribute. +func NewFrame(a *nfq.Attribute) (frame *Frame, err error) { + frame = &Frame{ + A: a, + Ethernet: &layers.Ethernet{}, + TCP: &layers.TCP{}, + } + + var decoded []gopacket.LayerType + var ip4 layers.IPv4 + var ip6 layers.IPv6 + + pktData := *a.Payload + + parser := gopacket.NewDecodingLayerParser( + layers.LayerTypeEthernet, + frame.Ethernet, + &ip4, + &ip6, + frame.TCP, + ) + + if err = parser.DecodeLayers(pktData, &decoded); err != nil { + return + } + + // Determine IP version from Ethernet EthernetType + for _, layerType := range decoded { + switch layerType { + case layers.LayerTypeIPv4: + frame.NetworkLayer = &ip4 + frame.IsIPv6 = false + frame.SrcAddr = fmt.Sprintf("%s:%d", ip4.SrcIP.String(), frame.TCP.SrcPort) + frame.DstAddr = fmt.Sprintf("%s:%d", ip4.DstIP.String(), frame.TCP.DstPort) + case layers.LayerTypeIPv6: + frame.NetworkLayer = &ip6 + frame.IsIPv6 = true + frame.SrcAddr = fmt.Sprintf("%s:%d", ip6.SrcIP.String(), frame.TCP.SrcPort) + frame.DstAddr = fmt.Sprintf("%s:%d", ip6.DstIP.String(), frame.TCP.DstPort) + } + } + + return +} + +// Serialize serializes the Frame back to a byte slice. +func (f *Frame) Serialize() ([]byte, error) { + if err := f.TCP.SetNetworkLayerForChecksum(f.NetworkLayer); err != nil { + return nil, err + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + var err error + if f.IsIPv6 { + ip6 := f.NetworkLayer.(*layers.IPv6) + err = gopacket.SerializeLayers(buf, opts, + f.Ethernet, + ip6, + f.TCP, + gopacket.Payload(f.TCP.Payload), + ) + } else { + ip4 := f.NetworkLayer.(*layers.IPv4) + err = gopacket.SerializeLayers(buf, opts, + f.Ethernet, + ip4, + f.TCP, + gopacket.Payload(f.TCP.Payload), + ) + } + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (f *Frame) SerializeWithFragment() ([]byte, error) { + fragmentedFrames, err := f.SerializeFragments(&FragmentConfig{ + Enable: true, + OutOfOrder: false, + MinFragments: 3, + FirstFragmentSize: 10, + FragmentSize: 256, + }) + if err != nil { + return nil, err + } + slog.Info("Serialized with fragmentation", + slog.Int("Original Payload Size", len(f.TCP.Payload)), + slog.Int("fragments", len(fragmentedFrames)), + slog.String("SrcAddr", f.SrcAddr), + slog.String("DstAddr", f.DstAddr)) + + combined := []byte{} + for _, frag := range fragmentedFrames { + combined = append(combined, frag...) + } + return combined, nil +} + +func (f *Frame) SerializeFragments(cfg *FragmentConfig) ([][]byte, error) { + if cfg == nil || !cfg.Enable { + data, err := f.Serialize() + if err != nil { + return nil, err + } + return [][]byte{data}, nil + } + + payload := f.TCP.Payload + if len(payload) == 0 || f.TCP.FIN { + data, err := f.Serialize() + if err != nil { + return nil, err + } + return [][]byte{data}, nil + } + + fragmentSize := cfg.FragmentSize + var numFragments int + + if fragmentSize <= 0 { + // fragmentSize not specified, calculate from MinFragments + if cfg.MinFragments > 0 { + numFragments = cfg.MinFragments + fragmentSize = (len(payload) + numFragments - 1) / numFragments + } else { + // default to 2 fragments if neither is specified + numFragments = 2 + fragmentSize = (len(payload) + 1) / 2 + } + } else { + numFragments = (len(payload) + fragmentSize - 1) / fragmentSize + if cfg.MinFragments > 0 && numFragments < cfg.MinFragments { + numFragments = cfg.MinFragments + fragmentSize = (len(payload) + numFragments - 1) / numFragments + } + } + + if numFragments < 2 && len(payload) >= 2 { + numFragments = 2 + fragmentSize = (len(payload) + 1) / 2 + } + + type fragment struct { + offset int + length int + seq uint32 + } + + fragments := make([]fragment, 0, numFragments) + offset := 0 + baseSeq := f.TCP.Seq + + // first fragment + firstSize := cfg.FirstFragmentSize + if firstSize <= 0 { + // random 1-5 bytes + n, err := rand.Int(rand.Reader, big.NewInt(5)) + if err != nil { + firstSize = 3 + } else { + firstSize = int(n.Int64()) + 1 + } + } + if firstSize > len(payload) { + firstSize = len(payload) + } + + fragments = append(fragments, fragment{ + offset: 0, + length: firstSize, + seq: baseSeq, + }) + offset = firstSize + + // remaining fragments + for offset < len(payload) { + length := fragmentSize + if offset+length > len(payload) { + length = len(payload) - offset + } + fragments = append(fragments, fragment{ + offset: offset, + length: length, + seq: baseSeq + uint32(offset), + }) + offset += length + } + + // Fisher-Yates + if cfg.OutOfOrder && len(fragments) > 1 { + for i := len(fragments) - 1; i > 0; i-- { + n, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) + if err != nil { + continue + } + j := int(n.Int64()) + fragments[i], fragments[j] = fragments[j], fragments[i] + } + } + + // serialize fragments + packets := make([][]byte, 0, len(fragments)) + for i, frag := range fragments { + data, err := f.serializeFragment(payload[frag.offset:frag.offset+frag.length], frag.seq) + if err != nil { + return nil, fmt.Errorf("serialize fragment at offset %d: %w", frag.offset, err) + } + slog.Info("Serialized fragment", + slog.Int("Fragment Index", i), + slog.Int("Fragment Size", frag.length), + slog.String("SrcAddr", f.SrcAddr), + slog.String("DstAddr", f.DstAddr)) + packets = append(packets, data) + } + + return packets, nil +} + +// serializeFragment serializes a single tcp fragment +// return ethernet frame +func (f *Frame) serializeFragment(fragmentPayload []byte, seq uint32) ([]byte, error) { + // Create a copy of TCP layer with modified seq and payload + tcpCopy := *f.TCP + tcpCopy.Seq = seq + tcpCopy.Payload = fragmentPayload + + if err := tcpCopy.SetNetworkLayerForChecksum(f.NetworkLayer); err != nil { + return nil, err + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + var err error + if f.IsIPv6 { + ip6 := f.NetworkLayer.(*layers.IPv6) + err = gopacket.SerializeLayers(buf, opts, + f.Ethernet, + ip6, + &tcpCopy, + gopacket.Payload(fragmentPayload), + ) + } else { + ip4 := f.NetworkLayer.(*layers.IPv4) + err = gopacket.SerializeLayers(buf, opts, + f.Ethernet, + ip4, + &tcpCopy, + gopacket.Payload(fragmentPayload), + ) + } + + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/src/internal/netfilter/packet.go b/src/internal/netfilter/packet.go index 7bd475d..047fba4 100644 --- a/src/internal/netfilter/packet.go +++ b/src/internal/netfilter/packet.go @@ -102,6 +102,57 @@ func (p *Packet) Serialize() ([]byte, error) { return buffer.Bytes(), nil } +// SerializeWithDesync splits the TCP payload into 2 fragments, +// discards the first fragment, keeps only the second fragment, +// and serializes the packet with the updated sequence number. +func (p *Packet) SerializeWithDesync() ([]byte, error) { + var err error + + networkLayer := p.NetworkLayer + tcp := p.TCP + isIPv6 := p.IsIPv6 + payload := tcp.Payload + + // If payload is empty or too small to split, just serialize normally + if len(payload) <= 1 { + return p.Serialize() + } + + // Split payload into 2 fragments, discard first, keep second + splitPoint := len(payload) / 2 + secondFragment := payload[splitPoint:] + + buffer := gopacket.NewSerializeBuffer() + serOpts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // Update TCP sequence number to account for the discarded first fragment + tcp.Seq = tcp.Seq + uint32(splitPoint) + tcp.Checksum = 0 + tcp.Payload = nil + err = tcp.SetNetworkLayerForChecksum(networkLayer) + if err != nil { + return nil, err + } + + if isIPv6 { + ip6 := networkLayer.(*layers.IPv6) + ip6.NextHeader = layers.IPProtocolTCP + err = gopacket.SerializeLayers(buffer, serOpts, ip6, tcp, gopacket.Payload(secondFragment)) + } else { + ip4 := networkLayer.(*layers.IPv4) + ip4.Checksum = 0 + err = gopacket.SerializeLayers(buffer, serOpts, ip4, tcp, gopacket.Payload(secondFragment)) + } + + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + func (p *Packet) GetCtMark() (uint32, bool) { if p.A.Ct == nil || len(*p.A.Ct) == 0 { return 0, false diff --git a/src/internal/server/desync/desync_linux.go b/src/internal/server/desync/desync_linux.go new file mode 100644 index 0000000..2a1534c --- /dev/null +++ b/src/internal/server/desync/desync_linux.go @@ -0,0 +1,99 @@ +//go:build linux + +package desync + +import ( + "log/slog" + + nfq "github.com/florianl/go-nfqueue/v2" + "github.com/sunbk201/ua3f/internal/config" + "github.com/sunbk201/ua3f/internal/netfilter" + "sigs.k8s.io/knftables" +) + +type Server struct { + netfilter.Firewall + cfg *config.Config + nfqServer *netfilter.NfqueueServer + CtByte uint32 + CtPackets uint32 +} + +func New(cfg *config.Config) *Server { + s := &Server{ + cfg: cfg, + nfqServer: &netfilter.NfqueueServer{ + QueueNum: netfilter.DESYNC_QUEUE, + }, + CtByte: 1500, + CtPackets: 2 + 3*2, + } + s.nfqServer.HandlePacket = s.HandlePacket + s.Firewall = netfilter.Firewall{ + Nftable: &knftables.Table{ + Name: "UA3F_DESYNC", + Family: knftables.InetFamily, + }, + NftSetup: s.nftSetup, + NftCleanup: s.nftCleanup, + IptSetup: s.iptSetup, + IptCleanup: s.iptCleanup, + } + if s.cfg.TCPDesync.Bytes > 0 { + s.CtByte = s.cfg.TCPDesync.Bytes + } + if s.cfg.TCPDesync.Packets > 0 { + s.CtPackets = s.cfg.TCPDesync.Packets + } + return s +} + +func (s *Server) Start() (err error) { + err = s.Firewall.Setup(s.cfg) + if err != nil { + slog.Error("s.Firewall.Setup", slog.Any("error", err)) + return err + } + err = s.nfqServer.Start() + if err != nil { + return err + } + slog.Info("TCP Desync server started", slog.Int("ct_bytes", int(s.CtByte)), slog.Int("ct_packets", int(s.CtPackets))) + return +} + +func (s *Server) Close() error { + err := s.Firewall.Cleanup() + s.nfqServer.Close() + return err +} + +func (s *Server) HandlePacket(frame *netfilter.Packet) { + fragment := s.cfg.TCPDesync.Enabled + if frame.TCP == nil || len(frame.TCP.Payload) <= 1 || frame.TCP.FIN { + fragment = false + } + s.sendVerdict(frame, fragment) +} + +func (s *Server) sendVerdict(packet *netfilter.Packet, fragment bool) { + nf := s.nfqServer.Nf + id := *packet.A.PacketID + + if !fragment { + _ = nf.SetVerdict(id, nfq.NfAccept) + return + } + + newPacket, err := packet.SerializeWithDesync() + if err != nil { + _ = nf.SetVerdict(id, nfq.NfAccept) + slog.Error("packet.SerializeWithDesync", slog.Any("error", err)) + return + } + + if err := nf.SetVerdictWithOption(id, nfq.NfAccept, nfq.WithAlteredPacket(newPacket)); err != nil { + _ = nf.SetVerdict(id, nfq.NfAccept) + slog.Error("nf.SetVerdictWithOption", slog.Any("error", err)) + } +} diff --git a/src/internal/server/desync/desync_others.go b/src/internal/server/desync/desync_others.go new file mode 100644 index 0000000..0bb6b43 --- /dev/null +++ b/src/internal/server/desync/desync_others.go @@ -0,0 +1,30 @@ +//go:build !linux + +package desync + +import ( + "github.com/sunbk201/ua3f/internal/config" +) + +type Server struct { + cfg *config.Config +} + +func New(cfg *config.Config) *Server { + s := &Server{ + cfg: cfg, + } + return s +} + +func (s *Server) Setup() (err error) { + return nil +} + +func (s *Server) Start() (err error) { + return nil +} + +func (s *Server) Close() (err error) { + return nil +} diff --git a/src/internal/server/desync/iptables.go b/src/internal/server/desync/iptables.go new file mode 100644 index 0000000..294ad86 --- /dev/null +++ b/src/internal/server/desync/iptables.go @@ -0,0 +1,77 @@ +//go:build linux + +package desync + +import ( + "strconv" + + "github.com/coreos/go-iptables/iptables" + "github.com/sunbk201/ua3f/internal/netfilter" +) + +const ( + table = "mangle" + chain = "UA3F_DESYNC" + jumpPoint = "POSTROUTING" +) + +var JumpChain = []string{ + "-p", "tcp", + "-j", chain, +} + +func (s *Server) iptSetup() error { + ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + if err != nil { + return err + } + + err = ipt.NewChain(table, chain) + if err != nil { + return err + } + + err = ipt.Append(table, jumpPoint, JumpChain...) + if err != nil { + return err + } + + return s.IptSetRuleDesync(ipt) +} + +func (s *Server) iptCleanup() error { + ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + if err != nil { + return err + } + ipt.Delete(table, jumpPoint, JumpChain...) + ipt.ClearAndDeleteChain(table, chain) + return nil +} + +func (s *Server) IptSetRuleDesync(ipt *iptables.IPTables) error { + var RuleDesync = []string{ + "-p", "tcp", + "-m", "conntrack", + "--ctdir", "ORIGINAL", + "--ctstate", "ESTABLISHED", + "-m", "connbytes", + "--connbytes-dir", "original", + "--connbytes-mode", "bytes", + "--connbytes", "0:" + strconv.Itoa(int(s.CtByte)), + "-m", "connbytes", + "--connbytes-dir", "original", + "--connbytes-mode", "packets", + "--connbytes", "0:" + strconv.Itoa(int(s.CtPackets)), + "-m", "length", + "--length", "41:0xffff", + "-j", "NFQUEUE", + "--queue-num", strconv.Itoa(netfilter.DESYNC_QUEUE), + "--queue-bypass", + } + err := ipt.Append(table, chain, RuleDesync...) + if err != nil { + return err + } + return nil +} diff --git a/src/internal/server/desync/nftables.go b/src/internal/server/desync/nftables.go new file mode 100644 index 0000000..d915785 --- /dev/null +++ b/src/internal/server/desync/nftables.go @@ -0,0 +1,66 @@ +//go:build linux + +package desync + +import ( + "context" + "fmt" + + "sigs.k8s.io/knftables" +) + +func (s *Server) nftSetup() error { + nft, err := knftables.New(s.Nftable.Family, s.Nftable.Name) + if err != nil { + return err + } + + tx := nft.NewTransaction() + tx.Add(s.Nftable) + + s.NftSetDesync(tx, s.Nftable) + + if err := nft.Run(context.TODO(), tx); err != nil { + return err + } + return nil +} + +func (s *Server) nftCleanup() error { + nft, err := knftables.New(s.Nftable.Family, s.Nftable.Name) + if err != nil { + return err + } + + tx := nft.NewTransaction() + tx.Delete(s.Nftable) + + if err := nft.Run(context.TODO(), tx); err != nil { + return err + } + return nil +} + +func (s *Server) NftSetDesync(tx *knftables.Transaction, table *knftables.Table) { + chain := &knftables.Chain{ + Name: "DESYNC_QUEUE", + Table: table.Name, + Type: knftables.PtrTo(knftables.FilterType), + Hook: knftables.PtrTo(knftables.PostroutingHook), + Priority: knftables.PtrTo(knftables.BaseChainPriority("mangle - 30")), + } + rule := &knftables.Rule{ + Chain: chain.Name, + Rule: knftables.Concat( + "ip length > 41", + "meta l4proto tcp", + "ct state established", + "ct direction original", + fmt.Sprintf("ct bytes < %d", s.CtByte), + fmt.Sprintf("ct packets < %d", s.CtPackets), + fmt.Sprintf("counter queue num %d bypass", s.nfqServer.QueueNum), + ), + } + tx.Add(chain) + tx.Add(rule) +} diff --git a/src/main.go b/src/main.go index fccfd44..ed6c0af 100644 --- a/src/main.go +++ b/src/main.go @@ -11,12 +11,16 @@ import ( "github.com/sunbk201/ua3f/internal/log" "github.com/sunbk201/ua3f/internal/rewrite" "github.com/sunbk201/ua3f/internal/server" + "github.com/sunbk201/ua3f/internal/server/desync" "github.com/sunbk201/ua3f/internal/server/netlink" "github.com/sunbk201/ua3f/internal/statistics" "github.com/sunbk201/ua3f/internal/usergroup" ) -var appVersion = "Development" +var ( + appVersion = "Development" + shutdownChain []func() error +) func main() { cfg, showVer := config.Parse() @@ -41,33 +45,32 @@ func main() { return } + helper := netlink.New(cfg) + addShutdown("helper.Close", helper.Close) + if err := helper.Start(); err != nil { + slog.Error("helper.Start", slog.Any("error", err)) + shutdown() + return + } + + if cfg.TCPDesync.Enabled { + desync := desync.New(cfg) + addShutdown("desync.Close", desync.Close) + if err := desync.Start(); err != nil { + slog.Error("desync.Start", slog.Any("error", err)) + shutdown() + return + } + } + srv, err := server.NewServer(cfg, rw) if err != nil { slog.Error("server.NewServer", slog.Any("error", err)) + shutdown() return } - - helper := netlink.New(cfg) - if err := helper.Start(); err != nil { - slog.Error("helper.Start", slog.Any("error", err)) - if err := srv.Close(); err != nil { - slog.Error("srv.Close", slog.Any("error", err)) - } - return - } - - shutdown := func() { - if err := helper.Close(); err != nil { - slog.Error("helper.Close", slog.Any("error", err)) - } - if err := srv.Close(); err != nil { - slog.Error("srv.Close", slog.Any("error", err)) - } - slog.Info("UA3F exit") - } - go statistics.StartRecorder() - + addShutdown("srv.Close", srv.Close) if err := srv.Start(); err != nil { slog.Error("srv.Start", slog.Any("error", err)) shutdown() @@ -89,3 +92,20 @@ func main() { } } } + +func addShutdown(name string, fn func() error) { + shutdownChain = append(shutdownChain, func() error { + if err := fn(); err != nil { + slog.Error(name, slog.Any("error", err)) + return err + } + return nil + }) +} + +func shutdown() { + for i := len(shutdownChain) - 1; i >= 0; i-- { + _ = shutdownChain[i]() + } + slog.Info("UA3F exit") +}