mirror of
https://github.com/shizunge/endlessh-go.git
synced 2026-06-23 04:10:08 +00:00
cf6fbb5f41
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.
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
// 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)
|
||
}
|