Files
x/internal/util/dns/conn.go
T
2026-07-04 13:01:21 +08:00

285 lines
5.6 KiB
Go

package dns
import (
"errors"
"net"
"sync"
"sync/atomic"
"github.com/go-gost/core/common/bufpool"
mdns "github.com/miekg/dns"
)
const (
defaultDomain = "gost.local."
defaultUDPSize = 4096
readBufferSize = 65535
edns0PayloadOption uint16 = mdns.EDNS0LOCALSTART
)
var (
ErrInvalidPacket = errors.New("dns: invalid packet")
ErrShortBuffer = errors.New("dns: short buffer")
)
type options struct {
domain string
udpSize uint16
}
// Option configures a DNS packet wrapper.
type Option func(*options)
// DomainOption sets the DNS question name used by wrapped packets.
func DomainOption(domain string) Option {
return func(o *options) {
if domain != "" {
o.domain = mdns.Fqdn(domain)
}
}
}
// UDPSizeOption sets the EDNS0 UDP payload size advertised in wrapped packets.
func UDPSizeOption(size int) Option {
return func(o *options) {
if size > 0 && size <= readBufferSize {
o.udpSize = uint16(size)
}
}
}
func parseOptions(opts ...Option) options {
o := options{
domain: defaultDomain,
udpSize: defaultUDPSize,
}
for _, opt := range opts {
opt(&o)
}
return o
}
type clientConn struct {
net.PacketConn
id uint32
options options
}
// ClientConn wraps outbound payloads as DNS queries and unwraps DNS responses.
func ClientConn(conn net.PacketConn, opts ...Option) net.PacketConn {
return &clientConn{
PacketConn: conn,
options: parseOptions(opts...),
}
}
func (c *clientConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
buf := bufpool.Get(readBufferSize)
defer bufpool.Put(buf)
for {
n, addr, err = c.PacketConn.ReadFrom(buf)
if err != nil {
return
}
m := mdns.Msg{}
if err = m.Unpack(buf[:n]); err != nil {
return 0, addr, err
}
if !m.Response {
continue
}
payload, ok := payloadFromMessage(&m)
if !ok {
continue
}
if len(b) < len(payload) {
return 0, addr, ErrShortBuffer
}
n = copy(b, payload)
return
}
}
func (c *clientConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
id := uint16(atomic.AddUint32(&c.id, 1))
wb, err := packQuery(id, c.options.domain, c.options.udpSize, b)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(wb, addr)
if err != nil {
return 0, err
}
return len(b), nil
}
type queryState struct {
id uint16
question []mdns.Question
}
type serverConn struct {
net.PacketConn
mu sync.RWMutex
queries map[string]queryState
options options
}
// ServerConn unwraps DNS queries as inbound payloads and wraps outbound
// payloads as DNS responses.
func ServerConn(conn net.PacketConn, opts ...Option) net.PacketConn {
return &serverConn{
PacketConn: conn,
queries: make(map[string]queryState),
options: parseOptions(opts...),
}
}
func (c *serverConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
buf := bufpool.Get(readBufferSize)
defer bufpool.Put(buf)
for {
n, addr, err = c.PacketConn.ReadFrom(buf)
if err != nil {
return
}
m := mdns.Msg{}
if err = m.Unpack(buf[:n]); err != nil {
return 0, addr, err
}
if m.Response || !validQuestion(&m, c.options.domain) {
continue
}
payload, ok := payloadFromMessage(&m)
if !ok {
continue
}
if len(b) < len(payload) {
return 0, addr, ErrShortBuffer
}
c.storeQuery(addr, &m)
n = copy(b, payload)
return
}
}
func (c *serverConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
state := c.loadQuery(addr)
wb, err := packResponse(state.id, state.question, c.options.domain, c.options.udpSize, b)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(wb, addr)
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *serverConn) storeQuery(addr net.Addr, m *mdns.Msg) {
if addr == nil {
return
}
state := queryState{
id: m.Id,
question: append([]mdns.Question(nil), m.Question...),
}
c.mu.Lock()
c.queries[addr.String()] = state
c.mu.Unlock()
}
func (c *serverConn) loadQuery(addr net.Addr) queryState {
if addr == nil {
return queryState{}
}
c.mu.RLock()
state := c.queries[addr.String()]
c.mu.RUnlock()
return state
}
func packQuery(id uint16, domain string, udpSize uint16, payload []byte) ([]byte, error) {
m := mdns.Msg{}
m.SetQuestion(domain, mdns.TypeNULL)
m.Id = id
m.RecursionDesired = false
addPayloadOpt(&m, udpSize, payload)
return m.Pack()
}
func packResponse(id uint16, question []mdns.Question, domain string, udpSize uint16, payload []byte) ([]byte, error) {
m := mdns.Msg{}
m.Id = id
m.Response = true
m.Authoritative = true
m.Rcode = mdns.RcodeSuccess
if len(question) > 0 {
m.Question = append([]mdns.Question(nil), question...)
} else {
m.SetQuestion(domain, mdns.TypeNULL)
m.Id = id
m.Response = true
m.Authoritative = true
}
addPayloadOpt(&m, udpSize, payload)
return m.Pack()
}
func addPayloadOpt(m *mdns.Msg, udpSize uint16, payload []byte) {
opt := &mdns.OPT{
Hdr: mdns.RR_Header{
Name: ".",
Rrtype: mdns.TypeOPT,
Class: udpSize,
},
Option: []mdns.EDNS0{
&mdns.EDNS0_LOCAL{
Code: edns0PayloadOption,
Data: append([]byte(nil), payload...),
},
},
}
m.Extra = append(m.Extra, opt)
}
func payloadFromMessage(m *mdns.Msg) ([]byte, bool) {
for _, rr := range m.Extra {
opt, ok := rr.(*mdns.OPT)
if !ok {
continue
}
for _, option := range opt.Option {
local, ok := option.(*mdns.EDNS0_LOCAL)
if !ok || local.Code != edns0PayloadOption {
continue
}
return append([]byte(nil), local.Data...), true
}
}
return nil, false
}
func validQuestion(m *mdns.Msg, domain string) bool {
if len(m.Question) == 0 {
return false
}
for _, q := range m.Question {
if q.Qtype == mdns.TypeNULL && mdns.CanonicalName(q.Name) == mdns.CanonicalName(domain) {
return true
}
}
return false
}