mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 08:44:29 +00:00
feat: introduce tcp desync
This commit is contained in:
parent
4598ac7fb7
commit
c23bbe5871
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"}]'
|
||||
@ -273,4 +273,25 @@ msgid "Issue Report"
|
||||
msgstr "问题反馈"
|
||||
|
||||
msgid "Log Management"
|
||||
msgstr "日志管理"
|
||||
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 检测"
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
300
src/internal/netfilter/frame.go
Normal file
300
src/internal/netfilter/frame.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
99
src/internal/server/desync/desync_linux.go
Normal file
99
src/internal/server/desync/desync_linux.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
30
src/internal/server/desync/desync_others.go
Normal file
30
src/internal/server/desync/desync_others.go
Normal file
@ -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
|
||||
}
|
||||
77
src/internal/server/desync/iptables.go
Normal file
77
src/internal/server/desync/iptables.go
Normal file
@ -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
|
||||
}
|
||||
66
src/internal/server/desync/nftables.go
Normal file
66
src/internal/server/desync/nftables.go
Normal file
@ -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)
|
||||
}
|
||||
64
src/main.go
64
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")
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user