From 43be2d8943be53be25ea0f1ba108aea7d91a048a Mon Sep 17 00:00:00 2001 From: Denis MACHARD <5562930+dmachard@users.noreply.github.com> Date: Sun, 13 Nov 2022 07:52:34 +0100 Subject: [PATCH] Major update to support transforms on loggers (#163) --- README.md | 2 +- collectors/dns_processor.go | 2 +- collectors/dnstap_processor.go | 2 +- collectors/powerdns_processor.go | 2 +- collectors/tail.go | 2 +- config.yml | 6 +- dnscollector.go | 12 ++- dnsutils/config.go | 167 ++++++++++++++++------------- doc/configuration.md | 2 +- doc/{overview2.png => metrics.png} | Bin doc/overview.drawio | 2 +- doc/overview.png | Bin 36873 -> 39023 bytes loggers/dnstap.go | 14 +++ loggers/elasticsearch.go | 13 +++ loggers/fluentd.go | 14 +++ loggers/influxdb.go | 14 +++ loggers/logfile.go | 13 +++ loggers/lokiclient.go | 38 +++---- loggers/pcapfile.go | 12 +++ loggers/prometheus.go | 13 +++ loggers/statsd.go | 16 ++- loggers/stdout.go | 14 +++ loggers/syslog.go | 13 +++ loggers/tcpclient.go | 14 +++ loggers/webserver.go | 13 +++ transformers/filtering.go | 40 +++---- transformers/filtering_test.go | 48 ++++----- transformers/geoip.go | 16 +-- transformers/geoip_test.go | 8 +- transformers/normalize.go | 6 +- transformers/normalize_test.go | 6 +- transformers/subprocessors.go | 32 +++--- transformers/suspicious.go | 16 +-- transformers/suspicious_test.go | 30 +++--- transformers/userprivacy.go | 4 +- transformers/userprivacy_test.go | 18 ++-- 36 files changed, 401 insertions(+), 223 deletions(-) rename doc/{overview2.png => metrics.png} (100%) diff --git a/README.md b/README.md index 73fab6f7..1b182b7c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ *NOTE: The code before version 1.x is considered beta quality and is subject to breaking changes.* `DNS-collector` acts as a passive high speed **aggregator, analyzer, transporter and logging** for your DNS messages, written in **Golang**. The DNS traffic can be collected and aggregated from simultaneously sources like DNStap streams, network interface or log files -and redirect them to several destinations with some transformation (filtering, sampling, privacy, ...). +and redirect them to several destinations with some transformations on it (filtering, sampling, privacy, ...). DNS-collector also contains DNS parser with [`EDNS`](doc/dnsparser.md) support. ![overview](doc/overview.png) diff --git a/collectors/dns_processor.go b/collectors/dns_processor.go index 4efe1703..cc1f0f04 100644 --- a/collectors/dns_processor.go +++ b/collectors/dns_processor.go @@ -83,7 +83,7 @@ func (d *DnsProcessor) Run(sendTo []chan dnsutils.DnsMessage) { d.LogInfo("dns cached enabled: %t", d.cacheSupport) // prepare enabled transformers - subprocessors := transformers.NewTransforms(d.config, d.logger, d.name) + subprocessors := transformers.NewTransforms(&d.config.IngoingTransformers, d.logger, d.name) // read incoming dns message d.LogInfo("running... waiting incoming dns message") diff --git a/collectors/dnstap_processor.go b/collectors/dnstap_processor.go index 84e6eb10..60f9e4ad 100644 --- a/collectors/dnstap_processor.go +++ b/collectors/dnstap_processor.go @@ -129,7 +129,7 @@ func (d *DnstapProcessor) Run(sendTo []chan dnsutils.DnsMessage) { d.LogInfo("dns cached enabled: %t", d.config.Collectors.Dnstap.CacheSupport) // prepare enabled transformers - subprocessors := transformers.NewTransforms(d.config, d.logger, d.name) + subprocessors := transformers.NewTransforms(&d.config.IngoingTransformers, d.logger, d.name) // read incoming dns message d.LogInfo("running... waiting incoming dns message") diff --git a/collectors/powerdns_processor.go b/collectors/powerdns_processor.go index 867937ad..91d43a87 100644 --- a/collectors/powerdns_processor.go +++ b/collectors/powerdns_processor.go @@ -74,7 +74,7 @@ func (d *PdnsProcessor) Run(sendTo []chan dnsutils.DnsMessage) { pbdm := &powerdns_protobuf.PBDNSMessage{} // prepare enabled transformers - subprocessors := transformers.NewTransforms(d.config, d.logger, d.name) + subprocessors := transformers.NewTransforms(&d.config.IngoingTransformers, d.logger, d.name) // read incoming dns message d.LogInfo("running... waiting incoming dns message") diff --git a/collectors/tail.go b/collectors/tail.go index 2d81757e..0879c9e2 100644 --- a/collectors/tail.go +++ b/collectors/tail.go @@ -98,7 +98,7 @@ func (c *Tail) Run() { } // prepare enabled transformers - subprocessors := transformers.NewTransforms(c.config, c.logger, c.name) + subprocessors := transformers.NewTransforms(&c.config.IngoingTransformers, c.logger, c.name) // init dns message dm := dnsutils.DnsMessage{} diff --git a/config.yml b/config.yml index f7885fb2..1f4a3621 100644 --- a/config.yml +++ b/config.yml @@ -77,6 +77,9 @@ multiplexer: - name: console stdout: mode: text + transforms: + normalize: + qname-lowercase: true routes: - from: [ tap ] @@ -408,8 +411,7 @@ multiplexer: ################################################ -# list of transformers -# transforms: +# list of transforms to apply on collectors or loggers ################################################ # # Use this option to protect user privacy diff --git a/dnscollector.go b/dnscollector.go index 254180ce..dbb64200 100644 --- a/dnscollector.go +++ b/dnscollector.go @@ -68,6 +68,7 @@ func main() { // load config cfg := make(map[string]interface{}) cfg["loggers"] = output.Params + cfg["outgoing-transformers"] = make(map[string]interface{}) for _, p := range output.Params { p.(map[string]interface{})["enable"] = true } @@ -76,6 +77,12 @@ func main() { subcfg := &dnsutils.Config{} subcfg.SetDefault() + // add transformer + for k, v := range output.Transforms { + v.(map[string]interface{})["enable"] = true + cfg["outgoing-transformers"].(map[string]interface{})[k] = v + } + // copy global config subcfg.Global = config.Global @@ -132,7 +139,7 @@ func main() { // load config cfg := make(map[string]interface{}) cfg["collectors"] = input.Params - cfg["transformers"] = make(map[string]interface{}) + cfg["ingoing-transformers"] = make(map[string]interface{}) for _, p := range input.Params { p.(map[string]interface{})["enable"] = true } @@ -144,7 +151,7 @@ func main() { // add transformer for k, v := range input.Transforms { v.(map[string]interface{})["enable"] = true - cfg["transformers"].(map[string]interface{})[k] = v + cfg["ingoing-transformers"].(map[string]interface{})[k] = v } // copy global config @@ -172,6 +179,7 @@ func main() { } } + // here the multiplexer logic // connect collectors between loggers for _, routes := range config.Multiplexer.Routes { var logwrks []dnsutils.Worker diff --git a/dnsutils/config.go b/dnsutils/config.go index ddb3d185..7a14f8ad 100644 --- a/dnsutils/config.go +++ b/dnsutils/config.go @@ -28,12 +28,6 @@ func IsValidTLS(mode string) bool { return false } -type MultiplexTransformers struct { - Name string `yaml:"naame"` - Transforms map[string]interface{} `yaml:",inline"` - Params map[string]interface{} `yaml:",inline"` -} - type MultiplexInOut struct { Name string `yaml:"name"` Transforms map[string]interface{} `yaml:"transforms"` @@ -45,6 +39,81 @@ type MultiplexRoutes struct { Dst []string `yaml:"to,flow"` } +type ConfigTransformers struct { + UserPrivacy struct { + Enable bool `yaml:"enable"` + AnonymizeIP bool `yaml:"anonymize-ip"` + MinimazeQname bool `yaml:"minimaze-qname"` + } `yaml:"user-privacy"` + Normalize struct { + Enable bool `yaml:"enable"` + QnameLowerCase bool `yaml:"qname-lowercase"` + } `yaml:"normalize"` + Filtering struct { + Enable bool `yaml:"enable"` + DropFqdnFile string `yaml:"drop-fqdn-file"` + DropDomainFile string `yaml:"drop-domain-file"` + KeepFqdnFile string `yaml:"keep-fqdn-file"` + KeepDomainFile string `yaml:"keep-domain-file"` + DropQueryIpFile string `yaml:"drop-queryip-file"` + KeepQueryIpFile string `yaml:"keep-queryip-file"` + DropRcodes []string `yaml:"drop-rcodes,flow"` + LogQueries bool `yaml:"log-queries"` + LogReplies bool `yaml:"log-replies"` + Downsample int `yaml:"downsample"` + } `yaml:"filtering"` + GeoIP struct { + Enable bool `yaml:"enable"` + DbCountryFile string `yaml:"mmdb-country-file"` + DbCityFile string `yaml:"mmdb-city-file"` + DbAsnFile string `yaml:"mmdb-asn-file"` + } `yaml:"geoip"` + Suspicious struct { + Enable bool `yaml:"enable"` + ThresholdQnameLen int `yaml:"threshold-qname-len"` + ThresholdPacketLen int `yaml:"threshold-packet-len"` + ThresholdSlow float64 `yaml:"threshold-slow"` + CommonQtypes []string `yaml:"common-qtypes,flow"` + UnallowedChars []string `yaml:"unallowed-chars,flow"` + ThresholdMaxLabels int `yaml:"threshold-max-labels"` + } `yaml:"suspicious"` +} + +func (c *ConfigTransformers) SetDefault() { + c.Suspicious.Enable = false + c.Suspicious.ThresholdQnameLen = 100 + c.Suspicious.ThresholdPacketLen = 1000 + c.Suspicious.ThresholdSlow = 1.0 + c.Suspicious.CommonQtypes = []string{"A", "AAAA", "TXT", "CNAME", "PTR", + "NAPTR", "DNSKEY", "SRV", "SOA", "NS", "MX", "DS"} + c.Suspicious.UnallowedChars = []string{"\"", "==", "/", ":"} + c.Suspicious.ThresholdMaxLabels = 10 + + c.UserPrivacy.Enable = false + c.UserPrivacy.AnonymizeIP = false + c.UserPrivacy.MinimazeQname = false + + c.Normalize.Enable = false + c.Normalize.QnameLowerCase = false + + c.Filtering.Enable = false + c.Filtering.DropFqdnFile = "" + c.Filtering.DropDomainFile = "" + c.Filtering.KeepFqdnFile = "" + c.Filtering.KeepDomainFile = "" + c.Filtering.DropQueryIpFile = "" + c.Filtering.DropRcodes = []string{} + c.Filtering.LogQueries = true + c.Filtering.LogReplies = true + c.Filtering.Downsample = 0 + + c.GeoIP.Enable = false + c.GeoIP.DbCountryFile = "" + c.GeoIP.DbCityFile = "" + c.GeoIP.DbAsnFile = "" +} + +/* main configuration */ type Config struct { Global struct { TextFormat string `yaml:"text-format"` @@ -108,45 +177,7 @@ type Config struct { } `yaml:"pcap"` } `yaml:"collectors"` - Transformers struct { - UserPrivacy struct { - Enable bool `yaml:"enable"` - AnonymizeIP bool `yaml:"anonymize-ip"` - MinimazeQname bool `yaml:"minimaze-qname"` - } `yaml:"user-privacy"` - Normalize struct { - Enable bool `yaml:"enable"` - QnameLowerCase bool `yaml:"qname-lowercase"` - } `yaml:"normalize"` - Filtering struct { - Enable bool `yaml:"enable"` - DropFqdnFile string `yaml:"drop-fqdn-file"` - DropDomainFile string `yaml:"drop-domain-file"` - KeepFqdnFile string `yaml:"keep-fqdn-file"` - KeepDomainFile string `yaml:"keep-domain-file"` - DropQueryIpFile string `yaml:"drop-queryip-file"` - KeepQueryIpFile string `yaml:"keep-queryip-file"` - DropRcodes []string `yaml:"drop-rcodes,flow"` - LogQueries bool `yaml:"log-queries"` - LogReplies bool `yaml:"log-replies"` - Downsample int `yaml:"downsample"` - } `yaml:"filtering"` - GeoIP struct { - Enable bool `yaml:"enable"` - DbCountryFile string `yaml:"mmdb-country-file"` - DbCityFile string `yaml:"mmdb-city-file"` - DbAsnFile string `yaml:"mmdb-asn-file"` - } `yaml:"geoip"` - Suspicious struct { - Enable bool `yaml:"enable"` - ThresholdQnameLen int `yaml:"threshold-qname-len"` - ThresholdPacketLen int `yaml:"threshold-packet-len"` - ThresholdSlow float64 `yaml:"threshold-slow"` - CommonQtypes []string `yaml:"common-qtypes,flow"` - UnallowedChars []string `yaml:"unallowed-chars,flow"` - ThresholdMaxLabels int `yaml:"threshold-max-labels"` - } `yaml:"suspicious"` - } `yaml:"transformers"` + IngoingTransformers ConfigTransformers `yaml:"ingoing-transformers"` Loggers struct { Stdout struct { @@ -294,6 +325,8 @@ type Config struct { } `yaml:"elasticsearch"` } `yaml:"loggers"` + OutgoingTransformers ConfigTransformers `yaml:"outgoing-transformers"` + Multiplexer struct { Collectors []MultiplexInOut `yaml:"collectors"` Loggers []MultiplexInOut `yaml:"loggers"` @@ -360,38 +393,8 @@ func (c *Config) SetDefault() { c.Collectors.IngestPcap.DropReplies = false c.Collectors.IngestPcap.DeleteAfter = false - // Transformers - c.Transformers.Suspicious.Enable = false - c.Transformers.Suspicious.ThresholdQnameLen = 100 - c.Transformers.Suspicious.ThresholdPacketLen = 1000 - c.Transformers.Suspicious.ThresholdSlow = 1.0 - c.Transformers.Suspicious.CommonQtypes = []string{"A", "AAAA", "TXT", "CNAME", "PTR", - "NAPTR", "DNSKEY", "SRV", "SOA", "NS", "MX", "DS"} - c.Transformers.Suspicious.UnallowedChars = []string{"\"", "==", "/", ":"} - c.Transformers.Suspicious.ThresholdMaxLabels = 10 - - c.Transformers.UserPrivacy.Enable = false - c.Transformers.UserPrivacy.AnonymizeIP = false - c.Transformers.UserPrivacy.MinimazeQname = false - - c.Transformers.Normalize.Enable = false - c.Transformers.Normalize.QnameLowerCase = false - - c.Transformers.Filtering.Enable = false - c.Transformers.Filtering.DropFqdnFile = "" - c.Transformers.Filtering.DropDomainFile = "" - c.Transformers.Filtering.KeepFqdnFile = "" - c.Transformers.Filtering.KeepDomainFile = "" - c.Transformers.Filtering.DropQueryIpFile = "" - c.Transformers.Filtering.DropRcodes = []string{} - c.Transformers.Filtering.LogQueries = true - c.Transformers.Filtering.LogReplies = true - c.Transformers.Filtering.Downsample = 0 - - c.Transformers.GeoIP.Enable = false - c.Transformers.GeoIP.DbCountryFile = "" - c.Transformers.GeoIP.DbCityFile = "" - c.Transformers.GeoIP.DbAsnFile = "" + // Transformers for collectors + c.IngoingTransformers.SetDefault() // Loggers c.Loggers.Stdout.Enable = false @@ -523,6 +526,10 @@ func (c *Config) SetDefault() { c.Loggers.ElasticSearchClient.Enable = false c.Loggers.ElasticSearchClient.URL = "" + + // Transformers for loggers + c.OutgoingTransformers.SetDefault() + } func (c *Config) GetServerIdentity() string { @@ -583,3 +590,9 @@ func GetFakeConfig() *Config { config.SetDefault() return config } + +func GetFakeConfigTransformers() *ConfigTransformers { + config := &ConfigTransformers{} + config.SetDefault() + return config +} diff --git a/doc/configuration.md b/doc/configuration.md index 4f462e45..49fc5e60 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -161,7 +161,7 @@ multiplexer: ``` ## Transformers -Some transformations can be done after the collect. +Some transformations can be done on collectors or loggers. ### Normalize diff --git a/doc/overview2.png b/doc/metrics.png similarity index 100% rename from doc/overview2.png rename to doc/metrics.png diff --git a/doc/overview.drawio b/doc/overview.drawio index 40719f94..19083026 100644 --- a/doc/overview.drawio +++ b/doc/overview.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/doc/overview.png b/doc/overview.png index eb56acc78328f4923f165cad5e8a50ac59ebbf3f..158e92c98d11132f8693133f59ae22adfba94ccb 100644 GIT binary patch literal 39023 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%0Dx9 zKRdlwk5x3x-O-g=zMwlE-a?ilA!_rS==Lm zqFs+Vfg(|jN9Glm)Hi^7$2an%LvQqwZkU(dn05qw-m*8>W0^UQnp5*MVKb@pfaJ)z zrFp>R-zS^#9Qj$6f%eb&03aN_{&8k8ZD%P=@22wZ4-<+9 z8oylS8K_+$Dar=N6!z*%kVFMXpHx{Bw=D(-vy4x&r@-B8Pui2!2sA1DxFf z6JOcd5lK%*5}H^*-T#k0*PqT*A3V0Of2~yY%>PH&Ux!8YzR|^I6=`C^9v?{s0n8liA3i6U_j2fMFGtd%7Z20DI z(Mv|puzN75J?auS!I&Jnr7)ismRqp+41y#{Z3Rq0A#+$ip*h!BUiMR#_rgX`ZyizFZ~9H<@U8k`0cdV z{5LJr>CxbXDduDsv#Ywr1T{LSk)hx(uFn+-4CV(Yshof3Z~-{v1+8Nf9$-PHca`FH zLAS?Nmb-tbNc}0p04vyX2!;rF8$v??6hO2jmS{!-fRxfdz3%~rRN)kIdpE!TA9n-n zR2*e5WrrTO=Bvo^rgQw@n=7BBZ936c5nq&IzF0K}y^Q+dJR#&f+d!+4!V9aL1k{1H zp*)QruSU7+&q4q67%^29;^hHY>Dq+L`E6jd25tM5`bM{+FKPsL4woY%I9EV;yKWm6LI$+Z@vp znN_2oDCh#qWi7KPML*x3uHElCJw1gJ(uv#!{I_?0iQ|E%y9+0E^MEJgFjFVGzuX%IVn*)smM}oC!s0eA`XJTj|-ej}d|RYPG>PGTcu~5a5ZXh|vG-;FD&f?(t*B0~=T(74!JGVdDj5xPw*>2o{xne@?Q9vo#Wpbf*gLqyUCo^x9X;d3Oj4YU zE7^g^pRl~Su~q?J@%{6xG?fP1?j)qo4in~`JK)&hn!pJS1l%$~yB^8`YE14W3)$D3 zA7^flLogS<8j`LLHpzT4s;7V6Vr6_zDADO#66Q&T<36o~sK}e=;{L~*9)y&19%vLE z9(pNapx`Ye)G9PC&$<*y zuCVX_5_EOGPj@cle!BaDw)%#%v4)O8&VE|Bf!ArzhR=L=6I1A zQmYU}?C|&d{ie%(jWfD8<-gNJH6qhU5wiZrn%Ak&3b{~BAT@w!&uh)T=I(%A10t11 zg7}-Sx@&{ExL6EeJ=GNqloOcZWN$7h0PB(8#DoydpE%?47@S-52J0Rygy=dEdXh

?z1x+2lgu( zaQ-;0nxtm^Hu&o0%^e-0|99PPMx3OmxbX5697Jr_N&UzSuoJPN{ZRe35`; zH5`4K{(tj=IN*0HPN?`DZ@vW_0qEn$bd;7)Ce*tOwAn-x#9ihCE>8uuv!u8}g8=(c zXbSI*AtB8}2L(z|O zkBvz~aD1h|*7Gz3;pUqJRJuA(L`2CpL}IwqtcK1yK9x~Ihm41reUf>pOD3u24|H3z z%3?Sb=+oxr(qYnQmY${ZXen9(Gs;By=`V`fr68ai*47Hg-O{DW6vW+Wz;(sl1L_=1 z9UVX>#s9lITqGCh+b(^73;&*xCDi+oh>0RFxh1E&G*SV~$Bu+4&hs{~#P^kLD&4lC zwV$X20xd#(Y`cP15%{keFBL5Ju7cgvGM>ExUAnST9}qIDH(PRH@nBc?4NfFCg1!>| z`VH8=CrUUe94y*FEg2G?`*Dnt)kC$O|L(Ef1On!6Ub$8Cbu_+3{v;LJE#Y=Cae{|bo#E7QF-92!$nOmp&{L10t{wT*F3f?F45mHpZb5e0JRtt_{qz# zX8>K!`e{Qo958Pz{_tCsgT+W~WW5#wCyT-76haH?kRQF0;NtCx;Vm=%M?8|K0{?dQ zW5A&f(YE^5`eX=BQ5}XH%dgr`C`7Uk(Il~&lfu9FOhe`)MgSke29EH~A%-;a|Dx6Ux9(C0{abaZ6G@-V^5B}HQ>%+dAEnK z`5U{hXY4@UhU120`@j2X{OwN_3`R`U=v)_H&l!%Hm7g|k0qbJ)`^OPj;o|a% zv0IVI|Hp$VNs|g>Xl0-~+DkA`vf1Zn-@{WmQP6GjiQ3`dTS3vF{&OHt@EIJ^;F$sG z_u~|hcqBhDpg|c}y#pP;h8M||XwC%qkA(GBpi%4$Hh*tR<5ca^ncC=Q(>MQ6q{yOo zll11X5AiJ&A4=8hzK&Mno!S+S^x2LG@6`Fcu_}1#vW7M+NNu#X%b3}v2n)wd?)v-* zV8r}F*#ED=j*+J6JKr9j99n6ILxAy595W}pztiJQ^VrK_1B5t@!!9HB0*|(5V%|3TbK&d-cE;a#J|}d}lqN_3x`~P2)V*V; z@>)t%?w8z!e?0L@#imO~0A*u8-)eEG;|GKwTNrMlPzi=iA zFymCauyPzqapRYdojnWjeGy<9`(L{TzN3|HU<^kJYMjX&yy zK_ZOFrgNe1${${CtELIwugVk1s983ww=2 zn#>3O?k{(7HEA9i&glBv=)AbuvNCeHeQ!ZA0z4Q}@Qi<0V9e6m*S}6{Op-STR0e-Q zeSfX-4T+%=cKVKZOVDeDm%*$;J>I@;d*)fAcY4IiWHL7wCLps59wTmG>t5ufbKjlB zs{drhO;7w>D4q?}t(3!vU7utlL)k1^%fAqEV%L^i8;{Ao`A{hCa~6Gi0*}y2kFO_T z8OU!ba2UwNB@VvHy!`pROeXYncfxb`3i*RmAY`^p;VR*ts5ClK>3>|)vz@{Sbo)X? zG)SMqx{kdPp|AU7C0um~q5NgJ^A-CLGmUS~ELJ8Y-L2Wt<8KeVoi2MV>D`amz z+(kPDPlP6$Ripo{4clhY`)q&SjjD3@KhcI02m}=35an?M!2iX6Gs(fcS88Toef`CI zlpx_zGNLmfv7iSR z9EkM#Zof!~GtfYu`;O{)|~wxX|}m zHAbtkG|lLNQbMoC)FD;DK$8H(h%eY{K{G;#`fRP1Tm2=-qw4e+iO~1u<-g69N-D>= zCt^E|Tc3R#1lY9-Icf0jl-us!UPp4e*q%q}W4PaHh&5?HuXRn7_d2I{t|m{m*EV;~ zBq?i?NAEobX-}Nti<9@>);2b<)g`qLzKcZD!Kb8lO{e%06))TWF^|cCPNq%Yy6DRd z1Bae=r}2k5spmfyaN58Dac~de=YB6sMt&FXcVUUVr2J*NTT`Nw6X2KkoLA^>4C~0Y zlgM5|n;Ga*+t6<{f!t;=M*(-F#Ds!Y_$A8jhp)u51`-B+fX}fwnK}byKT#>z4gc>k zvRMUU=QW>_hhDD@7Rd*Se0$1=hxk`wj%{#p)XuSEEZ?m5UeI;W`aN{kzV*STWhJEB z1|_yV5yN<}0<}*maTqBO*kAq~x+mSNhP4OUuAirkzc&N!-nr@b8u zkNOOXuAe(6&8}=>u1P1e=73^&lv2*~_~ zWc7VcC=?GNKy}vANpRZ4WnIJ5V1yr~lYu&mqfdgPn5Fz8P83%@pjoEW`&a%XIgfo6 z-%;SuopPBbx&zk8Zbv)4qy z!79aqSn>vFUV0AR zG^lF%Ellh;Q-ob18Q)O0Uy-+>;~Th z79k<>r3u%`O0M!WG?)z*kSYkTtNoC7`Jc+ktB87NV*yQe4Tu+dHLezC z@_U0SeMjO#2s3-L#YvI`k|?XOebv;`urJDogy^XDwERD5*eIJoAFgdow4To~`fRI_ z>vs~rnQyHOQ9Ehc{^mBBRQ-WU^!ZvvisCtMp8lnUW1xX0?7bY*2j;uHt_MOwH74(` zwV_e~VnkJ6U0uPh&b@DibRT5`>kgkAtW8vK0DQuEAE5)JegIrmI^LXk|2y=TRMg$# zpP(x^#mi^~fSO|`PuITOxux`(X|8hIJ0R#ymkR*O)Kj4Hdgy-~^AV13B*CeFtA`w0 z0X%*H%?~G(Ya9NzN?^|>(QuD0SCs?Y(Wj+$R9aPn_r%zuC3fzLF67#a&dq|rpy3|6@OhL z;k4gd?ncxo>|p$fd+4NUwiY~(ah^;z_*zOml^;|#uji}AEfos1GatkPyy}EJZOu|s z5WNJs?~q>^QH?^#lGCR-R{VPmgBq#9)_JPr`>5;sr;I>Jos=MOeG6KMnG*rt53Oje zN<)Y^eg;!&5_P0CK@<@^yR;Y~`bXD1wS6X%uGnQanx3SW;axpGin`2#qap?FdzH!| zVLaxHp3i=H%kN01VuZ$jzbWzaZg`3$UH4jzsLmFZi`(Csn9iyY@S+A4Ogd{t{s2u9 z3PDd44UwvRR(vx=S47IHErAMD#9>kZpawO+IWh!b8`i%tX0yCDRT7ef@V;k&GCNk7!GLlUCXr?dyuOrh$R&|B z&7%i-XM^ zM~^X1ZD4fHpzJ4Q+aFIAu(D*jt2kjG0#bTW8~jERML|T2)~mL5<>;oh;et*83oSLA zfyRQ@&TQZ!fc2an+q5`f|3_te{)pU{fKpFtt97D*a@?rc+Jn^U`*RHCI65|Iae)|{ z)|0KN0$S2$U+eo!*;XH|*O`3NezEE-QU>cFt7-7Y?-lCHwrN;_SS{OFNvVU<*(ZOt z%Y%i;UQ!}BGJv;Z$8jJea~57Y?7qA3Z^Z2PUwFkQ+`jcrWg773TY>qPZK>{U+x^J| zIor}kc)nKW9T?_{KN=>vIm&IrQGADd* zD@@73q&e#l@$Y)>Vs+vRExUjP|3ReB`947<*{lcmoZD5i+KtV$SsNDAF8HQ$Z9f=OBsFozbV0Ix?w zqISg6B+6i7ADqd?JUC@u_x)MM;6GYqMB+MCy=7q^E6)Sf_RN%Sk_}T3%DrWghfI$G zJ=9LBr5~OOmU{yblOx&Vw}MpU;+{&O$Ag$73OFLaK2v5=!ufTuEYEe#6#V)jc^{Ec zo{Y-_4W_g&UW=bzKuo_eGc$&66ni$(B>m8sR?YKb@mE@5VO^mA)JN+!@gVR4?jGk4 zdHCp<3(bR&)1^A*b_`h`y)r$UOA!}*OX`G;`go?8g2evs?xzncpxVz}H^~YXx8vk9 zhzU)lti_LSj;LQ%I>z`nZ?~7$`{)&#csi#Go5=+!pPfJwM*&Jv$Ce{{OAHV9c&bSk zX6H4%Uf#rw33ydEnou=s|KCofDjv}r!V%-v)z|h_%t_X46XyONo1`j`elU?X1)qzbO;q` zaRQxeXs^l?{R-guOoF27X%hvqP6C@U90?auC;c?E+%d_m|Jh}#+L2$R9ZKli&a5=8 zGr}bb^}$9AE8pMu8_tJD;}uX$J9Nr0mTF3#6=C0TWKiLJD0lZ7H2566bT3w1rZSjI`qn8AH0WOIpA}>*rXDB+CllQ9uu`y zQQ}#?awl+eSh7}Znli+R2<0f1Xuas3f=>l=JYeU&@n!9cwrRE}geOj&Wr^SaE>}f2 z#A;a_i0kDxgk_}Oj0p3ued+R>LF5+;g4yIq`lnrA1{Ufc9*Ovy0?pNd|z2V6F5kOGff_!&jXiTn>0%bZOFtNq2C zXKI+4eb#w5Xr<5=O6q+iJt^}0G%y2bkg*h3(O?~4&?i~yPYO?L{}R0EMdG7uhNRmT zRm}8xA)XVeAF`f1It=I?5&5>g|A$vonZ#IYGORzRW@(>*bgTvGnyIq+L<45De1?L- zrkS!Sf!0KWuW+@q+F8!aIbJ?1tqEqul7{YRyY|RFcA|}Eu8RLefyq!37R=8)Otn|Z z?}5PAgedX82?gia1J!@oOLll60~GEz9i}V2PMMx>=T9 zO{@+Ig-7oJDF{@1w^5ZLObC9iX-n7_QfE{&|I4a#nO)ELLl<)R^xv~wN{jSfu%@ixKl*rIroX$w2y)Je#rGm))>l~ORvI1~!iR8uO;DLiYo^!l}uFUYPkzBqYiz0BiyMn~u673d^tLt67xaATwR zb;7+G8%z(KMBBI58w(s6T(ex6ykiNv&SNi0Ym^>YSme^&QbAnnUvY~7!Rcs3RB2ro ziL;yU|C%c7>4NcOVz~2>1l}ZQ%qAP9?X}a36FD(omE>mvR#F6Av7pgT#v0_KJSd4U zE7mi`g*~2m=aIbg*z4I#SKo^&>(go#pRW`;PUd=tX)4oCOKDIpMt!Z`+I1@ahy;c5 z=eH69sMyuuH-|7l<+L60W+Q=FuRMWSzm$O`jk zpCU9kI43;m%jf{5eF%IXi1(RxmnPS!P>b!6wc?3(tk-+2Y`yZMj}x2Z;dC_7_G@8A zE@HkmglOXARQj6Fpa_adL8PM)_4`bGOt$!()Jz9*`+=taxNfn>Iv?vACWZ@_?8!qr zMZ8!iQIYmRsOu(?4$&{7rvp?7f`k|5aB=Il9SHGZ5RXO95^XTuM{J!T`zH-i=Nun| z5Ij{_23_|Hg6=IU@bv-OUp z+P?$$iQVKdB+c_QLaqC5a2(jreDG2v<#=JBt;6l6XCx7g-W2GsR2l{z=No%WS?~|{ zahY%<@aJOCtHLphpc~IgX6*hM+?JS9tSbNLE}E`crHvJ1_7nEIsAtt&w|vY)%W(N= zwR@T|Ib1%j7Hyu1_ah#?;N~;M-8)qwA@kJAcujD)39`q_*fADFIxO zC+RJZgeu6Vaq*lAO)^7~S=)n5*6#~y#8rBQe>ISSHX)zxRWs-yzMj-ARFNu;%zwsH zo+2-$?c5s6c2_LXV_?$vadA+?E@H0tBT3>lzPZ3XoOb9oZ!t_Z|F$zSST{h10OQgk z;cKjTOS3OGdIGV!W*^a%y&*4%3}pujgqKwVD#gq@%%QVK1n}-hfp-%&?_Pog(4w* zbCd@W>^_&W=CGc|asA0%_CbxRN3naV?$1GB+S-%h==$&$8Ls`GS|CflRWi$H`b!)f zG*Of`X5Zdo)S1>iN4K_8LOE49XqXQw|2O^k2O>+{D$bXeKe|e+H2|qoNwi6s4`3pr z`a?ahMTpTIC#wbMfy%nf`Yt>s319rY=tk~fE9oExA*{5wSGt^?EsGov-Pc7fAHpz) zE@IyMGZ=EdoQ6{XvT%6Bm~I7_kzpEjd2=sdHHBX}MvgGh-u+1Mk1Ezw)`&BFYipnt zaja$M^X}6cV9R#)ZHw!J49LW&4VpD6U1u2H?{2$%qNbRuP}I=itg&^z)vCm4i6jMh zuKgIP*K@`zxZmXQ$l$>D1h~6eftNn8rTT9nfDacrfO0Hsbxk9M>d&Em=>_rK zdrW&B-mezdOM}1rO+7k2JN+_RaHLyotz_slqkNsN{jSuYmSr;Nm{z$jGb*=0hn1h7 ziNY^1JW|?^#l{_H&0uvIT>iT0A}O^l0g~{a;XxnnfzBLm@Zpnz-mP{W$(Y-Ite2g50VumY zy~Y#-ez3eX%6Oyrv;%6H*FOGN(2YM!%0JR{>+$pP)-&XBzpR5a9DxP96F@JQ@yM1R zw4Y4HPAO&abWWRWI^Rh=Pz*!X)KI6C%!Qn_MLzFOWc^t07Y8|dO(Wr!72&1Z-pHM%jJ^`)PclIsX++hU+rs#HRmU|*DA9qP5{2UD0c*+}Y zJxx9bob8Ld&qE%2t%d*!gNbN4zy~ORf11({>-LhmzS$miX+SlxE+6Wvm-)A4up3Xo zpK9CA_i@sg3CZcL2hz91+(79`NtptF6cLmOIHAqYKS!ZZP+nd_ z04cP3|5yc}C5aE_pJ z{2AE`3}S1&*2tp~ZfU^EsvVE4epJ~x0Ge2FagSkkLWqg2K@!g~V<=EzQ zEd_wEPyme6YYqzByeF9m42k@~P?AlPK@{a)p7g^=Bzbohx?JB-2A)xkA&W~kE6?Db z@D?#t+dvH7E+4vovwb9l4Hlp*P=I|W6EyIT-A5hJ*@#p3lLUs#ev@~scdSuoB=~@b zfSFkNtlch$3nu{%?Ajs;X<1(Q1J^il7V9z)asg*l9KH*^_1y~C@8;k%Dx$3O*@+Ls zCa?x5?>B>j_=cd>fgEzQgb4QG8+J|JYWYGwBydBs0aT`MPfO{FjLTh5CIdij*pq}! z`+v9qor7a}Z58zfxFV#dA`G9HENlM$oCSknhs%nKWnm#G9#B%UL7BYm1+wO5W4tDM z+60`=C`;)qb*{sNH#q#-iJ-1HU5!lbyDzn{H(E`MSfmA@`)7NUGZLO#G(N6drqxe5 zvQO)G)#m@Sc6-P8a|+#?t$M0;HFnZPhxi*CEo6S{DgT@Q06`CIJvyR$02@{hh`A`C zO@Pm~IaQ-G_DLr2v35*DmSKW*kI&X5+l}_aY(SKuwJI|rrEXgqpk!=N``|j_6~%aF z8;>9a)>FEJHadW)7S$QGxUffdmw(py@(_ISqiv(x%Ki0Kaj${wKo<{G;Ae9!LsMc; zMbGMU-wi*+32VyyLc1E*%KT=P4NXXu(}d|BaK+`xx6RG3GIgqLQgMJ-3&$LC{ZAbu zV$x;@b=^f!Aa*7yaDkQFhpZh{m}2P<72q7T_>pS}Q|}XSggNx1<};;?B4O^pOvLQc z<*Zy*Nt_yFEv4|*rm5MetopC(+%bI2Q%1jFFHUJ3VB3lpqhCX&+ou$A>XRbxCJ_HV4rPU*c!5vYYs ze~d_>2d(=jx+FXWRlm|Ra2#jOB)}0qR>6!uN`3Ag*LvJ7>pRJ9;ZX^;+Iu5CzGTFb zPNS~^rgH16@(s+JZu=zdH~g)m$vDM zeE_wr^{`tbD-efqV;$wNjQ`6HzFzYpHBJ-F14dF^pM(om?%{{S_22&`X9ey*uheuT z_Z=HfgjU)NZepXy^5~dgN{kXned}1Ho@@uiwM)295Ka6jBKePJEHXK)*70b>8fA|N zw6>dt-(icW=o4qVulAyAEfdi1HzfkFuFPtLir<;4jG76xXfFgba2?kMaxo;XMB5)j zQ3%o}*uYKUzi$Dg`lHk+@uhtM6#c`dllU~+t)znqRM#?^5BddkKSS>En$jccDX8Q1Fq9GOwxNZH8#I3drb_SlZ=e1YY9zT7e#Nn_rQb+-;XrdBi&Q)I;(Tcfd49lArR#l~lDy<=t#{l+U zaF*7~T4qSlIO3_u1S<`PTQlt$0*?(@z>F%jZ45;=?qCDm)JDG{6U_Q;}M@JJn8?JI*0nC!prkX+N!eWAy#3PgtP8T!!}kgYtm=Rt~2+k7S8*H*iK|ii!R^ z{7Q*xcL2&93HBfL)XcDNI>gvf=YMAsW9X4=#0PKn;2%kdj&8Ox@je>1*PhG`_wxQG ztoU^o`3tLEQC3iSk%CPuL_?9~l`G}%{X#)@e)Gk!*?`r8pQ=mB*3+&{(;=Lw)kf;Q zx(+_Ng*4n6_dmHTF`MCpciJ}nPrQQN7+-$3Uz71ZA6YB&JhxJl9qu!1G&%|{?!Scp zE9L&gu*a*zWp@IoZV6d0e5(Sp$wH8pvsj3Y<%E*>NVn0=NeMXCTSq1#vb{N(Bjy#O zqp70ewXLHYh4*Lhr6zhI3i*J+oz&y{rfXaVvf!0|w;{KkhH$3SHj2vne0CSo=ED?; z%Ja}ULN5Cts_aip9e>hHESzXp8K7AhiAvO}THWskqU#DBit^Fb;n5{)_W60=l7LSb z|66e>csZcT2tZI<+$*kuA;|i3zdwG5av6SUer`+ajF+2A&0pDS`f|Rswtg+-n)@i8 zczrTGb@uAQ_i!ZJawTA|G$1YzW5$0tE}x2))T+a!{8RJ0*SjwBxe`e-+g*Z{s~y?H zc+?h}^j7eu><@u+7hBCYQD;}d=Pf5BN5LkJsR{f&V zt7waMoRQQx1gmI!&H$||6XIlGbyABmdM~g?hG$R?aUAzJ)VgC{_Dy~ox7@TnL5s~W z2EzaM3s;gr}XIe(#8J8XKbDY0@ajbjP#^8ho>|Kv93Zy}XLN6m0MApnFLX`XUPFE~(Ed zCy59yLeh&o+s?8PnBl#W*|=2DmOV>*C^m8d5)L zsg+Eh_gDH|ICFePBNtTkGz4!Nh2tWPeCHUV6O&HO#@}p7lP9bcA8%pH2M6^_4W)wRw=)J^+9Ya zC+X3S?3!7$vWF#dq@7fuhxHCxyI;F{k-BDsMP=IM&DI2%j$xkES1Kg#k?QYH?59(y zv&1PvkSABKMDHj975&fCGa!f=lLFX|y@W_^rG%V$VMhLsg+HQ(W2VKk?vcEYs1m;9 zFWlKDbvfqy-{mD97#H6`StO5F!$OcQ-5y2qhfud!=hbH4VGT^Iv#`YhL<=hp!oNFa zEshGBmF4=jR4C9UC`J`&e=x0^F9dxY!-L5$CH!9eix>AQbR3XUn)Dj7auleGKf-LALtV5^x z(_hPp)LUQ4aBJR+JDH!5<1G)jL<{Koe3R?e#XzguMkI<*K@bI8=JBF22e}gZ6a{d_ zU>dP~npPEh+a)$1a=Gan0va+{lnf5y-#tY7jo&f?*+mQXWvIA{l?aa@tK6TANEVM) zB}H?iZc$R>Jo?S0d`#^Vjpr5l;ofa6c8S9LLq?Uc@t%}YA}qhsg-UPg@JW5X6H5(I zLbs>&!26$rfTKOg?BfWa0%+rJ92fuMV$6i5PQPT0eZilykLV2`grJ4EbBHO$zlIE7 zkwakEJZ~2tu)m9m%Lt&iAJae_{L&!8PZ0{iT~fY7nmXLwTlFptJ>ZW(Fq1zt);CDv zOMQ{4WjJE0@@9sQ*!aWMN~SN{h^bW4B*7{7BkCAJgrv0OMEgRU2xZqdOvqkrkIUdE z`5#YV6dZabtOtwul>3Phg!7@+gEiiiQm_cf-tP;ZVQChYZCzN1xSSLW@9p&7mv|hy zvDfc%m$sJ^RV2=5OrL2pDzU06whHUn%1Kcy$}#KyBEW~BM32Z-I{ZxuBR%?9eFW8A z-47*oTe&z1=TSSue2_h_If;Z@68qPw@RS zj?SVpC#g7Ig>dFh>o&WPllGW$Wf`|$gcC6V90ml?vYW>qkDnx_2_@&&9Yox3y6?f) zoE>(u|JWJWJ^wpD+9>x}zni=T3&>Fy9i1ZQD374+Q9?;E(Kk#C-^HS-Jm75IEnP=Z zP*p$>#kypVJd`j(kUr8bn7#rZI&H#3C+|LVIBE4MqR=tqgpeBNe3^YT!dUqEM9TA( z7i2HP!>5CtfVBVP$Z8iZzW9tl=$|y1XIS|8;HS_pE9?+-nj~lD0tRYWkI1Ej5`r+} zipW;L@CFk)>bPd70f7nl>$q-0U=o>9Mg-7pE+5~Q--V#thnu$jh|tg{7^z^F*Ykc? z`F#f^jpoE{!$L@s>Wt?@U>4GgTd?jBfwTPFynhN1ivko}8bx!5lp1YjnngZrkm2$1y_r3*HyJ!azwc z^Y;_pgKpCrzxYhYIq%gBE^vqA)kCZhB#ySb?_CHi6}vCeHw6C3@FX}J0=v1O5jHIN z-{$vj+WpBEf=q4X5PKbh%+Ss%!X`cWP%kC?jF!~L@AXf6)GxW5kT>8Nc*&^1{1|=v zRznnmoV`|a0ux?6&dj7M0cC(RcaXqiJnx#=kF(dX(2gws40VT&-hgY+VIDK0+g_@uNt z4lb!NORl9d1nqa$CXkDPxM+X>QH7n-{l5c8c!BNe0)p7sVJ%<<3t*(%b_{_LWun9H zW1s@(6*d3Q)r`T_e2gslV(*};+`}j3!6L8YIYm*v!+;gob0|1*La>RsK9eAD-|okU znBi~@D2z1Dd0Tkl?v)2_Iq}3xZX;o31PjB&)qu)H0o_*khlvVYH6+S^0K)WvwdhCo zNCNG2y9p$0<+%$u}o9e#SA>KcU%E34(Und#Y1=k*X?g!K)_)T#)f~pljFfo ze(cwNVS|bApNsk^zw+PAK(i#Q@$x;A$gS|+E+s0F7*@kGDlOMa#0t%!JB zw`gMW8nRc?6|XH_^uL|(hmnL77s2!F3y6p;>kh5GP?;B$I|C|eMRI{=8N}5cKfk>D zFSbeWP$(4W{=x?9%#SE~uLgpad5a$wQ2FbrP(WaejbC1B)`cU}Oj_|NcbmY@*|~VQ z41zAWuEz8uM7H;s3H9j6$aDp4vBR$UVrHl55GWY3V@|5QKDyC#RNx1s4q?fGQ?uTbK_`89bcHR&z^A~ zH8R$+nF}oY6|ANOLCqM7)mLMnCq|hY6);S|L;qCwtz-hTQ}%pq5@(P3dN;~v)&~&O z7`#XH@ay`eDGvVi&((o0-VU&b^7I6oN^JB`nM(-#^^wYsuG~|Mf(y3qCI(N}ct`Ib z?x3i6rKg}Wc0C`WfS~C30FQ#5b5({zt7LiPL!oOw19ZE(b)tfVv_I>3l@{MtA0q_L zKB}E7a2x95?1aC+bsJmK{A}Jw*xWqISu2by+T?DzBo2TN2Z(OPS@Sr=Z&+0%Lw}KI2e!5rMI$ zT}(7Xuc171zsjR*L!=C#vw#1p*37)7Z@k>^MajQ7UpH=6KbiI<$qYJQTY198%llxq zQ&mYxNsYRXuV(s*k&%(tu$Gu(1=2ZOT;G7GKviD~G~mXHxjC-7|p6~$@4WH5l;=GzMgZd#F2x0cV3CpR*YI&!Opff zvjfgXxK|4{HG?G$SUa)Iz~c!K?{!0NF0LHF#UBJD)U`>c@;tD`2I<`z=3II7Z z_4L$59cxeadM|-}Ub(j{t`u@^ELbWu%PnwmZ@+Wzq3lNp5JOt)GBa` z7uhf}Hnz9Ln8bmB%%>zbli!QrRBzQOgO{gjB9 zhFy;AfTS$HbuNmccEL%2?1{2Y1khPDBTfqovebdY^;b07`W%R;)_(T#=O)Nr|Haxo zKlejRTC66jFbPgYx614Loy_>W6)5})Xn-=g1UnBA*`9q8b90UUCulyi>XM~JyiPuo zpSJ6|JHGG!9oQTWWR=M%D;-PQis6Y!jZ!j?@NvEzLLXm_1VL!RK91#7`wUpJJvyFL4*RREM-@#!^7$YVj4pl-I* z0ietHecxra;S)SIN*(~;%ZItlR|v3K?i{c|Eq~R9oZDylM)vgd{MO0=0BxXhz84KD z@nTDJ2T<_HZ>oM>Qm7OqITdnUI29FERD^c~sa;!rgzSwt>7D#Z>yuG)Ai}=P4Jm{2 zQ_h|by|LZBQO@#;6Mc6F6=nGWvX^g3D%qkZ zj#%0l2~F8?{0A1yi7Jxsuwhx2eR=u3SAQ}MO&^8zNo`iO1YBUzN-8mWA-kA^;+eXQ}Um*P>iu8|= z*^5`d4PW(qvd?mCtq}jI`>PV-@*~Fsu$)m3d5F+~ozyZnmj^Fh#qEPh`&%mcepxO> z#;}qpaci)Dq=E9+(is``wvT+!kqL;h3_Qj&XtgO~e}*Y>8*0}Na3po1>G-RJ=qJ=< zR0-9i3@MykVA?#%P{r_}JHz_t81QV)PndftDfiNZ?TH)DR?;hjZ-VF~U-SOSlAeli z)U#zpzx{`Zd2iS!ePUjBijG+;DlQWme%905yLLR~Rb4B7Ffi< z{pfFSPn)Y3RR$RUJ;tgHp0F;o`1T!MI0hBuH}SWc=-Cpq3{*hE10Q=|{q&-XjM?M< znSaF!ewL?|NPsG&Uj$)))V68;4}stzH@=6etl#_z%a=b93;#>Y_1 zNiI^yQ^flCjr9Ps)sCVNy`ga)XcYo4dBHdu1i$%wewIh!Fj81UbRJ~9c+c#prpaAK zNpc-t^Fjc@0jUkyjmDGx99@3-5p&u8Re{|Y?vfR0xwVUuh!k7M9n@%vTnLhmF|U-@ zqVDilG0P&y-TfVvJU6akJ z`%CBCr`*lOL?7s1bex5^)a^4njMP}sW1$nT$_KZb%_YBGfA2AH+?>$B4Q#&p5(hE?#juffB$;k6G2+Bxkp)ak~F*{}}U04Z0F z<$uhNdnFg!o1v~6>LEzJS{x%BgyeAl>kV@$Q6ZS&7G(*P+BJtRA#9@IJky) z+?^&6;@Vmg4@|7F0zMP`Btl20L^!>Kpv%+<+H~c3u&1&1njGNj5L`roCCPtTEe8$@ zD1a#0ZRInHSEdWO-VW{>LT3NrhrrjB@JNjhbKcN$ey+uI4Mid%ci@=^%@-cOF7$Pi z)KVXUrdvZWEFQwKrK3;-DT4O4IDnYp$1?~YiZhLv~Nb4Zh{Zo<#aL=R7q-J3q z_{DXI9)bxH`q^p=#)hwr@PoR|0V{fs#_HFgob(Z{)?J@^FQM2PlH$+RtwK!+tY@^_MI9IbTzRl5hv5XW z#4);wvcv^@ug?#)TNW4u1y&M?17DRdgK{*5+_w%>PO5Di1a``La(^<^VutmQ=ksQ~ z`S>rhuiMv4nF+9HJ9-QAR_{hfiZd+g14GUSyuY7>-UnrMvDv+eAmtcwwRggyVF6ES zUL25og222tIINT>NdS?8~ZAb9+q|01M(rwf7d-`7v#wuCFw6D}>NZ`_rT zle+Srf0F{)BlO1uIbYZYFc6KH( zTiNxSeVZxx1zMRtX|CDe+e2s-CSO}uqhu!2GP9a^ZK*v)ZXTXoBKk+VFqmK*y;!{I z>4Rv{he5DwrJ)EO{o(84KeYl|#OZ7Pv(<7|MCaMxmagi4&e^e**7fdKwZtNQV@oC2 z?B7qW7>I&mhDmvh3s8?QiypKIPih*`wm+1aCo7di^!nq8zUTVPhM?ZZX786iVE{%0 zarrvg+65UN_%=rlZimu9^sI3s7SXu^{1k^`^zx!<63 zRJQwYQg&ZFx`37z7hzUzH4=hZ1U5}C*II#nrQWZHLAE!tmYAsJvCvefPtd-a3$0}V zH(y_t35I%SMFy}$mP2@`_R`cr#wo6`5eI4agb@b;&b+#4fjm*u|Dg_)sv4kbs&Wnr zP{xg>>F5j)^a0k(k9Ylu_*l`F6h`+@zLu(5Kitc-p|cgivP!b$$CDnF4CUflc~uTy z?HWU=z-J-Z6z*`os|MT8Y3=en_K}!-%KxLNE02eIYvY!tvV|Hk$`Z+zWJ_scWV?l| z;SQPN+AC{}!3?rYb`lc9Sf{dQ8Di|DvSleXgKT5D*6iE7r|uu`{5$6}^EuD&_nh-x z&Uqf;bPWU*BZO`^ihw^A)&`IFTOHWvbDxU6QpHU8K;7={eZ_ma-d_B`24&;_?#j+n zBf&4D#jFRyY4(XucdEQZEHmt0w zdd58oui3j*IY#BnOIb>oyi}?+e2s~TqwSWqrnziX1@58)cJUIs>euUp=%u5`ot&)Q zit;Z&Gt<`(k1}p52O8B|DvxkyQ zU$$h-U(BgxWl|%OWN=Q{!{7Wg?DTECYWHd`0Q1&EK z%NBAQy-D8~z0T}y(jE^b#3tf23^8XA+BP9Lfi_hIypfaHb-pz3hb*E5N|=A*3BjQI z@A5+i9PjBaSRObvVs7k5pA#@xZplEETHQJ&c~)I|{)TsitDzJzqK+@y#nOcSmd zNr_bV5h6<)VE;CynCHs=r|b?7v$f^oYttzP^Xd;pQe z{U8~%!01{FIGu)ar!NnwT=q@uhqi_txOjLRYt zxwFsVh-%#Kx~u$5zV-IqdD#vVH_h7VKpjXhIoW`81d=S3&4rhJ8LK8i+Q}$Ry101}2ZD+emva+U(Fw8U;gvLfAzsoX6lak9pVK4v=)C$={3|ST17&?n z`7kZlWk-pjqp>rp@#SzAvpZ4`S#N1nH$c^{*!`mGS;XbN!+NO{`|m644)bXgG{QHf zf-0WKaygN(54w@N^Q)a^wpR_}Z|oJZ4Y`dnf-T?-I4y|wMEPKMq;fT!PO7U;!3K6_ zt`ARsnhVi@YsPlVRgOoDoIg}p`F4Y2ZvOpDzqsG$d=`rT=D?#Lv;VpWimTv%Ts1o@ z`ndL@G4mh6Q@t|zMqnedvIptd2+xfbkCH}WV>ZHgc~krU`lJR(g$J0khN$hZjaI95 zajV=j?j84go)QfQkuPR$W2U+aXP+9W6HVic-QSiJq~f+1v(&dZUH6M9$HV=4w-Oa% zV7qtkKx5+?do1i)(o8RfkUQ(1CQ5nhDBo`98CQV1eUC<4U(C9vC3{*fC!DeAUOAB( zy-ioCoZZrQ8QJ$Qh~2M;?>74t4Di*AX-2!(zVyXXQd8iE9k8>!a+Ll+h zb)TcVpuyyr+ay-VAP_7?ke#!dcevflhQG=8I_KI6=*V{dZ3LKLZeq&sSCtw%2-PVp zAbINTT15SrC*`=r=ZCaK?q9QHB#+u0N>x?Y48)A6_NDX1n`}^md=^8)FW|SD{Y3E5 zTrZ6%>ymZ8!v>f{@t+Hahkk-47LK84DQKRgE&7?Tb!Ea~zySv1v!TQ}|ITG~ZTdo> zb;vf3vL-|GYM(YA8`Msv!aL9EzpYbXv9yKKSW$#+O$P=&x(HvZ|#=nzy+`#86vtQ!o$6;JxBU()W+3H0bbV#J=ZA53bC{g z_PFKLBNs@CU}Ys{p1YH>I7&>+wGoa|poHf9G*N6|YI89EQg#Ai6Ke)g>+?Zq`B~l7 z)$pCb7~+^dzlgQfr%wKlr+>SMq+^f_08ETZ9{8z_@SwAZcJ?rOU>sxxOpt;gP}dA)hz>G$;T-{<2L|e$S>JXsyxDiFO)bVs2NiE8(MXp zp433gMZfv|C6zmSI+KuuYL>d!(dx6=L zucak*-x>g_`hUtTB36UeHl7s2gu8icsW=~CeKq7iwgKoK>1xj=S~AK937nky5RplM z-;(H1^&Wm6@(8#$(y2%?m>ggl{jU8} z`lhjGKtn*LR0S1jr5Wp>_Z$z(@-cbx$+R4WCV*G!QsF>0GO!x`=b*^I!>ne<=A^6P zapUxNp3e3&#t>10X5>GwWrNWvVSev^JPbx#4rZ4Kt2EIIuDp2|S$CZ@K5onI_k^VQ z`tzOhjR|UcXK(e_-*mVVPTapAvligDCac0}Z_LEb2|U)NhbDM?E^*GO)jJ25JEibu zqYtP(>5hfxW0X~u)zV7Hz$6i>Z z>~-Q=mImRqUD$TSXT_(XXC;5q4y)foz|oM;-oQI1c)#2T2Fkf$?cd^k&2us=AFqG! z!jmG`&}l#4gDOjYu=X8Uc0h9vL7|iX&Zl$SHqg2@P)ZmN1mMzu&B`%%lsK9C~Is8)BS6|f5K-)nDpvQmx zA~Y6!4|MUfVDfw)aPZZ9hcgdGu* zVcwK|+?%l5NSe+|b20cnJNJ`D=+6W2F>)IwWYAjCa6w^W46>gj#Ss-1HKRYM#n%n; z(QSr@ua)_a!JW+n!T=r`S{LM(lg$t=o5?d??b1*ga7le+3XVYj8}Nq)$CsQ4ChG|u zI-CfAeUOB34zL;myI9;gnT6%@P& zX1)gsrTF`6c`ztmffQztmLDw`d<3pDSsn~e2F>H2HBcQW^so;6FceCJ80>)|bl~8d z9!yIQZb<%TJ;jUW_wR;!S~_5Z{Z0_bfq$;if+=4A?Bh(qayVW(hDfe262c%G=#q3e z|E%HA0)rSVzyCZ=OGgXx&xPPnf7(BvdeArwDoyk$33p~#Kubp_U48H=gR7@Yv!k=1BrAdqo$2Mk@n(P}UcpdHD4t{C1%4;`bHR5{ED=Z7 z<5}AUW1aP2K^T}G7h~ZWY-z3MjVCa4ElD=!j%+VH-VkDIhcrN%VLZX^XeynI!g%@x zgn%8KLYZ(9-kyrHu=ciM=%Jt-D?1L+3JsK{5m*F-Uw}On4Q}GleNi@|_0Cr2I7A2v zjw1S4QFT27DHteE&(6!5gY`8x_oN`89zHg~EF25&i{Qa&4tOs*9)=3$*jpiN+@ZEX z2&N7y#M0Tv#?r=}Y!9LOo7s5<5Q3d`S+3+n&`%*K)GZ01FzA_<`gGYSPlBhu~6Nn9r{ z2Eo(I8AnFJ^hoyZehi8Y28psod7-hM4lum6CBe$t$5Nl?5oi_UPVlw1X7bp03tbwU zinO%0vLO3Y%~5nF#feTM(EYiAFdzrY+K+4F%OW|@Fb+(fx1+uv4Ffmu#4+&Jdf;-1 zfgz3NXo(q+0qBkHDf^;WIY5C zXN|E4@UyjH_#p639%Pt5#S%*4ng`*DK?HMP#b^W*;U2)nvOIO+?qm#shzfDwIa`A> z(zmz3>-dJE&Hd;;C^Fi>(mQ}kfco?FU|zs<&2;=62rwKJ&h)Us+LJKO4weSGmMkk4 z%mZBHu&@{egc_!d%yi<%fnK%xM@3-X@e{??Zzib@hDMST2s|2Ly2k z_u1)lC>RPH3JLZR`8ZnI^1hT;JUUuLEJlT%KMEFx678H9Af9p^>o#;SxW|Hto zI@jBghPNRygOM0VggZIV4DMwOr#S`C$yPjDZ=Pkap%oQ80cy_0GwAwkJ8L+>%G@9B z<>x>Su_fYcL9775+YI96YX}d3dFZ0q4qhMxnz5%$%EQW<7lOa?cM_Z;Bi^Qams6;DZUycM4)5g~_NQYx+PQcrP zm7Zoi4?{C|TA;hGPN1_W6zC&(en=fhdp|wVK?HKB`hlKYLt8ch?xpYH;AyDi6lwS$o3ao|a5sC%8U??L=aQI0SH6 zHa6}aG#k-w5QiXLLq}^Gi347mSrCSR^ntoNp-EIAoLQ(djbujEB_kL-M|X}6it3BU z+rsUvL%q1}_I4g9oClYP@~8UaEF2KvS?CZSbDpk+Ss>Zcz=uJ>!7MF;*=#=@QUHUf z>(91@hw2jTF$@D8hBeolY3AtdAB;x@`EdfFaAzxw2f@x)=dab^gP@)SD<7|529ira zAxRWE+0GGehviza{DZ6zOh1$vjYlwpq6rWkT?Z#T+`&oLBf#B37t40{0b*MLn?<86 zy}^MygtBcM96jvF-VhKEyde5mkk_zgIwTsMYl($;22$*H@_&w;~%HNw3DY`iJ7fTb_kn#^;u zgfO_lXb+HpeL}rFg8eBZYg-6Z-`UR0-`m;F*V^5kLB`qJ`00pT)X<-T0#-uyw6M2i zQgK`l58VJ@JBHQg-UbqYpNyxCSTZV1jL9S%VNjNOejF>74FYB9AK>X_OQrbI-0gfE*p50}U0o}tH4n;hV*0Sm zyc}2*3X#mj*pu}baF`F1K%oZ(_?vqM`3GCT^)ZIlL{AS7^8jZQ(GX)B$~53Y&}Mc9 zBs)9_;~0q1^QS}o*?58_+luOI<7I8*h=PW~sGgpVFmeFFz)ROrpF{Grg;V^1$@np` z&OuZt7tMxw!K_8%df}Xc>>Pd4>2$>0o1tQYLh@WeXnAZ$R&CnJ1C36H||=i)qI7FMiK zCe9gxuy@dP2elVkR6;t@(B5PpZ$p%gyAw7TOGZ-3L7|4$L3GdxfS72*qXaVp;0(5h zy_uO|5E^F?V5W~`2l{Y$9Ebq}Zp+psnHxgrq(Gn<8j5w~VW}`Y45RA+Zbuss8TJO? zCj#^b=Ab+Wzy7B}|68tuzyEDMU?@SQujRQKS4jvcpF_N-oa1-%Vn_dM+MovVGzRsHq)EfcmC`r2RLHb49G z=ahel-;4fdu%8j%ZiV?JuD!cKsmgALvGGmUs#8KzSDjL=^h1gJVw%<9N3!BtY3c0i z5b7i2Q$@#Q;y8aEPrbROz0UN6lelA);z>#Hqxz5HSv4d0V0=QPW_8;{{2YxwkIeb8 zr{q7I*ls>amruO&5?teW{*P-iLh%<;%7W##-!j5ebYkW1|Lhu-Ws=z?hAiKnb~~$A zKB?(i-G8Lg;jfw5b`tFK6UJZ4BK*4azeI_fi*j8h+UHap$5i*A+s3~-h*W7)Z}W)) z`@C%hLZH7`xr+oB$*(L}#go+m`~2DpgeVBN_*e1!V#u&##p0rUOo0&Fu4te8M_X_c zU4BGQr07?#|8r&fRoYgjLN}Q36yC_cpP96>RQ;y7=iHK@O8ZiA^4}+zl6G*$KgMj= zT6rK+UvN#NzIWRm2w!FQp^!JCQ;0ezt;0{>|3oR?oj#wvfR=sR=9l_trAr%G{--;& z_$=5*0w*3fSN+gXw8M@6jU9xOZ7aHm5*t#~v7g`%^2>LY-946-{jMlT`{40uWZ9(^ z1GZ|CdPl-3^87wY$@=U?2dj>*RcO1gyD#>AscT2h2p=AOH~!dm22m==2p)V7ml3*N zxxL0q^5($}(XMvZqw!WJH8@y(LEu2HiWC)%(r5 zVUyejN%#2vtd9yi#RE&jZ?tUYZq-nHk$6M!;FyfiZ_keeX@qTh?w7WCEW>XbK*gB&-zV5~GJY!kaqo+Ia zTCqi$CZAglZ|f=FB|$zy5HV(Untu z596hkB0hP>#LJr<0FHcV4-TOrHJ2F*tIEe6=VS(NEWq8+x%5NnkmZ}m-qDSM8eCBkH)DgAgadYP9F5~l>y9Dfw)v+fVWo zhCQ!8s#X@*T)eS{B1r^}SVV?a`2z2`<~h5Ntt>FP+VWg>d8@$aNr6>*2lV&2^^E0YDqonwmZJ*B^n4MMFRq;Pj_iry$tk1s zStst`j`C8r!I?qy;jo|+!`^1s^EBYVsWUMhvNgx^4(Y^k>^;6BoW$QtFG%p` zRdp^lgjYp9DN48f(yyJhxb4~LFc3z1eKuqDM1f4OA|remainwTS?9>Lw~;7`Pq6&t z&U@FiQLm({yVVnwnpaN8&HlM`>csO5+uO$9Hwdg=kWOzSYHwod4NU%$-lEVqYqQjX z|9Yrf<9$gAqxe(ijW;F3b>}oPBdVvS-(@qE+FR>?dYN;ZD|2PP{iAV`9 zOd46f_25tC`%yd~(R-h-DTjeX?S&a`n~>$-1kO+Hm?D?H|2dqUaqGDBh~)3=9WAPZ znz0F$8fGWYf3ei6n5vpw!Bq6!^YI~qg4lNF8;oHS2LpPokBqo!w-Fd9RwIA4(J|POYKehImOPEm0I)$^(E& zzd}(lr!4ODNBrCzuyi|;(5Q`R&uOH*D`ePgTM_!E&DI|C?#z~RdBI5_M?zv&j)b2- z5<1lSHxq%%0AS|B=op0xdE}gQ>IJ8c3o{2l0{%&(b1NH|z;za&Nn)U5C zvx(S_Z0B0W$jA*{m$b&{_>{Z!L1SIY3~7clEm0^Od>|zw?6d1j8;0_acWR}1erat> zemX1l-YVha&+`u6XW!mQ*DuQYROZ*VFq#k>&%^c?@#!s_z5DVW&+w$ubUpWfc};T4 z4ZUd;aRy0eb=}RM;`cufwTWp|HT`jIsz_d4?_5*rJ!`$DD%7v_ia9*l^mg9^%+L2o z#pc#a(;90~%CebN{cR<>?3(@g)a@T1Yi#3}rAGZ3s5BkDcS>Xb>D6P@J8z3_>}IW; zPYTm}AbuxltKiDP`dKIOAjK&*-K~_P`QGP;oANDDOPUUCU2KnRDI)AW?-t`zOI1P0{8c`uR&HJ2pDfR$qZ`{+D8079 z18o-Ddh*qlD?_!JJ^b`1tm_B-q{M_4mv-lxuiJ#r=iJ7h%l>tjysY)^>C1Rxk#)ef zm0n-S*Y4VT&3bup?aBhZv)|%v{w7`EFgjhA_oc*s3v0ZZ6k{;0`s^p+lXhj^_fnO$ zKD$zut}86lhl@3HWpi+&w>rMi+s>X#)k5yQra~xod1n4HB3C=+MWkJa$7_^yy!V%zWF+2^xOON0>sb0 zp@ytsukH48_qUF8|NKVG^H4aipb;nS!!16DET89>4ZV^PI`Ypq82&Qi`=rLFe>zZ| z!Eu$lAEhUH_1dma4!D$`iubz}cDZG4Wcu-p<7mUX`FkHO?K3_)yZuP)oL`33WTng5 zClePkZhjcqk!cdy=R4zi1uoGP7SU~I{66v8qRAp3;qZM|Y*T9L4ioRYP7_Q)1t%?P zj#qlH<2X0}&rhE>+tl94ysY1lPG?9#k;wdV6#yp1&&7ZfQaG%XJm?!IceLuOlG{QKz=(=f;u^pbN zLDN2w9yd3MT@_ZQ_!W~Fc7ky^@{z|mNxpu;@f)Yo{v>f_OQz4#T6cvm>iY?hwUxI} znB-dugAl_Yk00m0)V57*@`N%kf$(I(>;rYgSB7v*1HxZ=E*ZMAQW&L;FoLJ-nlaaM zICwSp4T;sh?=Y`3{)!c$cbnqK@dK-+0><*?Rt^<@;oeJ*8s$9btK+*}gWm&|;p2Mz z*kbojQo*0D+ZGU^5o5kmLqhPaRe6`Ouq*eWGIvN`eG%hb?$on3d}sYbOf1LOPt72_ z@MH6$?erO4#v)W)*o7<3Pd#wyS6v5%KEDoj18xz=>CA<*cEoXh^5x4>TlX}o`kCd&jY?T+J!h(Xzz_(i#dDW{KDIKtN zukIztKmzBkN7}mOI;usF-i9gNeZcpke;@xtSSDYoL%*}S#=>?}(d{iJkyhQ*jrQN> zWbb;F>F*-}QDYi|FB>nMeIOq(bvDrB&Hjp+sfHy>Af%$z4Hzzt6BG<*Zw3kV%iQQo zAfzDNIMmd!S}C*F{@0*T>g=`Dq3*$0%tvWEq~CGV=)S{*3(x1(Fr%G$8wdU@U42nQ zHO+Nfvu!smaUH*=O`X0aB;p0A?VhTOGH-#D%SL25q9(BHeiT~r_f|p4vza$Xhv^?G z9Af!XPuGejT1r?{>gLQ5Uwa#q z`Yc5~me7%VPL(|IGP*IOI z2-bb1!&kXm6gm60ZNhEt^S$Zqk`m&AO<8YN_-%K1}BXJ33HSYdTRO!r$N=lmq(VLbN`7y4>~Z? z?oA|jNox{V75w_J;SOZ^p0`7#77)-#nEcdTr$E^20`Vz6^& zAHDxM``#e$*OHiwa4-3KR$q0PeG;@JRI#}{Sw!3-C%Om zdqH=3oq$?*7V+iHHT@H?NVSuno-lW3s_hFNQqs!Md`NqtHv1F)-)L)*ZdjIdBrif{Gm?sGMLZjRW@V}#M|;P1a4;iZQy$3l-@)<68qocKUm z$~Fu%Y$bYbcUcFj+x!B;ApCpq4WiP2m)=QwSKjRhrFP0BGH#3?zExnqJ27ka$MW;# z&TDqcU)C=hD*`8YabjU8+zAMfppug1QhHih;9%9zR%8Qhd~l`aX$Oz|ZRAZ`h7t3^ zhd1+MbMqPIZM}_y&s{W-<$iX7nEO#hK;p53`75`;lRu<_9Xw=&g56YlNM3*KH^j_H zY%1*-l|m0W3{0m%vf-_>_;vy?rlB}@I*ADa&-+UdzR%^vtXo;VXBBfRfU|!g2iQ1P zUjrR;+0$7dj>=ilu7a%Ph3R2p0eCbx6Lp% zG~vx)$Mcc^vY>dtE(V>njrL~Xk#9HD`@{XMY6kwtD)wHR=qF5`f+PHS3$%-8Jz2=s+uBjjF^`1sIsjBBVAjY0}(aVYex^h1%L!|ND zJn;yi@s=wh*%pt5-L<>3S|_LHMcB1C`R%q*pMAQD)}4hvGLBriIn@k7iefjlT#kq@oZGl3 zJ5A4LO)&3urIK}50`t%+SfvpDPH-BqNfX3TB-Y8cs~MrA=X#z6b;hjQQ~=%woFXN= z&HTzihrslpURR61%D^8la+)y7j1Xo-9OuELoL{T-LY#z*a7!8JK)hFhbG~XFNspWw z5KmHuocY>(14$}&Rm_6D;LUE#tf05=l30D)FeuLQnwCutCM8e)=)y6vmD+fDN&IBr zi*!L5OM6F^^$jR7-&XB=_v3vJoFB&>37N2NOjgTDP|}t@x!suiFe)Ub*5J&Xnh|F$ z?$>B%z9e7!#M29La%z?ig~XjPv$;0u@Jpa{Eq1HQs9gN6S?F-@>eQ1kyHt^rWHi65 zlnoP<9;EjI&;3v;+nT7@W)Gao)u&OX!Bp`I1Y$73)*ByVc!T)gdliFfy|fTZ#&ZDeb8UwSR{^oquZl zBM3U;6;a`a&GicW+eg^v`%Wz}6`pG#jRv;wgp~R9W`pd5Im4+d8E%k>Tr=g>eEs6$ z%qa5k@h3#ZuQ{6=u6*e9be!Y+yl-t1ZWTQ6Rm)J(%)AXwe38Mx;y~=7(deYZH;%UX zZSVB!A1*=z?^Z zR}1!XjMDauUwduR*0P?RP(>KI_q1Pok+0U4e0~1+ub-VMrYrm5x0*mHz2;2FHvKJ6 zLzN8!bFb%jWQ%08KT`CVH_rH<#+`U(E(uWLM~2N$ zDB?yTm09(x=<>1=yGxSIdjH(|{YLrj2(9^@pSGmJ+J%z9>An0~i$wO>SMugAI}%k0 zJH6L`+|<5(?HIhA8@%gapK42{TFFR~ivE`DS9kmeYjh(PgAbDV`)N(7=eFt6PbM%L zzqSM0X*d1zTVrSt#D&9A$F<_+)Ur=J3v36n#Ky0TwC6nO0;gma(B_)Vz;IG)s3Vkn&zN-pTn^*F4y48tBcXkIYLVi^72-%+t>zB+PeA4 z%7~@9%YjjQDsy@imv~qmLeUAsiw{}|h)3(j8Auc3`Ze|yOB3Z2acy(YrRlfJ>9TKK z_*Vg%QN*(r#c-65&Z*zrm%;u$yQ~H-YG7ihTl7QR!lz@i#Wy&dnN3K?Rh{OXAXbL| zGCxx9_<+2Q3!-pY6xSpCE~=w^mzutH$NFmO?F@mRQFV3o$@^)Z2;8JM%O6X8>< zibLNS3kg|j&VK%vcOZ@2`=_Pi%eJ!?4mm$sTRYHzu=#6ucej2bxZFFo6x~+NKUE)o z(7r^9_BsR8PTRUpxKQ1YRu^9?aR0O5z&vg;1_SAG?|i+q6G>bn%Vq9~Y2<9p-Y9!6 zNnAK0gXQuE2SD2_1ic!H3D~T-Sa3;m=osx zedmtOdMYbp@!nw>p|@hfi9aCzYv#YSy!xY=A>|NOh!<(G+{Id zSzcM1)DV9Hqc?_VsRq50i$AkZh(*Y*QLhOZIF0cAFqNd~7 zw;w*9FkuRG1CE|n2ak|fe3o*IG$4pxA3WN3!0OEg*w8vjRLoX*BdOS}y>F~>l)c~+ zPyIMxQprC}tVa`Zui>C;hZtsItxreGx*&{NLJ)3II=hRj z<47@245B$6PmnIg=gPESsm80UGyYW-`KOY5DdppmK|roenPq|+-zb_L{N>e~Wd9ph zBtQIz=FtPkKgjF~Z|I)f`uGLPt2pMIimdsn9gxO5R@btZlV5Ppx<)mu?LVgWuC!xv z?Zvn`&n+=8Pu4PsxTVYcaCP>|Iue(1KgKtnv>9R>8cT)kc_{<>AaYA}$=|$A&iCF! zZ!CS)$5|Q+l$zw386DjrHn!j|<)NV>ZmbLC56|wgx_VaoeA5-p1ZB7}I%s}Dv1@R= zL@QHG<$f9S(_VKYg*WG#Q43~mIiPCH+BL5*B0XNtKgYRwTxA5hVQQV%^-q7+6UJNh zif@%c6<vhGyPU;7%nxS(FETC!b!P&N5fqci;F z%a=o5uP*Ey@>S7XmP=}mn==ZTID4Jr8`E&)^7==TyJFV+Nb_o5?~V7S;`UZn(dd|M zL552TVS+1Fs6)r?|2ovH*#jvd;f1hhz(t#maopjdS+jSu;iRctcTIeSjBx6DP*u7b+NX{WBuN0}#w_2BxoKeI3tdKVHkq0y#zAuZQ~fSJZHKMQ#6GmeW6|C*?o%d@B0S zla9%!?NAj#mdTuvnYM8^;`;iGO6p;XJ@kWG<^1%Kn_(xVcEet7QjOzSoznHbooc6y zDYQ0rN65JCP^w^DWWB3=l`-FRXQh18hinCS@`wA*oli!$M!f8dmc;;TtyRz1k~%Zm zna~*m(GE#*BlLGuU5**`O$Bbd+t!8;Ku!(SN2*lvPjtl%`_TVL_Rnm%+Twbp$s=-j zdZ9xa=D7KQ1M<{DolN~GH=3`uC4 z)SMhhx~iXDb2=6>u-xcnw4xG(+$dc=`QgFwdc?);k-tm0(muCWx*sG7Yp72q3`REh zUEX73t(3fyeZ;if_-G@)yfo&M^hvoWLoD{bmJK5t&4iC$A$@%7wnS!DUX!e3r0DRQJAMCqJNwE)aNLd4tAN#s`h zBxcChOCgBYgNo$F64sx8Ibg~);OswgAl;o>wa;&Ev@4!cwqDJ9b7TDldF6mHtm0Q` z!8G?BaG~nT$BiHf2LG;G`=42W++CkasPRwJF_!km8{?EPDMJZ^ka8{G%w8q<#xMcE z{(IuprB_4wrd5&W=lUzL$NgM4+8v2y$8ox>-lCcs3#N+dKee|UZ$dLKUd7=8TbHZu6WV~i~ZFznlCvNW8AO7OxqHHXu?mPtpm(xC$ zwyQ3YVxJ)+j8Y%}TA!!r`>s~pIx5iP%dtbM6{#PmlWaDBY)|T~!m7pYo;P?VQ-Yr2 z3Xn!M9;qHHZDH&7|Eq{)n>u?_A{E_sINfh1X;^!N}}m8s90EwR5-Hb`z2fahlE>^JaMb`6R$ zl)EEdF+2-;WPzIfNzWLBOf*1;*tvSOvU@UQwi1*DN%$*EGP;ryY68mSg<;tK&mR-@^R8_wwfeJF zwr{IotAM>2B^J!n_>yDWInUFNGc)SY7k9nTvt9C;or2~eq|uIu@9*wj%v9T{%~i|4 z9vc&bpP%SO9nu*(BR+H12ZPx5f_T^NT(jz2#vDMDp1%D}iTVC&IZfN`sDieza0lz4 zpYeu!ERPatzG>Jt^z_g^Jo|2?oUl;qLv)jjQ2lI!jnnD<()(lQltMV2o$2Xn-<7nP zhUe4seED4&{e6g5KW55YNa^gRJEsyh=C>$5xc$&*&4KX`r|FTn3v*1%3(sSQ7N_mD zWR8`uLH~GS6f*GU&DkG6P6kw?c=kNNU*0z4qWaF1%e@X7JFiWlY*&CqQsF?72 zF(`aLo^2=D;ocZOkPv2^ScNS2c@LO%XNR6R#k2RLw$ZC1Jq%2$O7+rWEY{EGS`nOeE?JoofLt)qIFaR-bxqCXRE%{3IKU+)@T&S7k53BJIK6J#SqTHs&6F zo58_I0CedTRn%cMpP+ZaP5K#c+iX7{#FcNedX(Q=N(LQ++`&PJAn%*e1Cc4eB|rGD_sqF+@*lReO)MS; z{M`HP3g86yP;uq(*C2{no64m?+-a!E%UQjuw?a4naRkA{W$w|w7W~mb>B?&!hiLQ<&7ZWr1)8NMW1s+D0FcTqDgn3uWNEmj}<4 zZ02z{?0MZEeMwQpvv5BeQT{#QBptLZFX*cJB4z)*ANVX8(FBAb6)ARkNZthSb&)$$ zhY#L_lU7lo#E}4wyi)7t>X$bmxf(Q$sqLT)jl2DBv|IN<|M^;~fpNoq2au$%b{Hp{ ziUve#p34Yjga@xC+$%)imMZSlJ=~ilD+oAqK&e&n<0D!E&5sxU%m2>ib;9JKpFOIl zKK&TA%FfsROkH2<<~d|gyx*dyOJEe(UR$(Yg+3pfL_8AaaPabrv@B}CxBF2W0knFh zV0YKLUFR>OZKB2QYDV}~K4mVHRwiCN@7iE3RAp|Glz z-1ovgjx+haH75)M@W9EDmYhOBrntQDTV=9hDz@uSWh&00WxTQtG(oJz{ajRwGJ%l7g>GPai^*=umoo{@j67bMD4ib zqBU3&A!%NDN-kx(c2+d>_CGYdB~ma^9AXG&?{ppyC3w(DhptQlKpWg18kPsS$>H#aWY#JY-bkF4cgP_`X}6P zZ#$;QA6`p7YUq$2@z%f)fb;1|5_k2GVLP1Diya)nx!04={)G({WhHXG>(*yCVq~kF z;yAT?=I)9UkzxM+I^uoL3P_p%l`dnYA7j33Q!ltGKR)J3Cs88fIF0TgY&e;-3O!ph zT-Yjp8=S$*-9%db-zl*lFmRj>%Bg_daH$UraL_|1DhIFp4!y-&U*Q+{`4s~2NdiE8 z{8v8d*A4K(4$3r%9M1jO)9AUgXV?B+@84P5=9k?IxbV&C@pif3?04OGdg=B7;Jk)m zi5}cIP8{b}-=8SkI8JLlILQ-6DP$-=Y`2O&6c2=3RsW^v@VzKI`utZ(r@dh^q5x&f zS{=t>nnC$KojA6S2*(Rp6%BY7eW#NXeV2go-P2hwT6g}-P$8HafbZNYC>_mzt|y9f zPfx!D3ses2gs?0`T3(O+M@!J4SsYr^3ohk9sLcS>h}fVbA0`#Cur3w}{-a2Ey9k`u}T_Qp2| z{fn6*a`$lXHhsnZ3Fm(>t0Vw8Zamd|NdESk_8g@WUHR8-6J)bE&ReTQqmz<=MXTQ> zio_w0PE;p#KlaMMdieN3GTt*(fJ7ok$BL`+*kd2gZE{N3e88+WPWG{3-f&|IZs*RO zsX7$1{GStjFN^>nk@TEBmNz13E~slOy4>^^U478${@<9Wf3+7SxLb#=i5M38njVRP z-k0UhZG9D?L6G6W+IV|F<1hgH^bQon0M)&AeGG_G05I{*n?L`4eH0j7>sq&ag@3sU z-m;94tjPBZi{p$5;=H~ZiDF-ix<~LEzDVSS`i1xEtqp+1>eo+5;>M3hJE_CmA8!Wn6^Q^d6#PRF|?*3H0fqdWmY7 zXEQcpaYjKsu=%CB0K8P&kjcI~=I1uHpWC#rfRKX@o*5yC4}-Eg=ZYo?xXfkWEYr2C z1dqMsBmOMUr|!e4E3$SA9Y6^2dDXU39Ou|`miOxiZE^f@>quJ`a%S!mD*@vS*u~2K z22XA}U_C8W-_PoKsvvA9{rd5xUTRCO@dNw(C<|_xh1LI{)%hGh zb!#O~(mS|qd4BDAn=!cW#^s@<)sn8C3#tH7n11y4RIUE-pRp5Xa@rF6zct^`aQ}2m zvHPdo(>3^X=YhHtfVGSG_2t^G8OZ@Zc_F|lAj>~nP2DX4)S3+_&ySfAbK~9b_zZhN zRbYA2f%KNE?HT+`N7i440 zS`EXV(r>M|Z#N`v&3S)68*oHg(1YJ&vRp1&=Q)5Jp1cwbK2SX zHFZ^xOBKvllei3Hh2^GhTh(PbddjAKE$P#LB&o+TwpR65MY-F**lKxrF8>bXH#pgTiVy!4>QH=dZY2DXK+ z2$)uGZqF$$Xgeho=rvu~xymGL&blXHe!SbBJ1Q=m8hKMP8M!=r?PKnlr=})g#8v&^ z!Levji|hFw=xsH+f0(Nv7{nv2A;xJRNiqAbisfNa)Y}1hRRFl+nx)YzyAUEEuf9#O z>FS0*e!T(s`F^*0tR=0fYvZzCf2GUsgywyO$Tj@oFw5p|{V*y(W4)Uul|@*o+*>gx z40c_G&@;9%Rm+7O1@ADHsF?0eo*c|V#@m7Ls)#JV`9mSupzhJ_qY-IGe|_H6uJ0$Y z;Z1V>)Y^Q6OO7ZzN^AZ_5^NK)Zq=;`f19Q!BIMzQASY@6C~ay>)+av?9<3XDYIT$| zetiKSFf*cgkUtzS0Qkgp+fze-epT$_Z51RaYNAC{+=x=fnw||6+-b9W&L|K{T|9{I zz^u@&8`k$e%hJD%LoS8BSS$2@x`UtP{l0LQZZ5q&Ti5&D{)^ARY_(ktW2khKz%Z|O zPVL*PH-7SCKjLGY+mh3d#1#7p_!cpxp06*V!uPGJAf)mH!k_%;{-X;6s}!UXR_P61 z0hVMqEBtir2xpw$^lb-F4f?-Sb2)STb*)yIV7NYE-)>ZaOrI~5zp5iZ2{;i*!+{GU zV}LP!l2j1&^QK`?$IIw7sFC#(Px>R~T~zq_);H|LOq%*5{~Y#HpAoo#nrXH-s{&Yg z;DhFZQCWH<*Zokey1Kc=kNHfG`nVebH=aNY!V>QW-rJs-gAj zQ57)v%zkwF-|z`=i89uStf$kP@Eslp6x}Xm?8`P|RqMJ7F1jfKl&$kKK-o?YkF+b* zP#z9?fWTLy;r7JWxn!LlK(TjyWdam**X}ig5bUZwlTILhm2iFIoOu5~rr6#&aYxe; zeh694y>uw7+vVjEWci~#Uy82!wI#Qm_I#U$l15bztQE9g)80Ni+222UVgspyQY$5H z;;B@3U?*b*XS$rrp#L4G$a{@V0zG|eMqGRI^r{0k&#j5i-DR6D^+xb^n*jL0ZonBc zu2JKDL9k9tC@mO7h0KhsS7G|EijvhjOKEIxa4Mhrs!=ue`SoSg_trJ?>Sy0-bHmq9 z9`W1k*?b)VrghuB9oqo~rnFU&4RQ{4Z|6Y}PWQ4ugXtl)^{s7M72_X^_cY!wKb{5b z|7m)Bab=6Bp8X_XU;Po#b=7U1>HZs!zi`$AMy1?ODL-3-D^Tz`r*S!Hzl;!F@ck*6 z0{Y2!go3%ejaOs}YQen|9?ma2lWl4&fnV6&xf#CCB5cAPI~ual zzR{ZmS=N_7EAyt(($X8Bp)TG(1rj~+;YozFynH^eolWrNtm<@A_qw$NB^9xfb&`LQ z&d6$FyW`&(wIts`MQ~9U4{cYGFDa2MX&Psce5OZcUA{1GL;S={WL|jr#Q5KUUi$Xp zWQkq-5qEkBbA22~=;OeUml0ZCf#$0t%MFiU02c?!{ro$PV2BE9Zz7E1U_wj8$H4@L z>cCUyaT&MD@MqYpXeubKw&PUv{Wkl{y+TWE;KE!G<&UY10O}c)RWbD^b9TwSJxu}w zRiDhvd`NAl#@qY^4}aJ!YzpKz9eA$#vb-2+BzLkJ{SH(xbfxzQAVb`%?+@+&Ik`*v znV`qQUZseN#vf7+`1mZB(^##->vF>6AMP3%YUwi@RwbT^W?tCE>aUC#O0`tAT~&Y) zZ`Obu-O;DWH!`~V1Pn9mk}vyhF7CPnW@%vja;M!-pCOFP%71)DSs#g*tLXGz!(nEB zYt29(aYDq+Kb;=3Ki6@kfP42m(crjy2vTxpth_~-okI#4%|qau*Giwy`W^G!Y_$K* znU0jSM`JZof(VMnmA?c-abDhC(irGh99HPjZprg8TCwpX(7X4tzKu35!)9!+S`xLo z(p5h?YTh(y@ZNF`x;>fk@BA3b`{WV6QB#5}J^q`@F)gLf?)42vRgDsc2fswG7Mo*! zdZOsMP|%jlKV`5-W)K42b8OikkfC4y)~$|#Et%^UO#Mg1;JRH6~|#YHT`Mmnf|_7 zSXpY{wH}OD?${S5P)h*y@yTiPBH$lnC^b{9@wo&xN1d|Nj@eES3QAE!p<<77Dn3SW zer>jFo&W{rP_;_6-o`-!)_zN}iThYyDI59)V*4)e;)szgn^7CeS+Rw4G%BeejT=N~|Ci={@GfWkWF_IzG zM=Hjrb7wB}$)q@gBI0THOCHi2dtWog%lYn!3MI3y$=-IRA#U4{AFHD^D5z zD!u~7^#NHg;QMCV+h+HVWG;dFZr9rkO*&@b4Hz9W)AN3}p|J{}mpPwH#1$_rgPAhX z=sN1p19E9LD1=Vb$*vj@A9)crp9dPG4~O}~Vo~5Vp4;?6{b>09K<03@+T%<${q?U7 z+CB2HU|tRC+@`Pn@Z94wHsxDkp6}Rl=c$6}hUE|sUvtTapS}gm@8xTpkQPvWK6c3J zJq{6)tB+e9cTu|A)M8+azH?HF)CJ4y9k~0w<8OJitqWvDZ|1+s~)Ab zBKHE3c*K1`+pTiEqci#8h92y|luF%OjW;r1eYWt|kTW=;rH7XTj9p%E8=fqvzI^^W zyDU=;7w&JcS~_`8Lwv$Gqw%Hn)2%Q32hSLUi22Cx%i;kpIjm?=*x0q-X4e<}doscd zAEvog97lub(6%qW(O}EDESHx<=S~35zSN@uQU2ua0>|fXZgAAQANX`*(eRJhPJVdH z>Hv!yxyHh&`{hSGULS8u{$==+KaAd}pS$*bjXjuHoH&boRsryL6Ox+xAK;4x!B6B5 z1pVmoGXl|1^Wt5_3UT306Eqa^7fqN97PYzGpC~wd(haZrXYlmiE;sxVBgHp0hFmq1 zm(Sk)D-8e0y*Babq5OzI?my|^(z9giuxvA?sLK*(8#^_sVuqW=pa;M2;1ad8|0f{5?*u z*XR5FeBYn%@1NhVn_Jy(Ip=v@&+B=O$K(FE-|tUKOvm$c3jW83VjR0NTEp!>7hoQS zA2LnBd4|z%*sUMmog;@rMIEBt18|lWhP&U`@I&^YV7B7CLX$+u{Wd@f9QMYH)8^$2 z#Z5zz1?ox_VaYf+Bck-Y=2r!8aG>x2G6#G%1ytYU?(bZK?Am=xBe!I9xyB%bs5GDj6^wyW~r z0?yOYUZnc`fudEFX*qfpFigsp3(*LFErNF}$7D_8#FF1zj4`G02ZI`~Z%ce%ph>;X z$>NJx>vfs6=jC3SSK8)Qkn)b*>Fu0QPPm3n?T){eRPOTvA*PPV*qh%t`&Hh%d#u1a zm(dY$pdd)n4v8`D_fCd-%tQ_N-QRl5*3>%s#37z4AiCHibK=a`F4 zD?v)ruJ)tz${#Q0rEBp_4U}!4lLU1KyOT>MUjzklZhmZU16XYIeJdW362Epy$@KR{ z6mR2oI$plx6|%;Q1%4z|8Yq63t-LQ){6#@?8)xaFb02U!d14rDYq%!x?)D9QC0oeL z5Pi&IJNh(YBJc!2X8%f=+^=87PX(%ORUM|&*hniEWBiGSIv9votyF;`s1htHzW8fMe+12wPCj1o%kumZfb}mJGY`h)Op@PytNj{}skxs@k&p>RvTWNxj4}G~9MAGId{#-t z%W-<GU+W?t!pFZVMet*>fqT5vtN2cm4(@@QL7LIjsi6n({0yceYB@TNdI$O0Dz-VX zc~Z?C{u_!(lGt?-dpTcBeU33hmx=airW?lf4=;s;g$)(4-7OMyT^O7kxO0BTTCCC( zY5+<3kMQG-SmJ2mUx?eie>)xN6oiAkU*|(Gh15EbOK0bL>g-32e<~WCWt6dDY|&B_ zaqW|2d}(aEE7Lo-A5hfzUtEA62^Yo~U%X`#c z2yEY>04k7|MKjOdxMG-i&8}&%{Ak`&d(q{$>ExasdeD_e@@EQp3=dyOhrTXaQ75CP z(-os7W3AnEv9ruD0yb#}BHNCLVf;q3M`26mChfKLsn+aG!X0)W+-f{~;15a%8;wr` zj`g#x5}CTTZRfoc`KM)8nwadwevQXe4BqM-I{GQ8|Mp-3A&~j@0z<$4i|07h$0(`C z7`N2p?(>2ao~SUly=U-e@mosmF#pZuis@DbP5a?&Qu~pC_G!^@?@48Uh1a4EjK`Bl zuTBUr{(3RE8dvdDDOJ>y-JN-d5^K_!EWb8g)OSgFu=Cdfk9K|8w(qpGzOcD8#{Kto z%Jgkwhg2zz?dtVK!DWeuL_3bZ&egHdUppG9b?5cw>)jcdZ2B-Xm2c>DHD012c^6>7 z5`*@W!mWndNxwdeAowKAGO{NdkapP5v{OC? zpa=N39b@&{(|GwN_e$Brp1EYC=hL~NJ2Z@!fa#&BT$zk~l!=2IL+2|+A$sT3=;?2S zFS{+NJ1mdp8MvR zXtQX0;n|;f3HuRMc>;`mqJYW|A8-Y5{-OIaMkc4yxIR14-EJsuhuQYqFv9KeS9Og4 zL9&7^*{5+I4A-@LXDKkp((bACy%`EKG&9>;dD@7z@Q3oPUJ&)`JFm{OMQ|MNj;{-Q?AXmXIEi_ZAMOkZZSK}43FzfC?vSF6 zy%wwH0@>ti!>DqEl%j4@`VgX=(gmX<=Xy2#Gj6K&?rj&)wUyQIz0IZl^pTF*sqK<> zlZ35Ga?e0thN8cw&o0gA%PYR#MZZRfF)cJh6&|>=Dgk{1jv?Ce)IZ@QG8X<{(SVpS z_z;93>zSOdQWmQ&?kle`gd8%d|#l)m=NqGL(!;jp*qXcaCv}Fb{oigEV zVN$-zJX1b^o>>gFy_CfMGB}pcp=OJSO&y$U%fDG|sgf$4eYjZ45HIfKV%%oQrFrj7 ztf*F&&1=0TS3^6~N~!|Q6d_vFx|B=&o@P4t2kZM@m=%KxMatI(42N}#<1x#=cTy}! z+g-$+%tMeZ(|M<{-n`50M|ep{KRy+K(EDJHJkz|5i=xeqQ<+6~F%vgpB|ERfb|cL)rZu2@h^$*}ZrW zGK#y;T&!Ch&GPmgNtLeHk`uO-Y4yHRa69p~CninfLG<*zZtlZNdpZ6x)jVa#pVUy+ zeUCTML_At7Q+^%;W|c11f*wAwKa1B5dEG#@>15_ip3L$V?+t|Y5&O5?4OQb5Clr`~ zP)Zm*Obk4My`{(C;!R|y?{k(C`|FC+pLpx;#ZjJDA!IGc1^(e?13sD|vo}*67Os!J z?snaYS^qq+JugFHdu$dNcx>6vntJQDAGFHDltgerA@bos^}n~aZv z>^_c|tfXMBv*R~6Zp;#72S-02PubOZ=uhKAjGpmUY8+~%yqIYpA6-Ks=3h<&GepO} zAGHjUuP8LR@-C8M?XU;;#YL)WNxtP3Y)6ti8P$2+=>5Iqk8)h5uQN?Co?e86Ty(+ld5kH(YX#Dh|307aF`dD^Fm?P2SG=<8?kG)_<5@kcTjLoqX{byY zorzP5V$`X%>BSd9wwH>sl+V{bc>j!^kx`k6|NfI(Nx<;eeZ{F+41met@?8+fhHP49{Jb$0PxY^_O$^rU*YHmQGU-&cd%yz8+(zX`?eMg=j- zV{ss`R6VWI|LD)7aV3I)sYI8^-|H31ABbad6on(P0>|D z+|lH_n(e#tB1rgQmqn_CNOIg2JVR*n5r=g7`U08zB3FAv8uaezCWaSk=e3bSiD8P9Nz~$6iuAJ6==_!SpX`G*lPa729k@GyHyR4e6dFNl1#)+Bf_rBwL zRoi*>K$lc_-HnbIFsijF08EeEv!ksef7N)b#yW4kV7M-bjJgnHyW~cjie$*pFjlc3 zPy}6(TvAKFSzQcA@U`kG&D-Q@!UXZSG#YLsa0Vu95W_V2HK;di% zBznOMY?am^=n6f3PF}C@b`EIu29wPFcrrJ#ZyGn;`}k&T^^)~+={4&S`S*G`Al))P zSnEvHpGSxt@*7~gaDjVJ(k<+PP@X>J(Bcdn1fe+7FlxPx_>%4M=S$mn)WoX~>pH*cwOU#6dO55NJ*KC+7F9TLhJ?aA#0uL1~# z0kdHoz-z+>LDQlI#mYRW*62d%qC_i0d^k^qnoy>usKu30EDWCdct{_7Zo;thN2IVBhh^zeBldL0x%Al5BoE;P? zZCSNthGOFP_r!!V0b7vZqp4*M*xz}9cuHSOoQX&*=#AGncR?9x@y9jncmq>CDT;Wu znw6mIHxw^h;>DcIs&Q5PPlyc*Z=b8~t1=^4B5?0z%SzXH4cZ`DI&w7e9{ne*IS#pVbP# zcq$h>?a^N%w!1d3VBY8`bwxe-&YV$JtsMi)O=S8Mui5jTF&8)_Wh18e)Toi`HS-?Y z276MCO|B9wNzE%8sLC|)lqvhExV4EUmAb3(MHO*|6mP%HAda7Yd!l|ufVF(8Ipo9T z$}?1m)Zt2jD{;+Hz)pXz`IOhLRdKjDVal9mX{Bu{J!*C3X@G99&a64Kx`KvY4oZT z_$G_P^OV-rS`+OqOSb@dkphFQRw7Ew`tjE^+6C<8!>2QhQ_;aV?JF@q8qD2`qv0#Q2p7d5I5}t5R5qHP2CCMja=W+xk=GQqYA3Hokkh z(w*Zs8~2k$A;;QdU?|gz@yooPsoruo`NZwLKrQp;%ZJi0o^fgCNbD+r`*KQRz*K6U1Vz?A^gP+TMf?@3P;?x6bwqRgU?!%twp#qV`;Wqo@`nTZ3PfZ%8N zK&~dGzd|vkyc@wL`6JnH&kHTcuze3$4(&H<@_gBSFnJgPRQ2+CpbHK~*{9{nvdc-(x73TvwD`Fg5j077 ziqT@6XKkle#};EIeLf+I_7nLTPdMFpa^`NVg>%|yX$f8ruXfR5Qim$zAnd&VxNb zR>K|>hSN*Y|Gd~pRmh6=fMc@iRf*5F<`ZUP$5H&NPH*iN%5N*Vv{4{7^S{7|1OW-< z6t#rxsm=C{{H}qiH_n!X>I5$IkL82O=*JXtLp$Ep?rnCkMS0g0v82(`$b%w|)x;f7+cSk4^-?30 z7Na^(hD4$EFk@s$gavsMy*-r}k;^T&#WS$2YD`ErV#2Q~_yDcpeuatWZ|ikQN7{|c z*}w#0EeAi5nCbFmW(e~}pPQVml+A{9X0^7q^nh-Jp_!QiOXhE^j3_ue&4(^5UjNxs zmx09ady*%-k{)1@^FixcFHU7iJO=)^Bvz@QqnYKh_AT{d6rq6Y{MBvSX;x;Gb{y-D zmPvg&x-c^R!{hR~_78ztc{aU;DQLB_N2AAAKe&f#XU(9F`s2t}p3^^)tk!n=$XG5T zyMMRywV%;e>39+^dfiaIU(F1c9&fZ&-+jgA6Wz0h$>L{Rd_I*dMWZywUYJXlpT=wo z)U;i3xWkvqPi?K4smY`*k$niKY|eV?zSg^6ct*o#opTr(48a-GHoq3~7=q_5yHh^G zs@_{9i*omcn&-k19w5RSCa!;SgFu^8GJPTc2_K3q`MqV#09o(=dvAWT*Px~EmA`r{ zf@LsAxrq(UIc!TKZ^>a1C3*KT=tV!bR~wNs>iRRyZBYck6UXGDF?=CW@6&(NmK}{pD-kAzCz~7UltsDwX-o0-^ zHcyhAM=9uxjXSyusQbH;tQ!R2_Wh4|e4Rh#YK&IuO{Lc1v4Y4Pse&^B|)#0(u zg?<6oUBP5@#=WP<-_dJ)Wc1LoQ0& zpT~YkW40N=g?xd=&6b@Q>!fMYBLeJ;slgXi_vKr@%|PxL?A&>3)e?k_-d_9qvEuD0 zV$4f-VxtGZXt>zijvJt&zV@9g6Ahx@lQEJXY-d%|QVI9IsfK%cdM?549Y8)q1EdDh z3>km3PgjYTW4%aN?X2S7pAYlc>0tBEKYdQfc0Zu*R7^^fZM2@JcA~JL>CL?;m9#s8 za#S&Y`fuwX$GxyHs=@zKx%1p3Z zXp=e~ZKV2UBH^)*uRWezn{G1?{>XlxY=zbLdk^7+zIP2LAp9-Xck6&8%4rxGyw~yJ zD)X3_om7JEXKCM@=}?=3fujQ)l{eB}Mfjz~l^%Ht*XA)2D|0icRz+?8Yc*DfOuNU0 zMTVowFDQO{jE#Nt8Tgn)|EuC?AUL{MT@rilszD*!-frF=n||SuE_v@auU{m-7C8nn=@R z)tuUsBp@ID zwAh)CE`B?7f+s~M(FT{TAgW5^D=&5 zo?dR$vb?gx2^FewpRtS&z}2v)Yf}AlmSL*SbDX$ysAANo%Y6T#t=M4AX>8Xg8(WX^ zS5VZ;boJynmwHQ&cveko<358hKZ~^}yK~%rca4iHeB|qa7#~e4lgpjQvvQ$`#)(Q(*~}!oCPGeDWqvs@Z4R0f6C0 zlluw+c%K?5Z>4+#Gm2oO^~kSi+Rs67cjW5 z&bZz-9LetkD9c&Qd}zQfgC}}`c zNtiV%w;i)wm0Z!GHr=wem0@?_ZdV>&)xF=?26`+3qqrQ?8>nuU;81DcUM@=yQ8sCm}E^?@jNNFWLiOx*yf7t0}XwGf1_qv=36%-;BvLBb&r4) zT_>6gi6l+w)x}qMhQ@m#9o_cZRJuZJ=C40Om8m_RU;gh!fGT#;7zOE0KP9nkOjb$Z zm(GfOF#FNzY!=5Gda)ob|HUE_GgB_)55@C+1rZ85>c|4}U>MKjv zfsQGqh_YpydWO8UP*;0l^#9}cp}Xfob0#Ir6P|AinVpsLK6}Vs>51rWyh$Un$J1)? z+W)fhRgA~}<$p*R$Lq2+|7^}jTw7NM#dTTmH~KZ|vorCHTdGAoCZ)_!@af5Mqq$mM z^f4X?re_9oeSuc`=ygELCzn1d0q}V8Sx@ z&HHLGOJLt1rN12`4ScJre{^7BJn-x|a+ToOiS=K{4|BOyzE~!XaR4KSlz}IB}R_eoH{JmYI2Y_o$cIm%Hu3L5=%WSx~Dx-oYqR~rN{f^lgW0x!I=4FmU`0M6UVvMZyNMczBhI*@4=Yq9)@c@B%oPH7$s z)O34mWlbnfDp-?Q0+?hu1DLgdDvZd^+|z6@Sfx_d{rBM~g2_cUnyq6fBh9cxaY;S$ z`()RE^n-TV(&6rO5tnx69pD=lyUcfqx|W$XCF0}m<=yq^EXclp3=C1lhH*hU z9i*SHN^@Ssj=tqrpM-bv-R&7ZZ6IoWZ+~%9yNRiN^r0=x1I6up;6H^TwMU|ZM89V< z379Pon|ybNZd{1uFhvII6Rj`1H(LCq9k347U?Pe)>dk@VfJ6Yfm>b+B?HHojWHLn^ zC%E4=!N?D{`uY%lgPee7<_7Sd`S^@csssl>q@|(22>!u?Y`~N)hQvLWO=U(9KrEVv zLO}Y=7qK6?v(T5Z1OlAK;02I6`FMoW=^QU>M*Kp?`L#ar`2xDux+SQ*`-k9II&8v| zaUMf+MKvLqajPGzg5H(v8T&t==KPFE{H?9tJ2$-1P#^(Y0G+HT(B0x?{QXje5wr<> zCW*MUNnY7U$47^y6=nCTEl^Kp?6a1%pHff3xMznOhqJDqR)%a zvLfT>yOV~%+JyA!$5Z|@A4t^`t|oZ$0emCuG$r|U!0gSFhabe7y?$}xjeoWi0GK#0 zfxHIJ6?)V|HASX*k)s9+MWfpMryh8at)Ir8Zy$O4woM+a?GBZ2$+KXc*&cTZ7L%D+ zsCx%wAgGCISJLJaSaP)h54wPJ7cN|5Ujiod_Qs6D9^f*IlOEG|t;Ng(aDtYfiyJw$ zd4F`Od3wGn>XowF6~B0*g{s5F<-Vg@ny$H|Kc`uI*6MfV6YZ>1rygmn4*u>t-^r{a z1LRJ^en<%D?mD}Et9LM=p?xLpzN{LvT7^3^V!_55BnmG5vM}g+I^b6; zE~cX~y#*?dek!lmTz*DC=THkE^kabGl*tNNYQ=9_ zn|#E-@%J+{LzTM5%6$c>%VIotk@Sl6T+v;kfp<*zCZ0NvZ=BjGijN76Vjv*)WWnTY zg0GI!^XnE7H0l6IsLUX;B)x<R^QwgCqVF!%<_A#)xhd@mq zQX~b8^Vv}RoSdx#pOo-#9(4m$mEzwI;J5Ym?=Zayyh$5BS9B^?&)wF3K^aXo<8?x} zr#UF^Q0id#Oyo_e9>ihsK!vDb9qXtSlC7p8;|#;^b>BjK5n60qsG`FXYSB`jqCdHz z@cyE&RVZ>a=Me5u*lqC+;m?(kaysVUt2r&e)Id2+OTVB-Na5a7F zdH00pfyeNfb9o;u!JL^xzRK_T(1RxKE>OE@nvVV~Wa$Cw&F%WL=R;Gg+mbGge2}+Y zP7ApESp4Go(Dc218r;DaPo?S6T~W`EI=42_Dx1T>l)K9y!>mI@W@P$-pGK->5#^_E zn^gNr(IH#dzUSAt#Nt>vi&dQ-7k0t&S3M9*NTz~VTl14W_?}!W;OFhwl>k>rU*;`_ z;{Q_7eXt+k??83u(0==Em?IxzJMbbp5JUj9qRB$mCo?3ybccxp93~s@9%OmDG~4<%+k85yt&jg^2s+K0R&m@}%GI509p zmW0!B0=nnt zb3#_+cT03bV*ijn6atX85ccFnMs%(fmHNtWaa7ypC!gJT^|2vK1*Q1^;sQ7>JZR`- zyMfeIPp2NU7RS4HRH(Zy|KUZMNc%j$^>bb)9VTC&#z!;4N`)rLbDsS)uFum)z=$AV z8|>_qpZx`m087H?0Fk(d4NrzrXll;}IcigoyB7;24X}0dxzrPXd$~i(>RBep%V-{k z4z+1|(4GGjH6ld*W(=JC(>qPgs;*?S z%#eRst40iQ%Cl;F3`j#&qAOEM0`JC=k5;%QB+vFeVPWI!iYI2qI!ng=GQ(8^<3U96 zy%In+&G%G+&|~v&)GAcgeF+YZA>(zZDS7jD9sYR+532RSm0v#>MQmf3#VWpVJ{u0^ z(aq1sy9a`{BDWnSv4#5BP4!o(b5o18`qLNwpm;wBxC3!yIUXPw`;8Ai+IgYQRi_Wc zo}{>sk@b>T{s(wE%V%Xs62;Vw)<(PVWSlYapZ&dVwqnP$(JVSkIcg9C3Yze?qh7bL z;_-RpEgLB_j0?S(rcvEbF;weKnwZ|Qpg{l{3y@jDq@ZaVqq;6VTffElb+Cl5y^! z;B0uoS9YBazHRW=YZ8%81pzUU@f!$;e?9;EId$bNL)(U@xa70d5wr}t7P;?A>5k;L zNG^ySq@Kf2CVeY90b^^+|M%qoyaY-pj_i=Xf!)o20=vs_5cB!$51DI!DE9n7qF+5n zo<^V8$x3(W1qn7JA(^{sSy6T1=y&nVC{Uf=e3BM{)uQbB*C0Yz3#iQ0gnScV)8X|Q zQY>xzD`2n-(Rlzw=P#7NxUS_J-b?YT!y7p(Jk6?t{M}8LPfff~_Mk83%55C_eB?eA zzHAH@7`#xq*`s1Fi$lqk2;E=Jg4)4;-O*gnw!997bxWZ{%du21HSj z-a16Pk>W0T5n{Q^_*|n3)>9{IZP081pZ(iaK|4M$UNjK21k)?#*Of#6m=hR#+bCaH z6t>oICkExTNb0A^D9~x=>F`ZVEJO8Tk=*-<+vO{K6WV)AP7kk4YDecxwkX?-Kq#!| z&{X|bC~oX+Fn1+`zc=0yj``TJzZW3;XpDjg?APi4_Eh|EXW`zS_`2L~ifeGl7l&_s zWtwRRillhBQ@hd^NOT_ZVMt-@LjN5*SmEo9Re?zJZ>j~gh6O@nugCJguL`OggfOi@ zl+8FD*_^+35tzylWp9?hAqSDp^)jU`N6E7q z9~IAaPvPd0hXBW zs|e8{QvS zLQXQo_r8}a|8GTU*4>;ENH-YWZoXPg?*$dcpeup#G(vw1y#ehRE;jQk09LBfhy zLKg^oZCQ!C%#pK*yM^=pI+>ygj?c+^>8R-k_Z(fAd%9o ze>}DbDv}SPYu_j7Y~dIXk7kuJ{x(o1gvSvLK;FcC)Z)7EVmZfuZ;7ibIp@!7pl;t_=2aYthD4l<$2*VVy6iFG=Urbh0JL`mP6sOZsW6E*=NYI zNhrl+LE1NmOo6gmBmkoG^&o07iFjHId`bvcm}>zZ3SE}uWsn-1vteDqC}uEe%w$HF_R`5vLp2gw8x9E&JgS@Dv1Sk3WnlNbHe= zBnOJaC$)V+@K6G3HT_x}&8}3D*Bj(q>hY?ma?_^z#Zw!VCD&1>{(vME=PmxsjL&Q#mA2`Ihotp;N^@}d{iKPX47n+cVxBNL z>HJk#%3g+1vz>I>gNNLX`QZKf?rX+dTYu#h&QC1EovVt7!~)(N!H5q*u*D5a3LiM1 zq@bV~4cNH}-0Mc?AECuDMY5ie!7EU^7ra|`@8x+h$J;Uo8$oXbqv^O_bYB52#1f1B zXKaQY&^SrgzZ-Fv?+De(Tx2S$>ui#owM_l5-nw*ut@2(*j1tvnx;=vf*iMkbo%Ty&&Aedm%ix8a2P z$ve*KECaeEq)h688z*R(x-9g}zENn;oI{#{?XGzkoukY8{I-;DpVZ{S0mywjvGXg_ zql42+<3&yr>)0-p<=0D#ea8i=3cYU|Bj#EZv~zKG)IM85#Uq)>*Ef#5KsT+)`~U~%GkC7BU+n)yQK1h+}UDt3A{aH3xR+^fmo3GbXPhCht<;yG+%Gd+IGe<>+6{y80XtBLOErUW^k|4*`~ zcZ zYisLS<>&3`O6mSbASjNQb}(viWbws~J=zAHvFN_f_gmYuQ38>;-<$zXjG)=$t^ajF z0{NVg|GsLI$9f^i#UuXw-s%?UaEO_yk*tJT9T(=!&s6*1cl}v1=HxQFo&9zj2M2Kf zpPCe#(S4h{Q1@1qYP0{e&cE3tdB85JA>ouY!)GGIly4du7ii6nJ;B*q6w85?4peDi zf08JxfoRA)g`fy$JCF6AZ*iXRo33YXRPoiBSl8ndIA28-AlNWA)&iYEz)vFCMFgf)?k5C%)U;$+YRLKlHQ)%Z*5AD^Zn2 zV8}oF6}e)UCUgCd`u>bE;(5zzS5yM7rhvXA#t0FEU~gDx(_3qMo>%`z=Z~{f6 zaYRNj|N8Qw-_;rzHqPk2Rj-|C@2dEQ_6<#X7<1m$gA9R86DV+c-9Riz8&sO{nJwJ< z;=6%lwfSsf>G}LWX7gksGAlIDQSRM6BI8v=OioA5)Q5JNfvcD4`^Qa?}t)TaT_Bh@D^b?LaE&eSBq}qEcE-WPd@Qpq^S)zt57D- zI^z7g`5MB%-kBQh+W}lQTiU#;s3n49`ocXECyV#SHI9>#UmnNINN$zzYD?|VsOyhUOSWw^Q=oF&D!ny)yuN}a$$kvHwzq# zxNwIX2R?osh4bnXyS*kT&lqnnueWcE=6?VF8R$gt+mxf#`MZN5kct@5eXkFnUUDC^ zPKub`tY>Wv{NA!YA(n4?*nj;DpDc#>$wF_^0qhU`ViRV?oQ_ zH?6LoXIItBtiVE!C!=YIk(PLitbRXgWZdlX^vp?4$L|mh-!zx9W6FEd# zv9nf>63b+*&BV+lecwJiS`!@l>^1?~(H6;p<>8`&@exvOIxlp?P65;V<<%>0_ar2j zr98_S@*9MzO*vy>sG{_nsy^PN=8*8f6^~8n}nPs4F8z!%i|xD zhD80nqUYr$L{8;>;fexdoSr_X4KUV-_M{-xcx@0WjSm}L)NvrZKBU>{Ma2gp7lm&F9V(hyK4LhL>(ed}-%v7q-*T|=XnCFI1*O4Fth+`&$j z+Wt#(YIKEp{lMKI6`i#7dR?Pu|Bg{vOeU+|vmwiT8_AO)@ZhCZZ+t)a5r(q#`$TIS zhrBSq_|)P{*PF+;7G4UE3@_2?B(;jf9BrE1I&}V$l|RRVcC1Ie?lT=G3#_6EK!x%B zD2Zl5XP&RjP6|ae#XdATm&8|eK28hm_h5Z`F{oW2g2TF@vpT^89;>PQQJyq;vLZ}m z-Fh?|Z@$!FkNX-8dQBc_WqrRs-A+|pO(G`5aJ+jrL4Cnb+D{qmyhd3T~7rJ5Kb#?&E(#pI$_7>v^01)1fb05uYJET=W^VYNJF zsQ>-~D+iX}O2y!F7$vi^Wygzx42yfvp3eSM`wgoU|xbqtV>?h_T&d#WeCidkLe zL*CrGW2fn!;3)-mH@`Dq4&IYCm^$TytXjD`T*>x#kuJZjV@KMWs<71NEl~a-B0RGZ z0@Iag^|RYagc#>t1O+{MjmX9b0q?4~K0iMXCurcQmej8#QQ{ij-RmRV_a>rLq6?1G zH=JxJuk{YHPBp#Iim!}X5y{h_&`F}%9Mq53&A0GHZ}AnhY;GsDeNJ$CB_lb5xL%T| zeS_bPj0i(FV2;UStB9Y}1SKZ*bzP>zCk4}z>x}5NTNZiZ0q`{VjSve7IB<%ZN>0E* z#IG@QOD|0=p>FW_4c8mb5a0Q*sFM0Z<7T@EiDRE`J|RD+7#;zEBFGSQb#!Pl05gXK zTE4x#eKyn*Z2R#AzvaQaFkz;KQ>+I_#*8tXesl0{hFr>_P5WW%!v%l6Ij7oJiB3FN zuKJ@8t;ZR3hSCjB&d)Py#q9f*&QJaf#L2#MBf@C-DBI`ardlN)TTxvSQ7mfkW);t$ zbp534PJr;>Fs(uJn`nt#p2VU2Ymp3_hN}sy5B1v$4pSaw=HTmcU_uG*L7Jb|U+CYx ztAj*}MxVP83Z_Z2@ZN#DnlFFsHKY9wzNfy~OEFd9G(|Z5_BBOsMHpg!82)^f(d3xX zJyk=TKD2hTz4)vUew(MGg00BeCX5mq_pcH9+jvHPOX*e?%695>X+d#V4P2>%0W+j1o$(9|9F1|A$1%uR$ud!}%xMMG0s-3sZZNo)5TW|0i z!`DACd+ZNrVOLihi;Q4^6JWN7iWyeS(Z4IEQB}ezdgisJmdO%_9EJmnenm@Ds0Xn<$!+|LK z*kG9GFi;Es4wP!sRXN`k1SlE^8PV^M87p+;^FAr_Mh|35?b4E36JxIsGKbb5ka=*$y&0`yvN z>Irvt?D@+aPdQ{^<-h%BL8JL*#l$Aq8$Kn#4$6QK@gZ`t75V9PNFHM*9l8z0RXp+5sa>JOeP9UB) zEqfT;P_^9M-SKtf4u?+$%@ETe0N{yFNf|3^Ve|VPSg4hu4t2R)JP|vD$xIcR&YPIH zZ@3q2Pl8^fG(>ru5gDudKs?B2-kJ`=2u;h+&vOWgPYl3YdhqK2|C+GU($d`3)yEFH zynn}#41$p>#ZJDeO|5U`tk}Q*4bDpIQ_cQ3Qkaeua3yRX1iNcc7bwM900xV-@4Jbrl-N zLwK1>KA>SkgLx_?kShXo#~$32lM@Ka3}vu~sE^$MutwL+jI}pYD$TP% zDg^}vn-^#c7RaA>qH#~%QF7!P~0WU}C`KdYBb7nUCsm`1bNNnC`97hLXj<)1@_ zHs3TMyLE>HyZ7_N%fJ9SB0trpI*ggA;SjxxuGg+AzJ8?%W#U}?+mH5k*R=sKWX}Ne zCj6U_ycaS&g5QDuX2Bw=8|*GZ+$3Hir&5QSJe>A`FpmIFD2r3Qcp3&6@jJ)yL%1a} zY;c(XVMy}jtMxyp)jx|Q47HNRtayS*i~KhS_QE5;JRVOtZ>_yVwQYPjwT&-1GdneN zG|B+vY3jXHpl@WP)@f8tZ8S&qy%)kn&>GP?fp*?bRlzG#)#v^yck;m zOHtlCc7r2O23M2;Yk?vvmdm?4Tf9cilV#S9X0 zR7Kv4v7Mz=THind@=s@~@(<%C%!XTBvkDD`E~vxZQOAOv&&r>f#z+cQ;$ja?Y))Cs08QMOwU=0%?oL- z5^zao-VtU#SdqrX+^E!Qs|YZCPjhXJMJ}w~84`p&!_sZxYF9-z{+RBZOF(|D&hxW_ z9in7Y9&zSLTEG>oV*~Gy*=6H!rNfEc#Wtnm2&oKg|6xVJk?)a0;oD&AxD3{hyjc9( zIq>X~;n&5*+0UL4RipdJ?o7D$OS%1s7KhlW%q&(2 zT&FT_hG;E4L-EciSU-ZM``(DWcK>?Ua1b&|ZeHG%=JE`9rF*lIn9H0hJ|rN?pVVs( zw7Y<2Eq1fGeu9=;rZtM{!Z-tym$3ncTFDS|X<-TxRneewrPaBmMa5CSy)V2S$8A2r5UoHhb@|gFuy~lB_4{ z@!HD4!$_DnOm__phc;$Dwujy5y!QOoQoj!D`j;LT=fl-1K5Ikf^+Qgdsqm$MrYy$y z{R=I*cZmT>Xx65#O#Knxc%-!UAqc+;K=KvNz1I9o%lUG?0f2R{-GZ;2Rc$(zf{%?& zla*VA2@$)b65^zUCPZdxO?6#ZsnzhwS%*@KynZ zYLmdXK_2pNv>WOe2Z_(>O&2bzHeKL~cS(Q{kTm^&UKk?Ys4vs-#Qgt+p**132z;pe zADxaxnkXJI2B7x4W%66#x2lr=v%vt=_{FUTXvJLoD3*+XfS5#0QBMDe1KXm8(a_LP zhEV+c)L_Z;Tg(@gt7Noz(rX57iBA7D_~kUpX2D@tS5V73~)oTeoQC{Vwe0{ zDk-5742M#HnPC{pNAoOJ0rOFi3_A0rmN_pN)g(vb5D>5romso#UZ& zRJXHGDSiHAvD=^X*YYO~4;C)~fCC+ozQvAB<>F4Bk%LAIRecCvEf2=BV_5^0YT2_@ zp}XV*E6cISN{YNg61Xa1I%6!3fb(k}#7t5|Q8se2F9AHD$sYa6fi;+%7jXe_VrLQBK9+$kc+=7x@Q*iA2jRoEokY%9)4QAFwau(vXHcfCs?WfU l=bLb-1^m~~S?%w~ggPHqKAp+d2q1tzYD!v)74i?B{XfeqrCk63 diff --git a/loggers/dnstap.go b/loggers/dnstap.go index e066fb37..c7743068 100644 --- a/loggers/dnstap.go +++ b/loggers/dnstap.go @@ -8,6 +8,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-dnstap-protobuf" "github.com/dmachard/go-framestream" "github.com/dmachard/go-logger" @@ -83,6 +84,9 @@ func (o *DnstapSender) Stop() { func (o *DnstapSender) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + dt := &dnstap.Dnstap{} frame := &framestream.Frame{} @@ -147,6 +151,11 @@ LOOP: select { case dm := <-o.channel: + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + dt.Reset() t := dnstap.Dnstap_MESSAGE @@ -240,6 +249,11 @@ LOOP: o.LogInfo("closing tcp connection") o.conn.Close() } + o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + o.done <- true } diff --git a/loggers/elasticsearch.go b/loggers/elasticsearch.go index 32407f05..7b78cd1a 100644 --- a/loggers/elasticsearch.go +++ b/loggers/elasticsearch.go @@ -6,6 +6,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" "net/http" @@ -80,7 +81,15 @@ func (o *ElasticSearchClient) Stop() { func (o *ElasticSearchClient) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + for dm := range o.channel { + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + data := ElasticSearchData{ Identity: dm.DnsTap.Identity, QueryIP: dm.NetworkInfo.QueryIp, @@ -107,6 +116,10 @@ func (o *ElasticSearchClient) Run() { } o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/fluentd.go b/loggers/fluentd.go index c7bac837..d7bac805 100644 --- a/loggers/fluentd.go +++ b/loggers/fluentd.go @@ -7,6 +7,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" "github.com/vmihailenco/msgpack" ) @@ -73,6 +74,9 @@ func (o *FluentdClient) Stop() { func (o *FluentdClient) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + LOOP: for { LOOP_RECONNECT: @@ -119,6 +123,11 @@ LOOP: for { select { case dm := <-o.channel: + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // prepare event tm, _ := msgpack.Marshal(dm.DnsTap.TimeSec) record, err := msgpack.Marshal(dm) @@ -161,6 +170,11 @@ LOOP: o.LogInfo("closing tcp connection") o.conn.Close() } + o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + o.done <- true } diff --git a/loggers/influxdb.go b/loggers/influxdb.go index af5a0e9a..fa61295f 100644 --- a/loggers/influxdb.go +++ b/loggers/influxdb.go @@ -5,6 +5,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" influxdb2 "github.com/influxdata/influxdb-client-go" @@ -81,6 +82,9 @@ func (o *InfluxDBClient) Stop() { func (o *InfluxDBClient) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + // prepare options for influxdb opts := influxdb2.DefaultOptions() opts.SetUseGZip(true) @@ -105,6 +109,12 @@ func (o *InfluxDBClient) Run() { o.influxdbConn = influxClient o.writeAPI = writeAPI for dm := range o.channel { + + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + p := influxdb2.NewPointWithMeasurement("dns"). AddTag("Identity", dm.DnsTap.Identity). AddTag("QueryIP", dm.NetworkInfo.QueryIp). @@ -121,6 +131,10 @@ func (o *InfluxDBClient) Run() { } o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/logfile.go b/loggers/logfile.go index bff4e59f..89a3968b 100644 --- a/loggers/logfile.go +++ b/loggers/logfile.go @@ -17,6 +17,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -341,6 +342,10 @@ func (o *LogFile) Rotate() error { func (o *LogFile) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + + // prepare some timers tflush_interval := time.Duration(o.config.Loggers.LogFile.FlushInterval) * time.Second tflush := time.NewTimer(tflush_interval) o.commpressTimer = time.NewTimer(time.Duration(o.config.Loggers.LogFile.CompressInterval) * time.Second) @@ -355,6 +360,11 @@ LOOP: break LOOP } + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // write to file switch o.config.Loggers.LogFile.Mode { case dnsutils.MODE_TEXT: @@ -385,6 +395,9 @@ LOOP: o.LogInfo("run terminated") + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/lokiclient.go b/loggers/lokiclient.go index 0037e047..633ab7fa 100644 --- a/loggers/lokiclient.go +++ b/loggers/lokiclient.go @@ -14,6 +14,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" "github.com/gogo/protobuf/proto" "github.com/grafana/dskit/backoff" @@ -165,17 +166,26 @@ func (o *LokiClient) Stop() { func (o *LokiClient) Run() { o.LogInfo("running in background...") + + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + + // prepare buffer buffer := new(bytes.Buffer) + // prepare timers tflush_interval := time.Duration(o.config.Loggers.LokiClient.FlushInterval) * time.Second tflush := time.NewTimer(tflush_interval) LOOP: - /* for { - LOOP_RECONNECT:*/ for { select { case dm := <-o.channel: + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + if _, ok := o.streams[dm.DnsTap.Identity]; !ok { o.streams[dm.DnsTap.Identity] = &LokiStream{config: o.config, logger: o.logger, name: dm.DnsTap.Identity} o.streams[dm.DnsTap.Identity].Init() @@ -200,10 +210,7 @@ LOOP: o.streams[dm.DnsTap.Identity].stream.Entries = append(o.streams[dm.DnsTap.Identity].stream.Entries, entry) // flush ? - //fmt.Println(o.streams[dm.DnsTap.Identity].sizeentries) if o.streams[dm.DnsTap.Identity].sizeentries >= o.config.Loggers.LokiClient.BatchSize { - // fmt.Println("batch completed!") - // encode log entries buf, err := o.streams[dm.DnsTap.Identity].Encode2Proto() if err != nil { @@ -216,13 +223,6 @@ LOOP: // send all entries o.SendEntries(buf) - /*err = o.SendEntries(buf) - fmt.Println(err) - if err != nil { - o.LogError("error sending log entries - %v", err) - break LOOP_RECONNECT - })*/ - // reset entries and push request o.streams[dm.DnsTap.Identity].ResetEntries() } @@ -244,13 +244,6 @@ LOOP: // send all entries o.SendEntries(buf) - /* err = o.SendEntries(buf) - if err != nil { - o.LogError("error sending log entries - %v", err) - // restart timer - tflush.Reset(tflush_interval) - break LOOP_RECONNECT - }*/ // reset entries and push request s.ResetEntries() @@ -265,12 +258,13 @@ LOOP: } } - /* o.LogInfo("retry in %d seconds", o.config.Loggers.LokiClient.RetryInterval) - time.Sleep(time.Duration(o.config.Loggers.LokiClient.RetryInterval) * time.Second) - }*/ // if buffer is not empty, we accept to lose log entries o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/pcapfile.go b/loggers/pcapfile.go index a966948c..c14bb9e8 100644 --- a/loggers/pcapfile.go +++ b/loggers/pcapfile.go @@ -16,6 +16,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -349,6 +350,9 @@ func (o *PcapWriter) Write(dm dnsutils.DnsMessage, pkt []gopacket.SerializableLa func (o *PcapWriter) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + eth := &layers.Ethernet{SrcMAC: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, DstMAC: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} ip4 := &layers.IPv4{Version: 4, TTL: 64} @@ -366,6 +370,11 @@ LOOP: break LOOP } + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // prepare ip srcIp, srcPort, dstIp, dstPort := o.GetIpPort(&dm) @@ -442,6 +451,9 @@ LOOP: } o.LogInfo("run terminated") + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/prometheus.go b/loggers/prometheus.go index 38a63775..41dd0aff 100644 --- a/loggers/prometheus.go +++ b/loggers/prometheus.go @@ -12,6 +12,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" "github.com/dmachard/go-topmap" "github.com/prometheus/client_golang/prometheus" @@ -790,6 +791,9 @@ func (s *Prometheus) ListenAndServe() { func (s *Prometheus) Run() { s.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&s.config.OutgoingTransformers, s.logger, s.name) + // start http server go s.ListenAndServe() @@ -805,6 +809,12 @@ LOOP: s.LogInfo("channel closed") break LOOP } + + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // record the dnstap message s.Record(dm) @@ -819,6 +829,9 @@ LOOP: } s.LogInfo("run terminated") + // cleanup transformers + subprocessors.Reset() + // the job is done s.done <- true } diff --git a/loggers/statsd.go b/loggers/statsd.go index 9069a405..681774bb 100644 --- a/loggers/statsd.go +++ b/loggers/statsd.go @@ -9,6 +9,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -82,7 +83,10 @@ func (o *StatsdClient) Stop() { func (o *StatsdClient) Run() { o.LogInfo("running in background...") - // init timer to compute qps + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + + // prepare timer to compute qps t1_interval := 1 * time.Second t1 := time.NewTimer(t1_interval) @@ -99,6 +103,12 @@ LOOP: o.LogInfo("channel closed") break LOOP } + + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // record the dnstap message o.stats.Record(dm) @@ -198,6 +208,10 @@ LOOP: } o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/stdout.go b/loggers/stdout.go index 65e02311..1699be82 100644 --- a/loggers/stdout.go +++ b/loggers/stdout.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -79,8 +80,18 @@ func (o *StdOut) Stop() { func (o *StdOut) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + + // standard output buffer buffer := new(bytes.Buffer) + for dm := range o.channel { + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + switch o.config.Loggers.Stdout.Mode { case dnsutils.MODE_TEXT: o.stdout.Print(dm.String(o.textFormat)) @@ -92,6 +103,9 @@ func (o *StdOut) Run() { } o.LogInfo("run terminated") + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/syslog.go b/loggers/syslog.go index 850ecb42..997c1c0e 100644 --- a/loggers/syslog.go +++ b/loggers/syslog.go @@ -11,6 +11,7 @@ import ( syslog "github.com/RackSec/srslog" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -137,6 +138,9 @@ func (o *Syslog) Stop() { func (o *Syslog) Run() { o.LogInfo("running in background...") + // prepare enabled transformers + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + var syslogconn *syslog.Writer var err error buffer := new(bytes.Buffer) @@ -171,6 +175,11 @@ func (o *Syslog) Run() { o.syslogConn = syslogconn for dm := range o.channel { + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + switch o.config.Loggers.Syslog.Mode { case dnsutils.MODE_TEXT: delimiter := "\n" @@ -183,6 +192,10 @@ func (o *Syslog) Run() { } o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + // the job is done o.done <- true } diff --git a/loggers/tcpclient.go b/loggers/tcpclient.go index 2c721615..9a32181c 100644 --- a/loggers/tcpclient.go +++ b/loggers/tcpclient.go @@ -10,6 +10,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -82,6 +83,9 @@ func (o *TcpClient) Stop() { func (o *TcpClient) Run() { o.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&o.config.OutgoingTransformers, o.logger, o.name) + LOOP: for { LOOP_RECONNECT: @@ -129,6 +133,11 @@ LOOP: select { case dm := <-o.channel: + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + if o.config.Loggers.TcpClient.Mode == dnsutils.MODE_TEXT { w.Write(dm.Bytes(o.textFormat, o.config.Loggers.TcpClient.Delimiter)) } @@ -161,6 +170,11 @@ LOOP: o.LogInfo("closing tcp connection") o.conn.Close() } + o.LogInfo("run terminated") + + // cleanup transformers + subprocessors.Reset() + o.done <- true } diff --git a/loggers/webserver.go b/loggers/webserver.go index 628efff4..1cb06577 100644 --- a/loggers/webserver.go +++ b/loggers/webserver.go @@ -10,6 +10,7 @@ import ( "time" "github.com/dmachard/go-dnscollector/dnsutils" + "github.com/dmachard/go-dnscollector/transformers" "github.com/dmachard/go-logger" ) @@ -432,6 +433,9 @@ func (s *Webserver) ListenAndServe() { func (s *Webserver) Run() { s.LogInfo("running in background...") + // prepare transforms + subprocessors := transformers.NewTransforms(&s.config.OutgoingTransformers, s.logger, s.name) + // start http server go s.ListenAndServe() @@ -448,6 +452,12 @@ LOOP: s.LogInfo("channel closed") break LOOP } + + // apply tranforms + if subprocessors.ProcessMessage(&dm) == transformers.RETURN_DROP { + continue + } + // record the dnstap message s.stats.Record(dm) @@ -462,6 +472,9 @@ LOOP: s.LogInfo("run terminated") + // cleanup transformers + subprocessors.Reset() + // the job is done s.done <- true } diff --git a/transformers/filtering.go b/transformers/filtering.go index e332ac51..e8537a46 100644 --- a/transformers/filtering.go +++ b/transformers/filtering.go @@ -14,7 +14,7 @@ import ( ) type FilteringProcessor struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers logger *logger.Logger dropDomains bool keepDomains bool @@ -32,7 +32,7 @@ type FilteringProcessor struct { activeFilters []func(dm *dnsutils.DnsMessage) bool } -func NewFilteringProcessor(config *dnsutils.Config, logger *logger.Logger, name string) FilteringProcessor { +func NewFilteringProcessor(config *dnsutils.ConfigTransformers, logger *logger.Logger, name string) FilteringProcessor { // creates a new file watcher watcher, err := fsnotify.NewWatcher() if err != nil { @@ -67,11 +67,11 @@ func NewFilteringProcessor(config *dnsutils.Config, logger *logger.Logger, name func (p *FilteringProcessor) LoadActiveFilters() { // TODO: Change to iteration through Filtering to add filters in custom order. - if !p.config.Transformers.Filtering.LogQueries { + if !p.config.Filtering.LogQueries { p.activeFilters = append(p.activeFilters, p.ignoreQueryFilter) } - if !p.config.Transformers.Filtering.LogReplies { + if !p.config.Filtering.LogReplies { p.activeFilters = append(p.activeFilters, p.ignoreReplyFilter) } @@ -79,7 +79,7 @@ func (p *FilteringProcessor) LoadActiveFilters() { p.activeFilters = append(p.activeFilters, p.rCodeFilter) } - if len(p.config.Transformers.Filtering.KeepQueryIpFile) > 0 || len(p.config.Transformers.Filtering.DropQueryIpFile) > 0 { + if len(p.config.Filtering.KeepQueryIpFile) > 0 || len(p.config.Filtering.DropQueryIpFile) > 0 { p.activeFilters = append(p.activeFilters, p.ipFilter) } @@ -100,15 +100,15 @@ func (p *FilteringProcessor) LoadActiveFilters() { } // set downsample if desired - if p.config.Transformers.Filtering.Downsample > 0 { - p.downsample = p.config.Transformers.Filtering.Downsample + if p.config.Filtering.Downsample > 0 { + p.downsample = p.config.Filtering.Downsample p.downsampleCount = 0 p.activeFilters = append(p.activeFilters, p.downsampleFilter) } } func (p *FilteringProcessor) LoadRcodes() { - for _, v := range p.config.Transformers.Filtering.DropRcodes { + for _, v := range p.config.Filtering.DropRcodes { p.mapRcodes[v] = true } } @@ -152,16 +152,16 @@ func (p *FilteringProcessor) loadQueryIpList(fname string, drop bool) (uint64, e } func (p *FilteringProcessor) LoadQueryIpList() { - if len(p.config.Transformers.Filtering.DropQueryIpFile) > 0 { - read, err := p.loadQueryIpList(p.config.Transformers.Filtering.DropQueryIpFile, true) + if len(p.config.Filtering.DropQueryIpFile) > 0 { + read, err := p.loadQueryIpList(p.config.Filtering.DropQueryIpFile, true) if err != nil { p.LogError("unable to open query ip file: ", err) } p.LogInfo("loaded with %d query ip to the drop list", read) } - if len(p.config.Transformers.Filtering.KeepQueryIpFile) > 0 { - read, err := p.loadQueryIpList(p.config.Transformers.Filtering.KeepQueryIpFile, false) + if len(p.config.Filtering.KeepQueryIpFile) > 0 { + read, err := p.loadQueryIpList(p.config.Filtering.KeepQueryIpFile, false) if err != nil { p.LogError("unable to open query ip file: ", err) } @@ -170,8 +170,8 @@ func (p *FilteringProcessor) LoadQueryIpList() { } func (p *FilteringProcessor) LoadDomainsList() { - if len(p.config.Transformers.Filtering.DropFqdnFile) > 0 { - file, err := os.Open(p.config.Transformers.Filtering.DropFqdnFile) + if len(p.config.Filtering.DropFqdnFile) > 0 { + file, err := os.Open(p.config.Filtering.DropFqdnFile) if err != nil { p.LogError("unable to open fqdn file: ", err) p.dropDomains = true @@ -193,8 +193,8 @@ func (p *FilteringProcessor) LoadDomainsList() { } - if len(p.config.Transformers.Filtering.DropDomainFile) > 0 { - file, err := os.Open(p.config.Transformers.Filtering.DropDomainFile) + if len(p.config.Filtering.DropDomainFile) > 0 { + file, err := os.Open(p.config.Filtering.DropDomainFile) if err != nil { p.LogError("unable to open regex list file: ", err) p.dropDomains = true @@ -214,8 +214,8 @@ func (p *FilteringProcessor) LoadDomainsList() { } } - if len(p.config.Transformers.Filtering.KeepFqdnFile) > 0 { - file, err := os.Open(p.config.Transformers.Filtering.KeepFqdnFile) + if len(p.config.Filtering.KeepFqdnFile) > 0 { + file, err := os.Open(p.config.Filtering.KeepFqdnFile) if err != nil { p.LogError("unable to open KeepFqdnFile file: ", err) p.keepDomains = false @@ -230,8 +230,8 @@ func (p *FilteringProcessor) LoadDomainsList() { } } - if len(p.config.Transformers.Filtering.KeepDomainFile) > 0 { - file, err := os.Open(p.config.Transformers.Filtering.KeepDomainFile) + if len(p.config.Filtering.KeepDomainFile) > 0 { + file, err := os.Open(p.config.Filtering.KeepDomainFile) if err != nil { p.LogError("unable to open KeepDomainFile file: ", err) p.keepDomains = false diff --git a/transformers/filtering_test.go b/transformers/filtering_test.go index a89e3fa3..1fe70079 100644 --- a/transformers/filtering_test.go +++ b/transformers/filtering_test.go @@ -14,9 +14,9 @@ const ( func TestFilteringQR(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.LogQueries = false - config.Transformers.Filtering.LogReplies = false + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.LogQueries = false + config.Filtering.LogReplies = false // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -35,8 +35,8 @@ func TestFilteringQR(t *testing.T) { func TestFilteringByRcodeNOERROR(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropRcodes = []string{"NOERROR"} + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropRcodes = []string{"NOERROR"} // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -50,8 +50,8 @@ func TestFilteringByRcodeNOERROR(t *testing.T) { func TestFilteringByRcodeEmpty(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropRcodes = []string{} + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropRcodes = []string{} // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -64,9 +64,9 @@ func TestFilteringByRcodeEmpty(t *testing.T) { func TestFilteringByQueryIp(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropQueryIpFile = "../testsdata/filtering_queryip.txt" - config.Transformers.Filtering.KeepQueryIpFile = "../testsdata/filtering_queryip_keep.txt" + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropQueryIpFile = "../testsdata/filtering_queryip.txt" + config.Filtering.KeepQueryIpFile = "../testsdata/filtering_queryip_keep.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -101,8 +101,8 @@ func TestFilteringByQueryIp(t *testing.T) { func TestFilteringByFqdn(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropFqdnFile = "../testsdata/filtering_fqdn.txt" + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropFqdnFile = "../testsdata/filtering_fqdn.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -121,8 +121,8 @@ func TestFilteringByFqdn(t *testing.T) { func TestFilteringByDomainRegex(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropDomainFile = "../testsdata/filtering_fqdn_regex.txt" + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropDomainFile = "../testsdata/filtering_fqdn_regex.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -146,10 +146,10 @@ func TestFilteringByDomainRegex(t *testing.T) { func TestFilteringByKeepDomain(t *testing.T) { // config - config := dnsutils.GetFakeConfig() + config := dnsutils.GetFakeConfigTransformers() // file contains google.fr, test.github.com - config.Transformers.Filtering.KeepDomainFile = "../testsdata/filtering_keep_domains.txt" + config.Filtering.KeepDomainFile = "../testsdata/filtering_keep_domains.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -178,14 +178,14 @@ func TestFilteringByKeepDomain(t *testing.T) { func TestFilteringByKeepDomainRegex(t *testing.T) { // config - config := dnsutils.GetFakeConfig() + config := dnsutils.GetFakeConfigTransformers() /* file contains: (mail|sheets).google.com$ test.github.com$ .+.google.com$ */ - config.Transformers.Filtering.KeepDomainFile = "../testsdata/filtering_keep_domains_regex.txt" + config.Filtering.KeepDomainFile = "../testsdata/filtering_keep_domains_regex.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -216,8 +216,8 @@ func TestFilteringByKeepDomainRegex(t *testing.T) { func TestFilteringByDownsample(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.Downsample = 2 + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.Downsample = 2 // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") @@ -241,7 +241,7 @@ func TestFilteringByDownsample(t *testing.T) { } // test for default behavior when downsample is set to 0 - config.Transformers.Filtering.Downsample = 0 + config.Filtering.Downsample = 0 filtering = NewFilteringProcessor(config, logger.New(false), "test") if filtering.CheckIfDrop(&dm) == true { @@ -255,9 +255,9 @@ func TestFilteringByDownsample(t *testing.T) { func TestFilteringMultipleFilters(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Filtering.DropDomainFile = "../testsdata/filtering_fqdn_regex.txt" - config.Transformers.Filtering.DropQueryIpFile = "../testsdata/filtering_queryip.txt" + config := dnsutils.GetFakeConfigTransformers() + config.Filtering.DropDomainFile = "../testsdata/filtering_fqdn_regex.txt" + config.Filtering.DropQueryIpFile = "../testsdata/filtering_queryip.txt" // init subproccesor filtering := NewFilteringProcessor(config, logger.New(false), "test") diff --git a/transformers/geoip.go b/transformers/geoip.go index 5fbb247b..e742c189 100644 --- a/transformers/geoip.go +++ b/transformers/geoip.go @@ -32,7 +32,7 @@ type GeoRecord struct { } type GeoIpProcessor struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers logger *logger.Logger dbCountry *maxminddb.Reader dbCity *maxminddb.Reader @@ -40,7 +40,7 @@ type GeoIpProcessor struct { enabled bool } -func NewDnsGeoIpProcessor(config *dnsutils.Config, logger *logger.Logger) GeoIpProcessor { +func NewDnsGeoIpProcessor(config *dnsutils.ConfigTransformers, logger *logger.Logger) GeoIpProcessor { d := GeoIpProcessor{ config: config, logger: logger, @@ -58,8 +58,8 @@ func (p *GeoIpProcessor) LogError(msg string, v ...interface{}) { } func (p *GeoIpProcessor) Open() (err error) { - if len(p.config.Transformers.GeoIP.DbCountryFile) > 0 { - p.dbCountry, err = maxminddb.Open(p.config.Transformers.GeoIP.DbCountryFile) + if len(p.config.GeoIP.DbCountryFile) > 0 { + p.dbCountry, err = maxminddb.Open(p.config.GeoIP.DbCountryFile) if err != nil { p.enabled = false return @@ -68,8 +68,8 @@ func (p *GeoIpProcessor) Open() (err error) { p.LogInfo("country database loaded (%d records)", p.dbCountry.Metadata.NodeCount) } - if len(p.config.Transformers.GeoIP.DbCityFile) > 0 { - p.dbCity, err = maxminddb.Open(p.config.Transformers.GeoIP.DbCityFile) + if len(p.config.GeoIP.DbCityFile) > 0 { + p.dbCity, err = maxminddb.Open(p.config.GeoIP.DbCityFile) if err != nil { p.enabled = false return @@ -78,8 +78,8 @@ func (p *GeoIpProcessor) Open() (err error) { p.LogInfo("city database loaded (%d records)", p.dbCity.Metadata.NodeCount) } - if len(p.config.Transformers.GeoIP.DbAsnFile) > 0 { - p.dbAsn, err = maxminddb.Open(p.config.Transformers.GeoIP.DbAsnFile) + if len(p.config.GeoIP.DbAsnFile) > 0 { + p.dbAsn, err = maxminddb.Open(p.config.GeoIP.DbAsnFile) if err != nil { p.enabled = false return diff --git a/transformers/geoip_test.go b/transformers/geoip_test.go index 14d91404..3293ef29 100644 --- a/transformers/geoip_test.go +++ b/transformers/geoip_test.go @@ -9,8 +9,8 @@ import ( func TestGeoIP_LookupCountry(t *testing.T) { // enable geoip - config := dnsutils.GetFakeConfig() - config.Transformers.GeoIP.DbCountryFile = "../testsdata/GeoLite2-Country.mmdb" + config := dnsutils.GetFakeConfigTransformers() + config.GeoIP.DbCountryFile = "../testsdata/GeoLite2-Country.mmdb" // init the processor geoip := NewDnsGeoIpProcessor(config, logger.New(true)) @@ -37,8 +37,8 @@ func TestGeoIP_LookupCountry(t *testing.T) { func TestGeoIP_LookupAsn(t *testing.T) { // enable geoip - config := dnsutils.GetFakeConfig() - config.Transformers.GeoIP.DbAsnFile = "../testsdata/GeoLite2-ASN.mmdb" + config := dnsutils.GetFakeConfigTransformers() + config.GeoIP.DbAsnFile = "../testsdata/GeoLite2-ASN.mmdb" // init the processor geoip := NewDnsGeoIpProcessor(config, logger.New(false)) diff --git a/transformers/normalize.go b/transformers/normalize.go index eb24ac3e..d6792084 100644 --- a/transformers/normalize.go +++ b/transformers/normalize.go @@ -7,10 +7,10 @@ import ( ) type NormalizeProcessor struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers } -func NewNormalizeSubprocessor(config *dnsutils.Config) NormalizeProcessor { +func NewNormalizeSubprocessor(config *dnsutils.ConfigTransformers) NormalizeProcessor { s := NormalizeProcessor{ config: config, } @@ -19,7 +19,7 @@ func NewNormalizeSubprocessor(config *dnsutils.Config) NormalizeProcessor { } func (s *NormalizeProcessor) IsEnabled() bool { - return s.config.Transformers.Normalize.Enable + return s.config.Normalize.Enable } func (s *NormalizeProcessor) Lowercase(qname string) string { diff --git a/transformers/normalize_test.go b/transformers/normalize_test.go index 0951cfc7..c0a18dfe 100644 --- a/transformers/normalize_test.go +++ b/transformers/normalize_test.go @@ -8,9 +8,9 @@ import ( func TestNormalizeLowercaseQname(t *testing.T) { // enable feature - config := dnsutils.GetFakeConfig() - config.Transformers.Normalize.Enable = true - config.Transformers.Normalize.QnameLowerCase = true + config := dnsutils.GetFakeConfigTransformers() + config.Normalize.Enable = true + config.Normalize.QnameLowerCase = true // init the processor qnameNorm := NewNormalizeSubprocessor(config) diff --git a/transformers/subprocessors.go b/transformers/subprocessors.go index 115da065..5eff7f0c 100644 --- a/transformers/subprocessors.go +++ b/transformers/subprocessors.go @@ -12,7 +12,7 @@ var ( ) type Transforms struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers logger *logger.Logger name string @@ -23,7 +23,7 @@ type Transforms struct { NormalizeTransform NormalizeProcessor } -func NewTransforms(config *dnsutils.Config, logger *logger.Logger, name string) Transforms { +func NewTransforms(config *dnsutils.ConfigTransformers, logger *logger.Logger, name string) Transforms { d := Transforms{ config: config, @@ -42,33 +42,33 @@ func NewTransforms(config *dnsutils.Config, logger *logger.Logger, name string) } func (p *Transforms) Prepare() error { - if p.config.Transformers.Normalize.Enable { + if p.config.Normalize.Enable { p.LogInfo("[normalize] enabled") } - if p.config.Transformers.GeoIP.Enable { + if p.config.GeoIP.Enable { p.LogInfo("[GeoIP] enabled") if err := p.GeoipTransform.Open(); err != nil { p.LogError("geoip open error %v", err) } } - if p.config.Transformers.UserPrivacy.Enable { + if p.config.UserPrivacy.Enable { p.LogInfo("[user privacy] enabled") } - if p.config.Transformers.Filtering.Enable { + if p.config.Filtering.Enable { p.LogInfo("[filtering] enabled") } - if p.config.Transformers.Suspicious.Enable { + if p.config.Suspicious.Enable { p.LogInfo("[suspicious] enabled") } return nil } func (p *Transforms) Reset() { - if p.config.Transformers.GeoIP.Enable { + if p.config.GeoIP.Enable { p.GeoipTransform.Close() } } @@ -84,31 +84,31 @@ func (p *Transforms) LogError(msg string, v ...interface{}) { func (p *Transforms) ProcessMessage(dm *dnsutils.DnsMessage) int { // Normalize qname to lowercase - if p.config.Transformers.Normalize.Enable { - if p.config.Transformers.Normalize.QnameLowerCase { + if p.config.Normalize.Enable { + if p.config.Normalize.QnameLowerCase { dm.DNS.Qname = p.NormalizeTransform.Lowercase(dm.DNS.Qname) } } // Traffic filtering ? - if p.config.Transformers.Filtering.Enable { + if p.config.Filtering.Enable { if p.FilteringTransform.CheckIfDrop(dm) { return RETURN_DROP } } // Apply user privacy on qname and query ip - if p.config.Transformers.UserPrivacy.Enable { - if p.config.Transformers.UserPrivacy.AnonymizeIP { + if p.config.UserPrivacy.Enable { + if p.config.UserPrivacy.AnonymizeIP { dm.NetworkInfo.QueryIp = p.UserPrivacyTransform.AnonymizeIP(dm.NetworkInfo.QueryIp) } - if p.config.Transformers.UserPrivacy.MinimazeQname { + if p.config.UserPrivacy.MinimazeQname { dm.DNS.Qname = p.UserPrivacyTransform.MinimazeQname(dm.DNS.Qname) } } // Add GeoIP metadata ? - if p.config.Transformers.GeoIP.Enable { + if p.config.GeoIP.Enable { geoInfo, err := p.GeoipTransform.Lookup(dm.NetworkInfo.QueryIp) if err != nil { p.LogError("geoip lookup error %v", err) @@ -122,7 +122,7 @@ func (p *Transforms) ProcessMessage(dm *dnsutils.DnsMessage) int { } // add suspicious flags in DNS messages - if p.config.Transformers.Suspicious.Enable { + if p.config.Suspicious.Enable { p.SuspiciousTransform.CheckIfSuspicious(dm) } diff --git a/transformers/suspicious.go b/transformers/suspicious.go index 2cd7f997..412bbcf5 100644 --- a/transformers/suspicious.go +++ b/transformers/suspicious.go @@ -8,13 +8,13 @@ import ( ) type SuspiciousTransform struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers logger *logger.Logger name string CommonQtypes map[string]bool } -func NewSuspiciousSubprocessor(config *dnsutils.Config, logger *logger.Logger, name string) SuspiciousTransform { +func NewSuspiciousSubprocessor(config *dnsutils.ConfigTransformers, logger *logger.Logger, name string) SuspiciousTransform { d := SuspiciousTransform{ config: config, logger: logger, @@ -28,13 +28,13 @@ func NewSuspiciousSubprocessor(config *dnsutils.Config, logger *logger.Logger, n } func (p *SuspiciousTransform) ReadConfig() { - for _, v := range p.config.Transformers.Suspicious.CommonQtypes { + for _, v := range p.config.Suspicious.CommonQtypes { p.CommonQtypes[v] = true } } func (p *SuspiciousTransform) IsEnabled() bool { - return p.config.Transformers.Suspicious.Enable + return p.config.Suspicious.Enable } func (p *SuspiciousTransform) LogInfo(msg string, v ...interface{}) { @@ -54,13 +54,13 @@ func (p *SuspiciousTransform) CheckIfSuspicious(dm *dnsutils.DnsMessage) { } // long domain name ? - if len(dm.DNS.Qname) > p.config.Transformers.Suspicious.ThresholdQnameLen { + if len(dm.DNS.Qname) > p.config.Suspicious.ThresholdQnameLen { dm.Suspicious.Score += 1.0 dm.Suspicious.Flags.LongDomain = true } // large packet size ? - if dm.DNS.Length > p.config.Transformers.Suspicious.ThresholdPacketLen { + if dm.DNS.Length > p.config.Suspicious.ThresholdPacketLen { dm.Suspicious.Score += 1.0 dm.Suspicious.Flags.LargePacket = true } @@ -72,13 +72,13 @@ func (p *SuspiciousTransform) CheckIfSuspicious(dm *dnsutils.DnsMessage) { } // count the number of labels in qname - if strings.Count(dm.DNS.Qname, ".") > p.config.Transformers.Suspicious.ThresholdMaxLabels { + if strings.Count(dm.DNS.Qname, ".") > p.config.Suspicious.ThresholdMaxLabels { dm.Suspicious.Score += 1.0 dm.Suspicious.Flags.ExcessiveNumberLabels = true } // search for unallowed characters - for _, v := range p.config.Transformers.Suspicious.UnallowedChars { + for _, v := range p.config.Suspicious.UnallowedChars { if strings.Contains(dm.DNS.Qname, v) { dm.Suspicious.Score += 1.0 dm.Suspicious.Flags.UnallowedChars = true diff --git a/transformers/suspicious_test.go b/transformers/suspicious_test.go index 54024051..d87b359c 100644 --- a/transformers/suspicious_test.go +++ b/transformers/suspicious_test.go @@ -9,8 +9,8 @@ import ( func TestSuspiciousMalformedPacket(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") @@ -32,9 +32,9 @@ func TestSuspiciousMalformedPacket(t *testing.T) { func TestSuspiciousLongDomain(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true - config.Transformers.Suspicious.ThresholdQnameLen = 4 + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true + config.Suspicious.ThresholdQnameLen = 4 // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") @@ -56,9 +56,9 @@ func TestSuspiciousLongDomain(t *testing.T) { func TestSuspiciousLargePacket(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true - config.Transformers.Suspicious.ThresholdPacketLen = 4 + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true + config.Suspicious.ThresholdPacketLen = 4 // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") @@ -79,8 +79,8 @@ func TestSuspiciousLargePacket(t *testing.T) { func TestSuspiciousUncommonQtype(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") @@ -101,9 +101,9 @@ func TestSuspiciousUncommonQtype(t *testing.T) { func TestSuspiciousExceedMaxLabels(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true - config.Transformers.Suspicious.ThresholdMaxLabels = 2 + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true + config.Suspicious.ThresholdMaxLabels = 2 // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") @@ -124,8 +124,8 @@ func TestSuspiciousExceedMaxLabels(t *testing.T) { func TestSuspiciousUnallowedChars(t *testing.T) { // config - config := dnsutils.GetFakeConfig() - config.Transformers.Suspicious.Enable = true + config := dnsutils.GetFakeConfigTransformers() + config.Suspicious.Enable = true // init subproccesor suspicious := NewSuspiciousSubprocessor(config, logger.New(false), "test") diff --git a/transformers/userprivacy.go b/transformers/userprivacy.go index b09b7be6..01ff2167 100644 --- a/transformers/userprivacy.go +++ b/transformers/userprivacy.go @@ -14,12 +14,12 @@ var ( ) type UserPrivacyProcessor struct { - config *dnsutils.Config + config *dnsutils.ConfigTransformers v4Mask net.IPMask v6Mask net.IPMask } -func NewUserPrivacySubprocessor(config *dnsutils.Config) UserPrivacyProcessor { +func NewUserPrivacySubprocessor(config *dnsutils.ConfigTransformers) UserPrivacyProcessor { s := UserPrivacyProcessor{ config: config, v4Mask: defaultIPv4Mask, diff --git a/transformers/userprivacy_test.go b/transformers/userprivacy_test.go index 6fa0bd9b..54225dfa 100644 --- a/transformers/userprivacy_test.go +++ b/transformers/userprivacy_test.go @@ -8,9 +8,9 @@ import ( func TestReduceQname(t *testing.T) { // enable feature - config := dnsutils.GetFakeConfig() - config.Transformers.UserPrivacy.Enable = true - config.Transformers.UserPrivacy.MinimazeQname = true + config := dnsutils.GetFakeConfigTransformers() + config.UserPrivacy.Enable = true + config.UserPrivacy.MinimazeQname = true // init the processor userPrivacy := NewUserPrivacySubprocessor(config) @@ -36,9 +36,9 @@ func TestReduceQname(t *testing.T) { func TestAnonymizeIPv4(t *testing.T) { // enable feature - config := dnsutils.GetFakeConfig() - config.Transformers.UserPrivacy.Enable = true - config.Transformers.UserPrivacy.AnonymizeIP = true + config := dnsutils.GetFakeConfigTransformers() + config.UserPrivacy.Enable = true + config.UserPrivacy.AnonymizeIP = true // init the processor userPrivacy := NewUserPrivacySubprocessor(config) @@ -53,9 +53,9 @@ func TestAnonymizeIPv4(t *testing.T) { func TestAnonymizeIPv6(t *testing.T) { // enable feature - config := dnsutils.GetFakeConfig() - config.Transformers.UserPrivacy.Enable = true - config.Transformers.UserPrivacy.AnonymizeIP = true + config := dnsutils.GetFakeConfigTransformers() + config.UserPrivacy.Enable = true + config.UserPrivacy.AnonymizeIP = true // init the processor userPrivacy := NewUserPrivacySubprocessor(config)