How to Optimize bannedbook/fanqiang for Speed: 7 Proven Techniques

Re‑use UDP sockets, disable verbose logging, cache resolved addresses, and pool buffers to eliminate the socket creation and per‑packet allocation overhead that dominates STUN discovery latency.

The bannedbook/fanqiang repository implements a complete STUN client and asset extraction pipeline used by Android and macOS VPN bundles. To optimize bannedbook/fanqiang for speed, you must target the high‑frequency allocations in fqnews2/libcore/stun/client.go, the serial I/O in fqnews2/libcore/io.go, and the unbuffered logging in fqnews2/libcore/stun/log.go that trigger excessive GC pressure on mobile devices.

Performance Bottlenecks in the Codebase

STUN Socket Creation Overhead

In fqnews2/libcore/stun/client.go, the Discover() method creates a fresh net.ListenUDP socket whenever c.conn is nil. This kernel handshake repeats for every NAT discovery run, adding tens of milliseconds on high‑latency mobile networks.

Archive Extraction Serialization

The Unzip function in fqnews2/libcore/io.go processes files sequentially, opening and creating each file individually without buffering. Large asset bundles unpack slowly because the code lacks parallel workers and relies on small default io.Copy buffers.

Verbose Logging I/O

The logger in fqnews2/libcore/stun/log.go writes to os.Stdout on every packet when SetVVerbose(true) is active. In production builds, this saturates the I/O pipeline and triggers garbage collection for every STUN response.

Optimization Strategies

Reuse UDP Connections with NewClientWithConnection

Instead of letting NewClient() spawn a socket per discovery, instantiate a long‑lived net.PacketConn at application startup and pass it to stun.NewClientWithConnection(). This removes the syscall overhead from the critical path.

Key implementation: Create the connection once in main() or init(), then reuse it across all Discover() calls.

Disable Debug Logging in Production

Call c.SetVerbose(false) and c.SetVVerbose(false) immediately after client initialization. Alternatively, compile with -ldflags="-s -w" and a build tag that stubs out the Debug and VVerbose methods in log.go.

Cache DNS Resolutions

The current SetServerHost implementation resolves the address via net.JoinHostPort on every Discover() call. Resolve the address once using net.ResolveUDPAddr, store the *net.UDPAddr in your client wrapper, and bypass the redundant lookup.

Implement Buffer Pools for Packet Handling

Functions like packet.bytes() in fqnews2/libcore/stun/packet.go allocate new slices per request. Introduce a sync.Pool pre‑sized to 128 bytes (typical STUN packet size) and reuse buffers across sendBindingReq iterations.

Parallelize Archive Extraction

Replace the sequential Unzip logic with a worker pool limited by runtime.GOMAXPROCS(0). Use io.CopyBuffer with 32 KB pooled buffers to reduce system calls when writing asset files to disk.

Tune Go Runtime Settings

Explicitly set runtime.GOMAXPROCS(runtime.NumCPU()) at startup. Some Android builds limit the default value, leaving cores idle during parallel extraction or STUN handling.

Upgrade Heavy Dependencies

The project imports github.com/sagernet/sing/common and github.com/ulikunitz/xz. Newer releases contain internal buffer optimizations. Run go get -u ./... and verify API compatibility to inherit these gains.

Implementation Examples

Reusing a Single UDP Socket

// app_start.go – initialise once
package main

import (
    "log"
    "net"

    "github.com/bannedbook/fanqiang/fqnews2/libcore/stun"
)

func main() {
    // Open a single UDP socket for the whole lifetime of the app
    conn, err := net.ListenUDP("udp", nil)
    if err != nil {
        log.Fatalf("cannot open UDP socket: %v", err)
    }

    client := stun.NewClientWithConnection(conn)
    client.SetVerbose(false)   // disable noisy logs
    client.SetVVerbose(false)  // disable packet dumps

    // Example: discover NAT type once
    nat, host, err, _ := client.Discover()
    if err != nil {
        log.Fatalf("STUN discovery failed: %v", err)
    }
    log.Printf("NAT type: %s, external address: %s", nat, host)
}

Source: fqnews2/libcore/stun/client.go defines the entry point for socket handling.

Parallel Unzip with Buffer Pool

package libcore

import (
    "archive/zip"
    "io"
    "os"
    "path/filepath"
    "runtime"
    "sync"

    "github.com/sagernet/sing/common"
    E "github.com/sagernet/sing/common/exceptions"
)

var bufPool = sync.Pool{
    New: func() interface{} { return make([]byte, 32*1024) }, // 32 KB buffers
}

// UnzipParallel extracts files concurrently (max GOMAXPROCS workers)
func UnzipParallel(archive, dst string) error {
    r, err := zip.OpenReader(archive)
    if err != nil {
        return err
    }
    defer r.Close()

    if err = os.MkdirAll(dst, os.ModePerm); err != nil {
        return err
    }

    wg := sync.WaitGroup{}
    sem := make(chan struct{}, runtime.GOMAXPROCS(0)) // limit parallelism

    for _, f := range r.File {
        f := f // capture loop variable
        wg.Add(1)
        go func() {
            defer wg.Done()
            sem <- struct{}{}        // acquire slot
            defer func() { <-sem }() // release slot

            path := filepath.Join(dst, f.Name)
            if f.FileInfo().IsDir() {
                _ = os.MkdirAll(path, os.ModePerm)
                return
            }

            rc, err := f.Open()
            if err != nil {
                return
            }
            defer rc.Close()

            out, err := os.Create(path)
            if err != nil {
                return
            }
            defer out.Close()

            buf := bufPool.Get().([]byte)
            _, err = io.CopyBuffer(out, rc, buf)
            bufPool.Put(buf)

            if err != nil {
                err = E.Errors(err, common.Close(rc, out))
                if err != nil {
                    // handle or log as needed
                }
            }
        }()
    }
    wg.Wait()
    return nil
}

Original implementation: See fqnews2/libcore/io.go for the baseline sequential version.

Cached Address Resolution

type CachedClient struct {
    *stun.Client
    resolved *net.UDPAddr
}

// ResolveOnce caches the UDP address to avoid repeated DNS lookups
func (c *CachedClient) ResolveOnce() error {
    if c.resolved != nil {
        return nil
    }
    addr, err := net.ResolveUDPAddr("udp", c.serverAddr)
    if err != nil {
        return err
    }
    c.resolved = addr
    return nil
}

func (c *CachedClient) Discover() (stun.NATType, *stun.Host, error, bool) {
    if err := c.ResolveOnce(); err != nil {
        return stun.NATError, nil, err, false
    }
    // reuse the cached address
    return c.discover(c.conn, c.resolved)
}

Why: This avoids the net.ResolveUDPAddr overhead in fqnews2/libcore/stun/client.go on every discovery round.

Summary

  • Reuse sockets: Pass a pre‑opened net.PacketConn to NewClientWithConnection() to eliminate per‑discovery syscall overhead.
  • Silence logs: Disable Verbose and VVerbose in production to prevent I/O blocking on every packet.
  • Cache addresses: Resolve the STUN server address once and store the *net.UDPAddr pointer.
  • Pool buffers: Apply sync.Pool to packet buffers in fqnews2/libcore/stun/packet.go and extraction buffers in fqnews2/libcore/io.go.
  • Parallelize I/O: Use worker pools with io.CopyBuffer for archive extraction.
  • Tune runtime: Explicitly set GOMAXPROCS to utilize all CPU cores on Android.
  • Stay current: Upgrade sing/common and ulikunitz/xz to inherit upstream optimizations.

Frequently Asked Questions

What is the biggest performance bottleneck in bannedbook/fanqiang?

The repeated socket creation in fqnews2/libcore/stun/client.go dominates latency. Each call to Discover() without a pre‑supplied connection triggers net.ListenUDP, which costs a kernel handshake and port allocation. Combining this with per‑packet logging in log.go makes the discovery flow several times slower than necessary.

How do I disable logging without modifying the source code?

Set the build tags to exclude debug symbols and stub the logger at link time: go build -tags=release -ldflags="-s -w". Alternatively, call client.SetVerbose(false) and client.SetVVerbose(false) immediately after NewClient() in your initialization code, which sets the internal verbosity flags to false without recompilation.

Why does STUN discovery slow down on mobile devices specifically?

Mobile networks exhibit higher RTT and packet loss, amplifying the cost of redundant operations. When Discover() creates a new socket for each attempt, the kernel must rebind and renegotiate NAT mappings over the high‑latency link. Buffer allocations also trigger GC pauses that are more noticeable on low‑RAM Android devices.

Can these optimizations be applied to Android builds?

Yes. The fqnews2/libcore package compiles for Android via CGO. Ensure you call runtime.GOMAXPROCS(runtime.NumCPU()) in the Java‑to‑Go bridge startup, reuse the UDP connection across JNI calls, and disable verbose logging in release APKs to achieve the same sub‑millisecond discovery times observed on desktop builds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →