@@ -4,12 +4,18 @@ import (
44 "bytes"
55 "encoding/binary"
66 "errors"
7+ "fmt"
8+ "io"
79 "net"
810 "os"
911 "strconv"
1012 "strings"
1113 "sync"
14+ "time"
1215 "unicode"
16+
17+ iradix "github.com/hashicorp/go-immutable-radix"
18+ "github.com/jedisct1/dlog"
1319)
1420
1521type CryptoConstruction uint16
@@ -171,3 +177,151 @@ func ReadTextFile(filename string) (string, error) {
171177}
172178
173179func isDigit (b byte ) bool { return b >= '0' && b <= '9' }
180+
181+ // ExtractClientIPStr extracts client IP string from pluginsState based on protocol
182+ func ExtractClientIPStr (pluginsState * PluginsState ) (string , bool ) {
183+ switch pluginsState .clientProto {
184+ case "udp" :
185+ return (* pluginsState .clientAddr ).(* net.UDPAddr ).IP .String (), true
186+ case "tcp" , "local_doh" :
187+ return (* pluginsState .clientAddr ).(* net.TCPAddr ).IP .String (), true
188+ default :
189+ return "" , false
190+ }
191+ }
192+
193+ // FormatLogLine formats a log line based on the specified format (tsv or ltsv)
194+ func FormatLogLine (format , clientIP , qName , reason string , additionalFields ... string ) (string , error ) {
195+ if format == "tsv" {
196+ now := time .Now ()
197+ year , month , day := now .Date ()
198+ hour , minute , second := now .Clock ()
199+ tsStr := fmt .Sprintf ("[%d-%02d-%02d %02d:%02d:%02d]" , year , int (month ), day , hour , minute , second )
200+
201+ line := fmt .Sprintf ("%s\t %s\t %s\t %s" , tsStr , clientIP , StringQuote (qName ), StringQuote (reason ))
202+ for _ , field := range additionalFields {
203+ line += fmt .Sprintf ("\t %s" , StringQuote (field ))
204+ }
205+ return line + "\n " , nil
206+ } else if format == "ltsv" {
207+ line := fmt .Sprintf ("time:%d\t host:%s\t qname:%s\t message:%s" , time .Now ().Unix (), clientIP , StringQuote (qName ), StringQuote (reason ))
208+
209+ // For LTSV format, additional fields are added with specific labels
210+ for i , field := range additionalFields {
211+ if i == 0 {
212+ line += fmt .Sprintf ("\t ip:%s" , StringQuote (field ))
213+ } else {
214+ line += fmt .Sprintf ("\t field%d:%s" , i , StringQuote (field ))
215+ }
216+ }
217+ return line + "\n " , nil
218+ }
219+ return "" , fmt .Errorf ("unexpected log format: [%s]" , format )
220+ }
221+
222+ // WritePluginLog writes a log entry for plugin actions
223+ func WritePluginLog (logger io.Writer , format , clientIP , qName , reason string , additionalFields ... string ) error {
224+ if logger == nil {
225+ return errors .New ("Log file not initialized" )
226+ }
227+
228+ line , err := FormatLogLine (format , clientIP , qName , reason , additionalFields ... )
229+ if err != nil {
230+ return err
231+ }
232+
233+ _ , err = logger .Write ([]byte (line ))
234+ return err
235+ }
236+
237+ // ParseTimeBasedRule parses a rule line that may contain time-based restrictions (@timerange)
238+ func ParseTimeBasedRule (line string , lineNo int , allWeeklyRanges * map [string ]WeeklyRanges ) (rulePart string , weeklyRanges * WeeklyRanges , err error ) {
239+ parts := strings .Split (line , "@" )
240+ timeRangeName := ""
241+
242+ if len (parts ) == 2 {
243+ rulePart = strings .TrimSpace (parts [0 ])
244+ timeRangeName = strings .TrimSpace (parts [1 ])
245+ } else if len (parts ) > 2 {
246+ return "" , nil , fmt .Errorf ("syntax error at line %d -- Unexpected @ character" , 1 + lineNo )
247+ } else {
248+ rulePart = line
249+ }
250+
251+ if len (timeRangeName ) > 0 {
252+ if weeklyRangesX , ok := (* allWeeklyRanges )[timeRangeName ]; ok {
253+ weeklyRanges = & weeklyRangesX
254+ } else {
255+ return "" , nil , fmt .Errorf ("time range [%s] not found at line %d" , timeRangeName , 1 + lineNo )
256+ }
257+ }
258+
259+ return rulePart , weeklyRanges , nil
260+ }
261+
262+ // ParseIPRule parses and validates an IP rule line
263+ func ParseIPRule (line string , lineNo int ) (cleanLine string , trailingStar bool , err error ) {
264+ ip := net .ParseIP (line )
265+ trailingStar = strings .HasSuffix (line , "*" )
266+
267+ if len (line ) < 2 || (ip != nil && trailingStar ) {
268+ return "" , false , fmt .Errorf ("suspicious IP rule [%s] at line %d" , line , lineNo )
269+ }
270+
271+ cleanLine = line
272+ if trailingStar {
273+ cleanLine = cleanLine [:len (cleanLine )- 1 ]
274+ }
275+ if strings .HasSuffix (cleanLine , ":" ) || strings .HasSuffix (cleanLine , "." ) {
276+ cleanLine = cleanLine [:len (cleanLine )- 1 ]
277+ }
278+ if len (cleanLine ) == 0 {
279+ return "" , false , fmt .Errorf ("empty IP rule at line %d" , lineNo )
280+ }
281+ if strings .Contains (cleanLine , "*" ) {
282+ return "" , false , fmt .Errorf ("invalid rule: [%s] - wildcards can only be used as a suffix at line %d" , line , lineNo )
283+ }
284+
285+ return strings .ToLower (cleanLine ), trailingStar , nil
286+ }
287+
288+ // ProcessConfigLines processes configuration file lines, calling the processor function for each non-empty line
289+ func ProcessConfigLines (lines string , processor func (line string , lineNo int ) error ) error {
290+ for lineNo , line := range strings .Split (lines , "\n " ) {
291+ line = TrimAndStripInlineComments (line )
292+ if len (line ) == 0 {
293+ continue
294+ }
295+ if err := processor (line , lineNo ); err != nil {
296+ return err
297+ }
298+ }
299+ return nil
300+ }
301+
302+ // LoadIPRules loads IP rules from text lines into radix tree and map structures
303+ func LoadIPRules (lines string , prefixes * iradix.Tree , ips map [string ]interface {}) (* iradix.Tree , error ) {
304+ err := ProcessConfigLines (lines , func (line string , lineNo int ) error {
305+ cleanLine , trailingStar , lineErr := ParseIPRule (line , lineNo )
306+ if lineErr != nil {
307+ dlog .Error (lineErr )
308+ return nil // Continue processing (matching existing behavior)
309+ }
310+
311+ if trailingStar {
312+ prefixes , _ , _ = prefixes .Insert ([]byte (cleanLine ), 0 )
313+ } else {
314+ ips [cleanLine ] = true
315+ }
316+ return nil
317+ })
318+ return prefixes , err
319+ }
320+
321+ // InitializePluginLogger initializes a logger for a plugin if the log file is configured
322+ func InitializePluginLogger (logFile , format string , maxSize , maxAge , maxBackups int ) (io.Writer , string ) {
323+ if len (logFile ) > 0 {
324+ return Logger (maxSize , maxAge , maxBackups , logFile ), format
325+ }
326+ return nil , ""
327+ }
0 commit comments