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.PacketConntoNewClientWithConnection()to eliminate per‑discovery syscall overhead. - Silence logs: Disable
VerboseandVVerbosein production to prevent I/O blocking on every packet. - Cache addresses: Resolve the STUN server address once and store the
*net.UDPAddrpointer. - Pool buffers: Apply
sync.Poolto packet buffers infqnews2/libcore/stun/packet.goand extraction buffers infqnews2/libcore/io.go. - Parallelize I/O: Use worker pools with
io.CopyBufferfor archive extraction. - Tune runtime: Explicitly set
GOMAXPROCSto utilize all CPU cores on Android. - Stay current: Upgrade
sing/commonandulikunitz/xzto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →