وسائل نقل DNS
المصدر: dns/transport_registry.go، dns/transport_adapter.go، dns/transport/base.go، dns/transport/connector.go، dns/transport/udp.go، dns/transport/tcp.go، dns/transport/tls.go، dns/transport/https.go
سجل وسائل النقل
يستخدم السجل أنواع Go العامة (generics) لتسجيل وسائل النقل بشكل آمن النوع:
func RegisterTransport[Options any](registry *TransportRegistry, transportType string,
constructor TransportConstructorFunc[Options]) {
registry.register(transportType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.DNSTransport, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}يتم محو النوع العام Options عند وقت التسجيل عبر أغلفة any، مما يسمح للسجل بتخزين مُنشئات غير متجانسة مع توفير إنشاء آمن النوع عند التسجيل.
تسجل كل وسيلة نقل نفسها:
func RegisterUDP(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteDNSServerOptions](...) }
func RegisterTCP(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteDNSServerOptions](...) }
func RegisterTLS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](...) }
func RegisterHTTPS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](...) }وسيلة النقل الأساسية
توفر آلة حالة وتتبع الاستعلامات قيد التنفيذ لإيقاف التشغيل بشكل سلس:
type TransportState int
const (
StateNew TransportState = iota
StateStarted
StateClosing
StateClosed
)
type BaseTransport struct {
dns.TransportAdapter
Logger logger.ContextLogger
mutex sync.Mutex
state TransportState
inFlight int32
queriesComplete chan struct{}
closeCtx context.Context
closeCancel context.CancelFunc
}دورة حياة الاستعلام
func (t *BaseTransport) BeginQuery() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != StateStarted { return false }
t.inFlight++
return true
}
func (t *BaseTransport) EndQuery() {
t.mutex.Lock()
if t.inFlight > 0 { t.inFlight-- }
if t.inFlight == 0 && t.queriesComplete != nil {
close(t.queriesComplete)
}
t.mutex.Unlock()
}إيقاف التشغيل بشكل سلس
func (t *BaseTransport) Shutdown(ctx context.Context) error {
t.state = StateClosing
if t.inFlight == 0 {
t.state = StateClosed
t.closeCancel()
return nil
}
t.queriesComplete = make(chan struct{})
t.closeCancel()
select {
case <-queriesComplete: // Wait for in-flight queries
case <-ctx.Done(): // Timeout
}
t.state = StateClosed
return nil
}الموصل العام
يوفر إدارة اتصال من نوع singleflight مع كشف الاتصال التكراري:
type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connectionCancel context.CancelFunc
connecting chan struct{} // Singleflight signal
closeCtx context.Context
}الحصول على اتصال من نوع Singleflight
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
for {
c.access.Lock()
// Fast path: existing connection
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
return c.connection, nil
}
// Recursive dial detection
if isRecursiveConnectorDial(ctx, c) {
return zero, errRecursiveConnectorDial
}
// Singleflight: wait for in-progress dial
if c.connecting != nil {
<-c.connecting
continue // Retry after dial completes
}
// Initiate new dial
c.connecting = make(chan struct{})
c.access.Unlock()
connection, cancel, err := c.dialWithCancellation(dialContext)
// Store and return
}
}يستخدم كشف الاتصال التكراري مفتاح سياق لتتبع الموصل الجاري الاتصال به:
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
return loaded && dialConnector == connector
}وسيلة نقل UDP
وسيلة النقل الأكثر تعقيداً، تنفذ تعدد إرسال قائم على الاستدعاءات الراجعة عبر اتصال UDP واحد:
type UDPTransport struct {
*BaseTransport
dialer N.Dialer
serverAddr M.Socksaddr
udpSize atomic.Int32
connector *Connector[*Connection]
callbackAccess sync.RWMutex
queryId uint16
callbacks map[uint16]*udpCallback
}إدارة معرف الاستعلام
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
start := t.queryId
for {
t.queryId++
if _, exists := t.callbacks[t.queryId]; !exists {
return t.queryId, nil
}
if t.queryId == start {
return 0, E.New("no available query ID")
}
}
}تدفق Exchange
- الحصول على اتصال UDP أو إنشاؤه عبر الموصل
- تعيين معرف استعلام فريد وتسجيل الاستدعاء الراجع
- إرسال رسالة DNS بالمعرف المعين
- انتظار إشارة الاستدعاء الراجع، أو إغلاق الاتصال، أو إغلاق وسيلة النقل، أو إلغاء السياق
- استعادة معرف الرسالة الأصلي في الاستجابة
حلقة الاستقبال
func (t *UDPTransport) recvLoop(conn *Connection) {
for {
buffer := buf.NewSize(int(t.udpSize.Load()))
_, err := buffer.ReadOnceFrom(conn)
// Parse DNS message
// Look up callback by message ID
callback.response = &message
close(callback.done) // Signal waiting Exchange
}
}الرجوع عند الاقتطاع
إذا كانت استجابة UDP تحمل علامة Truncated، تعيد وسيلة النقل المحاولة تلقائياً عبر TCP:
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
response, err := t.exchange(ctx, message)
if response.Truncated {
t.Logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.exchangeTCP(ctx, message)
}
return response, nil
}تتبع حجم UDP لـ EDNS0
تتبع وسيلة النقل الحجم الأقصى لـ UDP من سجلات EDNS0 OPT وتعيد تعيين الاتصال عند طلب حجم أكبر:
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
udpSize := int32(edns0Opt.UDPSize())
if t.udpSize.CompareAndSwap(current, udpSize) {
t.connector.Reset()
}
}وسيلة نقل TCP
نموذج اتصال لكل استعلام بسيط:
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
defer conn.Close()
WriteMessage(conn, 0, message)
return ReadMessage(conn)
}تنسيق DNS عبر TCP على مستوى السلك
func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error {
binary.Write(buffer, binary.BigEndian, uint16(requestLen))
// Pack DNS message into buffer
writer.Write(buffer.Bytes())
}
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {
var responseLen uint16
binary.Read(reader, binary.BigEndian, &responseLen)
// Read responseLen bytes and unpack
}بادئة طول من بايتين بترتيب big-endian متبوعة برسالة DNS الخام.
وسيلة نقل TLS (DoT)
تجميع الاتصالات عبر قائمة مرتبطة:
type TLSTransport struct {
*BaseTransport
dialer tls.Dialer
serverAddr M.Socksaddr
connections list.List[*tlsDNSConn]
}
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
// Try pooled connection first
t.access.Lock()
conn := t.connections.PopFront()
t.access.Unlock()
if conn != nil {
response, err := t.exchange(ctx, message, conn)
if err == nil { return response, nil }
// Discard failed pooled connection
}
// Create new TLS connection
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
}بعد تبادل ناجح، يُعاد الاتصال إلى المجمع:
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
// ... write request, read response ...
t.connections.PushBack(conn) // Return to pool
return response, nil
}المنفذ الافتراضي: 853.
وسيلة نقل HTTPS (DoH)
تستخدم HTTP/2 POST مع نوع المحتوى application/dns-message:
const MimeType = "application/dns-message"
func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message
exMessage.Id = 0 // DoH strips message ID
exMessage.Compress = true
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType)
response, err := currentTransport.RoundTrip(request)
// Parse response body as DNS message
}إعادة تعيين وسيلة النقل عند انتهاء المهلة
إذا انتهت مهلة الاستعلام، تتم إعادة تعيين وسيلة نقل HTTP لمسح الاتصالات القديمة:
if errors.Is(err, context.DeadlineExceeded) {
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
t.transportResetAt = time.Now()
}المسار الافتراضي: /dns-query. المنفذ الافتراضي: 443.