Ctrl K
ComunidadGuías…

agent/connreporter.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package agent

import (
	"context"
	"net"
	"sync"
	"time"

	"google.golang.org/protobuf/types/known/emptypb"
	"google.golang.org/protobuf/types/known/timestamppb"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/agent/proto"
)

const (
	// reportConnectionBufferLimit limits the number of connection reports we
	// buffer to avoid growing the buffer indefinitely. This should not happen
	// unless the agent has lost connection to coderd for a long time or if
	// the agent is being spammed with connections.
	//
	// If we assume ~150 byte per connection report, this would be around 300KB
	// of memory which seems acceptable. We could reduce this if necessary by
	// not using the proto struct directly.
	reportConnectionBufferLimit = 2048
)

type connectionReportSink interface {
	ReportConnection(ctx context.Context, in *proto.ReportConnectionRequest) (*emptypb.Empty, error)
}

// connectionReporter is a subcomponent of the agent that handles connection and
// disconnection reports from the various connection listeners.
type connectionReporter struct {
	ctx    context.Context
	logger slog.Logger
	report chan *proto.ReportConnectionRequest

	mu   sync.Mutex
	cond *sync.Cond
	// open counts connections that have not reported their disconnect.
	open int
	// pending counts reports that are queued or being sent.
	pending int
}

func newConnectionReporter(ctx context.Context, logger slog.Logger) *connectionReporter {
	r := &connectionReporter{
		ctx:    ctx,
		logger: logger,
		report: make(chan *proto.ReportConnectionRequest, reportConnectionBufferLimit),
	}
	r.cond = sync.NewCond(&r.mu)
	return r
}

// enqueue queues a report for reportLoop, dropping it if the buffer is full.
func (r *connectionReporter) enqueue(payload *proto.ReportConnectionRequest) {
	r.mu.Lock()
	defer r.mu.Unlock()
	select {
	case r.report <- payload:
		r.pending++
	default:
		r.logger.Warn(r.ctx, "connection report buffer limit reached, dropping report",
			slog.F("limit", reportConnectionBufferLimit),
			slog.F("payload", payload),
		)
	}
}

// done marks a queued report as sent or failed.
func (r *connectionReporter) done() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.pending--
	r.cond.Broadcast()
}

// closeConnection marks a connection as having reported its disconnect.
func (r *connectionReporter) closeConnection() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.open--
	r.cond.Broadcast()
}

// waitUntilReported blocks until every connection has reported its disconnect
// and every queued report has been sent or has failed, or until ctx ends.
// Shutdown uses it so the disconnects of the connections it closes reach
// coderd before the agent stops reporting.
func (r *connectionReporter) waitUntilReported(ctx context.Context) error {
	stop := context.AfterFunc(ctx, func() {
		r.mu.Lock()
		defer r.mu.Unlock()
		r.cond.Broadcast()
	})
	defer stop()
	r.mu.Lock()
	defer r.mu.Unlock()
	for (r.open > 0 || r.pending > 0) && ctx.Err() == nil {
		r.cond.Wait()
	}
	if r.open == 0 && r.pending == 0 {
		return nil
	}
	return ctx.Err()
}

// reportLoop reports collected connection events to the API for auditing.
func (r *connectionReporter) reportLoop(ctx context.Context, sink connectionReportSink) error {
	for {
		select {
		case payload := <-r.report:
			logger := r.logger.With(slog.F("payload", payload))
			logger.Debug(ctx, "reporting connection")
			_, err := sink.ReportConnection(ctx, payload)
			if err != nil {
				// Do not fail the loop if we fail to report a connection, just
				// log a warning and continue processing new reports.
				// Related to https://github.com/coder/coder/issues/20194
				logger.Warn(ctx, "failed to report connection to server", slog.Error(err))
			} else {
				logger.Debug(ctx, "successfully reported connection")
			}
			r.done()
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

// Connect reports a connection event, then returns a reporter that can report
// the disconnection event for that same connection.
func (r *connectionReporter) Connect(connectEvent proto.ConnectEvent) proto.DisconnectionReporter {
	// A blank IP can unfortunately happen if the connection is broken in a data
	// race before we get to introspect it. We still report it, and the recipient
	// can handle a blank IP.
	if connectEvent.IP != "" {
		// Remove the port from the IP because ports are not supported in coderd.
		if host, _, err := net.SplitHostPort(connectEvent.IP); err != nil {
			r.logger.Error(r.ctx, "split host and port for connection report failed",
				slog.F("ip", connectEvent.IP), slog.Error(err))
		} else {
			// Best effort.
			connectEvent.IP = host
		}
	}

	// If the IP is "localhost" (which it can be in some cases), set it to
	// 127.0.0.1 instead.
	// Related to https://github.com/coder/coder/issues/20194
	if connectEvent.IP == "localhost" {
		connectEvent.IP = "127.0.0.1"
	}

	r.mu.Lock()
	r.open++
	r.mu.Unlock()
	r.enqueue(connectionRequest(&connectEvent, nil))

	return &disconnectionReporter{
		reporter:     r,
		connectEvent: &connectEvent,
	}
}

type disconnectionReporter struct {
	reporter     *connectionReporter
	connectEvent *proto.ConnectEvent
	once         sync.Once
}

func (r *disconnectionReporter) Disconnect(disconnectEvent proto.DisconnectEvent) {
	r.once.Do(func() {
		// Queue the report before closing the connection, so a waiter never
		// sees both counts at zero while the report is still unsent.
		r.reporter.enqueue(connectionRequest(r.connectEvent, &disconnectEvent))
		r.reporter.closeConnection()
	})
}

// connectionRequest converts connect and disconnect events into a proto
// connection request.  Disconnect events must pass both the original connect
// event and the disconnect event.
func connectionRequest(connect *proto.ConnectEvent, disconnect *proto.DisconnectEvent) *proto.ReportConnectionRequest {
	payload := &proto.ReportConnectionRequest{
		Connection: &proto.Connection{
			Id:               connect.ID[:],
			Action:           proto.Connection_CONNECT,
			Timestamp:        timestamppb.New(time.Now()),
			Ip:               connect.IP,
			StatusCode:       0,
			Reason:           nil,
			ClientSessionId:  connect.ClientSessionID,
			AppName:          connect.AppName,
			ConnectionMethod: connect.Method,
		},
	}
	if disconnect != nil {
		payload.Connection.Action = proto.Connection_DISCONNECT
		payload.Connection.StatusCode = int32(disconnect.Code) //nolint:gosec
		payload.Connection.Reason = &disconnect.Reason
		payload.Connection.RxBytes = &disconnect.RxBytes
		payload.Connection.TxBytes = &disconnect.TxBytes
	}
	return payload
}