1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Net ;
5+ using System . Text . Json ;
6+ using System . Threading . Tasks ;
7+ using DnsServerCore . ApplicationCommon ;
8+ using TechnitiumLibrary . Net ;
9+ using TechnitiumLibrary . Net . Dns ;
10+ using TechnitiumLibrary . Net . Dns . ResourceRecords ;
11+
12+ namespace SourceFilterApp ;
13+
14+ public sealed class App : IDnsApplication , IDnsPostProcessor
15+ {
16+ #region IDisposable
17+
18+ public void Dispose ( ) { }
19+
20+ #endregion
21+
22+ #region properties
23+
24+ public string Description => "Filters answer records by client network according to include/exclude rules and optional splitNetworks." ;
25+
26+ #endregion
27+
28+ #region private
29+
30+ private Rule GetRule ( string name )
31+ {
32+ Rule best = null ;
33+ var bestScore = - 1 ;
34+
35+ foreach ( var rule in this . rules )
36+ {
37+ var score = rule . Match ( name ) ;
38+
39+ if ( score <= bestScore )
40+ continue ;
41+ bestScore = score ;
42+ best = rule ;
43+ }
44+
45+ return best ;
46+ }
47+
48+ #endregion
49+
50+ #region variables
51+
52+ private bool enabled ;
53+ private List < Rule > rules ;
54+
55+ #endregion
56+
57+ #region public
58+
59+ public Task InitializeAsync ( IDnsServer dnsServer , string config )
60+ {
61+ this . rules = [ ] ;
62+
63+ if ( string . IsNullOrEmpty ( config ) )
64+ {
65+ this . enabled = false ;
66+ return Task . CompletedTask ;
67+ }
68+
69+ using ( var json = JsonDocument . Parse ( config ) )
70+ {
71+ var root = json . RootElement ;
72+ this . enabled = ! root . TryGetProperty ( "enabled" , out var jsonEnabled ) || jsonEnabled . GetBoolean ( ) ;
73+
74+ if ( root . TryGetProperty ( "rules" , out var jsonRules ) && jsonRules . ValueKind == JsonValueKind . Array )
75+ foreach ( var jsonRule in jsonRules . EnumerateArray ( ) )
76+ this . rules . Add ( new ( jsonRule ) ) ;
77+ else
78+ foreach ( var prop in root . EnumerateObject ( ) . Where ( prop => ! prop . NameEquals ( "enabled" ) ) )
79+ this . rules . Add ( new ( prop . Name , prop . Value ) ) ;
80+ }
81+
82+ return Task . CompletedTask ;
83+ }
84+
85+ public Task < DnsDatagram > PostProcessAsync ( DnsDatagram request , IPEndPoint remoteEP , DnsTransportProtocol protocol , DnsDatagram response )
86+ {
87+ if ( ! this . enabled )
88+ return Task . FromResult ( response ) ;
89+
90+ if ( response . Answer . Count == 0 )
91+ return Task . FromResult ( response ) ;
92+
93+ var clientIp = remoteEP . Address ;
94+ var answer = new List < DnsResourceRecord > ( response . Answer . Count ) ;
95+
96+ foreach ( var record in response . Answer )
97+ {
98+ var rule = this . GetRule ( record . Name ) ;
99+ if ( rule is null )
100+ {
101+ answer . Add ( record ) ;
102+ continue ;
103+ }
104+
105+ if ( ! rule . IsClientAllowed ( clientIp ) )
106+ continue ;
107+
108+ if ( rule . PassesSplit ( clientIp , record ) )
109+ answer . Add ( record ) ;
110+ }
111+
112+ if ( answer . Count == response . Answer . Count )
113+ return Task . FromResult ( response ) ;
114+
115+ if ( answer . Count == 0 )
116+ return Task . FromResult ( response . Clone ( [ ] ) ) ;
117+
118+ return Task . FromResult ( response . Clone ( answer ) ) ;
119+ }
120+
121+ #endregion
122+
123+ #region inner
124+
125+ private sealed class Rule
126+ {
127+ private readonly NetworkSet include ;
128+ private readonly NetworkSet exclude ;
129+ private readonly NetworkSet split ;
130+ private readonly string pattern ;
131+ private readonly int specificity ;
132+ private readonly bool wildcard ;
133+
134+ public Rule ( JsonElement json ) : this (
135+ ( json . TryGetProperty ( "pattern" , out var jsonPattern ) ? jsonPattern . GetString ( ) : null ) ?? "*" ,
136+ json ) { }
137+
138+ public Rule ( string pattern , JsonElement jsonRule )
139+ {
140+ this . pattern = pattern . ToLowerInvariant ( ) ;
141+ this . wildcard = this . pattern == "*" || this . pattern . StartsWith ( "*." ) ;
142+ this . specificity = this . wildcard
143+ ? this . pattern == "*" ? 0 : this . pattern . Length - 2
144+ : this . pattern . Length ;
145+
146+ this . include = new ( GetNetworks ( jsonRule , true , "includeNetworks" , "include" ) ) ;
147+ this . exclude = new ( GetNetworks ( jsonRule , false , "excludeNetworks" , "exclude" ) ) ;
148+ this . split = new ( GetNetworks ( jsonRule , false , "splitNetworks" ) ) ;
149+ }
150+
151+ private static List < NetworkAddress > GetNetworks ( JsonElement json , bool addDefault , params string [ ] names )
152+ {
153+ var list = new List < NetworkAddress > ( ) ;
154+
155+ foreach ( var n in names )
156+ {
157+ if ( ! json . TryGetProperty ( n , out var value ) || value . ValueKind != JsonValueKind . Array )
158+ continue ;
159+
160+ foreach ( var str in value . EnumerateArray ( ) . Select ( x => x . GetString ( ) ) )
161+ if ( NetworkAddress . TryParse ( str , out var addr ) )
162+ list . Add ( addr ) ;
163+ }
164+
165+ if ( addDefault && list . Count == 0 )
166+ {
167+ list . Add ( NetworkAddress . Parse ( "0.0.0.0/0" ) ) ;
168+ list . Add ( NetworkAddress . Parse ( "::/0" ) ) ;
169+ }
170+
171+ return list ;
172+ }
173+
174+ public int Match ( string name )
175+ {
176+ name = name . ToLowerInvariant ( ) ;
177+
178+ if ( this . pattern == "*" )
179+ return 0 ;
180+
181+ if ( this . wildcard )
182+ {
183+ if ( ! name . EndsWith ( this . pattern [ 1 ..] , StringComparison . OrdinalIgnoreCase ) )
184+ return - 1 ;
185+ if ( name . Length == this . specificity )
186+ return - 1 ;
187+
188+ return this . specificity ;
189+ }
190+
191+ return name . Equals ( this . pattern , StringComparison . OrdinalIgnoreCase )
192+ ? this . specificity
193+ : - 1 ;
194+ }
195+
196+ public bool IsClientAllowed ( IPAddress clientIp )
197+ {
198+ if ( ! this . include . Contains ( clientIp ) )
199+ return false ;
200+ if ( this . exclude . Contains ( clientIp ) )
201+ return false ;
202+ return true ;
203+ }
204+
205+ public bool PassesSplit ( IPAddress clientIp , DnsResourceRecord record )
206+ {
207+ if ( this . split . IsEmpty )
208+ return true ;
209+
210+ IPAddress recordIp = record . Type switch
211+ {
212+ DnsResourceRecordType . A => ( record . RDATA as DnsARecordData ) . Address ,
213+ DnsResourceRecordType . AAAA => ( record . RDATA as DnsAAAARecordData ) . Address ,
214+ _ => null
215+ } ;
216+
217+ if ( recordIp is null )
218+ return true ;
219+
220+ var clientInside = this . split . Contains ( clientIp ) ;
221+ var recordInside = this . split . Contains ( recordIp ) ;
222+
223+ return clientInside == recordInside ;
224+ }
225+ }
226+
227+ private sealed class NetworkSet
228+ {
229+ private readonly NetworkAddress [ ] nets ;
230+
231+ public bool IsEmpty => this . nets . Length == 0 ;
232+
233+ public NetworkSet ( IReadOnlyList < NetworkAddress > nets ) => this . nets = nets . Count == 0 ? [ ] : nets . ToArray ( ) ;
234+
235+ public bool Contains ( IPAddress ip )
236+ {
237+ foreach ( var net in this . nets )
238+ if ( net . Contains ( ip ) )
239+ return true ;
240+ return false ;
241+ }
242+ }
243+
244+ #endregion
245+ }
0 commit comments