Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion component/dns/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func New(dns *config.Dns, opt *NewOption) (s *Dns, err error) {
Network: opt.UpstreamResolverNetwork,
FinishInitCallback: func(i int) func(raw *url.URL, upstream *Upstream) (err error) {
return func(raw *url.URL, upstream *Upstream) (err error) {
if opt != nil && opt.UpstreamReadyCallback != nil {
if opt != nil && opt.UpstreamReadyCallback != nil && upstream.Scheme != UpstreamScheme_Fakeip {
if err = opt.UpstreamReadyCallback(upstream); err != nil {
return err
}
Expand Down
12 changes: 8 additions & 4 deletions component/dns/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import (
"github.com/daeuniverse/outbound/protocol/direct"
)

var (
ErrFormat = fmt.Errorf("format error")
)
var ErrFormat = fmt.Errorf("format error")

type UpstreamScheme string

Expand All @@ -35,6 +33,7 @@ const (
UpstreamScheme_HTTPS UpstreamScheme = "https"
upstreamScheme_H3_Alias UpstreamScheme = "http3"
UpstreamScheme_H3 UpstreamScheme = "h3"
UpstreamScheme_Fakeip UpstreamScheme = "fakeip"
)

func (s UpstreamScheme) ContainsTcp() bool {
Expand Down Expand Up @@ -76,6 +75,11 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por
if __port == "" {
__port = "853"
}
case UpstreamScheme_Fakeip:
__port = raw.Port()
if __port == "" {
__port = "53"
}
default:
return "", "", 0, "", fmt.Errorf("unexpected scheme: %v", raw.Scheme)
}
Expand Down Expand Up @@ -139,7 +143,7 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot
switch u.Scheme {
case UpstreamScheme_TCP, UpstreamScheme_HTTPS, UpstreamScheme_TLS:
l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP}
case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_H3:
case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_H3, UpstreamScheme_Fakeip:
l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP}
case UpstreamScheme_TCP_UDP:
// UDP first.
Expand Down
4 changes: 1 addition & 3 deletions control/control_plane.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func NewControlPlane(
return nil, fmt.Errorf("failed to setup dae netns: %w", err)
}
pinPath := filepath.Join(consts.BpfPinRoot, consts.AppName)
if err = os.MkdirAll(pinPath, 0755); err != nil && !os.IsExist(err) {
if err = os.MkdirAll(pinPath, 0o755); err != nil && !os.IsExist(err) {
if os.IsNotExist(err) {
log.Warnln("Perhaps you are in a container environment (such as lxc). If so, please use higher virtualization (kvm/qemu).")
}
Expand Down Expand Up @@ -901,8 +901,6 @@ func (c *ControlPlane) chooseBestDnsDialer(
req *udpRequest,
dnsUpstream *dns.Upstream,
) (*dialArgument, error) {
/// Choose the best l4proto+ipversion dialer, and change taregt DNS to the best ipversion DNS upstream for DNS request.
// Get available ipversions and l4protos for DNS upstream.
ipversions, l4protos := dnsUpstream.SupportedNetworks()
var (
bestLatency time.Duration
Expand Down
65 changes: 62 additions & 3 deletions control/dns.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2025, daeuniverse Organization <dae@v2raya.org>
*/
*/

package control

Expand Down Expand Up @@ -55,6 +55,10 @@ func newDnsForwarder(upstream *dns.Upstream, dialArgument dialArgument) (DnsForw
return &DoQ{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument}, nil
case dns.UpstreamScheme_H3:
return &DoH{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument, http3: true}, nil
case dns.UpstreamScheme_Fakeip:
return &DoFakeip{
pool: GetGlobalFakeipPool(),
}, nil
default:
return nil, fmt.Errorf("unexpected scheme: %v", upstream.Scheme)
}
Expand Down Expand Up @@ -202,8 +206,8 @@ func (d *DoQ) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, err
}
return msg, nil
}
func (d *DoQ) createConnection(ctx context.Context) (quic.EarlyConnection, error) {

func (d *DoQ) createConnection(ctx context.Context) (quic.EarlyConnection, error) {
udpAddr := net.UDPAddrFromAddrPort(d.dialArgument.bestTarget)
conn, err := d.dialArgument.bestDialer.DialContext(
ctx,
Expand All @@ -226,7 +230,6 @@ func (d *DoQ) createConnection(ctx context.Context) (quic.EarlyConnection, error
return nil, err
}
return qc, nil

}

func (d *DoQ) Close() error {
Expand Down Expand Up @@ -440,3 +443,59 @@ func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg,
}
return &msg, nil
}

type DoFakeip struct {
pool *fakeipPool
}

func (d *DoFakeip) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) {
var query dnsmessage.Msg
if err := query.Unpack(data); err != nil {
return nil, fmt.Errorf("failed to unpack DNS query: %w", err)
}

if len(query.Question) == 0 {
return nil, fmt.Errorf("no questions in DNS query")
}

question := query.Question[0]
domain := question.Name

resp := &dnsmessage.Msg{
MsgHdr: dnsmessage.MsgHdr{
Id: query.Id,
Response: true,
Opcode: query.Opcode,
Authoritative: false,
Truncated: false,
RecursionDesired: query.RecursionDesired,
RecursionAvailable: true,
Rcode: dnsmessage.RcodeSuccess,
},
Question: query.Question,
}

switch question.Qtype {
case dnsmessage.TypeA:
fakeIP := d.pool.allocate(domain)
resp.Answer = append(resp.Answer, &dnsmessage.A{
Hdr: dnsmessage.RR_Header{
Name: question.Name,
Rrtype: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
Ttl: 300,
},
A: fakeIP,
})
case dnsmessage.TypeAAAA:
resp.Rcode = dnsmessage.RcodeSuccess
default:
resp.Rcode = dnsmessage.RcodeNameError
}

return resp, nil
}

func (d *DoFakeip) Close() error {
return nil
}
193 changes: 193 additions & 0 deletions control/fakeip_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2025, daeuniverse Organization <dae@v2raya.org>
*/

package control

import (
"net"
"strings"
"sync"
"time"
)

const (
defaultOffset = 2
// FakeIPTTL is the default TTL for fakeip entries (1 hour)
FakeIPTTL = 1 * time.Hour
// FakeIPCleanupInterval is the execution interval for cleanup tasks (10 minutes)
FakeIPCleanupInterval = 10 * time.Minute
)

// fakeipEntry represents a fakeip entry
type fakeipEntry struct {
ip net.IP
domain string
expireTime time.Time
}

// fakeipPool is a fakeip pool implementation with automatic cleanup
// Uses the 198.18.0.0/16 address range as the fakeip pool
type fakeipPool struct {
mu sync.RWMutex
domainToEntry map[string]*fakeipEntry // mapping from domain to entry
ipToDomain map[string]string // reverse mapping from IP to domain
baseIP net.IP // base IP address (198.18.0.0)
currentOffset uint32 // current allocation offset
maxOffset uint32 // maximum offset (65536)
ttl time.Duration // TTL for entries
stopChan chan struct{} // channel to stop cleanup goroutine
cleanupTicker *time.Ticker // cleanup ticker
}

var (
globalFakeipPool *fakeipPool
globalFakeipPoolOnce sync.Once
)

// GetGlobalFakeipPool returns the global fakeip pool singleton
func GetGlobalFakeipPool() *fakeipPool {
globalFakeipPoolOnce.Do(func() {
globalFakeipPool = newFakeipPool()
globalFakeipPool.startCleanup()
})
return globalFakeipPool
}

// newFakeipPool creates a new fakeip pool
func newFakeipPool() *fakeipPool {
return &fakeipPool{
domainToEntry: make(map[string]*fakeipEntry),
ipToDomain: make(map[string]string),
baseIP: net.ParseIP("198.18.0.0").To4(),
maxOffset: 65536, // 198.18.0.0/16 can allocate 65536 addresses
ttl: FakeIPTTL,
stopChan: make(chan struct{}),
currentOffset: defaultOffset,
}
}

// startCleanup starts the automatic cleanup goroutine
func (p *fakeipPool) startCleanup() {
p.cleanupTicker = time.NewTicker(FakeIPCleanupInterval)
go p.cleanupLoop()
}

// cleanupLoop runs the cleanup loop
func (p *fakeipPool) cleanupLoop() {
for {
select {
case <-p.cleanupTicker.C:
p.cleanup()
case <-p.stopChan:
p.cleanupTicker.Stop()
return
}
}
}

// cleanup removes expired entries
func (p *fakeipPool) cleanup() {
p.mu.Lock()
defer p.mu.Unlock()

now := time.Now()
expiredDomains := make([]string, 0)

// find all expired domains
for domain, entry := range p.domainToEntry {
if now.After(entry.expireTime) {
expiredDomains = append(expiredDomains, domain)
}
}

// delete expired entries
for _, domain := range expiredDomains {
if entry, exists := p.domainToEntry[domain]; exists {
delete(p.ipToDomain, entry.ip.String())
delete(p.domainToEntry, domain)
}
}
}

// Stop stops the cleanup goroutine
func (p *fakeipPool) Stop() {
close(p.stopChan)
}

// allocate assigns a fake IP address to a domain
func (p *fakeipPool) allocate(domain string) net.IP {
p.mu.Lock()
defer p.mu.Unlock()

now := time.Now()

// if the domain already has an IP, update expiration time and return
if entry, exists := p.domainToEntry[domain]; exists {
entry.expireTime = now.Add(p.ttl)
return entry.ip
}

// allocate a new IP
offset := p.currentOffset
p.currentOffset++
if p.currentOffset >= p.maxOffset {
p.currentOffset = defaultOffset
}

// calculate IP address: 198.18.0.0 + offset
ip := make(net.IP, 4)
copy(ip, p.baseIP)
ip[2] = byte(offset >> 8)
ip[3] = byte(offset & 0xff)

// create new entry
entry := &fakeipEntry{
ip: ip,
domain: domain,
expireTime: now.Add(p.ttl),
}

// if this IP is already occupied, clean up the old mapping first
ipStr := ip.String()
if oldDomain, exists := p.ipToDomain[ipStr]; exists {
delete(p.domainToEntry, oldDomain)
}

// save mapping relationships
p.domainToEntry[domain] = entry
p.ipToDomain[ipStr] = domain

return ip
}

// lookup finds the corresponding domain for a fake IP
func (p *fakeipPool) lookup(ip net.IP) (string, bool) {
p.mu.RLock()
defer p.mu.RUnlock()

domain, exists := p.ipToDomain[ip.String()]
if !exists {
return "", false
}

// check if expired
if entry, ok := p.domainToEntry[domain]; ok {
if time.Now().After(entry.expireTime) {
return "", false
}
}
if strings.HasSuffix(domain, ".") {
return domain[:len(domain)-1], true
}

return domain, true
}

// GetStats returns pool statistics
func (p *fakeipPool) GetStats() (total int, allocated int) {
p.mu.RLock()
defer p.mu.RUnlock()
return int(p.maxOffset), len(p.domainToEntry)
}
Loading
Loading