shadow1ng/fscan · warning

network_rate_limited

Error message

network_rate_limited

What it means

ScanSession.DialTCP checks CanSendPacketWith before every outbound TCP dial. If the session's rate/packet limits are exhausted, the dial is refused and this error wraps the limit reason. It prevents the scan from exceeding configured network budgets.

Source

Thrown at common/session.go:97

func (s *ScanSession) LogVuln(result string) {
	if s.loggingEnabled() {
		LogVuln(result)
	}
}

// LogError writes through the session's logging policy.
func (s *ScanSession) LogError(errMsg string) {
	if s.loggingEnabled() {
		LogError(errMsg)
	}
}

// DialTCP 创建 TCP 连接,内含限速检查、代理、计数
func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
	// 检查发包限制
	if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
		s.LogError(i18n.Tr("tcp_connection_restricted", address, err.Error()))
		return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
	}

	// 获取 dialer
	dialer, err := s.getDialer(timeout)
	if err != nil {
		s.LogError(i18n.Tr("proxy_dialer_failed", err))
		s.State.IncrementTCPFailedPacketCount()
		return nil, err
	}

	conn, err := dialer.DialContext(ctx, network, address)
	if err != nil {
		s.State.IncrementTCPFailedPacketCount()
		s.LogDebug(i18n.Tr("connection_failed", address, err))
		return nil, err
	}

	// SO_LINGER=0: 连接关闭时立即发送 RST,避免 TIME_WAIT 堆积

View on GitHub (pinned to 95cc12e753)

Solutions

  1. Wait/back off until the rate window resets, or reduce scan concurrency so sends stay under the limit.
  2. Raise the packet/rate limits in the ScanSession Config if the budget is genuinely too small.
  3. Check the wrapped reason in err.Error() to distinguish a temporary rate window from a hard cap, and handle accordingly.

Example fix

// before
conn, err := session.DialTCP(ctx, "tcp", addr, timeout) // fails with network_rate_limited
// after
if ok, err := common.CanSendPacketWith(session.Config, session.State); ok {
    conn, err = session.DialTCP(ctx, "tcp", addr, timeout)
} else {
    time.Sleep(backoff) // wait for rate window to reset, then retry
}
Defensive patterns

Strategy: retry

Validate before calling

ok, limitErr := common.CanSendPacketWith(session.Config, session.State)
if !ok { /* defer or back off before dialing */ }

Try / catch

conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
    var backoff = initialBackoff
    for retries := 0; retries < maxRetries && err != nil; retries++ {
        select {
        case <-time.After(backoff):
        case <-ctx.Done():
            return ctx.Err()
        }
        conn, err = session.DialTCP(ctx, "tcp", addr, timeout)
        backoff *= 2
    }
}

Prevention

When it happens

Trigger: Any code path that dials TCP through the session (connectWithRetry, reconnectIfNeeded, service identification, port reachability checks) after the configured send-rate or packet count limit has been hit.

Common situations: Long-running scans hitting the configured packets-per-second/total caps; misconfigured ScanSession.Config limits set too low for the target scope; concurrent goroutines consuming the budget faster than expected.

Related errors


AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06). Data as JSON: /api/errors/1d8fec2642642e15. Report an issue: GitHub.