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