Files

442 lines
10 KiB
Go

package transport
import (
"context"
"errors"
"net"
"sync"
"sync/atomic"
"time"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
const (
reuseStateUnknown int32 = iota
reuseStateProbing
reuseStateSupported
reuseStateUnsupported
)
const (
reuseProbeTimeout = 5 * time.Second
reuseProbeRetryInterval = time.Minute
reuseDemoteFailureLimit = 3
reuseProbeQueryIdA uint16 = 1
reuseProbeQueryIdB uint16 = 2
)
type queryMultiplexerOptions struct {
dial func(ctx context.Context) (net.Conn, error)
write func(conn net.Conn, message *mDNS.Msg, queryId uint16) error
readNext func(conn net.Conn) (*mDNS.Msg, error)
retryReadError bool
probeReuse bool
}
type queryMultiplexer struct {
options queryMultiplexerOptions
connection *ConnPool[*multiplexConn]
queryAccess sync.Mutex
queryId uint16
queries map[uint16]*pendingQuery
reuseState atomic.Int32
demoteFailures atomic.Int32
probeAccess sync.Mutex
probeEpoch uint32
lastProbeTime time.Time
}
type multiplexConn struct {
net.Conn
readEpoch atomic.Uint64
}
type queryMultiplexerReadError struct {
cause error
}
func (e *queryMultiplexerReadError) Error() string {
return e.cause.Error()
}
func (e *queryMultiplexerReadError) Unwrap() error {
return e.cause
}
type pendingQuery struct {
conn *multiplexConn
message *mDNS.Msg
readEpoch uint64
callback func(response *mDNS.Msg, err error)
stopContext func() bool
stopConn func() bool
retryCtx context.Context
}
func newQueryMultiplexer(options queryMultiplexerOptions) *queryMultiplexer {
return &queryMultiplexer{
options: options,
queries: make(map[uint16]*pendingQuery),
connection: NewConnPool(ConnPoolOptions[*multiplexConn]{
Mode: ConnPoolSingle,
IsAlive: func(conn *multiplexConn) bool {
return conn != nil
},
Close: func(conn *multiplexConn, cause error) {
conn.Close()
},
}),
}
}
func (m *queryMultiplexer) Close() error {
return m.connection.Close()
}
func (m *queryMultiplexer) Reset() {
if m.options.probeReuse {
m.probeAccess.Lock()
m.probeEpoch++
m.reuseState.Store(reuseStateUnknown)
m.lastProbeTime = time.Time{}
m.probeAccess.Unlock()
m.demoteFailures.Store(0)
}
m.connection.Reset()
}
func (m *queryMultiplexer) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
done := make(chan struct{})
var (
response *mDNS.Msg
err error
)
m.ExchangeAsync(ctx, message, func(callbackResponse *mDNS.Msg, callbackErr error) {
response = callbackResponse
err = callbackErr
close(done)
})
<-done
return response, err
}
func (m *queryMultiplexer) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
m.dispatch(ctx, message, callback, true)
}
func (m *queryMultiplexer) dispatch(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error), retryReadError bool) {
if m.options.probeReuse && m.reuseState.Load() != reuseStateSupported {
m.maybeStartProbe(ctx, message)
go m.exchangeSingle(ctx, message, callback)
return
}
m.exchangeAsync(ctx, message, callback, retryReadError)
}
func (m *queryMultiplexer) exchangeSingle(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
conn, err := m.options.dial(ctx)
if err != nil {
callback(nil, err)
return
}
defer conn.Close()
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop()
err = m.options.write(conn, message, message.Id)
if err != nil {
ctxErr := ctx.Err()
if ctxErr != nil {
callback(nil, ctxErr)
return
}
callback(nil, E.Cause(err, "write request"))
return
}
for {
var response *mDNS.Msg
response, err = m.options.readNext(conn)
if err != nil {
ctxErr := ctx.Err()
if ctxErr != nil {
callback(nil, ctxErr)
return
}
callback(nil, E.Cause(err, "read response"))
return
}
if response == nil {
continue
}
response.Id = message.Id
callback(response, nil)
return
}
}
func (m *queryMultiplexer) maybeStartProbe(ctx context.Context, message *mDNS.Msg) {
if len(message.Question) == 0 {
return
}
m.probeAccess.Lock()
if m.reuseState.Load() == reuseStateProbing {
m.probeAccess.Unlock()
return
}
if !m.lastProbeTime.IsZero() && time.Since(m.lastProbeTime) < reuseProbeRetryInterval {
m.probeAccess.Unlock()
return
}
m.reuseState.Store(reuseStateProbing)
m.lastProbeTime = time.Now()
epoch := m.probeEpoch
m.probeAccess.Unlock()
go m.runReuseProbe(context.WithoutCancel(ctx), message.Question[0].Name, epoch)
}
func (m *queryMultiplexer) runReuseProbe(ctx context.Context, questionName string, epoch uint32) {
supported, dialFailed := m.executeReuseProbe(ctx, questionName)
m.probeAccess.Lock()
defer m.probeAccess.Unlock()
if m.probeEpoch != epoch {
return
}
switch {
case supported:
m.reuseState.Store(reuseStateSupported)
m.demoteFailures.Store(0)
case dialFailed:
m.reuseState.Store(reuseStateUnknown)
default:
m.reuseState.Store(reuseStateUnsupported)
}
}
func (m *queryMultiplexer) executeReuseProbe(ctx context.Context, questionName string) (supported bool, dialFailed bool) {
probeCtx, cancel := context.WithTimeout(ctx, reuseProbeTimeout)
defer cancel()
conn, err := m.options.dial(probeCtx)
if err != nil {
return false, true
}
defer conn.Close()
stop := context.AfterFunc(probeCtx, func() {
conn.Close()
})
defer stop()
queryA := new(mDNS.Msg)
queryA.SetQuestion(questionName, mDNS.TypeA)
queryAAAA := new(mDNS.Msg)
queryAAAA.SetQuestion(questionName, mDNS.TypeAAAA)
err = m.options.write(conn, queryA, reuseProbeQueryIdA)
if err == nil {
err = m.options.write(conn, queryAAAA, reuseProbeQueryIdB)
}
if err != nil {
return false, false
}
var seenA, seenAAAA bool
for !seenA || !seenAAAA {
var response *mDNS.Msg
response, err = m.options.readNext(conn)
if err != nil {
return false, false
}
if response == nil {
continue
}
switch response.Id {
case reuseProbeQueryIdA:
seenA = true
case reuseProbeQueryIdB:
seenAAAA = true
}
}
return true, false
}
func (m *queryMultiplexer) recordConnDeath(conn *multiplexConn) {
if !m.options.probeReuse || m.reuseState.Load() != reuseStateSupported {
return
}
if conn.readEpoch.Load() == 0 {
return
}
m.queryAccess.Lock()
var pendingOnConn int
for _, pending := range m.queries {
if pending.conn == conn {
pendingOnConn++
}
}
m.queryAccess.Unlock()
if pendingOnConn == 0 {
m.demoteFailures.Store(0)
return
}
if m.demoteFailures.Add(1) < reuseDemoteFailureLimit {
return
}
m.probeAccess.Lock()
if m.reuseState.Load() == reuseStateSupported {
m.reuseState.Store(reuseStateUnsupported)
m.lastProbeTime = time.Now()
}
m.probeAccess.Unlock()
m.demoteFailures.Store(0)
}
func (m *queryMultiplexer) exchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error), retryReadError bool) {
for firstAttempt := true; ; firstAttempt = false {
conn, connCtx, created, err := m.connection.AcquireShared(ctx, m.dialConn)
if err != nil {
callback(nil, err)
return
}
if created {
go m.recvLoop(conn)
}
queryId, err := m.register(ctx, connCtx, conn, message, callback, retryReadError && m.options.retryReadError && !created)
if err != nil {
m.connection.Release(conn, true)
callback(nil, err)
return
}
writeErr := m.options.write(conn, message, queryId)
if writeErr == nil {
return
}
pending := m.take(queryId)
m.connection.Invalidate(conn, writeErr)
if pending == nil {
return
}
if !created && firstAttempt {
continue
}
callback(nil, E.Cause(writeErr, "write request"))
return
}
}
func (m *queryMultiplexer) dialConn(ctx context.Context) (*multiplexConn, error) {
conn, err := m.options.dial(ctx)
if err != nil {
return nil, err
}
return &multiplexConn{Conn: conn}, nil
}
func (m *queryMultiplexer) register(ctx context.Context, connCtx context.Context, conn *multiplexConn, message *mDNS.Msg, callback func(response *mDNS.Msg, err error), retryReadError bool) (uint16, error) {
m.queryAccess.Lock()
defer m.queryAccess.Unlock()
start := m.queryId
for {
m.queryId++
if _, exists := m.queries[m.queryId]; !exists {
break
}
if m.queryId == start {
return 0, E.New("no available query ID")
}
}
queryId := m.queryId
pending := &pendingQuery{
conn: conn,
message: message,
readEpoch: conn.readEpoch.Load(),
callback: callback,
}
if retryReadError {
pending.retryCtx = ctx
}
m.queries[queryId] = pending
pending.stopContext = context.AfterFunc(ctx, func() {
m.completeContextDone(queryId, ctx)
})
pending.stopConn = context.AfterFunc(connCtx, func() {
m.completeConnDone(queryId, connCtx)
})
return queryId, nil
}
func (m *queryMultiplexer) completeConnDone(queryId uint16, connCtx context.Context) {
pending := m.take(queryId)
if pending == nil {
return
}
connErr := context.Cause(connCtx)
_, readFailed := connErr.(*queryMultiplexerReadError)
if pending.retryCtx != nil && readFailed {
m.dispatch(pending.retryCtx, pending.message, pending.callback, false)
return
}
pending.callback(nil, connErr)
}
func (m *queryMultiplexer) take(queryId uint16) *pendingQuery {
m.queryAccess.Lock()
pending, loaded := m.queries[queryId]
if !loaded {
m.queryAccess.Unlock()
return nil
}
delete(m.queries, queryId)
m.queryAccess.Unlock()
pending.stopContext()
pending.stopConn()
return pending
}
func (m *queryMultiplexer) complete(queryId uint16, response *mDNS.Msg, err error, releaseConn bool) {
pending := m.take(queryId)
if pending == nil {
return
}
if releaseConn {
m.connection.Release(pending.conn, true)
}
if response != nil {
response.Id = pending.message.Id
}
pending.callback(response, err)
}
func (m *queryMultiplexer) completeContextDone(queryId uint16, ctx context.Context) {
pending := m.take(queryId)
if pending == nil {
return
}
err := ctx.Err()
if errors.Is(err, context.DeadlineExceeded) && pending.conn.readEpoch.Load() == pending.readEpoch {
m.connection.Invalidate(pending.conn, err)
} else {
m.connection.Release(pending.conn, true)
}
pending.callback(nil, err)
}
func (m *queryMultiplexer) recvLoop(conn *multiplexConn) {
for {
message, err := m.options.readNext(conn)
if err != nil {
m.recordConnDeath(conn)
m.connection.Invalidate(conn, &queryMultiplexerReadError{cause: err})
return
}
conn.readEpoch.Add(1)
if message == nil {
continue
}
m.complete(message.Id, message, nil, true)
}
}