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

> Boost bannedbook/fanqiang speed with 7 proven techniques that eliminate overhead. Optimize UDP sockets, logging, caching, and buffering for faster performance.

- Repository: [如何翻墙/fanqiang](https://github.com/bannedbook/fanqiang)
- Tags: performance
- Published: 2026-06-15

---

**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`](https://github.com/bannedbook/fanqiang/blob/main/fqnews2/libcore/stun/client.go), the serial I/O in [`fqnews2/libcore/io.go`](https://github.com/bannedbook/fanqiang/blob/main/fqnews2/libcore/io.go), and the unbuffered logging in [`fqnews2/libcore/stun/log.go`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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

```go
// 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`](https://github.com/bannedbook/fanqiang/blob/main/fqnews2/libcore/stun/client.go) defines the entry point for socket handling.

### Parallel Unzip with Buffer Pool

```go
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`](https://github.com/bannedbook/fanqiang/blob/main/fqnews2/libcore/io.go) for the baseline sequential version.

### Cached Address Resolution

```go
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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/fqnews2/libcore/stun/packet.go) and extraction buffers in [`fqnews2/libcore/io.go`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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`](https://github.com/bannedbook/fanqiang/blob/main/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.