mirror of
https://github.com/apernet/OpenGFW.git
synced 2025-04-19 11:29:14 +08:00
189 lines
4.6 KiB
Go
189 lines
4.6 KiB
Go
package udp
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"math/rand/v2"
|
|
"net"
|
|
"os"
|
|
|
|
"github.com/apernet/OpenGFW/modifier"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
)
|
|
|
|
var _ modifier.Modifier = (*DNSModifier)(nil)
|
|
|
|
var (
|
|
errInvalidIP = errors.New("invalid ip")
|
|
errInvalidIPList = errors.New("invalid ip list")
|
|
errInvalidIpListFile = errors.New("unable to open or parse ip list file")
|
|
errNotValidDNSResponse = errors.New("not a valid dns response")
|
|
errEmptyDNSQuestion = errors.New("empty dns question")
|
|
)
|
|
|
|
func fmtErrInvalidIP(ip string) error {
|
|
return fmt.Errorf("invalid ip: %s", ip)
|
|
}
|
|
|
|
func fmtErrInvalidIpListFile(filePath string) error {
|
|
return fmt.Errorf("unable to open or parse ip list file: %s", filePath)
|
|
}
|
|
|
|
type DNSModifier struct{}
|
|
|
|
func (m *DNSModifier) Name() string {
|
|
return "dns"
|
|
}
|
|
|
|
func (m *DNSModifier) parseIpEntry(entry interface{}, i *dnsModifierInstance) error {
|
|
entryStr, ok := entry.(string)
|
|
if !ok {
|
|
return &modifier.ErrInvalidArgs{Err: errInvalidIP}
|
|
}
|
|
|
|
ip := net.ParseIP(entryStr)
|
|
if ip == nil {
|
|
return &modifier.ErrInvalidArgs{Err: fmtErrInvalidIP(entryStr)}
|
|
}
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
i.A = append(i.A, ip4)
|
|
} else {
|
|
i.AAAA = append(i.AAAA, ip)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DNSModifier) parseIpList(list []interface{}, i *dnsModifierInstance) error {
|
|
for _, entry := range list {
|
|
if err := m.parseIpEntry(entry, i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *DNSModifier) parseIpListFile(filePath string, i *dnsModifierInstance) error {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return &modifier.ErrInvalidArgs{Err: fmtErrInvalidIpListFile(filePath)}
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if err := m.parseIpEntry(line, i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return &modifier.ErrInvalidArgs{Err: fmtErrInvalidIpListFile(filePath)}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DNSModifier) New(args map[string]interface{}) (modifier.Instance, error) {
|
|
i := &dnsModifierInstance{}
|
|
i.seed = rand.Uint32()
|
|
|
|
for key, value := range args {
|
|
switch key {
|
|
case "a", "aaaa":
|
|
if err := m.parseIpEntry(value, i); err != nil {
|
|
return nil, err
|
|
}
|
|
case "list":
|
|
if list, ok := value.([]interface{}); ok {
|
|
if err := m.parseIpList(list, i); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIPList}
|
|
}
|
|
case "file":
|
|
if filePath, ok := value.(string); ok {
|
|
if err := m.parseIpListFile(filePath, i); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIpListFile}
|
|
}
|
|
}
|
|
}
|
|
return i, nil
|
|
}
|
|
|
|
var _ modifier.UDPModifierInstance = (*dnsModifierInstance)(nil)
|
|
|
|
type dnsModifierInstance struct {
|
|
A []net.IP
|
|
AAAA []net.IP
|
|
seed uint32
|
|
}
|
|
|
|
func (i *dnsModifierInstance) Process(data []byte) ([]byte, error) {
|
|
dns := &layers.DNS{}
|
|
err := dns.DecodeFromBytes(data, gopacket.NilDecodeFeedback)
|
|
if err != nil {
|
|
return nil, &modifier.ErrInvalidPacket{Err: err}
|
|
}
|
|
if !dns.QR || dns.ResponseCode != layers.DNSResponseCodeNoErr {
|
|
return nil, &modifier.ErrInvalidPacket{Err: errNotValidDNSResponse}
|
|
}
|
|
if len(dns.Questions) == 0 {
|
|
return nil, &modifier.ErrInvalidPacket{Err: errEmptyDNSQuestion}
|
|
}
|
|
|
|
// Hash the query name so that DNS response is fixed for a given query.
|
|
// Use a random seed to avoid determinism.
|
|
hashStringToIndex := func(b []byte, sliceLength int, seed uint32) int {
|
|
h := fnv.New32a()
|
|
seedBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(seedBytes, seed)
|
|
h.Write(seedBytes)
|
|
h.Write(b)
|
|
hashValue := h.Sum32()
|
|
return int(hashValue % uint32(sliceLength))
|
|
}
|
|
|
|
// In practice, most if not all DNS clients only send one question
|
|
// per packet, so we don't care about the rest for now.
|
|
q := dns.Questions[0]
|
|
switch q.Type {
|
|
case layers.DNSTypeA:
|
|
if i.A != nil {
|
|
idx := hashStringToIndex(q.Name, len(i.A), i.seed)
|
|
dns.Answers = []layers.DNSResourceRecord{{
|
|
Name: q.Name,
|
|
Type: layers.DNSTypeA,
|
|
Class: layers.DNSClassIN,
|
|
IP: i.A[idx],
|
|
}}
|
|
}
|
|
case layers.DNSTypeAAAA:
|
|
if i.AAAA != nil {
|
|
idx := hashStringToIndex(q.Name, len(i.AAAA), i.seed)
|
|
dns.Answers = []layers.DNSResourceRecord{{
|
|
Name: q.Name,
|
|
Type: layers.DNSTypeAAAA,
|
|
Class: layers.DNSClassIN,
|
|
IP: i.AAAA[idx],
|
|
}}
|
|
}
|
|
}
|
|
buf := gopacket.NewSerializeBuffer() // Modifiers must be safe for concurrent use, so we can't reuse the buffer
|
|
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
|
|
FixLengths: true,
|
|
ComputeChecksums: true,
|
|
}, dns)
|
|
return buf.Bytes(), err
|
|
}
|