Files
darkwolf cf6fbb5f41 use min(interval, 30s) for TCP keepalive period
Adapt keepalive to the configured interval: for short intervals
(e.g. 10s) detection is faster, for long intervals (e.g. 10min)
it caps at 30s as a safety net. Add comments explaining what
problem keepalive solves and its detection time limitation.
2026-03-17 06:40:43 +01:00

124 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2021-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
package client
import (
"math/rand"
"net"
"strconv"
"sync/atomic"
"time"
"github.com/golang/glog"
)
var (
numCurrentClients int64
letterBytes = []byte(" abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890!@#$%^&*()-=_+[]{}|;:',./<>?")
)
func randStringBytes(n int64) []byte {
b := make([]byte, n+1)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
b[n] = '\n'
return b
}
type Client struct {
conn net.Conn
next time.Time
start time.Time
last time.Time
interval time.Duration
bytesSent int
}
func NewClient(conn net.Conn, interval time.Duration, maxClients int64) *Client {
for numCurrentClients >= maxClients {
time.Sleep(interval)
}
atomic.AddInt64(&numCurrentClients, 1)
// Enable TCP keepalive to detect dead peers at the kernel level.
// This is a safety net for long intervals where the write deadline
// alone would be too slow to detect dead connections. For example,
// with interval=10min, without keepalive a ghost connection would
// go undetected for 10+ minutes.
// Keepalive cannot detect peers that are alive at the TCP level
// (kernel still responds to probes) but stalled at the application
// level — the write deadline in Send() covers that case.
// Detection time: ~idle + 9 probes (e.g. 10s + 9×10s ≈ 100s on Linux).
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
keepalive := interval
if keepalive > 30*time.Second {
keepalive = 30 * time.Second
}
tcpConn.SetKeepAlivePeriod(keepalive)
}
addr := conn.RemoteAddr().(*net.TCPAddr)
glog.V(1).Infof("ACCEPT host=%v port=%v n=%v/%v\n", addr.IP, addr.Port, numCurrentClients, maxClients)
return &Client{
conn: conn,
next: time.Now().Add(interval),
start: time.Now(),
last: time.Now(),
interval: interval,
bytesSent: 0,
}
}
func (c *Client) RemoteIpAddr() string {
return c.conn.RemoteAddr().(*net.TCPAddr).IP.String()
}
func (c *Client) LocalPort() string {
return strconv.Itoa(c.conn.LocalAddr().(*net.TCPAddr).Port)
}
func (c *Client) Send(bannerMaxLength int64) (int, error) {
if time.Now().Before(c.next) {
time.Sleep(c.next.Sub(time.Now()))
}
c.next = time.Now().Add(c.interval)
length := rand.Int63n(bannerMaxLength)
// Set a write deadline to detect dead connections where the kernel
// buffers data but the remote peer is gone. Without this, Write()
// can succeed indefinitely on dead connections, causing goroutine leaks.
c.conn.SetWriteDeadline(time.Now().Add(c.interval))
bytesSent, err := c.conn.Write(randStringBytes(length))
if err != nil {
return 0, err
}
c.bytesSent += bytesSent
return bytesSent, nil
}
func (c *Client) MillisecondsSinceLast() int64 {
millisecondsSpent := time.Now().Sub(c.last).Milliseconds()
c.last = time.Now()
return millisecondsSpent
}
func (c *Client) Close() {
addr := c.conn.RemoteAddr().(*net.TCPAddr)
glog.V(1).Infof("CLOSE host=%v port=%v time=%v bytes=%v\n", addr.IP, addr.Port, time.Now().Sub(c.start).Seconds(), c.bytesSent)
c.conn.Close()
atomic.AddInt64(&numCurrentClients, -1)
}