From 366cc4442e5400d9897fc289183dca13cd3d002b Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 15 Jan 2024 16:50:45 +0100 Subject: [PATCH] dnsdist: Document the XSK feature --- pdns/dnsdist-lua.cc | 2 +- pdns/dnsdistdist/docs/advanced/index.rst | 1 + pdns/dnsdistdist/docs/advanced/tuning.rst | 31 +++++ pdns/dnsdistdist/docs/advanced/xsk.rst | 112 ++++++++++++++++++ .../docs/imgs/af_xdp_refused_cpu.png | Bin 0 -> 14861 bytes .../docs/imgs/af_xdp_refused_qps.png | Bin 0 -> 30140 bytes pdns/dnsdistdist/docs/reference/config.rst | 11 +- pdns/dnsdistdist/docs/reference/index.rst | 1 + pdns/dnsdistdist/docs/reference/tuning.rst | 1 + pdns/dnsdistdist/docs/reference/xsk.rst | 29 +++++ 10 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 pdns/dnsdistdist/docs/advanced/xsk.rst create mode 100644 pdns/dnsdistdist/docs/imgs/af_xdp_refused_cpu.png create mode 100644 pdns/dnsdistdist/docs/imgs/af_xdp_refused_qps.png create mode 100644 pdns/dnsdistdist/docs/reference/xsk.rst diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 793f226df6..724af95c67 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -648,7 +648,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) else { mac = getMACAddress(ret->d_config.remote); if (mac.size() != ret->d_config.destMACAddr.size()) { - throw runtime_error("Field 'MACAddr' is not set on 'newServer' directive for '" + ret->d_config.remote.toStringWithPort() + "' and cannot be retriever from the system either!"); + throw runtime_error("Field 'MACAddr' is not set on 'newServer' directive for '" + ret->d_config.remote.toStringWithPort() + "' and cannot be retrieved from the system either!"); } memcpy(ret->d_config.destMACAddr.data(), mac.data(), ret->d_config.destMACAddr.size()); } diff --git a/pdns/dnsdistdist/docs/advanced/index.rst b/pdns/dnsdistdist/docs/advanced/index.rst index e390db09eb..de4e053949 100644 --- a/pdns/dnsdistdist/docs/advanced/index.rst +++ b/pdns/dnsdistdist/docs/advanced/index.rst @@ -22,3 +22,4 @@ These chapters contain information on the advanced features of dnsdist tls-sessions-management internal-design asynchronous-processing + xsk diff --git a/pdns/dnsdistdist/docs/advanced/tuning.rst b/pdns/dnsdistdist/docs/advanced/tuning.rst index c8d78a0d67..e831485a79 100644 --- a/pdns/dnsdistdist/docs/advanced/tuning.rst +++ b/pdns/dnsdistdist/docs/advanced/tuning.rst @@ -70,6 +70,11 @@ For DNS over HTTPS, every :func:`addDOHLocal` directive adds a new thread dealin When dealing with a large traffic load, it might happen that the internal pipe used to pass queries between the threads handling the incoming connections and the one getting a response from the backend become full too quickly, degrading performance and causing timeouts. This can be prevented by increasing the size of the internal pipe buffer, via the `internalPipeBufferSize` option of :func:`addDOHLocal`. Setting a value of `1048576` is known to yield good results on Linux. +UDP buffer sizes +---------------- + +The operating system usually maintains buffers of incoming and outgoing datagrams for UDP sockets, to deal with short spikes where packets are received or emitted faster than the network layer can process them. On medium to large setups, it is usually useful to increase these buffers to deal with large spikes. This can be done via the :func:`setUDPSocketBufferSizes`. + Outgoing DoH ------------ @@ -193,3 +198,29 @@ Memory usage per connection for connected protocols: +---------------------------------+-----------------------------+ | DoH (w/ releaseBuffers) | 15 kB | +---------------------------------+-----------------------------+ + +Firewall connection tracking +---------------------------- + +When dealing with a lot of queries per second, dnsdist puts a severe stress on any stateful (connection tracking) firewall, so much so that the firewall may fail. + +Specifically, many Linux distributions run with a connection tracking firewall configured. For high load operation (thousands of queries/second), it is advised to either turn off ``iptables`` and ``nftables`` completely, or use the ``NOTRACK`` feature to make sure client DNS traffic bypasses the connection tracking. + +Network interface receive queues +-------------------------------- + +Most high-speed (>= 10 Gbps) network interfaces support multiple queues to offer better performance, using hashing to dispatch incoming packets into a specific queue. + +Unfortunately the default hashing algorithm is very often considering the source and destination addresses only, which might be an issue when dnsdist is placed behind a frontend, for example. + +On Linux it is possible to inspect the current network flow hashing policy via ``ethtool``:: + + $ sudo ethtool -n enp1s0 rx-flow-hash udp4 + UDP over IPV4 flows use these fields for computing Hash flow key: + IP SA + IP DA + +In this example only the source (``IP SA``) and destination (``IP DA``) addresses are indeed used, meaning that all packets coming from the same source address to the same destination address will end up in the same receive queue, which is not optimal. To take the source and destination ports into account as well:: + + $ sudo ethtool -N enp1s0 rx-flow-hash udp4 sdfn + $ diff --git a/pdns/dnsdistdist/docs/advanced/xsk.rst b/pdns/dnsdistdist/docs/advanced/xsk.rst new file mode 100644 index 0000000000..c61f88e9b2 --- /dev/null +++ b/pdns/dnsdistdist/docs/advanced/xsk.rst @@ -0,0 +1,112 @@ +``AF_XDP`` / ``XSK`` +==================== + +Since 1.9.0, :program:`dnsdist` can use `AF_XDP `_ for high performance UDP packet processing recent Linux kernels (4.18+). It requires :program:`dnsdist` to have the ``CAP_NET_ADMIN`` and ``CAP_SYS_ADMIN`` capabilities at startup, and to have been compiled with the ``--with-xsk`` configure option. + +.. note:: + To retain the required capabilities it is necessary to call :func:`addCapabilitiesToRetain` during startup, as :program:`dnsdist` drops capabilities after startup. + +.. note:: + ``AppArmor`` users might need to update their policy to allow :program:`dnsdist` to keep the capabilities. Adding ``capability sys_admin,`` (for ``CAP_SYS_ADMIN``) and ``capability net_admin,`` (for ``CAP_NET_ADMIN``) lines to the policy file is usually enough. + +The way ``AF_XDP`` works is that :program:`dnsdist` allocates a number of frames in a memory area called a ``UMEM``, which is accessible both by the program, in userspace, and by the kernel. Using in-memory ring buffers, the receive (``RX``), transmit (``TX``), completion (``cq``) and fill (``fq``) rings, the kernel can very efficiently pass raw incoming packets to :program:`dnsdist`, which can in return pass raw outgoing packets to the kernel. +In addition to these, an ``eBPF`` ``XDP`` program needs to be loaded to decide which packets to distribute via the ``AF_XDP`` socket (and to which, as there are usually more than one). This program uses a ``BPF`` map of type ``XSKMAP`` (located at ``/sys/fs/bpf/dnsdist/xskmap`` by default) that is populated by :program:``dnsdist` at startup to locate the ``AF_XDP`` socket to use. :program:`dnsdist` also sets up two additional ``BPF`` maps (located at ``/sys/fs/bpf/dnsdist/xsk-destinations-v4`` and ``/sys/fs/bpf/dnsdist/xsk-destinations-v6``) to let the ``XDP`` program know which IP destinations are to be routed to the ``AF_XDP`` sockets and which are to be passed to the regular network stack (health-checks queries and responses, for example). A ready-to-use `XDP program `_ can be found in the ``contrib`` directory of the PowerDNS Git repository. The name of the network interface to use might have to be updated, though. Once it has been updated, the ``XDP`` program can be started:: + + $ python xdp.py + +Then :program:`dnsdist` needs to be configured to use ``AF_XDP``, first by creating a :class:`XskSocket` object that are tied to a specific queue of a specific network interface: + +.. code-block:: lua + + xsk = newXsk({ifName="enp1s0", NIC_queue_id=0, frameNums=65536, xskMapPath="/sys/fs/bpf/dnsdist/xskmap"}) + +This ties the new object to the first receive queue on ``enp1s0``, allocating 65536 frames and populating the map located at ``/sys/fs/bpf/dnsdist/xskmap``. + +Then we can tell :program:`dnsdist` to listen for ``AF_XDP`` packets to ``108.61.103.88:53``, in addition to packets coming via the regular network stack: + +.. code-block:: lua + + addLocal("192.0.2.1:53", {xskSocket=xsk}) + +In practice most high-speed (>= 10 Gbps) network interfaces support multiple queues to offer better performance, so we need to allocate one :class:`XskSocket` per queue. We can retrieve the number of queues for a given interface via:: + + $ sudo ethtool -l enp1s0 + Channel parameters for enp1s0: + Pre-set maximums: + RX: n/a + TX: n/a + Other: 1 + Combined: 8 + Current hardware settings: + RX: n/a + TX: n/a + Other: 1 + Combined: 8 + +The ``Combined`` lines tell us that the interface supports 8 queues, so we can do something like this: + +.. code-block:: lua + + for i=1,8 do + xsk = newXsk({ifName="enp1s0", NIC_queue_id=i-1, frameNums=65536, xskMapPath="/sys/fs/bpf/dnsdist/xskmap"}) + addLocal("192.0.2.1:53", {xskSocket=xsk, reusePort=true}) + end + +We can also instructs :program:`dnsdist` to use ``AF_XDP`` to send and receive UDP packets to a backend: + +.. code-block:: lua + + newServer("192.0.2.2:53", {xskSocket=xsk}) + +We are not passing the MAC address of the backend (or the gateway to reach it) directly, so :program:`dnsdist` will try to fetch it from the system MAC address cache. This may not work, in which case we might need to pass explicitly: + +.. code-block:: lua + + newServer("192.0.2.2:53", {xskSocket=xsk, MACAddr='00:11:22:33:44:55'}) + + +Performance +----------- + +Using `kxdpgun `_, we can compare the performance of :program:`dnsdist` using the regular network stack and ``AF_XDP``. + +This test was realized using two Intel E3-1270 with 4 cores (8 threads) running at 3.8 Ghz, using 10 Gbps network cards. On both the injector running ``kxdpgun`` and the box running :program:`dnsdist` there was no firewall, the governor was set to ``performance``, the UDP buffers were raised to ``16777216`` and the receive queue hash policy set to use the IP addresses and ports (see :doc:`tuning`). + +:program:`dnsdist` was configured to immediately respond to incoming queries with ``REFUSED``: + +.. code-block:: lua + + addAction(AllRule(), RCodeAction(DNSRCode.REFUSED)) + +On the injector box we executed:: + + $ sudo kxdpgun -Q 2500000 -p 53 -i random_1M 192.0.2.1 -t 60 + using interface enp1s0, XDP threads 8, UDP, native mode + [...] + +We first ran without ``AF_XDP``: + +.. code-block:: lua + + for i=1,8 do + addLocal("192.0.2.1:53", {reusePort=true}) + end + +then with: + +.. code-block:: lua + + for i=1,8 do + xsk = newXsk({ifName="enp1s0", NIC_queue_id=i-1, frameNums=65536, xskMapPath="/sys/fs/bpf/dnsdist/xskmap"}) + addLocal("192.0.2.1:53", {xskSocket=xsk, reusePort=true}) + end + +.. figure:: ../imgs/af_xdp_refused_qps.png + :align: center + :alt: AF_XDP QPS + +.. figure:: ../imgs/af_xdp_refused_cpu.png + :align: center + :alt: AF_XDP CPU + +The first run handled roughly 1 million QPS, the second run 2.5 millions, with the CPU usage being much lower in the ``AF_XDP`` case. diff --git a/pdns/dnsdistdist/docs/imgs/af_xdp_refused_cpu.png b/pdns/dnsdistdist/docs/imgs/af_xdp_refused_cpu.png new file mode 100644 index 0000000000000000000000000000000000000000..f9d2debdf2a32b29dcf69de2c5ca6df48e15e35f GIT binary patch literal 14861 zc-qao2T)Vp`z{(qnka&xfPjef4k9gdqzJ}Hks@7DdJza9AVrD_Qlv@?AT83ROK749 zBE2_(4`~Tinv_t^3YHIlzyG;&=ghe?caJm1owe6`*ISTs3QCoi0YKp>}8 zuPbUoAOw^U$T1<30lE|7fbv(S6UOD@;xC3t{*k@@2x=kxmW~1cjbbR#&e~Ab5WH z5-I5k<|mgb5d}y%2Nv#zu9a0wj9brymgRa{ERUDy)g;W#dCcVpeDfL~F1x{@diEFu zf5`_0{UAU5v`+9+`0!o;kpva~Z#m2Vn>QLM+#lS%`+9lLu4453Hsj%vB(FH9V3DlN z4%|FEEK$OJy}kDzJW!H5VRYakPt1t8jM-^LrFf(7ek4k4JHNwI)eWkx~_AWMoiNQ*++YdcFb^5fSO!P+@F(jh{`UYlDsz z#*n`I?1c>8HVVHWDh_2&8sDc3;t%DTtuey#O$A|-ovHrh=r8PMzX6g?i*NMn z@dfYQh8l|gUwloau4`=LCO%5pvq4wu0(o=HiyZ=aeqv3Gp|yxr+FjSX)a}GN;d=ZQ zkyjSacOy~9yh-t^m=nF)dW)XH0tR-B5nVqYlz*PY^?_>b+5nSLr#vdVpt_&GZu|Ru zFbSsjeFZs0gd{?E<@e)?JrahpA@_&15E5TCZZ4<{gd@Pplkvbn=U{I76mJ~Qb`6_E zd*s<=9H)i4W*8fn!*w(p8~>Cm1AJ3OMS#47HLS}1r4H`o?voYR*j2gs0v9@IYQ5%AjF!xz&3h(>a=Q#^T_vIW~@9yV1MnAGrXX(dq))gGc=BIkUz=ihvxpmZY0zc5sYcF%imV z<5xEs=EccRLovrS?v~XRr$66)L@EF_>Uby6zroGnCZz&!gQr`@oA*8SA*Z1JTaIOc3R*4>GCNEZqw7;Q9({@l4Zx6KC9cV@DCIk`^PgdtWaZ%SS?^NO z%7S4rTN4%@vq-mnT-^bV>Xy`Nrxqx3X{l;CS=3HnUq7K`nwy`0tkg+TFyl`sbPi9j0fwZ!o_7`wvf z08*;C`FX-yaU?si&Z1xW)K^}*dB;KxHed7hLymTP!+!nLIsf|a>piy)c4uY9Xivq& z#0b>w7WZJQH~I~j9{qDnHB#Tez{Gtz28~8fe0(Om)+#(bJ?&3?#(SZtX=*tuBcnB0 z+Oyt|aJC)E67bHjAm5{=zw z#no(I(SelQZ01Pm72NVZjyEzFZ)B=g*tkqoS!TM!twJ)=apI^4jp0lw(a~2O#;TuP z^mvn(XZ!gjvz3+A3mO6Ml}|LY9VtFr-zS=we3B@-`}$&#NUzl?RTGd1&24RVwzkn! zSubfrl`A9%1;YER+ge*$13=J6Y|AU1XO{+xKH3j(-&|HXT6TvPP%&p1ChAb(*S~P# zLP6zB!pdY*A}s5S%+@~(LnUIP2rVnC>?EC~u^QY=qJt>ysYupO4GoX?w`WJ|Y(x{m zb&Wejd2*^bDk?J4?`r)0fjA+vSOJq-i;jygK1IHIRpBsRJKge1z@XfA;zJHJHv9hy^shCX9%w%I>VR`Ph zx3ghfI54?lSp8@v842Awjg288r|vqlGZ(b2c)2GZ);C8&LEAL8 zq<#4jd)iQ$#E&!VHrFq{8lZak=;gpdFp}n$mc%O$z7!Z$ zkubZBxzF}yYmqYN=jWG|m4T8UEHbOCtQ;B|3V5lJuAHw|bou3{iHFso6hX>e6uK7x zM+sjDjbgv4Utw=#Yis*{l)Reb3VW1Qdy)jbm_2Ji@4!HdSKbo>;vb)D0*-sJV+Q+v zMwCN6B^&-}mr(v6oT>GbY^)&L zgGCm}?&gglbdjt^wU6C#KNjIA=TE`sZp8932RxEW2C*R=3KbiFTvA!N4T=eN>y}%k zp^}mk$gJgsg$2j)q)6$A%H>*|>%w5y@UR+*1On695JdHg-^gQU-6{e&GP$c|bK3oP zM4Np^hb)XzS6~4zJ5o}HXWNMU_WbG2eGf6N)z}I##=HIBN`a>a1X&7~u#FQ_{p7Ac z5s8MkvHrvQ{Smvu^N&hC!R(D^*dZEKtmWJ@Ma|Dl0)UjrFU>&1f}Rq<0#39damZ*HOTsL=O(YR8G6 z080*?K7kd^uAZBvsXdc*9#;KuLMMDySz&j-vTri}(S$|Uu74s8)_0(}@#rB3jfpZBa=V_an*dv=m{}zxw%Rpzs*|qZ$2(iv)&!ac0=vP4N_)zcXxp9t3JC;K7+QRY~yWNLj2Za z)$Wr`;Xc^RSM5ki8hUz3ubuVTWY49dri)Rs8`AMhuj+QEiyySiv?E6*-QSPKU$yFv zU{NUt@Z~t!=(9I(!vE@qx1!OVJMlYv_bJxnQ_Xan94&#mNy2*)q`ER=jWIy*DbG`e zj*ctq*d9C45wy!ZJ1p*!0E49K!qe0Ar-(k5bjVcQ{rD8npTM2s`|o{^b;#gs@7?M*FdgnOl4=@=(aP3MX{l{l#OaOYx?yrQ) z9&C4vx?KVu`>`;Dx3UhmB&ix{$Uo4ZZ*Z`Vl67%$*%<7j;(*`Q)t#N0AtL2qVM#A4 zilDexF#;eP#@_R_(s^2?*D;7ff@=H$Rnza3V(OSml^%tR6ARBFTwLsc|F&1BK|RJ_ zd7$RufgV|84*(1?`(wo6)c~EC{n!0HT+{zRVK)7FE03hYrGLZ?%mW-;c_C0M?z6uO zwjjH|H5ourIDjij*;@bvg6AB-6e;dA@ef*J_a@Hx#7Vh3fu$UXot&NN>FEKH&bn=S zm|ndwJxUhPXB{J4J~eeeu}#!%=_{k8>)>L7lym|iUZU|Q{$>}BM=9A~)wT~fYR|Bf zRUho*&PaWK4I*f(;;Lhb!^G zFn^#$cFG_2+!Tw6js5b>esgIQk4E*Tmjc9*?GAN&u@stj?zr!)W5dG2e*E|W@Vm5> z$Esq;*52NpyfR8T-EnoQ8MFXwY;5v6BQ>6z|BpQBC8OB-Z>|S)XEP9l;bykd3kvLk z0+DoEVh}WaKYp;kX<%PDom*JT&BaA0>1u0dX9wbKguh1ri!y(BNsEZMG(4wF6t#;y z@BphcG$<-uS7|!4=Hgy6wzO<^uV7sTXh0xf{1!%bHs))L3!5e@AFX`Tx+UE=HpcH@ z2xz&#Ykp(3th&FyU+Km9W-{G8y(06r1W+R}uU>)3yZ7a2$H&J5GjhVuU&CYNUYz|; z&{c=V+zNCq6IHrmcW>A9huc{yD!%hA+iSDabx>sc&W?vf&=7D;q4!2^LPCNU?niUV z`NNByon7W&cg=0oWdQIhzktILRz+oGa1K4`?(XjG?X9k^{`T!&9nV&yFYci@1OF=? z9-iFlYC~hEjXqt2{Qjl+&)4dJV3qV($2@&Xq;TEL%uHFC$U-8DVqoS-(hz&GH#*Xu zDtMoQCUK-KDCOWi-;$%9s~pC_bpQmTL(Rlf`lXGxJotSB$tgCK1fqhxGiBG33WG3(wp=Xi7iiYmyPJeO-KfM+RLlracs zdY8CWcZSP+|Mt_&P36dp(JD9D-R~dGOX4X&YKT4f5^YrFx~(G^4|ASr1xf*@`Ti&G zibIa_F39z*duIDD*X2@v9K&+~11&T!?}O4;3YU z3VIOim|pIs9UdcU_h zUT4*lMLFzzcb$p*`z<%I@a-~;E;ku#-cr^_)6T1IY*?!aeiSP_ykm(7$BK$oc zxCa^>2+QAs8vb2|YQqBc-AnM8y8nN`>9VkfkL$@l0^;{W4PDvG1b3yVAR(8I(yj`s z_DsME3?k_F_cXw`|6G)O=l^UNoB`R_|5}r#y5qLS4fld$u|TwoL`}UB8*tBm-4d3y-K3MFiILkr%*20kcH;> zmpQneSC&9Wzw-ZfK-enA+F*FZ@M6rX**|O8sQ)(VuB((*V$U@at{mHt5OUu)EcmFL z{_^wi1TN7UW|8xDn3VYwjrKGQBmBfW0Pj%|&fgyW8lpi^;>8{xTx(2+5Fb^F3H90W zNvYoIF!LHY4xw!9e75|kW(?oeJg+zH#3BM-7T_;_|9T24XExHcDnah=M*dE}Qe#6R z^LxE*ifZF<_uVoVJbDCB`qRZ zvw=ijoo=C{qXRhXQ8Q*=0ooK-S6B1<_fIj1ciM`2){Kpjl?{3Z0?1TcjXr7QgODd? zEP?9UXtDNW$tddH9unu8&KYX~E0Hkk7*v5a6?4z5s4n7`6T&1pU#b1XAjd*dc3Ig5 zAQ>3|rmgY9;97LWS)_RN4-Y3Gku`41W0Fhn%mCdW!M|u4o0@DWeb4F@$iFsgAY??e z&e1LlQz{sbYOK$NrPX4mFufh2q;e-nmUHe{SXco10OH=&Rg}45a^VTY|{^ zK;8aEG?!M`Md{(Lt}f5lEz{lR2j}Wn^@uUFj)!{?Z30TzsQnoO@}rV-Fc;+N<&Y%R z$PB<%fLZoO>h=<=8x~z(y?O-}NrbVN3?(6vqn(|XR5h0aSGQ1>^+$rujDzVI5Gxto zPRo@`Azdf_@Hu`e*XiTa(=DGrzkGE)vUg}GK02B;e6=5oV2{eq$@#LZP=R}P<=cmF zpgR-Wlu5dFHh@N=e7j|AV{LtT=9?sxK~=Fy9J zY<7aTb|5b=?|J1`7`e*Xu8mDhFgJal#W`pV^At29ynRf8gJt0&vR_sbayu=GRkhiJ zkB@Jp!T~6p>o6E=z}2f)f#4o>Y+?kB=ecv|gw0#Q{k(#LF(5iW1d_FU`Qjn4?t*%3 zmPpPd^BA{SVMDFwwpELI`V%Fd7rZH6!B*61W|O&(e@!wulj1p2#?2=~t*pX!wVMxJ z!Bl0`96;E0?^{Dq*BD_cGch@Xda*w3W?LwoaO0~b2B}Bi(enj2VE7AsHZ?O#ADEX0 zMECaYMQtn1Oz(6srG zJ|5Tl_&fHtV`3^VNK}Y@FGT-{U-zgfH6pe275y%*#VSAf0u$^kwy z9^&K2KS=&-LV)Ch3>}Iumez!oy2<324xyT8`ZuD>144Nlegyu0ntt5Z3$MWcH5;Fe z^8jgmDUaUogr#xX^Tn3;)AVj{7Ouesf8&c}m0($wONE1(ASq zKf_^(E!Q+qf>8Y)uGbwWb|~>jsspKiGGyivJNU*WJZ$NjDI_)QC|*+-B}OR_Ur$5O!%(Y_Vh_cNq$VKnGOdWkXIw+Uk<$XhtK|XlH(5S z6Ac$Ek=)nyZ@d4c-{@L16ue}$%KGAJgluV~03?aC(!K;NB( z_&g{9+u#D*u*%1o;oQuqYP_~CCwj4SYb8;I0G2#?ge8%d=q6D3Biw{M2*%6%|LbL#(~WQkR54e5lam*Nw^AeW^Or7u8>( z-x||`ddhbn+Zq{k%=Q)ogOoNgbAm(+Z%_X@CROY??Q|fB9@2EGHtlk#kP>p=Pm`z( zP&JmpZe$uH$4RN=goXFH9TbRrvD=V2vA@3xqQ5YZ+Nm@Ok||-1mpt&CpQmWM^QnY+o8Rsu4)Nqyx^rKo!S2rpt_Z=4AXXW|yxtnqm24 zDPb@|V^LVb(5pHXuqZwY-w{n49IC3U7OvgWWi+IvX^0zF-DU~}0lfq{>ZgUdSEBM{5OjIsg0B-S9Duk|AFxP;w-`z+JNiPs~Iy|QF(SrZ=G ze6Jd0AtA^@je+e|+LD2EHJYvTq@1-?`e4;ljl#z$@M5jqM&0``4N0IT7o&R8tT+80 z4>?sDw#Dwv1Lf%pIhyE$eT4g*v!A_p{+OKzjDc1DGN!fPdI#D{KrROXl_Op#zkS$A zDPV8IGpG%fy+j}4zq@qwyUxJZWII-V?Kkc`1M>WY9Kg#J0Iuip;99_sL2)~LSxd)5 z>2Z7oyPy72`12S|46{q8jZRI;spjekC-?fV?=83_3e-?DEnb~UWgWMl2>s)G7c5I# z2w<@ugrV2PrJNiJC7e&n$>`4yvQNowQ`**KflyQa<+VEo(C2f&sp?)?0YG9bZua6^ zi(`Kkc!t)tukl$Ysh%MRZTmVD`Q(2)hV{XhG@jbQ>o=asP+{;t8Wb46z?rox+9Ty-*zZQjCo}Z$Nsp!1c7|{dx&sVuQ`OdQMk(+ z=Fp1+2>1dl+yWL3`I#P_x3Xok#OCmarT9){8hBP2cor1_-!p`yg#u={b2J!V=NaG? zyUO6ZlV&`qtNyNFPCMC)P>9~OBTuX7>e^@iI7JayBw>iq41Kd1Z^`BZQC9&&>cNoy zpP}nL`7!d;RhO-=apQk455Bjkxp6v1ZOF`c6>B2A-@hfOfhYl?opxptRD zEiFB+f2-0VY>e+2q!aIwwF}WEKY!|@MXU4ZF<3KMZBilqFq~N;w7s?@upA8y zrV{233GFXc2H*Xi$6x0;;jM23MfLCK_cs0KmiZ9dZR1*<+2e|Of9l2`L@DoQdb~N; zD@zoZMTdnz=sx4$1=!;FGpqy*TX^^Huq_U>m{`Y#m!JxQ1+ zGO0c;k*l?nQ8U*0zU*r;F)_foCoV4}H7~llxw@i9Ww|WTXf#w(vc}lqbD+kP-@a|C zfw!AFos`W?B`fnA($61rQUZklIVQ+yQkI+BVqal`Mr4P7%z3+S>a9cIKi~TvSw#=fwAk zkCT&=BB87#esnTrw;PpXWEtA?hz>~|!1=%&r|VvlMDPmwS) zGBSepW&H74TL38)0%`YYJkwryCg(|Z!H<*_+k07APgoa+S zBgp^a2|WLeVk{Mhh9jjYK2z8yM#W|yigWYukexiay0%tWwS<26?%h&~TI7^dWn^UJ zE`RU552wGicf-U(Y8o0Ug;MX)Iv*c=_mq&3@TYs4$IRslP$&eGYHXuT>Y=jlF4G#> zED~}ThMrUMer2;#LLkQmRn^o-mR8`?z-qP~sfB{q%;n^^nwPXp@604RaC38;x5h=p z#pxLt)i_Qz29nXl#>8wG$3;XiJD|$Tb-bN-w^o{?IOtCig*W-@R)0HJC{@Gnu}trv zHEH7$Kf~N0_q@L_ym04Tsdk04V?g^GE#1dUi*vzU^CBKz)AcK2_c^r2okb4r;D32; z8#p%k^CghGAr{v5_WYcjaZnd)W#y+mSX)a=OFKJ8D8f=(TN|XtjTG!dvMlfPv69Z>DY_qvaBrjd;GQOA0reIdr)*7tu(XjDPQ$9{X$bD zlVL;%yb!fIXYYWmsjluX_|!oTiQ(K6ae^W(+LIhB4VUWeKEWZo*<)i$4701b$7-IFT#;%jG(-MaYr7AE4?9iN6NEoCH4!J6axvpC`Q{In z^`=1D+u)_WF3Oa#Bp2Dx*&MHj!6%l2RQjv7N`q9hFNbJznd{>&fDz^Yz~bv)05V+? zJPL83pty{Ctmjixf0KfvvH$8%S=%{{l)A2hGDQZE6EkO&2Yx_$@UF*y!AcX*LV}Nu zOeMPpMkSSuYxV>N7rcpV-}AlVjf0WX%v^x*UIr4v92l;3XgG~39X(*qLB%7cHC2S! za}WWRIfT-`YyCVN0VoeheQuZECs{TCT~46^x}fblw}lai+vRD7s4dg>J=cNmI#>rx z@Tswg;P1hI;Q9ohKXFE+pNmu20!IL~NyfvQ>f80LxCN7dmmFtmdMtF!{Z4EL!xY_% znGjE#jjv=Tt|s|mjfz?jb&e2=^&cjKd4LOkM@Nfc^lb*ufmf@^RSOyBeoC=^@(#X> zA`>lBDcXb5()~>F48qvmE>!{E#cY-UiYxZ>jk+{n7Yr=z&_#(VdtEX%Dr2=E9zg4+TIM|0c8=7fc@05|t+$V@7Y2)t~xF(H3&!o}A zWe})p$Ql zp6JP6gWeT|sdb!!ppTAvH*nfX3n!3LCKDBs`J|mUVf4Z1?BnCU@_*|MUUKV?@6Rd?BH4YYM4-q^nDZ(5WM?GuxsvO|#pc{aq z?U&oybcvpnNvteLyk`KVk=F-0D-2#Vp{t{av#~%_EGmmWAnviv^cgn?P=J5GI*yj27;cpF3$=*z%DL`HbQ4PQyP%`T>Smhn^7h)0W{5|!!s zBh4M-I69*F>aBbQyyk!K9}O5@J7ZQ8>%ynVA*l?1hvSotCY^c1b%Y%pMnWEU?QfL+ z`7QMZ#)&`rtOB>-0L1jn(M6lkPB~G+l!*2!cJjw#SbI@;Nvw*RK zzm3)Bf2Gw~bb%3~q4Jk^_#>|nb|G^zB*s1$p3-_kAo}0=(>I=0$cmPq?RZma*tKoI zd5h4#%%!s!B1ia_AEDfn!37xV!g%NVbvp*@F$^$(WHGTk==V&0<(&-YiF`edx|d-- zublkXBUa^_pZoMMX{Vi742~a?&F=`OXO;|e9{6+&KVts^^&#UUPlytM$dUcEJ3xt# z!hgQ%DWnZjC+iS$0!vmu-!HRpIS-_3hxlRt`e$D&`NCo<&KDoXoN*wfL++F=aDcg2 zwx|IIAIaxh7m2`>?%u%4+K2}qXcoV*vLt1m)GRx3eT=( ze&awt*Mp?u&mkqjwp;PrjtyI)usZ9_p*&rprO@Hz{lh?HWI6lA^Pk*Mqy;>j2R!@+ zQ%YOT;A(5HJwQxH2Nv`{8t6BeymHq$nwSrve5hRQn6S_l7%(kceL#@F7m7b$>IHB+ z4{C)Q`mE>XURq?_X4@ULu!>5lh_6%-1$>;~V0$Hp+vBO|b_jhdDjQWPadyp@;Qghe z6X>IpSa4kP9}LHxRwT92>cqmR?^<|^ffFyL96xj-5;)NuI8jB(EvL8{h>bilXD^;0 z?MyNcLG7{VSrwWs7AYCRw@;6RA70Zwfw1YiaMxN&i+)j6(qBdQwyJwE52W|f5#zK9 zc`AnaG&#JC;^t=!Ikhv*CTk@Wu6wU-MvR<+P~(SQ2SZy9huS!!o%Jys3=@s9o!uE~ zO^sg85W(kQO&0tCSrvZG0wAi3nVX zC`=i0j5vh(h>7W`HkO@=s87oNm^iHPw9h&`kqUs%Q@r-b1gv%!uv&I+%)s!Pb%#|N z*#VD)ON`e5p>&-8919e8+s--B|HlFmWStJItAod6ur5*!S(Yh%kXw_0`Ea1-?&3V| zB7cT`LrA$ky(K6r*wLL_5dTv-8U7Fmzo*Q0$I95HYf1%}(m_iKqfY-j88uHjl|7lv{Q1$*9 z8Ut<74b;beI0*K~;0nlOlCij88Hb@dehdl}MY!?)TpyznBGc;<8ynkj6{J*Q`;6F0 ziJm&|sda46sw_O;lWsE;q~g?ezUOP9 zuI>!oX@8|Gj#e!c*y}S8ps^cQncvKCXiXG5V@35UBO?QRx;60ysv3E9l>YF46OdDU z8)m?N@KyW&KZ$C^7if9wnN1a>ir1m=;#7og0A?(2Zf3eWKxop-SAcS*RE(P z4mg9%<9E5aJZx+ahD)umSZubIxHwLq=%^@RuN_3FVZh7S*jPV5 zzpSh*&}(CRP@t37LTq%#L`N?Sl{9TejN1F`U{RCbzFqC6R8Uh>tKD5~iHoC6?-F;J zyPfaDcPmB4>!0^e+S}XZ{fXKVL|X1&(I`5x6N7(443*+N=UPQf&LD2npBE5t;--sE z?#2s&T23Aw&$(>Wx5>%ucYc_w(99FvRtPoH{f_t@u1|&Vd99NW{hnoK_VA_>i;m=s z4A$+!n>TM(xh{HaO+4%A?j~o1mf8#y;Fjx5B#rFs7HLc#uY6_+XJuzUfByW^*Gerd zqnCmDm?XHxQl_X=D5f*X8BwL&e6#ou)o_cX#98_p)#Ay(V_A;W;IX^5^9` z+)|ZUQ&`u{I1kK%E3Uw(>O8OB%a<=7u1+;GO1es|cU~LD+KP@eG5PF%|9A$4LiwyV zb9nw(*dAT>R#BST)ct@(oTsM#>Nf7>Pkd&7{9td~sLFNJxkGvwOTzq&oDq%9O02~# zNpi>j16_o3dAkfFHT`y-`@cn>#po(kfn8Ikj+-RnzP%7wU0d^dfB-GC{~4LM*jQzf z%9#Y43-t6NeVG{VxT zl-_cWiRt?6$N7z=kqQT}jf6Q{(ZnI!*eU^FVE@i$Mse}#Ty9}3jtXpC+Ii*_U&Odc z(CX^y!ootmKQWbpea%L{^K83|i;ESCC7?A~T9Sj~S@7iyc$HSL3DOl6ed!>nOgwX| z&&osX*rns^%Sp}FWybFwPQ)76ukZ=Q!w87UqsMh`-!}Bx=u4O*Iz@Xa1ttlFhFz3S zNKDMl=I>MpMZi%#(@yx86F27i62%+}3JV9?AC&29X==&jk|fd6ElVQ+8^xi0q+9zNVsYhnT7s`S!;leogE%0$Sfcrz|X%lUs$(4 zD|=9BT7Nv$5RO^{De5y7p>jo7_!bgcW;Y_rT_6W#G6CKI)s_9+4-=w1>EOMVm~T+Q z&dT~oD%o?nMptXJ!}~``hYT*SvQk1sWMgY(5{hh}Tv6}Z-ivuyHL9H6RS{Niidk6b zg{wSh@#@r&%{|zoU635Xa zApI+6lUq7EeAe2;*i^gWH20ZGJJ~om#+_RDR^?KJ%o+!6%lrFvLJbK(*u)>2nwzsT z-=F#%A)1(9Q)5)a^AbL@I^AMiIB>5>oszIiTU-0w`Sbg;KKrwNgk zs;XW3uM!gGoGOV2XfsAyPJs@cemFDOnjuz-FS$96H{B)h>e5)+S5~x;tb@^{J*>$ zk?RV*AY$G7Mk9bCw@?4}ZN+I~jvk!uWfqpn?#vsjC_6j5)zc=!SWCM*mGE>>E=JDI zCCY!c_2m5VPKTH8OaWqUY-~ibN+8<@j6L%U3iz*}0q7MN*P!$BZ$@SSq|!BZSJE>2 z9Efnq%*+Hpip5}FvSK@5UrQ9V>uv|JL%YnY-MGP~3U&O}&=^XeU6o->yohuglW>`f zKA(0G9UL5du$^+?2t`J+YU}9iFFN?FdDIMJ8OWRH9jd#!x+XR}WVinD2cXccxdl=T ziuA#ajOXO!u&NGMxjD$oKgohJwZ!s89bm0;4pR&@%EdM%Yk>y)L6anDLOQ9(oOX+g zi_6Q)-NxLTD(>&^ZkO`I)@O!o3iESwM@p@t2Ig;GzrMA%>{A%Q$vrhQqo3bjcd)k% zMe<8aBbVN#jI}07IOBdSdTuV=gu#+TY+|$c<3R%I3zHIBU^-umjXz$SX-nJyIaLUr z^SR}Sde}{@sssH+w&1r{PG3T3utI)mn9yG%Auj1!$(6q zt9kw`_=It!`#l;OHJXH|u!3#GJiITQ9Th)P*7ZO9llKo9ajpn{%l**X) ztv)U28CH`RZeT7dNeS{gPRj(NahqKt<*e6{am9gjYqSd2xtUEv z+vjG6Yy{#116zcS9Ot?-;Bfe`*?Iqv=;-JVA3nHKIrr{7y+sr$k<>HSHUQ@0C?|Kn z`Yj$_#U2YCgF^|M8HaKA?Mdl!d_qFl@~H51UAAD+%vT>WFnpNrL~FDg)Lv8gv#j~y zCbul|IVLYbUGeJo2j-|n@T!)5WM}vAaCNOWShqGFq+?@i8<0U`4+spL*hgFE62_wF zU_nMth$|~^WkoXJBGLTMba&9>%A&nKSwNEQ(8`3UsE9lMdp<*gi))|HJP|N`R$=R! zDL%`Ca~Xo1Y=0nnr6hb;D=Sw9uDS|%i ze{iVw`am$vT>;k_k6M$Le5p7B=p&&Iv_( zh;5JZWxc2515w)Z`Bn@98z0K_FJ6cpe|F)5WtHa8xe6Iaw_$o@w0z-32y(lPjzM0T zX>xlz>X3|k@vAQdrw(RU*Fk>l8MNU~;eM-dj70NGuQHQN85Uw8SWK37t=^0I`D?o8@sb(vV&!bL`~qeBF0G9h zX)p7ESs0^%DW_+S#|`QBln+yJ3dsatW|<$>GV-ae;4=31Oy+K_H%g~Yye{b|b@X9fy4{W%D9F`h%b`OFowzz_V0Ys^8YU*=?;n|!Gc{JMMQAZ?+J&(Y zxtguuzsj)4rz_*NR@2Q0KAS}XS^lx5rK%TW_sxsy={QN7{I82-YAtx4^9>n5r+`V9 zMZ4iZvXK1mYM<@w&SgJc@i+tGca6qeZn4^o11%!iK)wI<698Z4H}wGBe5wKaY&KRKLAkQc|)YYrqw*xf0eig+v;-eIA{d=x`2J&WdHYXHEcXWkZFJ z?p)2xz^hjQAcbx`+tMpF<9IRp31s1RV8zlBPH!tpzlKXOzAhirSd$RrC||woQ2+;iOGgQFxl&PwY9Y*moV96_Q$K|iSdp$5P3 zi_6jKs-?TkIAx(s)2P7N*;y=ZC9`bN5@~96pSU6@CY{FNMfL)!+YQGJT*D_QHp|SXPM0sHUa&WwsW}rkJQwR z*h4L?tm=Fz$Vf>gMdcHvd2JRo#FwIXEKL|(olQ?rbSRVyMmSf7)WgM*`^qrZ($P+Fh$DY2hp1 zzJ0@?fN(9i%ZK*nW49h;h(dYOzXUA-t1fI>{nSeLT)LVfSyYrVX_ zt4o^;14ZT|i!XxJgJrL0R}Ru0`Kh=-^-O(bU$Jtz8*$>B!KOA|d$@ z2{rC1wfnL-_Pj(2m*ZKD$Gdm$cGjjH%C2j~Z2w?!5M4n>w8M_;Ke~r!EbBOyVU1WXMvpJ-Vq~saglIFGYOY873JcX(iubHzkmQ)9y^EWSzAkx z(XB$`ek7BxM&RvTF(fw1lAa{vsLavIsBm1zur!NSiqsJNTB1^&kHrZeUHSdjb)`@Z zy7J25K%-FL=&GZGr4e9YS)l{iNTpMYz?R07Cr^r6xQsfJHOs8w8}h2EsyD=a=L_?( zzWlikR9SR(rVoo%Errc|V3cQPXQ#BAe4iBM++SCx-;pR&Sy?&6H;^C|9BDi5$>Jm$ z*LI17eR8%dZHQx2T17V8{*-m`=W5*RJT*lrTN&M3TU*5`?Pzb@h)Yg>c;$6zX=#s^ zLqP1H(i}4zpVY}&tqLIV3sH5A0aI?w!_kDeRhK7dW+~$;b zjXJTY99!Go8x`0$h22#zG8@X#5x2K5jf$eedmt`Ob_tV;Dr&Rro;dK{hQ~NIB;lmA z(RO9t-yEIs~!{&|`n4j%q<2)E~RmNml2Ti|%#quV2R}Bu5-H!Thg<7A|!rlzCQ9sBZzm{p4$0=tRA!5`Dpe1jMe*NuG7 z+Hves%B%?P- zV@_F9b76UVbaXU1IeC8GWTkF={22l&E=nKRpqB@?kN^Cnx7=rRJ8w{TFH`c{_GUk= z6=I!=mrJ3v9es7Mw>4nG6(mV#Hs6utx1Cs6*!|=Vu;GwhKy);<@Rk0EzOnbB@jY`J z_7%`1hna+6fKrb^{4K9F?sAoF&iCCg&F25SOHFaA_%PmMfZicy2+tFBQ(K4+{(8SN z`cPTv+-MFGGVo?pxN?_?Z$(zdod zc(pY(&-L`IX)SFh*JnBc0|GqipR+3}C?qSS9z&d?d)Z*c6^&yerqhoBvfu7O&t=QE zm*bi#GcBh-7L7RX<QlHn=)pf_A=&uCWy)Zzr7_b=d z!Xwq`C~~@OZewL-W!MY@(5IAR&U{k<(VZDIAAyY>-y;vjp`(3h4Gor%mj19G;U{!x ztD!Nt)*@tH%RW8~I8EKonRMLt6{jT3Use>Jwgct`F>(aroK?NBalrHp;?pj*rYuRS zJM{4hej@p0t*r8R5;1$RD*j@3#%Tm~Q@&JN@f2uyASNo)NRWi7C-IDf#(>JJ4$D|Iii&{aPRyi;=uRAFEtpsuwXo)-$l zSw|mSFf}qYy-Vst%%Z{Luxax6@na{a3hMJ48yn!0n@mg`;Z5UDo0^({C*Hhy13uP- zxX3a_M$3OVo(2vCEn?KHUjfLGEr6teVx^^~LL9eWn3oUe4#u=0hH{lwH7IsU7B2wE79335R6M0jgccu1lxN+mg#fuli!%&wO zh`Jj$%3?-;x3;UgVW3jl$4nAZtf@n z2kY(SF;hXUH4Xq#6pX)n#6r^6Foj_O&EsJ$hbi<5p>%17=S~D z0AmBwFdHuCeewwn0qvP`3li$+?X{h1B%v`nQhWS(>_wS$mK)~f)sTyAjNA|n2~l^X zhEHb9E01Q4id(uc*L$M+l9HuPTxg{}-vkr81%MWCTIesRcE<%22B1#w$Vik&YFgTe z?Szkhq}|kbtv7I=D2h$g*FUt-IXXP3ZBbHG^fU*QBT+UgQ!b@O@zSCrGb=0Z#f!#O znu$GNhQLquU*8}?{Z4yMSm3_^-D&!F zvMA3}miiD=eHQ(bR5H5ovNhSmtRB>!Oxaan|9G(k8k&H8s<4SVMz;6k6EoWuo-KnU zo}QkRTxVtf*!gg4W`dxRqlnm6FYkX1kN?AIBR&9BEt~i%kwrjEotzas*@#w*^M}?; zA?Q)WmnS!bv$2T^CF!EsUIC*{c`+cPCm??h%BIW@X591druZLL0Y+(t;dHE*Nq>B? z7CAoQnT>g7nG=jDt-t|lnZOCFra;31VsKWqmH>94?fiNn5#+#?Uv`)1pl>uidmA+sjTLkXn8vA@{KLBc z-xK+7nYR{+kMEIfMsRGke}47T9%-hT#esq0yq z*pO?uGid%)|E~h_zhxN7_S!lEJPM)32ECV*IR838xpvb%z|ns)bu^yW|CeS5mDF?+ zz}csG;d*YF6Y zZ}r)^IaZD0M@mY1T;a9Mf04{h9gWwg7DC(M$zANpU1oO=jPZio)X2o-HVcd3i_uSj zTP+PbFOPnX6ZccQO|(*%n3zbw043*n5uG-zpN8_EUs?2=Cs5CeLllYckc6@4K3!i- zlu=46$j*MTI@z#b<5}@2BO`+=yvcxbxD=j7cIxjeN)ddFJRv~cAov7gtj<-!(Rf5% zOJu4JH?lB<4XOm#*>k}#pI)B16_lEse3P9Wj2LJ`+1lAjTbU#4V1)a(A%=UHwV|R4 z%HpyD5A<8w8$&f)@!EcT7Zs(WQ+RX_1B26wo>^tNV^>UW;=r5H7cEj?4i#3uWfd}{ zKJ+|1y9?S0Y10`xA$etGn;+k<0_wQC(WA{b$kH(f_$L3< zeb_f29A*5rhO0viQI9l-g4*I{hHSE~w9RE{OFWEzjZUm>-aEp;&;kCR*E!-Na~Zo$ zrcG>_Gr}X2^KQd-*~JT`_eCj=F1EKZp$&c22LyA`Q7f(uXhFcKT(*X6s`h`d?yb*= zh>rUKuFS&9y0x;BqlP*y0V=|V@&;uh;L@ZrK<~j|`ATU(JD9GK5)w|LsNTWu#@b)> zmO&ZD%iCY=?vy0caX2GeDW7CyY~+Y_sTGH8%3*6LvwSnJ%FnC)u;pl5a4&tcL)Piy z#e#;0%9xl)-^E?{LZ)VA^;<(?mJcg7!S7?}R-JZ!uppWcE*rxRO9D26IM*0>dHZ(G z+;X2V)=VfZ-Sy!fww(&q-`!ernf4=Q)w<2ZbR!l7len1Juw5epxv{&wZR@UIX)*DZ zVeeIpo9|s>n1ED~HM8#XNA?hSS~NUb3Yxez9EWkSlubUBuaplM5TAfiFw&PQ_;HdP z9-hPsZm2gcJ-vM+2EQy6>brJskRm=U6Ll>j>i`v=>&eotaz;gv!}Qi6U^1 zx#5PLO8Ij?Glq4pCTO9Q&u!op3ymw$ZEBiO`*2zz20p->#uurF5KdAH*q1I5_7q(@ ze?FV_UTcCd1Pu)-T+cSCfI7!|{OAgak6dJ-d^}~m)g{cCm+|$24|_%!Nqxi|C+6mi zM|u<1*8YN&X&JiU}*Z|s-+*@ri^E9;tLPH=54v3VisJuF%3 z;dakXg4@YAP9xK_jAjvK@^}DLx;7pf9#R~FI=~_)7(Vj{WDa+F8v68d&}KlT!`FAs zqDqV*b}zuqsqgor^J9Up5`eF^+O=R%!0?eZ`6=-=2RO{Zu+^6(GA~hVJS15UP)G+B zq`f~hB9~d*oB+?$hDiznotA^O3*g{dg`-{kZDMs9(8jN{6``4d$U*heyzdY+epH$1 zNVpCYP(;-tJ9(SU+ifj@aQe*G#OL8c%Vvc$sDxyEH;?_t{x$ z+}|phoy_#6S}+3n{9$-+MF?6w=5MRj>L{t`*6o4GeJVPsC%?^W-t-pphCdXOd~Q;; zf#9X@Zw0a07*gU8Zi^V8WqCn8W3O;hA3Oo9BFm>;Gn0@tF55%uKRkwzgvkIDPcuh> z2MiDTJ3XNh<}8|Zm&%9<(E9!ynh|pb(k3Br*<%QJ(&RtYqO6%3hrvOWxm@svqv}{M zMSiP!Yhlpxx~7W!6aqx28*E-n8tu8;Z+{*Mmm5iI5rsW_6u^>>_D98B^IhDwpoz7Z z;ExYHPY-)WOxjLA%J{pdn=87oAUzg+1L!aHQI$OwRg{-XqUZ1+o^m9GQ(T^!2H4n? z?Po^u{umo7tvlF!=b8^R^rz@BFg&l>(*5tn8+_f1Aq|#Epv=wV&@JPg21yY%CwOw^ zw<=m57dYN<3o%!dkKGb~?$hP}Q2r4%FgbqJgo_Z*C-)he<9g-<`tVx=pZ?|!J_uuE z`gj0~U?g%n;mqLMHsF&86ytl0VpUrj|IVsdku|*iuU=8MOJI$hERN-V{_?M0fKxLd zhT#v1O$R{Ru|{%j+0OWr{I(vR9*Nyi^{Lo%_niOJQix&gm)mo$A)dkiITu>{TrVjI z8!rLDU0ft>g2+&7jA(!l|JhOUKknw)5GvZD{FDx2QNa{1^XUdyXO0|doxvKdD_LvL z%>(0Mu>?^hw=>5V&QPFHq9QTqIO-=|eO)NBtit0R^s=RXrm;F8`K@+?04?NZX`Nij ztlG{{3xQlPL6N*^i__%jl)*1TEO7h>T2|nS3>1!Yf3iBQR!^&!@eji_+C6cr0>>*k zh*;^-*nd9)IfBeYW_?kD$+g_WI!?Y*+%}0H!PBA{dzb&r3lv*cKL#`~#p%+CI!}-B z34!=P!e2P^Yh|eYfp!7yH?>H#Km4=ZBeCUpl_>7qJ-y$9H6I!g##|eysL?KQ;b#Dx z@>LT2SwHRZZ}IHI(Oz8qW6Y;!1ZZaeiKp^s5I8u|lE&+>Kw6OTb3*5$<^M4S9on`J z%Pi^mEQ04{1rZ@N@5rVr6RZ%hyaF^NAs9_56~(CoE}~8(B!ZvwbV!8ZSZAL5B!8@E zNbmoyXlm6i|J?ZC&*+%*baqY+!-CO__l6Dxz6vP|`IGYt+-GRxp@;)MXZR2DqB#y? z;kg_mA`qWDzD&-NM5#zv&cC1lQT@Yv%9 z8*DWF0;sv$>ChEssDPGR;M%mGX@vtLsidI5ONs?)+wGa>O{?Y|cW#S%W=8*(!l<|7Tg7evBwujCaF%x%j)^#o=;EGMXMxuj#aG zINOT~tHLa| zxEj!=xXuv$?j?_d^qDf(xN%PEgP03KRoCTxBL!)G4FzL)g7Z{%U) z)CdPVOp&(hre`Bjr&BS<2{z{HtB&YwSy>QeIQJ{zMUL~b8iS$;vNd%TuG&Lsp%z4D zOu+_YO9)BDc**p;y?GI~#AJYnwMkg$P@`p{a_13BZ4JLbmGUfx)lo?Q0$!KnI+v$a z_+a=L&hY?j3aI$t2GEv5u=n6C&c-14mW6uLw^~|hY^+GP(=(JsbowdIh)!QA!YWiL z385L+nYjbSI^GaG^&K8gdU$^)Vwj#g@o}Cd+k+Y$hIpA{Z0Joz#oPo7a=Q~DAVs_4 z_(C7z?jaH0i;s&OOe8?qvJL%OSB@fTTEIOP$$ZWc7cLkeA2oC&QVtE!YDrtf3ZUhF z2Fp`IEsxShxGrXKqsP-DAW$IQ%9V#IxlDi)ktIoo69I^STWw8iYm9=NtLNA~rPFh; z?FjnN8Y3bq`u(Hb{b_Qxj2Sa``ROz{i7+lWjRj2P?M=OFD&$t&o%oVC~YHWvs+ z6ciN~ST=iR@83>TD<8j_Wn0UnrRyZHDkoQT^ka8_t)<9pNX;FqO)os<>c$cF?QP|p4(?p%0 zcDe1!8)7YFN0Q%Q>DqM5+{W(5j~_pM`Xo+iCpvgDibY81s53<#=v!f7VL$%WhgHr8 z&%1d0Zk99~6;!T%V}c-{gLfUbm&K`^6a*>ox_Wx3_-(Tb3kz+QhW!M04Qy?Tr8B|6 zM-x;>*-NGHx$bX4Ob%O%%z?fh%~3)}KyYr&c1gvx(bCa19O&DPeR&fR5drk+t8-Y* zkpk*zU6Rjckh0^mU1?D+hjvm@-(Q|Nr`HlWRAjEL3TJmpfW-&SFeThTNNY1TAK84| zXR3{9KBvuy2E#Q+sq_u1qiykf_g*@EZwO?V7$0ZRuAD!kX==KlALX>U-XZJ$`gH`q z-D+Nopu=hd!;cO{1qE^;XUDeK2ViAcJye2@DoThzA4_DTo>i2f(??cqmXi7yUk@Bg z{%d4pWE2#pcdj)@@ZNaZ-}&|G@kLiz<+x+1r4^aaD^E6*pKY+peQXBCxRWo@U8 zKK-aB1fQ}XMZ;#$_#=i9WiJtgqc$V-(B|U=Q$n23Rn;mvG$hwBc|4!<{&um&#A3Rc z%U0RQWh%izj?zk}J;9Am`RmuOi*i1z@dyheRfPkWHZ(LmTS0x$->;?$hjM+3;RtWC zRDraU_(9_k`wX?V9?R4kq)2Eh4 zhz}n>4m)iPJ!7p!b-Spj+zhH8ixLGg-qzMuMdccvOa#vga6O2m)tQHdS%mJ8ok}_K z*~vXgi`A+;a&1N&xTQ-1TB~XKN3NlAyS(CJ+b^%NhuaMc`p=xh^jLD)9&;~Ss#I4( zL`6kyJN0*Vt`FIa_>gdjDQSw6>9&V9>3P@Y>eTtlgmW$Q=ES9@N-7OCg>$>Py5f-Y zEO)D$n+}y0#Kzti3`V%Rx@KqJF?3fNn8UnGiaIsa&=_oQw_F@7aagKQR-(X5QMk3@ zBNkWUv{%??^0DT}vTST&Ar}vi3je2`9_6&I?w+3SNrAY3FJcDy?Ato<%k-G`O|;Aj zgfK+-3d*M=`%~PTSthKrl9G~@dy8dBk@nxtm9kdtuZEjfl5iTdg6z`mYOr}j5viKg zArr~J1HLskRJK(#;#{WR5(xsQJ_-0l=x}=?_JQ|(5s|8+!@YyOEvZ;5=eEP07MH_C zn<}f3agC%l1AKh^&BdVskQ!-S5c6SvRk+C8OFLCZdqQz-*T~5och?PrBy|ha=-NPl z85kP28E|gubQ-~}tnwTo63v5`5tAUGV3v*vJe>_&r&cqZxKz1k)*3C`4xLztTpX|U zj&fXWD7RZXd+wY-v;zPZZO4^bi;1rlV2Q5O^+|zC7x?(Co;(?SjZIJ^A%xoJnVX%J zrVn(42nkgIhrWLM7XL8}2Gi6;9kA(IdVUBB>N6=UTP*$V<$dj z7{P1Rlc~uSE+eS<;>9wEKf&!$x1wQtsSKqwgGLJ}~XBU(;U87V36gKOTk<8yON zQP2Pm8%2gXWiJB6adfRZfmPXU6JK~aXPZ=%lA1c(Utnxru|b3fVjV!$O-9C_vbz;(nA)01rU$Om<^nVX5E#2=EBM?OWhZOhcEASEH0sf?A^)Ybhl`q`~^P#4)6bNToR zX#BKrvor*CU853jw(IidfH~PEOyMi5YimWO1N3Q9 zS6-vGEk|6ADkeW}mQp!yj{)%J4sUX$2Dx9k*>8O8AQ6%3&4q#RurL#@fyJRR*$AHN zL0`tAUiX{s!Q;zMU^66r?Pzy%fzPNj82~z{Zo>d4s&>1w;vsp} zLf9EfsD=ZOl-%6hxTKs0-RY{i$HxX&_1*r-os^ZQ z7v7?IVqdr*DIsA8N&$i@F?X6pOEjw|t_V!ewpK$w^JcQgLyu)^5!xrf0rjD_bT@Az zLAEvfJ#{ie4Il01d-voFgS<3gg6_!iAd0f1;esW-uf>nx@AmzZV=Su3Xq_gY<;jt^ zF5F_FD4k@ZKwCvoQIi#PP1%!On}!&Uo>0%^*yt}D5VT1Yf$kf}E{#k-X^s(LpqDmF zltKPHh&=swNh0P1qi8^3>7hDTa!<~a1Sx>B!a@;sEq>u1j-TD~$lDu401Y-1^~3aXITyxo&|SbVC@R_GDjEg6)@(;6Hwnz*Y?*|u0VT;fs#8d{I0)F#Z=)VX1kjM zG9%Wq`ZR&QFuFP^8htVArAuhVxN~l>d1Betje95Cc6ktrnfir+6|@%wf1kEPMdwYQ z@tT%8AJ|)1G z5&?7Zq)G%1;XFG3-VUG4OzNXo?|)r6OKZbgMCH{6BRK(^l#t3ap{K(xf}wdekZ15W|mp3 zJ6ZoeO)2Bnty>o^UQ{oI8Q#DD1r%R3b@e8_wXZ&8S(;_mOBGv*2?C3!nMBMKg#=Sxi(kxoft)y&aU-@TPevd*IbmeySNBs98;4KHVFSpd|Tcb9_GU z69AmR44S#m8vt=8BO;9^QH=zo`C^!^~ZMeAXxek5W|YH!LhHFx`Y3RjYpWdLD7T;4~X7UaC48 ze$I~97Od^eXhw)00~{spOP&)IlbY(~;Y-0sL_*RBSQAaI%Izd5f+m)L0s7kA9Sai^ z|FMb5{7gsEzqHv)I8YG&V}cX5W*rlLLS_YOR#sM~T+osgK*} zsn4U@_L+&ip-CMxu#;|5Ot+^%$XQJ_@&cy;dR?HFF&TWH@rl`A4bP%Hkps`!LxHu* zY*)sKwQOH`m~e$lCg9r?R25|&w~u<;C=dw^_n~zkERlEC92}OFOrl#(0Em2*&mMoWo>gt-RFE1}=)A{O==2LgvNLt7{ zy1%owHDsfW>gDw#cx-022=s`Eh=`}bw-1aU70aCVxbQYWEC972y8h`LE+8Xe(upU{ zrZi!KPWEzg_;{hxiC$h89F%&_o$3V_ov)B-WEtW#>CJ{xWwy8J1J+`_&|d)T1g7z4 zyJ_CmCC_Ui!*hz|DmxlDTRiEaTR1&r=_laHd4IzdKbm`GtQSujD~|r@78xpz7MzBZ0r8SI5OHFt)^g<5p>y#_{>fIQFk2^pLJ! zPkWt;cZtkEBTVj*wAbenUjEgJ|u*k5yaYwUB6n~0Z)W4a*DR$;2Tv3nP3wGXA) zRez#WnDhFeMCN+!b+>f4FE<;ur73X1MPApKA7Y;SMp8@}vI&)%a^rHY?8Rdw~?^Fo1{Qu&32 zAW0{F1ZzWOWsjQ7ojbC$h1m2jU}H;2Nqw)ce<&j(0H5vjy2`Y;ub`)=CokVYb+lc_ zXGM>I1_uPJ0?wBdA5VDossIqZqR;dC-d-ZH1bGP%(;j5}MWWl(u@Zsw80hHn@$t7c zikE>N1vw}HU=Uz~=S>Kt6v%sab~(qWsHmlEx{QpBv9U1>3=DwUaNAEH!%(>Kydgk8 z%8Bdt?Z)By;^N}{{r%0$%byZH5}~%h-DiIuI|JuYxLAb?f|*QCP6Ai}BG(jPLSL@# zt)N>hEX@Eudp5mUE3XMTm#Ymm6#F+k4`b7L%TubXqGBMcl&&+KMGlo&0@&%hwg|B2SN7OiZ{` zf{xqEPl4=aRxgwexpSR|hX*hTK0bB+=;C6PB&#diKwk$026`F*vw&a#?(uo|&cw_N z=fZ{D7T;2*y~1s=Mz}})lRGA$f;C6*8h59+&25BpnL>cc(^d0;G#cpdx1VYZ)&2hQ z&6_u1k=$Hd)}8o)4ZwRI^yEI&Z+6UAqex#~u2Sx`3DN4{O%!9m; z_U28X;#VrF&ykXnf;AZnsW=jlk*Pm=)cW|GMyXcgC)8sTr^~&LkiL;(ky=B4n--I* zdRN9K)l%tPCXgCp;^N*y&?}kr))PWprGId zq%=OBmWDl4`s&rIw}QMpDnZ@?aT+D$66pb;Mi8V8s;tfC)8XLqxE$^~Z4D7$xe|R` zJ!vRO$jF!?JD#5VfN`quTI!q2KM=0{6!MBvzU<*bw%RTFXO$>L-Y5) z5+n(V+(w8^MgNuy1 zQbRfPFStHZQnE(2UZ(*eZ3{%&(Rz{)-uur_REl53gxn|b5u0YVu)X*Aaa)XtJ3wJM zZEctNUN(!|&V#LyDyO|oU()E?3Y)`Rtg$_F6B82^Lnv|FZsoXO;q+Fk--@MP^~(Dz z*B5v-Y1!D?n6ySoEG;b~sE)nD{r2A9)$MRG{+#6T`EZQ=uhugirMkmG`c{G`e+mUq zO=;WyrMR)BiJ%U|1|_h5J>2@9%ckAVShAR4++9QV-ObG{IN+ipd61;%A%|+k?&=uv zZ~iQrLwy6!Gj&#WvOMu>36^qE`t|FKjEpYZ)l?oguU~%(Fa;pAUY;F@wVq7P zfjs?|T;1>AzJDhmAm~X~T^krV?0%Nh2qeSkr&m!g92+g`A|)b6c52`r3Wxbl#qf#Q$^*bFA~7r^z`(A zN#S*ow;E;E3qXPlmsm}^VqEkv09EPCnKQsQ7l~Owbc};aIpVk`lh#!Vn`xh&on2mD zc6D`4kP3eLFVF48cg)Di$wj&x+Od_{@2}K(KFlX1B;?=dl-JeO1?qLTBkFKn`)EVE zBT;6qCo4+eLRms)CUkIo-wlUsbom0$WOMZRu^Jq}Vp+&)7|Kt{%v@Y@Ioh)saZ*-P zgl{j84Gs=|{VGNSv0WZ*ixUS@$W{fA2TuY99CDu4uC=w_IPT_Byj38WwN`~a&4=rSressABcghWJd-@Ngp;tX%n zZ;rSg3uHGiE9c$2LzPZtiHY>Iv^91HP(ft&8Jw(pNyK-!xHi{kI-qBR(Tgx=K$?Mi z=jh}V7!Z)JW@>A@4JQ3eN2j)e$h+2na~1{(lGOd{L8kBie}i8sUytc{fb+pF5RaUk zoak6s;nG6zj9*eR8VHTXu)ArXW`d*x6H_fTQB_eDDvd`^vHlT#O zR#Tw5i)Hng_vg<*gBzX$<|#@@PELOHY7hd!Y{tsM(mey)*w{!(NqPVNeNz+ak?u|B zIIy}EAE5WwDhyz*fw&Npm(@(88XzA5LH&Mw4WOGXeSVK#)J`AX#AVPL-P-!7Ky6`S zLMKNBwR0{!NJc^erp5E*6B;N@3JQcIBzCKl4VkyLPiFze2rmI19WCwTk002sPdG^z ztg3+k2Bfdepd7H4UOf2Nmp34n$`(tRAdp>@d7;U_KN@-6n$@%SC?PW=%`2x*6KKyT8w{^zOUn3m~-Tx?12-xK&BPl6&2WIBZ z*7@SJ`w4OfbuRmL0H+JhN1&o5B_%p_zJUDG#R9q$!Dj=Vm)-@;UcV*M>2TkUrWE+S zGev%FZH=#8CQ49%klK~qW8z}XnlS@n5 z^71+bYH-Mq2(CbUmyu@Gc^qcwi(WK$cgycNtI>&-vJ3 zU=vXr7#Q4SW)=WBS!^*O=0mF6a^@e;sAZbQJlf&#@$nf?*5B&^Qrvw&*PepYz`stz z16tBHCP@Pk?MXnIwO-!RhR2s#dGj#`dqxC2J=zy`Dh(k&+-jlf>sbI_in`tGFyTZx%0A?p?w>hevYhZF2HW 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 Server diff --git a/pdns/dnsdistdist/docs/reference/index.rst b/pdns/dnsdistdist/docs/reference/index.rst index 2d53990a77..640dc3308a 100755 --- a/pdns/dnsdistdist/docs/reference/index.rst +++ b/pdns/dnsdistdist/docs/reference/index.rst @@ -27,3 +27,4 @@ These chapters contain extensive information on all functions and object availab web svc custommetrics + xsk diff --git a/pdns/dnsdistdist/docs/reference/tuning.rst b/pdns/dnsdistdist/docs/reference/tuning.rst index 756dc8cb89..c6147313f1 100644 --- a/pdns/dnsdistdist/docs/reference/tuning.rst +++ b/pdns/dnsdistdist/docs/reference/tuning.rst @@ -188,6 +188,7 @@ Tuning related functions Set the size of the receive (``SO_RCVBUF``) and send (``SO_SNDBUF``) buffers for incoming UDP sockets. On Linux the default values correspond to ``net.core.rmem_default`` and ``net.core.wmem_default`` , and the maximum values are restricted by ``net.core.rmem_max`` and ``net.core.wmem_max``. + Since 1.9.0, on Linux, dnsdist will automatically try to raise the buffer sizes to the maximum value allowed by the system (``net.core.rmem_max`` and ``net.core.wmem_max``) if :func:`setUDPSocketBufferSizes` is not set. :param int recv: ``SO_RCVBUF`` value. Default is 0, meaning the system value will be kept. :param int send: ``SO_SNDBUF`` value. Default is 0, meaning the system value will be kept. diff --git a/pdns/dnsdistdist/docs/reference/xsk.rst b/pdns/dnsdistdist/docs/reference/xsk.rst new file mode 100644 index 0000000000..d095024758 --- /dev/null +++ b/pdns/dnsdistdist/docs/reference/xsk.rst @@ -0,0 +1,29 @@ +XSK / AF_XDP functions and objects +================================== + +These are all the functions, objects and methods related to :doc:`../advanced/xsk`. + +.. function:: newXSK(options) + + .. versionadded:: 1.9.0 + + This function creates a new :class:`XskSocket` object, tied to a network interface and queue, to accept ``XSK`` / ``AF_XDP`` packet from the Linux kernel. The returned object can be passed as a parameter to :func:`addLocal` to use XSK for ``UDP`` packets between clients and dnsdist. It can also be passed to ``newServer`` to use XSK for ``UDP`` packets between dnsdist a backend. + + :param table options: A table with key: value pairs with listen options. + + Options: + + * ``ifName``: str - The name of the network interface this object will be tied to. + * ``NIC_queue_id``: int - The queue of the network interface this object will be tied to. + * ``frameNums``: int - The number of ``UMEM`` frames to allocate for this socket. More frames mean that a higher number of packets can be processed at the same time. 65535 is a good choice for maximum performance. + * ``xskMapPath``: str - The path of the BPF map used to communicate with the kernel space XDP program, usually ``/sys/fs/bpf/dnsdist/xskmap``. + +.. class:: XskSocket + + .. versionadded:: 1.9.0 + + Represents a ``XSK`` / ``AF_XDP`` socket tied to a specific network interface and queue. This object can be created via :func:``newXSK`` and passed to :func:`addLocal` to use XSK for ``UDP`` packets between clients and dnsdist. It can also be passed to ``newServer`` to use XSK for ``UDP`` packets between dnsdist a backend. + + .. method:: XskSocket:getMetrics() -> str + + Returns a string containing ``XSK`` / ``AF_XDP`` metrics for this object, as reported by the Linux kernel. -- 2.47.2