mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
One connection over the climiter quota killed the whole service. The conn limiter wrapper signals an over-limit connection by returning a plain error from Accept, which the accept loop does not recognise as either a listener.AcceptError or a temporary net.Error, so it fell through to the fatal branch and returned. program.go discards Serve's return value, so the process stayed up with no further logging while nothing accepted connections any more. Every later connection sat in the listen backlog, where the kernel completes the handshake but no service ever reads it, so clients saw a connection that opens and then never negotiates. Only a restart recovered the service. The counter itself was never the problem: llimiter.Allow rolls the count back when it refuses, and a test now pins that. A test also pins that the accept loop survives a rejection and still serves the connection that follows it. Export ErrConnLimit so the refusal can be told apart from a listener that has genuinely failed, and continue the accept loop on it. The plain-http-tunnel listener refused over-limit connections with an identical error and is fixed the same way. Closes #914
555 lines
14 KiB
Go
555 lines
14 KiB
Go
// Package service implements the core [service.Service] interface, binding a
|
|
// [listener.Listener] and a [handler.Handler] into a runnable proxy service
|
|
// that accepts inbound connections and forwards traffic through a proxy chain.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/admission"
|
|
"github.com/go-gost/core/handler"
|
|
"github.com/go-gost/core/listener"
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/core/metrics"
|
|
"github.com/go-gost/core/observer"
|
|
"github.com/go-gost/core/observer/stats"
|
|
"github.com/go-gost/core/recorder"
|
|
"github.com/go-gost/core/service"
|
|
xbypass "github.com/go-gost/x/bypass"
|
|
xctx "github.com/go-gost/x/ctx"
|
|
connwrapper "github.com/go-gost/x/limiter/conn/wrapper"
|
|
rate_limiter "github.com/go-gost/x/limiter/rate"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
xmetrics "github.com/go-gost/x/metrics"
|
|
xstats "github.com/go-gost/x/observer/stats"
|
|
"github.com/google/shlex"
|
|
"github.com/rs/xid"
|
|
)
|
|
|
|
type options struct {
|
|
admission admission.Admission
|
|
recorders []recorder.RecorderObject
|
|
preUp []string
|
|
postUp []string
|
|
preDown []string
|
|
postDown []string
|
|
stats stats.Stats
|
|
observer observer.Observer
|
|
observerPeriod time.Duration
|
|
logger logger.Logger
|
|
labels map[string]string
|
|
closers []io.Closer
|
|
}
|
|
|
|
// Option is a functional option for configuring a service.
|
|
type Option func(opts *options)
|
|
|
|
// AdmissionOption sets the admission controller that filters connections by
|
|
// source address.
|
|
func AdmissionOption(admission admission.Admission) Option {
|
|
return func(opts *options) {
|
|
opts.admission = admission
|
|
}
|
|
}
|
|
|
|
// RecordersOption sets the traffic recorders for the service.
|
|
func RecordersOption(recorders ...recorder.RecorderObject) Option {
|
|
return func(opts *options) {
|
|
opts.recorders = recorders
|
|
}
|
|
}
|
|
|
|
// PreUpOption sets shell commands to run before the service starts.
|
|
func PreUpOption(cmds []string) Option {
|
|
return func(opts *options) {
|
|
opts.preUp = cmds
|
|
}
|
|
}
|
|
|
|
// PreDownOption sets shell commands to run before the service stops.
|
|
func PreDownOption(cmds []string) Option {
|
|
return func(opts *options) {
|
|
opts.preDown = cmds
|
|
}
|
|
}
|
|
|
|
// PostUpOption sets shell commands to run after the service starts listening.
|
|
func PostUpOption(cmds []string) Option {
|
|
return func(opts *options) {
|
|
opts.postUp = cmds
|
|
}
|
|
}
|
|
|
|
// PostDownOption sets shell commands to run after the service stops.
|
|
func PostDownOption(cmds []string) Option {
|
|
return func(opts *options) {
|
|
opts.postDown = cmds
|
|
}
|
|
}
|
|
|
|
// StatsOption sets the traffic statistics collector for the service.
|
|
func StatsOption(stats stats.Stats) Option {
|
|
return func(opts *options) {
|
|
opts.stats = stats
|
|
}
|
|
}
|
|
|
|
// ObserverOption sets the observer that receives service state and stats events.
|
|
func ObserverOption(observer observer.Observer) Option {
|
|
return func(opts *options) {
|
|
opts.observer = observer
|
|
}
|
|
}
|
|
|
|
// ObserverPeriodOption sets the interval at which stats are reported to the
|
|
// observer. Defaults to 5 seconds with a minimum of 1 second.
|
|
func ObserverPeriodOption(period time.Duration) Option {
|
|
return func(opts *options) {
|
|
opts.observerPeriod = period
|
|
}
|
|
}
|
|
|
|
// LabelsOption sets the static labels attached to the service's records
|
|
// and logs.
|
|
func LabelsOption(labels map[string]string) Option {
|
|
return func(opts *options) {
|
|
opts.labels = labels
|
|
}
|
|
}
|
|
|
|
// ClosersOption registers io.Closers that are called on service shutdown.
|
|
func ClosersOption(closers ...io.Closer) Option {
|
|
return func(opts *options) {
|
|
opts.closers = append(opts.closers, closers...)
|
|
}
|
|
}
|
|
|
|
// LoggerOption sets the logger for the service. If not provided, a no-op
|
|
// logger is used.
|
|
func LoggerOption(logger logger.Logger) Option {
|
|
return func(opts *options) {
|
|
opts.logger = logger
|
|
}
|
|
}
|
|
|
|
type defaultService struct {
|
|
name string
|
|
listener listener.Listener
|
|
handler handler.Handler
|
|
status *Status
|
|
options options
|
|
}
|
|
|
|
// closedStop reports whether an accept error means the service was closed rather
|
|
// than broken: the listener's own sentinel, the socket's, or a pipe whose other
|
|
// end went away. It is consulted twice — once to end the loop quietly, and once
|
|
// so a closed listener is never logged as a failure.
|
|
func closedStop(e error) bool {
|
|
return errors.Is(e, net.ErrClosed) ||
|
|
errors.Is(e, listener.ErrClosed) ||
|
|
errors.Is(e, io.ErrClosedPipe)
|
|
}
|
|
|
|
// NewService creates a new service that binds the given listener and handler.
|
|
// The service is registered in the running state and pre-up commands are
|
|
// executed immediately. Call [Serve] to start accepting connections.
|
|
func NewService(name string, ln listener.Listener, h handler.Handler, opts ...Option) service.Service {
|
|
var options options
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
if options.logger == nil {
|
|
options.logger = xlogger.Nop()
|
|
}
|
|
s := &defaultService{
|
|
name: name,
|
|
listener: ln,
|
|
handler: h,
|
|
options: options,
|
|
status: &Status{
|
|
createTime: time.Now(),
|
|
events: make([]Event, 0, MaxEventSize),
|
|
stats: options.stats,
|
|
},
|
|
}
|
|
s.setState(StateRunning)
|
|
|
|
s.execCmds("pre-up", s.options.preUp)
|
|
|
|
return s
|
|
}
|
|
|
|
// Addr returns the network address the service is listening on.
|
|
func (s *defaultService) Addr() net.Addr {
|
|
return s.listener.Addr()
|
|
}
|
|
|
|
// Serve starts the accept loop. It blocks until the listener is closed or a
|
|
// fatal accept error occurs. Each accepted connection is handled in its own
|
|
// goroutine by the service's handler.
|
|
func (s *defaultService) Serve() error {
|
|
s.execCmds("post-up", s.options.postUp)
|
|
s.setState(StateReady)
|
|
s.status.addEvent(Event{
|
|
Time: time.Now(),
|
|
Message: fmt.Sprintf("service %s is listening on %s", s.name, s.listener.Addr()),
|
|
})
|
|
|
|
gctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
|
|
|
if s.status.Stats() != nil {
|
|
go s.observeStats(gctx)
|
|
}
|
|
|
|
if v := xmetrics.GetGauge(
|
|
xmetrics.MetricServicesGauge,
|
|
metrics.Labels{}); v != nil {
|
|
v.Inc()
|
|
defer v.Dec()
|
|
}
|
|
|
|
log := s.options.logger
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
var tempDelay time.Duration
|
|
for {
|
|
conn, e := s.listener.Accept()
|
|
if e != nil {
|
|
if _, ok := e.(*listener.AcceptError); ok {
|
|
tempDelay = 0
|
|
}
|
|
|
|
// A connection refused by the conn limiter is a per-connection
|
|
// outcome, not a listener failure: that connection is already
|
|
// closed and the listener is still usable. Returning it as fatal
|
|
// ended the accept loop, so one rejection stopped the service for
|
|
// good and every later connection was refused until a restart
|
|
// (gost#914). Keep accepting.
|
|
if errors.Is(e, connwrapper.ErrConnLimit) {
|
|
log.Warnf("accept: %v", e)
|
|
continue
|
|
}
|
|
|
|
// TODO: remove Temporary checking
|
|
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
|
if tempDelay == 0 {
|
|
tempDelay = 1 * time.Second
|
|
} else {
|
|
tempDelay *= 2
|
|
}
|
|
if max := 5 * time.Second; tempDelay > max {
|
|
tempDelay = max
|
|
}
|
|
|
|
s.setState(StateFailed)
|
|
s.status.setLastError(e)
|
|
|
|
// The retry itself is deliberate and stays: a remote listener
|
|
// wraps an accept failure in AcceptError, which always claims
|
|
// Temporary(), to say "rebuild the transport and try again" — a
|
|
// closed pipe under it is exactly the case that recovers that
|
|
// way, so ending the service here would break port hopping.
|
|
// What is wrong is the wording: on a stop the same error arrives,
|
|
// and a warning that promises a retry reads like a hiccup. Log it
|
|
// by what it is.
|
|
if closedStop(e) {
|
|
log.Debugf("accept: %v, retrying in %v", e, tempDelay)
|
|
} else {
|
|
log.Warnf("accept: %v, retrying in %v", e, tempDelay)
|
|
}
|
|
time.Sleep(tempDelay)
|
|
|
|
// Transition back to Ready so status observers see the
|
|
// recovered service immediately, instead of waiting for
|
|
// the next successful Accept (which may block arbitrarily).
|
|
if s.status.State() == StateFailed {
|
|
s.setState(StateReady)
|
|
s.status.setLastError(nil)
|
|
}
|
|
continue
|
|
}
|
|
s.setState(StateClosed)
|
|
|
|
if !closedStop(e) {
|
|
log.Errorf("accept: %v", e)
|
|
}
|
|
|
|
log.Debugf("service %s exited!", s.name)
|
|
|
|
return e
|
|
}
|
|
|
|
if tempDelay > 0 {
|
|
tempDelay = 0
|
|
}
|
|
|
|
ctx := gctx
|
|
if cv, ok := conn.(xctx.Context); ok {
|
|
if v := cv.Context(); v != nil {
|
|
ctx = v
|
|
}
|
|
}
|
|
|
|
sid := xid.New().String()
|
|
ctx = xctx.ContextWithSid(ctx, xctx.Sid(sid))
|
|
|
|
if len(s.options.labels) > 0 {
|
|
ctx = xctx.ContextWithLabels(ctx, s.options.labels)
|
|
}
|
|
|
|
log := s.options.logger.WithFields(map[string]any{
|
|
"sid": sid,
|
|
})
|
|
|
|
srcAddr := xctx.SrcAddrFromContext(ctx)
|
|
if srcAddr == nil {
|
|
srcAddr = conn.RemoteAddr()
|
|
ctx = xctx.ContextWithSrcAddr(ctx, srcAddr)
|
|
}
|
|
|
|
if dstAddr := xctx.DstAddrFromContext(ctx); dstAddr == nil {
|
|
dstAddr = conn.LocalAddr()
|
|
ctx = xctx.ContextWithDstAddr(ctx, dstAddr)
|
|
}
|
|
|
|
clientIP := srcAddr.String()
|
|
if h, _, _ := net.SplitHostPort(clientIP); h != "" {
|
|
clientIP = h
|
|
}
|
|
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: clientIP})
|
|
|
|
for _, rec := range s.options.recorders {
|
|
if rec.Record == recorder.RecorderServiceClientAddress {
|
|
if err := rec.Recorder.Record(ctx, []byte(clientIP)); err != nil {
|
|
log.Errorf("record %s: %v", rec.Record, err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if s.options.admission != nil &&
|
|
!s.options.admission.Admit(ctx, srcAddr.Network(), srcAddr.String(), admission.WithService(s.name)) {
|
|
conn.Close()
|
|
log.Debugf("admission: %s is denied", srcAddr)
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if v := xmetrics.GetCounter(xmetrics.MetricServiceRequestsCounter,
|
|
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
|
v.Inc()
|
|
}
|
|
|
|
if v := xmetrics.GetGauge(xmetrics.MetricServiceRequestsInFlightGauge,
|
|
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
|
v.Inc()
|
|
defer v.Dec()
|
|
}
|
|
|
|
start := time.Now()
|
|
if v := xmetrics.GetObserver(xmetrics.MetricServiceRequestsDurationObserver,
|
|
metrics.Labels{"service": s.name}); v != nil {
|
|
defer func() {
|
|
v.Observe(float64(time.Since(start).Seconds()))
|
|
}()
|
|
}
|
|
|
|
if err := s.handler.Handle(ctx, conn); err != nil {
|
|
if isServerError(err) {
|
|
log.Error(err)
|
|
if v := xmetrics.GetCounter(xmetrics.MetricServiceHandlerErrorsCounter,
|
|
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
|
v.Inc()
|
|
}
|
|
if sts := s.status.stats; sts != nil {
|
|
sts.Add(stats.KindTotalErrs, 1)
|
|
}
|
|
} else {
|
|
log.Debug(err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Status returns the runtime status of the service.
|
|
func (s *defaultService) Status() *Status {
|
|
return s.status
|
|
}
|
|
|
|
// Close shuts down the service by closing the handler, observer, and listener
|
|
// in order. All errors are collected and returned as a joined error.
|
|
func (s *defaultService) Close() error {
|
|
s.execCmds("pre-down", s.options.preDown)
|
|
defer s.execCmds("post-down", s.options.postDown)
|
|
|
|
var errs []error
|
|
if closer, ok := s.handler.(io.Closer); ok {
|
|
if err := closer.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if s.options.observer != nil {
|
|
if closer, ok := s.options.observer.(io.Closer); ok {
|
|
if err := closer.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
for _, c := range s.options.closers {
|
|
if err := c.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if err := s.listener.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func (s *defaultService) execCmds(phase string, cmds []string) {
|
|
for _, raw := range cmds {
|
|
cmd := strings.TrimSpace(raw)
|
|
if cmd == "" {
|
|
continue
|
|
}
|
|
s.options.logger.Info(cmd)
|
|
|
|
if err := s.execCmd(cmd); err != nil {
|
|
s.options.logger.Warnf("[%s] %s: %v", phase, cmd, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *defaultService) execCmd(cmd string) error {
|
|
ss, err := shlex.Split(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(ss) == 0 {
|
|
return errors.New("invalid command")
|
|
}
|
|
c := exec.Command(ss[0], ss[1:]...)
|
|
c.Stdout = os.Stdout
|
|
c.Stderr = os.Stderr
|
|
return c.Run()
|
|
}
|
|
|
|
func (s *defaultService) setState(state State) {
|
|
s.status.setState(state)
|
|
|
|
msg := fmt.Sprintf("service %s is %s", s.name, state)
|
|
s.status.addEvent(Event{
|
|
Time: time.Now(),
|
|
Message: msg,
|
|
})
|
|
|
|
if obs := s.options.observer; obs != nil {
|
|
obs.Observe(context.Background(), []observer.Event{ServiceEvent{
|
|
Kind: "service",
|
|
Service: s.name,
|
|
State: state,
|
|
Msg: msg,
|
|
}})
|
|
}
|
|
}
|
|
|
|
func (s *defaultService) observeStats(ctx context.Context) {
|
|
if s.options.observer == nil {
|
|
return
|
|
}
|
|
|
|
d := s.options.observerPeriod
|
|
if d == 0 {
|
|
d = 5 * time.Second
|
|
}
|
|
if d < time.Second {
|
|
d = 1 * time.Second
|
|
}
|
|
|
|
var events []observer.Event
|
|
|
|
ticker := time.NewTicker(d)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if len(events) > 0 {
|
|
if err := s.options.observer.Observe(ctx, events); err == nil {
|
|
events = nil
|
|
}
|
|
break
|
|
}
|
|
|
|
st := s.status.Stats()
|
|
if st == nil || !st.IsUpdated() {
|
|
break
|
|
}
|
|
|
|
evs := []observer.Event{
|
|
xstats.StatsEvent{
|
|
Kind: "service",
|
|
Service: s.name,
|
|
TotalConns: st.Get(stats.KindTotalConns),
|
|
CurrentConns: st.Get(stats.KindCurrentConns),
|
|
InputBytes: st.Get(stats.KindInputBytes),
|
|
OutputBytes: st.Get(stats.KindOutputBytes),
|
|
TotalErrs: st.Get(stats.KindTotalErrs),
|
|
},
|
|
}
|
|
if err := s.options.observer.Observe(ctx, evs); err != nil {
|
|
events = evs
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// isServerError reports whether err represents a server-side or infrastructure
|
|
// failure that should be counted as a handler error. Client-side disconnects
|
|
// (io.EOF, net.ErrClosed, context cancellation) and policy decisions (rate
|
|
// limiting, bypass routing) are not server errors.
|
|
func isServerError(err error) bool {
|
|
if errors.Is(err, io.EOF) ||
|
|
errors.Is(err, net.ErrClosed) ||
|
|
errors.Is(err, context.Canceled) ||
|
|
errors.Is(err, context.DeadlineExceeded) ||
|
|
errors.Is(err, xbypass.ErrBypass) ||
|
|
errors.Is(err, rate_limiter.ErrRateLimit) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ServiceEvent is an observer event representing a service state change.
|
|
type ServiceEvent struct {
|
|
Kind string
|
|
Service string
|
|
State State
|
|
Msg string
|
|
}
|
|
|
|
// Type returns the observer event type for service events.
|
|
func (ServiceEvent) Type() observer.EventType {
|
|
return observer.EventStatus
|
|
}
|