feat: introduce tcp desync

This commit is contained in:
SunBK201 2025-11-30 22:26:24 +08:00
parent 4598ac7fb7
commit c23bbe5871
14 changed files with 750 additions and 27 deletions

View File

@ -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

View File

@ -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

View File

@ -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() {

View File

@ -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"}]'

View File

@ -274,3 +274,24 @@ msgstr "问题反馈"
msgid "Log Management"
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 检测"

View File

@ -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 {

View File

@ -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
)

View 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
}

View File

@ -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

View 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))
}
}

View 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
}

View 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
}

View 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)
}

View File

@ -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")
}