pax_global_header00006660000000000000000000000064151056075520014520gustar00rootroot0000000000000052 comment=d51bcdd3fcc8770a6e2aa59ab91993a51fb9f9ab turn-4.1.3/000077500000000000000000000000001510560755200125155ustar00rootroot00000000000000turn-4.1.3/.github/000077500000000000000000000000001510560755200140555ustar00rootroot00000000000000turn-4.1.3/.github/.gitignore000066400000000000000000000001561510560755200160470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT .goassets turn-4.1.3/.github/fetch-scripts.sh000077500000000000000000000016001510560755200171670ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-master} if [ -d "${GOASSETS_PATH}" ]; then if ! git -C "${GOASSETS_PATH}" diff --exit-code; then echo "${GOASSETS_PATH} has uncommitted changes" >&2 exit 1 fi git -C "${GOASSETS_PATH}" fetch origin git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} else git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" fi turn-4.1.3/.github/gopher-pion.png000066400000000000000000001437161510560755200170260ustar00rootroot00000000000000PNG  IHDRzTXtRaw profile type exifxYrrk^ /_Vӵ-"ϩrDs_X()zQF|k 9|疯o_3_|o۟??ߤu_r;'z]PNǯ?}|K|{.F0ÜtŕIwɬ ]^nũ*I29}U$$ޯ7]݄`} sU㯵I{rHϤzfk_]VO! {j5SqjH_bk3lU{…3m6#:5FK Ydso'q0il,,g:ڭzgbj6/S,/6>Vlm3; &Je% ZWtvtQ90r$ ~DټYפ #[s<2tM_gj/5uVvMu=W[=ٱzǰOceuED586HNl,s"UAmNoiMs^:K#`0B9% Mijik*F ,П0?z&5xJRѪ VTQ!? 2,|v?=;Cyz-yw#6:%,2Bך9%,2߁ycNmܠg^9}mlyJk.+<*K~= AUmxz.WoʝOmz"r:=1o|C{Li׺aYe*s*N ͓O$iCqozsJ&D c% @)םHlcdlZ R"V22RCq'"EPڟ"cj츫N~+,h&\LbY|or47|Dn1[f=T&l,׭t= ZiPz8q'/ȲUʼn B{feՄԢYTz&7gж]|5 4>󖪃MؗaZUŠ`"[,aCdPBkr/RcDfL(ț=UcMyr{΂Z$X8FZ#̜ǀgFb"mVT'dBQAv!K cl/@|&A9]l LFI ;B @ 9*gp9hР7IUvs{\NI*Q,`=@u,?M8}MлhD )[P`g48m}X䡚A-c@]jS`ǔ @I'Nq"711a`mPl2Zc@}0CVI̦΢VGQFQ%;7hdB闶vyp6"d d1>A?M`hr;FX\vQbs$,ܼ^%XM<`FbT% G}S'*b?ǻtB۳h /UMt@@EH_ s pLU^4) v"%@ρc\(:L'_t6X^D`.8I z׀5إu$)(cЖ:#Hp8H}g0_%tA _M هL+}y#Ep NZ0Q#I"h8TZ^$ګ[7TS$n\~ !jŖֱ@=ՋKz 7w,9W=8'('pRF j^Sj{.DG7kT? 9OdK8%+aT-ŽLMHG,|aKv}A#)n?$=GH~T B?u}5DGKPH[Ɋ&t9U(9٨"#q!HIa6q`!yK B!iSy`v>P%QfB-> =,$Xjhm>kT,|_'+CLj.`8/FZha|4!N(5 s xXaUx(#*ŇJ'hmi-}ei7hRф$MeP1\2F,"!rnpvG= P CO YUVnx9 7o*|Gh %cO> AYbČ,/]&LװU/p@GPԸ $jg,EBĽ\;HvTJ Ah/zG eF\k !CoK|įHbR:P+MO&хHaOlM4[%:-D xJD')K2mQ-Sgi4 3{GpA$F\;CP.xP`ҭtևl׊(2Ԩ ACT  iiD Z_n/y@d9b.yo{[JÈG,1G|J˘M f(l&O,boF4|!) Aw:>-% ā,BuT. Ѧ"k nO!.=+ZKb* K!Mp FF@[Xp3W >3}0/fZ^EmY ~ L| W9&d VޢӀ(}cHZwj+01q| ٯH)FNxc~`Z5+u6 Bat'P#wʨmOq4_rt6Z(7 Ryn]a$ jJg}j(U}1GRDBfA`)Ċt "X)kn)P fq[:sG\[Cu35!,U@HPm%:,g=* S@[US]XjlF )q Lc1_ t$FM;&y]Dh٨A4;kPht&$S^ƕ7xt;@M^"vޅx#1(=@x N8n usi"` |;fI/YB}wvA%İ]H\ӦشdH Ƒ 0 :xt|!$طJ"^iҪrt.9>,D_[ s,I~ B ]>|ըB7n"Xh#. R$tDu@P.D1dV"T ,݂Dkl%R'\ n`';9[ ףP{Awq}jDN D5"<&XRoD/t3#|zd!ڴ2\C^: TҬXzOeٜ'tf9KŞ#jt¸x@ՃBAtjH."A]AT#nRCP ɀ.ms1N{ֳ *ut*~jw^NZ:aK~М<A#cAHcczTxn:&,|BsVzZgxYMc!tW6wѐIOurPªء}^1xsp7&Ƥ*pwnAnŽ-azJtDz9IJMHnn,1\{E#<8zlbDzXO鄔9Q75[Ь1=|N:0cN"OJ<{ܤkL4=QA.d$?*婵scɑpC'[z )N^\े@.-OH\OɣFQ4A] 4Y@AARu+o2iB7M1;@=cg- O( ^m] u_Dͱ U T,=#є^o ,);ɄJXtqkMNhhN$ RAecW8nf%4~MBH:x4euAj,z*1@iwR`{a 8ѫ`v*gv=4rQ[u~#>K ʖQRd=;j*r6gwS1>:?zBkhbE@_4jƣ'v"kauTMxK#3zƧ~?\V84J yHH~= [;fwtBfZJTiW~QLm W =|= ,&l|d 'i+.tEr'ȖlZCC~]M>4n.Q!M=N 'ԋ@7]|~$pi!U&R?wiDe@h&Y0IkyWP}?q+ԮƪcP 4|OmTota.#~ȨB&+Q)D^Nbt r7|ctP{3\0٘-5radev/ЗH2HV>ZMj5KƼiwY %-F|t t`Tb]O5!֫P>+Ga6TgR E=bFo.qnҦ1\d#藎Se0}j;SG[As>݆P)\M=o뉗V,zӃ``DyTЇ]hԢ7䬡tRٳ-@Q ACo(v a*&տh=/_P/ \<bKGD pHYs  tIMEgܰ IDATxwչgfԥ}[u ƀ1@4R(KBrIJn~N$\n `b 1ƽoV:9?FGciWZK<>f3{}                                                                                                                                                 cF r @Q{`  =6@L$qcP@]UtB15Gtzh'Q{5tݽ-3Lݛ &C04E6&ua6 (Gr`Tc nLj3jʣr*Q!&`"/qF->;t0?-s*:lʵZh\aP\P mbvZaN2 "^>tbHTj7p ~nD9AL5q%D^x"υx*Sw>ӑ ieYDC$)>tz8 F8bJs,2<$2IQ"+<QyaU ¡  `Ⱥ>&+\,20ʆ=>>!ot7{ 1z `bF7@[L%X t߉ĨОʱ*:;l&PWbRa"k,5W 13U{*ora0APTDaU^oXɀ_{0,P2+cJvx :'Px |c[/bpXflx#<$''s ,a(P1j땠pnR'9g . |608VlI`iD11;7wvΛ]g6jA3*- tT_>؄sfN1$CqoPzsW *oNp& ZiɯoZ2Y_x J>pBp5HB#89?ZwpQA98/@ SWlpH\>hMY&U,ЮG>!(WZ6h^wB8}˙~jK&#w{Z%0I@V;\q0~2A9 O/1{JIb}BJ`@)|7o5!@[ 1¶X$I )M"13-㋬A$d7 5Vbi+y%m8QU|# l-{4;q%(>){v@kۆuVF=CALжE&ZXFLH_ |3[nA|4ܘu Pc3 Y9o**|>kaOxIɼ4w_PD&[yn`խC6S2U3n}]^_z^ p[OD~" cNHQԆ~uap/4^l]xt2k&b^ l)2f1h31cLMwrFwyd˻g5E Xh^^ L1sybCͮV|j f!{5"frK$&w㪜G 1Ƹ+ix%BSwStжp('f٥L1h ldGct(sJ[c7FVJ1??2s?Y^L\=/t) )+jA`B  Gc2ALY/zfd[78瑈¥w$v\uj-dӦ9~Fω_ߍ qqjmI3Ҍv&.c(e _+5ƴr9>}e5{͒pGk>2ʿVN␤oA9 .j)<Pz) bŪؠ5nF\ΘSc;ӋۆSqho},pŁ2%:PZE&̂W9BsP6K_6_E=_ ;IUVT8S+}@r:dU1Q;|+c "E8c'GRsʏ=۳/VϜJ'U 0G]Ϯ[+XCBRsgO-;INаvO E9乳]مŗ#=lD[?e[Wxah{Z &rI@XN*wʍo,uJJ*BU;lN E61f^XoZkDo^Qff{JAe5&IL[ގU;r5*ۻbUO՞P5nG H↖"h~ dɰW1&D7wa,,Ypm % fI0y6H5Yy |E&Q|a5t>hWUCzZ%"hSnM  kc3n9},WPK/_<l>-*Z":r^̿bg2OK'S#b8A}y^ wToPe{ *?Y^kt9X7b,0P_$9ǖ@pgwH=+69XML&Ђ6XBw &h`V8!)o=8'&U8Guu] ?Y^ݵSz79|xVkNw5&R%L5E3\cC@h"/`J$ǗpI$I`vײVI;CR§\RWH"w] @EBO.QfIz{vK&a"XEMQ|/#>voyۀl2oA+Id9?\?SJyMYʜōo!,sG@tXR60j3eM91Ut,wn0 ZA LLE<'\ECeoG6Y$_nJ-w.џg@ .+_ICXGDh"BLd[%eC'7:\.'wN-`JDu0~ bhە&2G&SP1^m!#!CI|I{Hpa}gg%C<+ V0k&6q3&i/:5-A$r~x> 1 mxDZ-X/C`"1;v;w~4kV3gZ}DUq]];30րI|IqÁ>w]G -'mDq⒲=ԴEo?a6YW/eI'AH;3p׾ZR⋃NI:,֡!}˟G0SnB( \dˮO|,]j WxWr .UT6r73z/^EK## KDe|~~]d}gwWUW.I3vpWso'n$#M[. -++ @+n.8|qe%x1B|mԍI}amxP άVS8,b/m)%̓- 3ۜ؋M**OkkeSl6%R;RmU _+GAh-0;8#.7Ձs~X97oB|9lJb]/u3nzi3|JIMJL$}AYu*Du\أ.5Z ³_ֻK a7 ;,zpc}=Ͽ/HXj䓇oZ\\OQKy/_~<!4S2e'&⺓KF "pUXqo y}Po >;{Z}pUɷAǗ.zhٷh\GT_ fA5Ʒ-Ȉjb p:wuAbLxzQj/_^lhtUжĹ*(0*F}c}0\ဿ@=ڻ| -DC,.qHY((Ѱ% V~왢;|?ʺZ0-ah֯XQ Aˎ:h{?J$hvsHۆ_7ЭY̵k 1q':Թ]"d$ƃ@X~q`do9z vu m"Ҕ)2Ĭ̪*.Pb.yRyª{ܺɄ Bw;>0Ϙ 9ZDE~﨩# SHrA梉3,:='?X~9 !,7v*a9syg;n+\[RBW ~vvmJ_k= 1cK7\,?8SK1Lζ΢-pAE-B$\$f1͍lN 0 *ސb{0(* 桠*N++3EDG<-cP\ _ۇ>x9Hn!o9F?cl19*w]-a}t31Q`1:I DDEx,[z`܌z-ݱB~?.o$MxyEېH׌]Ftr]ӛZR2bCύ`c"tbXa>ᠲi;,3[߯ZO@i2hl]pceDLY/\^~ YC.(6eTZuXb*lN57]4}?0@bӉu_3Z ֩S&no$<MmoK^⩎μ6@,>,6׆ ZN_b|FVco5 Z~8Yo~i<= m2 :=f̟o߿?pBQftkR^-LKҼyrʉ%O!,]4h6=ziNyvD"HKKKgqqrl–.K,;>x#䑵}"0LԭXoݑsOu -)F|'ZRTǴ aI0}o' )XjrE+ G"]yak֥B$y+Vk:w=_ihG!LR=djG_U7!{~CRmrMM3Tn,˞Zwee\N M৯-Y jK;ϟZ梮=fxG8<]%!q;_-Oy!{dmECAFI𼂂L[dVRAYf2Yn\D=|| tzKl(jg8ɾf/ xh]۟l3S+۹m۶]{' ͛g[paUT|x&߭~>rG$-gd}r"ا~_H |.Iʨ`JPQw||h[򆁁QN$aI0 p^2Ya6,=D+fe IDATϞ|qL& Ore+c<7IU@o>^꯮Fuu5>cw{9{96mjLpG2Q{ikgO];s6WG.3L>]kT g_y^PH!m cFAXVQ~1==ES:(G< p^s]{-O-))n=@й !iξqOjO@IX#zHkk+;wjV [Z߾S땃ԽGV߯]`8pDGD/=uiLuU[Hm6d:N Vk< ?+$ioL|?ىBߐǦACi(1d'Zۑ$/73fH$MXBD{ 8|I!|W0k0jRVR7Xz}p=;?`}:W|qrw!t{|P~YNj!Lң'eD[n~S'+22'ҿL7,!Y3])l]ЭIL\2q˰bbfѧMsf}{ \9QH~RY$?@bn`Drʰ_~0X\\fgojH΋[Ы\_ٴ/eٸ!z|bL.,\80/4 /" [[}mmߺuGq@^~_ FoG;qv]h/c,y@L3"QUۦλ(3׏BY:-jiZVU~.~ia] DInۼ9!ycnۍ_Fx o^$,̲ygpBv+?:\y6lد^6 SVVN@-ȑNۿ C>,G@/^0i>w$ cG`<#%S+IׄW x֬l-YBNuӛ0c>}\p3te =nGUydCAPĉi M~w.K)Pnd e۝KL}g]k\7{|c4LJk;ԩWsR#{ZqCom޼L;1Scw0uvv:Kn Ɖ!"s c黅M</#t&c^XtHݛ;fwyG;wԆ U݇)/Cbn/pOo\3ɟcK 8`ɔi p}*_,+{_:I>gep7Xu n[jG6"igy'؝W - pvETd} &;>c̒V9k׊߱9ɳ} -%D t(K3VeuM} . ㏏\yioG~$]w<{!csXw-X<;d>c.']ltz89~:Dcq^/6s~;Wݩ5QKIpEgV)0} 5kT$xYbYD> /ҵ^Db8/]kй`{W5Y;|pkwDKq=7#[*Zhkۇ\TI%t R f#{Hk-GЖnV>4Ӡԓ$G OBs)$!xe[}}ݩB"NYM$Q_E֣`0*ཟ RlWOКDNPV0X7z>spR_ƃCQQv*03g=Kiwo׏hM|45\8M\:yМv!YX!,d}{pBf}}}nj34IEDV-~s0?~nz9rT j5GxƉtPV֠zhqɞPDc?b😘-c pNy+͵s74W\u# H% NirdCPV"wqGF.h(ې[G&,+ߧwqsΟ@?]=`.X!m,{~shhi&[tw|/JGy m"lf!kО¡Kl2妛ni˰1n0 IdU.&6kn-~hXAeKk+\nd"cXb#4P}qA,Xe̚^hUUU6-ӽ 2B& -"6[kXP':r $ki a9({GXİjUd?&NY8>oJ¹] qK<ى@s1+AAYhuscP.;Sp~=vG.mPĈ2,׺31ğ=!Qr'KX^j=}C|hc绒^`B1jw|c`ook$0(l'S4f뙌ʑKZph@sꨵ?,1z]`B %viY=L pJCUZ{FT.{~)b-5r49rhgkGoyn/NrgH|mCz#TΕ䉈}gߞ1ъ{2>(P9 o'&n<蹹qTkbCr$c8 8$GVf A |Ѻ`MPIDs0rsEQ!޶n̨,2F:fTZpB*#5c>+3 О:!BIN xΉeO#nwnID\BطQ?Tf68.ٲu*V֗ϟq\EGzh7#6ցYVy,2[.,OCKRk]=C{z(d/N`扙TYyI/nQIY؃ Qv5|t@e LJ\Cl, G`I`a1 `TVV69L X3*-b0\|ЭOZ4Cuh&|쳱q-OJMf& c^ rg,n5b}}P}nE Yalp)lC`KEc&Xb]9] {i8cT,Έʳn ~o-/i6a6i6UQU +.]VlXl==%1%WcEwBj,0mA0qG&BܐI}"C I"."? M0S`3IY.~\o!pxG#~$26Q1Lԝˡe5g9P&,#gM_1Hk2>$Ĩk(]Ѕ6"C,~_DYoX/!ŏ95l_{ ? WoXcfM.0axւMO: Q[-jʏ=qwի#N6 ,!71nNP@OWl.)@L;mhK1o㠕kd]̙Gشp9ΝpGNSeꗝH+B_<2ɢP͜ ܋" O6K ;2`5<\`U\v:ovYv$3MZ* :,>q x`ֲa}HX}⣏,`0F ruLW5;DHT95/;b̺a^l|6bAq2Y`"xix@2ug& [,m{{{do|DWUy'x?v_b-m+GiK K~8Ɵ]!7z"{zw?(^kC-Z"{:Mu:ȲOXxqC&MP@p>%H2ƩQ z8{I8/P3sPPY_랖wao[oB׋eMM8+֮b:/ܸvQв. 0)Sc3~W=OoBۜ5ӥ{(5Vks*,! HLwD 80}~Y9mڴԄO&%nwo_F[h^Mg2a5 ^}6ݘqcli g۔bsx]ÉZ`Ll#MŦg<`";wGR '@l8BOڝB)ơ2gg]ld̆: Y+5&I∩EA8l Skؗ)0$ZWWkP{;daJ9nۢ/E,cl)B]Q[˲\[y$ ml r!yc'xRh? 9A &~w8IKq*$dZryWBy:twwOpK$ڎ010l,/%%~+](gj?q "%|8<Q M_Cs;[fJ$>[8LB!ȪR,ΊrfI$cg&U]V, `U 鯱bL(O7PzN-~#Sk{` 3aR'6؍ όo5w°bclYᵛRUKbukUQ"A(Oy iOcWN/|3c4cwVPM,)ȓ=[ޔ͌-``" kJO&Gy*XM5Nk[[ۤ-<vLרN"!u6"KYI߷;?t*]_obb%4mE#ۣamk p#)UT=Outw LLLPG`O@jX> 5.ٳg2יH\2\~| }(QBf6UDjn)*8$]ϞU`5&|17j椔aTpcl-|+j&8h#6`趨*drwwNQ6u*njP{@ې&H/LX1$Cۤ<!;zƘxr#c(J$jO۠&-Y$O@kڵvc8 {nWY$${aUOy(hp%^S>s{~8Z0\STTf h۲F\dィZh&Jìؘe8^91ZR[Z7 01TꍟܰaY pD Td+ $W_>rL3ƖxZl^ cl$ ;48`1Iyri@C};+32Ъc룂lQq} e+47 {jm  4[fe3H1jp(悎/6)w$P IDATrMx``" Ng;*6JKM"+B7J'|o89xZZZҊ!Y&CcXrKpwk8/[PKPRm?%B(/{fVZ[WX;}0tb/+0R͎`FtҐKL"af:HiMuBF!J֠ncRK>UP]]-]uUgÈ& \s~,5cl?#%_]U';,bmf:y}W=m=֐,ms4Fd,sJsuKEYlL+bZKJDppK L$2+͂&Z7'5؋[*Yb6lQp(Z8/)MA6IE eY$H h.!eڇ]dF]<P! ~h[Ĭoc,XhhCzʽj2ƘX C~6ɰ:z؂+7h,5 Mq9B$O >__Du6b А{XfD1wdٴ"sd%5^2e>駟9w)Pjk`A},"X'~>g sk mb=yeS=E)G86tkICֿxS#{}C{ࡠ_.}-޲eKQ,MC[=kΩuo5 Os]aW`<C7࿍Ϫ1 Y[~wqvGk BKR񑖖L8a x@Z8 ,/ukMHW|@e1 G]Wdvh[ [o#_}ۢz2o 8S?V˪ڙDtvc8DV0 0aGH{]Яm 欣꫍Ǘ/_~>cJ7e?c m|#,lmʍe5zz?9=WTEU{BޱB|\ȳd'|{#=z }K?{ba(f@CD>%AQ__xqUcK93] -=,bY.9,\ʖ4~O-D`͟>E-tڔg7*vao{OE(0I"\Zˊ&1CUQ,+2A_ٱK~Ӗ=3N)ƤUxC@n p<0")s\\UUSm+" HeX4y[Ƙuh5g1?sO\/"E.B/E1mUiӜ'} ;maގXF*ʷ>g?wKw:?K lR0Z/1ٵ M(lwW(4,II3N+shL5G8 7<1mFetAsň(LF8 "/A7jyAtaDxz'Z7X̠,ceZPy:cw1X@jkU'jL }+0sUiSHl7%>n>}zasssɳ>kwx`N[nuYŋ}Ѷo"zpU6@w~_vEq7QM)OvRq]2@+Yj$ u!:s7n4X{ҌX~{_HP_~b|8^%Oݿw۲r.c!cWc3M޽;ţm6u`` }ODtcdopPyI<$04NdY֮MXM t$}dWvw +gNww4g'teۺeH&&tG&FfTZVIh=N)3y ̡?ԩSST\/RQ9c#sU=d@y*+$xƀcl 0pG25yg:jť 0 d \5jKJJf\Pi pCMVQ`u% w];09+U9L4Hea$c0w :uq ^lɈ d@sf49wOtS]zBHP +:+ %H<)j,Ǹa;*p&JC('-|~yٌZlڴ 10"<ϧEӛL&^lܸQ,p $JX)lP;__Rb%rg7 );\ 2P;}ZZD8a8IL};X̼S9cSӧOPIR$qϞ=V(Ƙi/_z[1CCd``"kk+&SjktW'>[v7K9L-#2ު t?:#G zJmmm<cCF(1N 6.¹v!.Кx3?_S×jf9TOWTD3Z>H-`p8*)\x10 )8&I8_bJ6.MX6+0y/B]I34Jidn PWe9mx ZWn\{5kRO~#ݣQ@!0ۼý\]5>~<Ч'nx#"Uи}@$g(Bu U2C;f0_<G v:::ƌ8 J~fb]RFz IۼLx5ehPX>^ 6N M|dEIb4O1&Ss|KFiW|,ތSZx޴+RuybPNj>H k@u-d hdHlp]jGly@{{HחdIf~q7:'ܼ<7+B͕x\|+OI|PcV> _Z*EJMH<8^>3V^ݟCuxn͚Ùd"X?ij쏛G 4-^QQі^ꫵp)5,˸{@T_8cJӖWnu>+G ZqN+T > e|˖-r GܬyWEmJTWz DCf_m7|SmmIRn^Xe_yj?b ) ไř󡦗M䟡w+s=f5PIߔ+MC KmԤ o>WX!P^L[n5\c555aB8z(^|EKذa`幱a8o^xhhgb 9ILP]?$[08G҅_4si&aˠ.?TXG.,/,U j]Һ(j͉ xi\.H&YyշCKsDpڴ$ZBV~}ěoV啯͍bCjjBp8|t"'a4%nӳw^X`^](1 =u6p5TWj4㇠^Q@YZ֨=3Y<r%݊#XQbyBr7- E|ye9-+_W \BVȱfsr-Mv.+,(14E"dt3Ugko 1:L<`:wAԶZxrPʏ|0p2@*^轣HXI[?[*Hn++-:B|L|@S &. I3$p$328( cxGcR}'n^TMb5iK6ɅyȌۛܿ^?SUUq]*3~[LrA嵋/FÑfuʕE ]6-+tV?Z_¹X`v氟1Frnqv ϕbTcvͼ$pC8T|I 9c{$ET=, Qd8hS4K1J.8S>\XvW dpPs;ҩ8kݧUf^]K_|B\]3F?c Qj^4{zbJ9ju6$xDv]RtX$f o͆J}9S7!JF7ؑ*11v;Mֻ//?wYA\1TrĬEa,!ߎu/BM╮IyxT0.ʬVTLVD-D<VCun//eM)s?W:Rc9Qq8s~~kEӝsMp_:k ǔf b{CRwox"#b2 IRLcn+O N$3B/.+(*+GWXTx7ȊJZ.u܋-t ߻aΰU<~lg0*LFxw<^ -mLShQPO|hZQ]&2'GSO D0MḺx 5aid"B׷Wم5W&v ERX$юȩo?ߎ^(4ꉞ{oJNXhK&I'ټ  *7we;kdkKK*U]@Y>ɯrBU^:#K/*Ӻ$91e()NJ;N[f[z3ʠP=5/EPhq;Ң8ŏ/{,H C=K]]۵W/vꋳy󊬮]Vľ|Uq<. ~P_?r2cS\eSʢ"mЧs,Å SY1< '|?ښժiZVen.;v:+ P|]pۼ^$I&_Ɂnjj.Z}yluA 0fZ;:Fӗ+DJiCwT]ahD #.Iś!;0z$ cI$#c=[aܱ>2ժY$R M < Pl0lņ;&R`ǥ Oܹ eOEĕ<م«#=+H; zߙraEE AE`~ 8[;4= d4YSb#{IPGsm}O|qeEgs{a)qs~ܹ|}Z>_tSh Ojʭv"YjN}$!mvUQQn&ppmhxl=) IWKy!߶@)& bt)D)Eܾk]f3/ bWט -XLZxB|]I ,qX,)DZh-f?6 G 0rKGOP EJ4&)X$ap>yQaI$,? I nW5FC8 *`VMF}7f^hss:s{_7Xg?ZOxL@%Z-Ţk!.삯m2]$7i&-NCaO96 IDATۥx˕&v{Fkf@SԲ$rX̜MX P8&·ەt!ꡮ:ŷ@W@p?"所 Jα9MK:A\{'Ub ݶ7lzAI:w<\{PRwcv 8]vh*xXy>s~v{ S{/T҈wP vA]?YP{V;ϵxH\ 1pܐ`?gW2pDx͡06f|˥NȣV%΄ hYy>`* `9fo/[K $p?u겥aE[4{R::1IybArl^?k7~PjH<1Cg,`#V&USa:5@o$MG.Gd ](X f w&`Md$LQ+xoy3mLAJ gO, =y𗺺UBƵV3, 9MP]wxB@pʵ )1_3ـ_i7,@Ɵ1 )Ҝ4 N+d?gd8^T#nn)< pFJ|fa[/8ӓuG>j4\vT2 AZ2&76y<2cLD\"vEeh0nן ,fP㉶ڥGgz96]M<8ڱe$C.˔'P4}Сݙ|fK9nX$$v$i`E5vFk%Y #n^1`Pz{;ljOZюoDjou ؤNc}`ǣ]"Dkl oyx$> 'CSnSů>f]|؈$)G V&ӘjjJߕ+3JvvRTtrC#2q%J V]ݶoD<9NSM;w]Px|#jRLHsܲs؞=i/_tܱ|[o3Kh&@8{@bB=A8,0NA %o+x?|Ͱ,d|&^gooO<=@镐PsgJ=H@j7jkc}1- R71p* TO{jW,ظqcW4՜pEwA[{ǏwOuLsl$ΔU67R}&4H_c/,".,۞ fOPʶɄ[c8|mن 9uj@@9 ቶ6᥮gM} t?}ýGW~Q>)zeUhƯ'ΘMڍ{NEԾ}5kD=vL^|Nd? c"c2<1˘ e'eu"t 6CSINcZx<ux/=F_&ʺwޙ~@OO7p__ F4{$`ad.t-AxjZ68nOjVt.(ݰ@v;GΣ>UfYp*i%0A82>|ܹN|w歕\Kl$ PI'/14|>ejH Z~`7I }3:]m 2{& ]%qr\YTtVY,%6`$$SՅ SDAuX/ϊ1h\ N`LT V/Ya$1pϧ~tiۯ,M[&O*}=ȽLeB`K`1B7i.tuuxu/ ~) O(1*F3Q1~t= TɘWh,#J$E?T>ͨo42w(9`Qɪ*ϟ-" fRWC:KtВʼ{Zx& B 4Üx^ʲ^9_K;T:73$jbdQeFWCC(D0 a؛||g'⊺pqlө 0גd"$gb3-}p=xٟ|q QQL̼*7ק;nC=8$ũy bf89L&BaAC\QԈSJ$ڊڂV0?TbI+9khy bf̳!px`LuA]n"-O2 84eLa`T%>_X8ͦNv}-ڍl~Z fa횶9qeQQ꼼|ض}~ﶇSLl|N#Q,RZӅCQq͡%!& YʦC]jν{+,&V/鎧O @u(8Plh .'=8(,DKO@\ǛӕglinԽE0 X ,81(]V_K*,U;y b F,(?OLlxCq•EE9c^>X}b dF Xߞݷh;}3k.ZݚP $`v)v(x] 4L31oNQ%K~x1Wp,DzV#b-$F&G% c<LkT&\hkIk*%sN-Xf(Lk EV+&L}A\ F/z=BMK x~1,,j- [y&KT/3C{,E@SeNË|s$7ͬY'"Sy bPw-ads5ʭ2n昢׺&6'E͛RȈLK'f5)3s\!at"Y$fn;gcnbcԼ1gٴR>)!htLĦgj{c0 YAO.N p CeUS(vC(3u`Ѳ.8Ksf:xF՚*B7kߦ_a9.D7_^XxzZ.@-K26=[4K<;,.b.+]tyyڷ>Ӿơ d6d-K(=?s% ذn|s c p~tlSA(2tȒnR6-?19,szk'&" xr Λ\PZ#ii G *#AAL=N~f?kZ~ V~Ȍqq[v+@`0ZXnMӄi+!1Ț 8}ڭ[$ggţmm8 \*鎽גrC|IaQj^8$()sܹ2$$zz̉, r=8-?"y "4YٞeK:_`óX,nhqұ@`VZ'EDb cLnƦKi7m,Z V2Ig"4EB!L\Լ]26yNE9Y~娰$f.XpZx6rCzU$E1X$DبthN &D ",mqSY<9@#Uw__=@Zx6 $1,{e{E[ ю^^3X?&(qO, Ng' 3Ƹր7O Լ=,<ٴS|h`E IIg% P#v1H9k@ڍNBK#WMO@\`jfv'_z< CjЬApogIԼ10ƤX6,@W,uLnݱd*Rn_:64ߺkHALFAy b;iX2 fsc5oųw$SNZBahr6]?"z`Ȋ3 }Z`}ww_}'V% @'.k(^akIӖ^&)&0hRPf>G{}NBxQIgX2{"F( ˨y b9#pτ _:|xR䂞8;YmoIs-ɍ\`ˬ QRuoqNǑ~Ҽyo쩻W.(;h7QY%R'Mp2KsC-D% W^:DC<٦pLEf3g K*l$/$)$,'4 8 [Eڍ/{ &" q'q}]enmZ gP5/ALX.+2r@rox8F(p&Uq|\0`oK b0\nW6xE.dJrڜ$$c2fj^Z1Rl_j%yyiolzk@g-ǃASI,YgkIsC7Լ15D~ *o'_hs>+N`3}DE?ffS4Ǯ7[`[j^<A MS@[q)e p`*  ~Ij1sK.ѺZl( PFe.tX6??u~:dAmj/6j3NloG( mHv {ivcoKX%Q2KfSͯT?٩!>M{NB. _oZ&R˧NtF ."b4̀5<{eH$h"ooǵ۶G9_KJOZȔ &/o kxeV++,0 0 ΛmW"K.Xlin'1N1ų5Met%a%祗*)*b$$g:" ,/"9V>.%fݮwx+,ď/yn7@8˗kC!>8aKf3>QU%M{EʊE(? ,pS$2hnx<ڒ >#y@0qԲ_fyG\e oOn<r* Qk +$eV+XS ۻצ;,`31!`2ceP-<咴uFvCOn#2r F1o,-N_h*=pgeQ%A:x=˧L%฼p;^pLѴBSid}Wt*mrqrD:AFO@^~5}E+Q` IIFRWq4Z,.6BRZzDra#6cY< X @~7_s ">Kx6"(8<0 חh7%hzE;S`[P,|W'zAB >9 6ˌQY*~ՠ( B Mzc⦓Azy,6UYW]@_<=$g)m[$,mzx DF?n%tު ZxJ74 8ʲНmj!೉ոEUSg4* sg |-4&7GxrCD:O/)|,˧*1+*J)dbP MOV(G'Np!I !5 '7' QRH+',x&˧V 0 3۫ď@EL ~RÁnQ; iR%ZL8yhK%}n.371(΄xZx :&)vŸVP>U]- h( 4E1D8GxWԊo{&ex%>p;7_vRe/)>UU}t>`J8?"kwbݬݸH(Kw>^j%ೕGLq誫sf855(,!v) z-"mq<&&VfՉx8˧u&q{Y6^rqkYYWHflkk킀WԊ |=BSa@~H+u&pgy,9j#tj@KZu 4> .IBVBjblO<<N#~ge%L }zR$g;/tv cqhjs~J6ns1$jflguZt 7듃;/UAf٩=UsJDX$]hrI3 IDAT=W%:h\[oX&&=\i8m̚50]8RK*&G(*(8`y9paxFI Xـ< pO=jAI:~HjQ j*`BA@<Ԅj4@9NpE3_F֧yjBD²8Gh7i8up/-@׈/OELǃA$|s,p\: o%GdlkZc4DlX϶9tC#SHB[W}ZHhʓ)Jr\aL*0 515"uz*&OLf&F/KOzyKO M8`kai]ɍ-}}P4VvPZ 볝 m`1\g,Qa⁇?"csHEYaYŎpLw86 8ls4KAD "ieR˓C$$MP%$ć Ш&7DJ53as}ƲRaQphcn8:MyF( nۖSI?I>KM ''AkN˜?80(x ״E*UӂDbaVU[Y %_?C8nTCo' }\ogsuAY8k#{|Y @TQpֶH$@Q0PPFdWl@[b\ PS 9Ľ 7on|Br 6M7Gs@IQx(b}ww1Q{G,^;$rgfis/tD(Ca 8a~u7[=Y3]Ktۡf8  Wl#?jm`bl~5CQx~pժ,/Mn8v7@ozB|y 4׵"Њ'.@Yv*+V=P `- nDGZ[},,d-ZbhbOØ5>CyNf)I10Ea1u_qg-rsJl‘DP[RkB]M!o荓&W86XoHjsD#DZ@>miIBQ23KSb,nAw;m.q?MU1)(*ܾ9//mkB+5y}Os8Bs&p6ydP]` 㸯 3 ܎=a+:$f7 k2>1x)J۶rZK!1CfL?}λjjZQ ,e6[g%ǐ2 }nZ<遷Ѹ"NK\-=OYZ6:{XTjKc_?Ҟc0UiV1[K#N]s Lѱ@߀2_%e2-900y>4'qۭ=l _꠺SVn_yްv @^]PFك*\S&u] e<eyRS6<&ܻ_)=lϧG+H@ HY759ò:MP^lU[u?_p0k `禺5SZ2H\ii7[y".yN펧 M14u<Ȫj;˓4qN'5aM^J(1U<%!&&Aêj `o3'Di _x@mԠOwtW K!بћ c!&#EV).Ldދi&RYHE%Zi.W_PYM}eC s"솚5M>W㸴h>& &@]NQS˲%!L&o⹓ews=8;L%ݗt. ̟cZag Dw 015Di|A3-KbAFNM~[?Oo?מv k+e"we;Gvxk]9ah`Ꞁtt ^8 ǔ Ekzh)yRcE1YaLF_oMPӿZd<obs}|S漏PTbɰ-?d}q\ \Ǚ3MØ"+',YE.׏H7-XUF/'5˒zJdbGe}s4ˎd'!H ނybr׏@bw]a!NJV!(h u꼫;ԥ腸~"Q>XMu\sތ1w9ε 0mx'rt ƽv7xĐ#_/Kg]7-͵7U%!=qY{06ׂLy1KHj]3L57q|5e@QE`F(%a/@ LXeVnZnv3 /<KPT>(<v6q|bbOOw%F5ﹷ -tŅqxJclɀ_&j>5xZ c t#Ff²jyIA"{|>$E  ΃} L[kH ]S3!Q 7O ["(qd8p ]{.mG0+q;Y nMXi-b?SkҽN/e"&ǡCf_U.)(@삐寿 eEl&n I,%zE.S ƭ|cr$Ed'$c«G*+|0 |jj_$w-?~yvi\( 88 Y|Yk :Ksr6߱{w΢o`s$g%_L<5pJԼfuׁNtL\'~9ǫ ,߸b!y"sǗYEy`M”UX_(tefw55EޘO,hC5'ϼP4Xw]...(ʊoc!(*avi+"Š. lŸP +_tiKayCwܙÚZp^rC+V\ *h\UQY}}|/ ~5P݊K[ʛ _k"rH\VsrLtqڅDզ`(),>1 I֣Qk?GJeJ#˛F7L⻖{su WbE_Cjw X}B D߶ͦIW3F-rc%Ib健Kw:h`37lǯίuwX,EjoPwΫv3<3:qW5oyݼ1am<70q -c`$Qb}ٽ!TnMa(XPd ˴R^!Ml6pC/&tK2}xt*IiwjT?Su۶Y7y<{,> 01N@]LG**.^VnW۳)f6nԷ+>`6]sMaSou?}#a^d\:8UmRዣ\%ͼuߣ|ݞV?^M d.|nqJlCҾ6 \u}' 2pFӵJsSB'92aijfg.5xj֜Ps3$Ub6AHy8@IR3DZjFJy,%w4dN8dM=}!I/mQ>+Au}DP _$/I.HtyՑ]j:KtĿ~k?nL%5]nj۸5Ywjm͝ zvs9Ԝ*+ y>-Ώ]5J MrnYq-'ңFw&0'ؤyŽa06 >s6tZBȠ$+|dպ7cp>IΓd$,9X[c1EVx<YrE7y<ڤ6 ?)%PL'. eʸӡ裻cP}aPst˞1,oP7ˬVl޼aCP2NOK01(%_?QE9|}j 󛿿pa̘]"?Au/o΃;{e"o8ecdž &4 (H! a:ӆeJ $@ҤB !xı%mŒڟ޾{>Y#83sGo}w9;dUl[ZpRFǷ̪+H]Σnr)̍3uigݦM> 0t=ke7*eM]Yٳ5T(1v%ؤ!#s!:#Bncԕj`Oc97ɥkk`xuCC뭀z=O?S>}on8Y#k$;{ !]̬Y+ɓp /d*Փ^~a-5G}v}x֝/T=tB脪wBW ޞ bA*>'NLS7[lGLo.Νܿ35Ӭ0s$l7CCW@^[\z>D"ozf ,:˶ըXlnUɐi}U*%O޽bZ;At:1ӛPN[+!ӴCR)r*k~oוshJ_ m̬Zշ|bRAׇ^rï5oaA@^ F*%Y2q ~kҰ MLEŋsu>_2`d.(%S)''Wl? R3rwנǪ#ZAO[ ]إ|ZΛ7)r]Db{;AOL_L|CWeX$ז/ǪX, |'ϖə_k70ĸך.[YUݖh.{y'?f d2؝H`MM ZApS6Mu4 ð5|,N_ye6(7sݒt.ÝX论o_^H_L&jϞ[& ]B68^dƝ3ve,Ѓ!.cl:_Mkv7f;cR^Az P +JMER|;\NR|l 5;j, 5o#~toR_-PRfuBAvx/$8Nc،WKyxF3F3TYg)kgx(:hmz_J[ht̙8p@kjjPeY#Mr`wE4ҫ׻~! z8 (?9q"x =r =r/4:݀i&:+G}S',Z7XQXVۇ )˩y2; 1^#^K<^}2X~Ә@χmZl13 K˖! B [JtT*–aF68R)#}jdv ]uR뵲g\)ݩTdQnUCnZ1%d!BW9?1%Kr ~1l%;hE)ݜ;$px~&@\ZQK#$Iy)L^c-J Bw]m ܌ |pΜ=?XꪠifJ*~g^ziey~EXذulGǏ/p :Qp91dp1F:gV LaBoS]溦:; " q2Go6B>!Žq<>0pN@`^ohhX )Pف55誩qUU ajXL>?}TƵMMGn]M@حz,@ǟYi=/=]iڅy~1dBv10ntE=3MsB!] f>}Bm)OcӼ19;hRPT*JN$2Q H_xAI&˟㬎ŜUՖBllҪ*f2xI<0ua}!¦zs9S-/:>FmU=6lPWmڲ*aH;f 5>v'8N#/!mjgŊpeIT/D1d>ai~}R*n7Q["^gg Be* IDAT"ܢ\TC?;֮qX.g޶w|\#~ )/!bpz[Tո 㶍qAXm ⩡!>e]f- +1έ+Bq|l֭7~TAFlj\#zz"뫮ß\ *Ӵ\ĢH aL0:A<10';9}Oqg40r&j}>(~!7ġ̙E(H @AJDq1ƣ'Nx{}=njmŸm/l)#d#l8)8æ{W:m[  "  ðHu_ nٵk\9ѹsmK@wᛘHxCw TI/Hӭ[3##KKmґu]'O>>0 jK8UY־te0]AwSzi9kFh4 R[o Q]fnڹR*;1\ .-Ȉ[O?0g}wG+sCTU/58e׮Y(*Kp{OL[)YݕH"SrehoOyv [,@u@_RíXfuM*6MiR#𒧟 F"xlZ#-WevlysZչőt',WNsue-@"# Q[=Wz-"z϶mm;IIcc4 Np+aۙ{{~P(%mjcNd+qJeyk=?룻vڵ͚嚆(0͜Yŗ^tۭ˖].4BDr9h6kE)1R(cfz={u%`z3 zA*kmuyk, ~R ?E`^(~km aH$G]2k/mmm}wutC:6_-Δ*Uܾ}C>zsk+@45*ďr;lN__]ן[ϚW@Q蝇5{0Bԃ]]NeE1}Ѳ=s\a^(ͣs~8J>>~_?t[-1ͭ?^&Fk|&02Vyc|cKaD]]X[[iB}{[8!\`ܶ?~GD֮y{}}su}Kd2p³##K݉;I[8 @VLl|>4^+ul;mǽν~809cVp,-BbqԞRĩ/CB R9uu2MϽbSL|u2|؟L"8`D=M]. 8DQ"`7t:]M#6B'?q??[:t=Ɩ*0ijrz eK3YJsNC X*YZbܻ-w[»3.U.Y6*qGE2U~ٻo'""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""<_9%IENDB`turn-4.1.3/.github/gopher-pion.png.license000066400000000000000000000001361510560755200204330ustar00rootroot00000000000000SPDX-FileCopyrightText: 2023 The Pion community SPDX-License-Identifier: MITturn-4.1.3/.github/install-hooks.sh000077500000000000000000000012421510560755200172020ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT SCRIPT_PATH="$(realpath "$(dirname "$0")")" . ${SCRIPT_PATH}/fetch-scripts.sh cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" turn-4.1.3/.github/workflows/000077500000000000000000000000001510560755200161125ustar00rootroot00000000000000turn-4.1.3/.github/workflows/api.yaml000066400000000000000000000011141510560755200175440ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: API on: pull_request: jobs: check: uses: pion/.goassets/.github/workflows/api.reusable.yml@master turn-4.1.3/.github/workflows/codeql-analysis.yml000066400000000000000000000013201510560755200217210ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: CodeQL on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - master paths: - '**.go' jobs: analyze: uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master turn-4.1.3/.github/workflows/fuzz.yaml000066400000000000000000000013421510560755200177740ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fuzz on: push: branches: - master schedule: - cron: "0 */8 * * *" jobs: fuzz: uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version fuzz-time: "60s" turn-4.1.3/.github/workflows/lint.yaml000066400000000000000000000011151510560755200177420ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Lint on: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/lint.reusable.yml@master turn-4.1.3/.github/workflows/release.yml000066400000000000000000000012501510560755200202530ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Release on: push: tags: - 'v*' jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version turn-4.1.3/.github/workflows/renovate-go-sum-fix.yaml000066400000000000000000000012671510560755200226200ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fix go.sum on: push: branches: - renovate/* jobs: fix: uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} turn-4.1.3/.github/workflows/reuse.yml000066400000000000000000000011511510560755200177560ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: REUSE Compliance Check on: push: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master turn-4.1.3/.github/workflows/test.yaml000066400000000000000000000033271510560755200177620ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Test on: push: branches: - master pull_request: jobs: test: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} secrets: inherit test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-windows: uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-macos: uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version secrets: inherit turn-4.1.3/.github/workflows/tidy-check.yaml000066400000000000000000000013021510560755200210160ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: push: branches: - master jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version turn-4.1.3/.gitignore000066400000000000000000000006321510560755200145060ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT ### JetBrains IDE ### ##################### .idea/ ### Emacs Temporary Files ### ############################# *~ ### Folders ### ############### bin/ vendor/ node_modules/ ### Files ### ############# *.ivf *.ogg tags cover.out *.sw[poe] *.wasm examples/sfu-ws/cert.pem examples/sfu-ws/key.pem wasm_exec.js turn-4.1.3/.golangci.yml000066400000000000000000000202661510560755200151070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT version: "2" linters: enable: - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - bidichk # Checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - containedctx # containedctx is a linter that detects struct contained context.Context field - contextcheck # check the function whether use a non-inherited context - cyclop # checks function and package cyclomatic complexity - decorder # check declaration order and count of types, constants, variables and functions - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together - err113 # Golang linter to check the errors handling expressions - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - exhaustive # check exhaustiveness of enum switch statements - forbidigo # Forbids identifiers - forcetypeassert # finds forced type assertions - gochecknoglobals # Checks that no globals are present in Go code - gocognit # Computes and checks the cognitive complexity of functions - goconst # Finds repeated strings that could be replaced by a constant - gocritic # The most opinionated Go source code linter - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - godox # Tool for detection of FIXME, TODO and other comment keywords - goheader # Checks is file header matches to pattern - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - goprintffuncname # Checks that printf-like functions are named with `f` at the end - gosec # Inspects source code for security problems - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - grouper # An analyzer to analyze expression groups. - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - misspell # Finds commonly misspelled English words in comments - nakedret # Finds naked returns in functions greater than a specified function length - nestif # Reports deeply nested if statements - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - noctx # noctx finds sending http request without context.Context - predeclared # find code that shadows one of Go's predeclared identifiers - revive # golint replacement, finds style mistakes - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - tagliatelle # Checks the struct tags. - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - varnamelen # checks that the length of a variable's name matches its scope - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - depguard # Go linter that checks if package imports are in a list of acceptable packages - funlen # Tool for detection of long functions - gochecknoinits # Checks that no init functions are present in Go code - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. - interfacebloat # A linter that checks length of interface. - ireturn # Accept Interfaces, Return Concrete Types - mnd # An analyzer to detect magic numbers - nolintlint # Reports ill-formed or insufficient nolint directives - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test - prealloc # Finds slice declarations that could potentially be preallocated - promlinter # Check Prometheus metrics naming via promlint - rowserrcheck # checks whether Err of rows is checked successfully - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. - testpackage # linter that makes you use a separate _test package - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - wrapcheck # Checks that errors returned from external packages are wrapped - wsl # Whitespace Linter - Forces you to use empty lines! settings: staticcheck: checks: - all - -QF1008 # "could remove embedded field", to keep it explicit! - -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive! exhaustive: default-signifies-exhaustive: true forbidigo: forbid: - pattern: ^fmt.Print(f|ln)?$ - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ - pattern: ^os.Exit$ - pattern: ^panic$ - pattern: ^print(ln)?$ - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ pkg: ^testing$ msg: use testify/assert instead analyze-types: true gomodguard: blocked: modules: - github.com/pkg/errors: recommendations: - errors govet: enable: - shadow revive: rules: # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility - name: use-any severity: warning disabled: false misspell: locale: US varnamelen: max-distance: 12 min-name-length: 2 ignore-type-assert-ok: true ignore-map-index-ok: true ignore-chan-recv-ok: true ignore-decls: - i int - n int - w io.Writer - r io.Reader - b []byte exclusions: generated: lax rules: - linters: - forbidigo - gocognit path: (examples|main\.go) - linters: - gocognit path: _test\.go - linters: - forbidigo path: cmd formatters: enable: - gci # Gci control golang package import order and make it always deterministic. - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofumpt # Gofumpt checks whether code was gofumpt-ed. - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports exclusions: generated: lax turn-4.1.3/.goreleaser.yml000066400000000000000000000001711510560755200154450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT builds: - skip: true turn-4.1.3/.reuse/000077500000000000000000000000001510560755200137165ustar00rootroot00000000000000turn-4.1.3/.reuse/dep5000066400000000000000000000011141510560755200144730ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock Copyright: 2023 The Pion community License: MIT Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 turn-4.1.3/LICENSE000066400000000000000000000021051510560755200135200ustar00rootroot00000000000000MIT License Copyright (c) 2023 The Pion community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. turn-4.1.3/LICENSES/000077500000000000000000000000001510560755200137225ustar00rootroot00000000000000turn-4.1.3/LICENSES/CC0-1.0.txt000066400000000000000000000156101510560755200153270ustar00rootroot00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. turn-4.1.3/LICENSES/MIT.txt000066400000000000000000000020661510560755200151200ustar00rootroot00000000000000MIT License Copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. turn-4.1.3/README.md000066400000000000000000000134021510560755200137740ustar00rootroot00000000000000

Pion TURN
Pion TURN

A toolkit for building TURN clients and servers in Go

Pion TURN join us on Discord Follow us on Bluesky
GitHub Workflow Status Go Reference Coverage Status Go Report Card License: MIT


Pion TURN is a Go toolkit for building TURN servers and clients. We wrote it to solve problems we had when building RTC projects. * **Deployable** - Use modern tooling of the Go ecosystem. Stop generating config files. * **Embeddable** - Include `pion/turn` in your existing applications. No need to manage another service. * **Extendable** - TURN as an API so you can easily integrate with your existing monitoring and metrics. * **Maintainable** - `pion/turn` is simple and well documented. Designed for learning and easy debugging. * **Portable** - Quickly deploy to multiple architectures/platforms just by setting an environment variable. * **Safe** - Stability and safety is important for network services. Go provides everything we need. * **Scalable** - Create allocations and mutate state at runtime. Designed to make scaling easy. ### Using `pion/turn` is an API for building STUN/TURN clients and servers, not a binary you deploy then configure. It may require copying our examples and making minor modifications to fit your need, no knowledge of Go is required however. You may be able to download the pre-made binaries of our examples if you wish to get started quickly. The advantage of this is that you don't need to deal with complicated config files, or custom APIs to modify the state of Pion TURN. After you instantiate an instance of a Pion TURN server or client you interact with it like any library. The quickest way to get started is to look at the [examples](examples) or [GoDoc](https://godoc.org/github.com/pion/turn) ### Examples We try to cover most common use cases in [examples](examples). If more examples could be helpful please file an issue, we are always looking to expand and improve `pion/turn` to make it easier for developers. To build any example you just need to run `go build` in the directory of the example you care about. It is also very easy to [cross compile](https://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5) Go programs. You can also see `pion/turn` usage in [pion/ice](https://github.com/pion/ice) ### FAQ Also take a look at the [Pion WebRTC FAQ](https://github.com/pion/webrtc/wiki/FAQ) #### Will pion/turn also act as a STUN server? Yes. #### How do I implement token-based authentication? Replace the username with a token in the [AuthHandler](https://github.com/pion/turn/blob/6d0ff435910870eb9024b18321b93b61844fcfec/examples/turn-server/simple/main.go#L49). The password sent by the client can be any non-empty string, as long as it matches that used by the [GenerateAuthKey](https://github.com/pion/turn/blob/6d0ff435910870eb9024b18321b93b61844fcfec/examples/turn-server/simple/main.go#L41) function. #### Will WebRTC prioritize using STUN over TURN? Yes. ### RFCs #### Implemented * **RFC 5389**: [Session Traversal Utilities for NAT (STUN)][rfc5389] * **RFC 5766**: [Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN)][rfc5766] #### Planned * **RFC 6062**: [Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations][rfc6062] * **RFC 6156**: [Traversal Using Relays around NAT (TURN) Extension for IPv6][rfc6156] [rfc5389]: https://tools.ietf.org/html/rfc5389 [rfc5766]: https://tools.ietf.org/html/rfc5766 [rfc6062]: https://tools.ietf.org/html/rfc6062 [rfc6156]: https://tools.ietf.org/html/rfc6156 ### Roadmap The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. ### Community Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) ### Contributing Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible ### License MIT License - see [LICENSE](LICENSE) for full text turn-4.1.3/client.go000066400000000000000000000434061510560755200143310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( b64 "encoding/base64" "fmt" "math" "net" "sync" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" "github.com/pion/turn/v4/internal/client" "github.com/pion/turn/v4/internal/proto" ) const ( defaultRTO = 200 * time.Millisecond maxRtxCount = 7 // Total 7 requests (Rc) maxDataBufferSize = math.MaxUint16 // Message size limit for Chromium ) // interval [msec] // 0: 0 ms +500 // 1: 500 ms +1000 // 2: 1500 ms +2000 // 3: 3500 ms +4000 // 4: 7500 ms +8000 // 5: 15500 ms +16000 // 6: 31500 ms +32000 // -: 63500 ms failed // ClientConfig is a bag of config parameters for Client. type ClientConfig struct { STUNServerAddr string // STUN server address (e.g. "stun.abc.com:3478") TURNServerAddr string // TURN server address (e.g. "turn.abc.com:3478") Username string Password string Realm string Software string RTO time.Duration Conn net.PacketConn // Listening socket (net.PacketConn) Net transport.Net LoggerFactory logging.LoggerFactory } // Client is a STUN server client. type Client struct { conn net.PacketConn // Read-only net transport.Net // Read-only stunServerAddr net.Addr // Read-only turnServerAddr net.Addr // Read-only username stun.Username // Read-only password string // Read-only realm stun.Realm // Read-only integrity stun.MessageIntegrity // Read-only software stun.Software // Read-only trMap *client.TransactionMap // Thread-safe rto time.Duration // Read-only relayedConn *client.UDPConn // Protected by mutex *** tcpAllocation *client.TCPAllocation // Protected by mutex *** allocTryLock client.TryLock // Thread-safe listenTryLock client.TryLock // Thread-safe mutex sync.RWMutex // Thread-safe mutexTrMap sync.Mutex // Thread-safe log logging.LeveledLogger // Read-only } // NewClient returns a new Client instance. listeningAddress is the address and port to listen on, // default "0.0.0.0:0". func NewClient(config *ClientConfig) (*Client, error) { loggerFactory := config.LoggerFactory if loggerFactory == nil { loggerFactory = logging.NewDefaultLoggerFactory() } log := loggerFactory.NewLogger("turnc") if config.Conn == nil { return nil, errNilConn } rto := defaultRTO if config.RTO > 0 { rto = config.RTO } if config.Net == nil { n, err := stdnet.NewNet() if err != nil { return nil, err } config.Net = n } var stunServ, turnServ net.Addr var err error if len(config.STUNServerAddr) > 0 { stunServ, err = config.Net.ResolveUDPAddr("udp4", config.STUNServerAddr) if err != nil { return nil, err } log.Debugf("Resolved STUN server %s to %s", config.STUNServerAddr, stunServ) } if len(config.TURNServerAddr) > 0 { turnServ, err = config.Net.ResolveUDPAddr("udp4", config.TURNServerAddr) if err != nil { return nil, err } log.Debugf("Resolved TURN server %s to %s", config.TURNServerAddr, turnServ) } client := &Client{ conn: config.Conn, stunServerAddr: stunServ, turnServerAddr: turnServ, username: stun.NewUsername(config.Username), password: config.Password, realm: stun.NewRealm(config.Realm), software: stun.NewSoftware(config.Software), trMap: client.NewTransactionMap(), net: config.Net, rto: rto, log: log, } return client, nil } // TURNServerAddr return the TURN server address. func (c *Client) TURNServerAddr() net.Addr { return c.turnServerAddr } // STUNServerAddr return the STUN server address. func (c *Client) STUNServerAddr() net.Addr { return c.stunServerAddr } // Username returns username. func (c *Client) Username() stun.Username { return c.username } // Realm return realm. func (c *Client) Realm() stun.Realm { return c.realm } // WriteTo sends data to the specified destination using the base socket. func (c *Client) WriteTo(data []byte, to net.Addr) (int, error) { return c.conn.WriteTo(data, to) } // Listen will have this client start listening on the conn provided via the config. // This is optional. If not used, you will need to call HandleInbound method // to supply incoming data, instead. func (c *Client) Listen() error { if err := c.listenTryLock.Lock(); err != nil { return fmt.Errorf("%w: %s", errAlreadyListening, err.Error()) } go func() { buf := make([]byte, maxDataBufferSize) for { n, from, err := c.conn.ReadFrom(buf) if err != nil { c.log.Debugf("Failed to read: %s. Exiting loop", err) break } _, err = c.HandleInbound(buf[:n], from) if err != nil { c.log.Debugf("Failed to handle inbound message: %s. Exiting loop", err) break } } c.listenTryLock.Unlock() }() return nil } // Close closes this client. func (c *Client) Close() { c.mutexTrMap.Lock() defer c.mutexTrMap.Unlock() c.trMap.CloseAndDeleteAll() } // TransactionID & Base64: https://play.golang.org/p/EEgmJDI971P // SendBindingRequestTo sends a new STUN request to the given transport address. func (c *Client) SendBindingRequestTo(to net.Addr) (net.Addr, error) { attrs := []stun.Setter{stun.TransactionID, stun.BindingRequest} if len(c.software) > 0 { attrs = append(attrs, c.software) } msg, err := stun.Build(attrs...) if err != nil { return nil, err } trRes, err := c.PerformTransaction(msg, to, false) if err != nil { return nil, err } var reflAddr stun.XORMappedAddress if err := reflAddr.GetFrom(trRes.Msg); err != nil { return nil, err } return &net.UDPAddr{ IP: reflAddr.IP, Port: reflAddr.Port, }, nil } // SendBindingRequest sends a new STUN request to the STUN server. func (c *Client) SendBindingRequest() (net.Addr, error) { if c.stunServerAddr == nil { return nil, errSTUNServerAddressNotSet } return c.SendBindingRequestTo(c.stunServerAddr) } func (c *Client) sendAllocateRequest(protocol proto.Protocol) ( //nolint:cyclop proto.RelayedAddress, proto.Lifetime, stun.Nonce, error, ) { var relayed proto.RelayedAddress var lifetime proto.Lifetime var nonce stun.Nonce msg, err := stun.Build( stun.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassRequest), proto.RequestedTransport{Protocol: protocol}, stun.Fingerprint, ) if err != nil { return relayed, lifetime, nonce, err } trRes, err := c.PerformTransaction(msg, c.turnServerAddr, false) if err != nil { return relayed, lifetime, nonce, err } res := trRes.Msg // Anonymous allocate failed, trying to authenticate. if err = nonce.GetFrom(res); err != nil { return relayed, lifetime, nonce, err } if err = c.realm.GetFrom(res); err != nil { return relayed, lifetime, nonce, err } c.realm = append([]byte(nil), c.realm...) c.integrity = stun.NewLongTermIntegrity( c.username.String(), c.realm.String(), c.password, ) // Trying to authorize. msg, err = stun.Build( stun.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassRequest), proto.RequestedTransport{Protocol: protocol}, &c.username, &c.realm, &nonce, &c.integrity, stun.Fingerprint, ) if err != nil { return relayed, lifetime, nonce, err } trRes, err = c.PerformTransaction(msg, c.turnServerAddr, false) if err != nil { return relayed, lifetime, nonce, err } res = trRes.Msg if res.Type.Class == stun.ClassErrorResponse { var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { turnError := &stun.TurnError{ StunMessageType: res.Type, ErrorCodeAttr: code, } return relayed, lifetime, nonce, turnError } return relayed, lifetime, nonce, fmt.Errorf("%s", res.Type) //nolint:err113 } // Getting relayed addresses from response. if err := relayed.GetFrom(res); err != nil { return relayed, lifetime, nonce, err } // Getting lifetime from response if err := lifetime.GetFrom(res); err != nil { return relayed, lifetime, nonce, err } return relayed, lifetime, nonce, nil } // Allocate sends a TURN allocation request to the given transport address. func (c *Client) Allocate() (net.PacketConn, error) { if err := c.allocTryLock.Lock(); err != nil { return nil, fmt.Errorf("%w: %s", errOneAllocateOnly, err.Error()) } defer c.allocTryLock.Unlock() relayedConn := c.relayedUDPConn() if relayedConn != nil { return nil, fmt.Errorf("%w: %s", errAlreadyAllocated, relayedConn.LocalAddr().String()) } relayed, lifetime, nonce, err := c.sendAllocateRequest(proto.ProtoUDP) if err != nil { return nil, err } relayedAddr := &net.UDPAddr{ IP: relayed.IP, Port: relayed.Port, } relayedConn = client.NewUDPConn(&client.AllocationConfig{ Client: c, RelayedAddr: relayedAddr, ServerAddr: c.turnServerAddr, Realm: c.realm, Username: c.username, Integrity: c.integrity, Nonce: nonce, Lifetime: lifetime.Duration, Net: c.net, Log: c.log, }) c.setRelayedUDPConn(relayedConn) return relayedConn, nil } // AllocateTCP creates a new TCP allocation at the TURN server. func (c *Client) AllocateTCP() (*client.TCPAllocation, error) { if err := c.allocTryLock.Lock(); err != nil { return nil, fmt.Errorf("%w: %s", errOneAllocateOnly, err.Error()) } defer c.allocTryLock.Unlock() allocation := c.getTCPAllocation() if allocation != nil { return nil, fmt.Errorf("%w: %s", errAlreadyAllocated, allocation.Addr()) } relayed, lifetime, nonce, err := c.sendAllocateRequest(proto.ProtoTCP) if err != nil { return nil, err } relayedAddr := &net.TCPAddr{ IP: relayed.IP, Port: relayed.Port, } allocation = client.NewTCPAllocation(&client.AllocationConfig{ Client: c, RelayedAddr: relayedAddr, ServerAddr: c.turnServerAddr, Realm: c.realm, Username: c.username, Integrity: c.integrity, Nonce: nonce, Lifetime: lifetime.Duration, Net: c.net, Log: c.log, }) c.setTCPAllocation(allocation) return allocation, nil } // CreatePermission Issues a CreatePermission request for the supplied addresses // as described in https://datatracker.ietf.org/doc/html/rfc5766#section-9 func (c *Client) CreatePermission(addrs ...net.Addr) error { if conn := c.relayedUDPConn(); conn != nil { if err := conn.CreatePermissions(addrs...); err != nil { return err } } if allocation := c.getTCPAllocation(); allocation != nil { if err := allocation.CreatePermissions(addrs...); err != nil { return err } } return nil } // PerformTransaction performs STUN transaction. func (c *Client) PerformTransaction(msg *stun.Message, to net.Addr, ignoreResult bool) (client.TransactionResult, error, ) { trKey := b64.StdEncoding.EncodeToString(msg.TransactionID[:]) raw := make([]byte, len(msg.Raw)) copy(raw, msg.Raw) tr := client.NewTransaction(&client.TransactionConfig{ Key: trKey, Raw: raw, To: to, Interval: c.rto, IgnoreResult: ignoreResult, }) c.trMap.Insert(trKey, tr) c.log.Tracef("Start %s transaction %s to %s", msg.Type, trKey, tr.To) _, err := c.conn.WriteTo(tr.Raw, to) if err != nil { return client.TransactionResult{}, err } tr.StartRtxTimer(c.onRtxTimeout) // If ignoreResult is true, get the transaction going and return immediately if ignoreResult { return client.TransactionResult{}, nil } res := tr.WaitForResult() if res.Err != nil { return res, res.Err } return res, nil } // OnDeallocated is called when de-allocation of relay address has been complete. // (Called by UDPConn). func (c *Client) OnDeallocated(net.Addr) { c.setRelayedUDPConn(nil) c.setTCPAllocation(nil) } // HandleInbound handles data received. // This method handles incoming packet de-multiplex it by the source address // and the types of the message. // This return a boolean (handled or not) and if there was an error. // Caller should check if the packet was handled by this client or not. // If not handled, it is assumed that the packet is application data. // If an error is returned, the caller should discard the packet regardless. func (c *Client) HandleInbound(data []byte, from net.Addr) (bool, error) { // +-------------------+-------------------------------+ // | Return Values | | // +-------------------+ Meaning / Action | // | handled | error | | // |=========+=========+===============================+ // | false | nil | Handle the packet as app data | // |---------+---------+-------------------------------+ // | true | nil | Nothing to do | // |---------+---------+-------------------------------+ // | false | error | (shouldn't happen) | // |---------+---------+-------------------------------+ // | true | error | Error occurred while handling | // +---------+---------+-------------------------------+ // Possible causes of the error: // - Malformed packet (parse error) // - STUN message was a request // - Non-STUN message from the STUN server switch { case stun.IsMessage(data): return true, c.handleSTUNMessage(data, from) case proto.IsChannelData(data): return true, c.handleChannelData(data) case c.stunServerAddr != nil && from.String() == c.stunServerAddr.String(): // Received from STUN server but it is not a STUN message return true, errNonSTUNMessage default: // Assume, this is an application data c.log.Tracef("Ignoring non-STUN/TURN packet") } return false, nil } func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error { //nolint:cyclop raw := make([]byte, len(data)) copy(raw, data) msg := &stun.Message{Raw: raw} if err := msg.Decode(); err != nil { return fmt.Errorf("%w: %s", errFailedToDecodeSTUN, err.Error()) } if msg.Type.Class == stun.ClassRequest { return fmt.Errorf("%w : %s", errUnexpectedSTUNRequestMessage, msg.String()) } if msg.Type.Class == stun.ClassIndication { // nolint:nestif switch msg.Type.Method { case stun.MethodData: var peerAddr proto.PeerAddress if err := peerAddr.GetFrom(msg); err != nil { return err } from = &net.UDPAddr{ IP: peerAddr.IP, Port: peerAddr.Port, } var data proto.Data if err := data.GetFrom(msg); err != nil { return err } c.log.Tracef("Data indication received from %s", from) relayedConn := c.relayedUDPConn() if relayedConn == nil { c.log.Debug("No relayed conn allocated") return nil // Silently discard } relayedConn.HandleInbound(data, from) case stun.MethodConnectionAttempt: var peerAddr proto.PeerAddress if err := peerAddr.GetFrom(msg); err != nil { return err } addr := &net.TCPAddr{ IP: peerAddr.IP, Port: peerAddr.Port, } var cid proto.ConnectionID if err := cid.GetFrom(msg); err != nil { return err } c.log.Debugf("Connection attempt from %s", addr) allocation := c.getTCPAllocation() if allocation == nil { c.log.Debug("No TCP allocation exists") return nil // Silently discard } allocation.HandleConnectionAttempt(addr, cid) default: c.log.Debug("Received unsupported STUN method") } return nil } // This is a STUN response message (transactional) // The type is either: // - stun.ClassSuccessResponse // - stun.ClassErrorResponse trKey := b64.StdEncoding.EncodeToString(msg.TransactionID[:]) c.mutexTrMap.Lock() tr, ok := c.trMap.Find(trKey) if !ok { c.mutexTrMap.Unlock() // Silently discard c.log.Debugf("No transaction for %s", msg) return nil } // End the transaction tr.StopRtxTimer() c.trMap.Delete(trKey) c.mutexTrMap.Unlock() if !tr.WriteResult(client.TransactionResult{ Msg: msg, From: from, Retries: tr.Retries(), }) { c.log.Debugf("No listener for %s", msg) } return nil } func (c *Client) handleChannelData(data []byte) error { chData := &proto.ChannelData{ Raw: make([]byte, len(data)), } copy(chData.Raw, data) if err := chData.Decode(); err != nil { return err } relayedConn := c.relayedUDPConn() if relayedConn == nil { c.log.Debug("No relayed conn allocated") return nil // Silently discard } addr, ok := relayedConn.FindAddrByChannelNumber(uint16(chData.Number)) if !ok { return fmt.Errorf("%w: %d", errChannelBindNotFound, int(chData.Number)) } c.log.Tracef("Channel data received from %s (ch=%d)", addr.String(), int(chData.Number)) relayedConn.HandleInbound(chData.Data, addr) return nil } func (c *Client) onRtxTimeout(trKey string, nRtx int) { c.mutexTrMap.Lock() defer c.mutexTrMap.Unlock() tr, ok := c.trMap.Find(trKey) if !ok { return // Already gone } if nRtx == maxRtxCount { // All retransmissions failed c.trMap.Delete(trKey) if !tr.WriteResult(client.TransactionResult{ Err: fmt.Errorf("%w %s", errAllRetransmissionsFailed, trKey), }) { c.log.Debug("No listener for transaction") } return } c.log.Tracef("Retransmitting transaction %s to %s (nRtx=%d)", trKey, tr.To, nRtx) _, err := c.conn.WriteTo(tr.Raw, tr.To) if err != nil { c.trMap.Delete(trKey) if !tr.WriteResult(client.TransactionResult{ Err: fmt.Errorf("%w %s", errFailedToRetransmitTransaction, trKey), }) { c.log.Debug("No listener for transaction") } return } tr.StartRtxTimer(c.onRtxTimeout) } func (c *Client) setRelayedUDPConn(conn *client.UDPConn) { c.mutex.Lock() defer c.mutex.Unlock() c.relayedConn = conn } func (c *Client) relayedUDPConn() *client.UDPConn { c.mutex.RLock() defer c.mutex.RUnlock() return c.relayedConn } func (c *Client) setTCPAllocation(alloc *client.TCPAllocation) { c.mutex.Lock() defer c.mutex.Unlock() c.tcpAllocation = alloc } func (c *Client) getTCPAllocation() *client.TCPAllocation { c.mutex.RLock() defer c.mutex.RUnlock() return c.tcpAllocation } turn-4.1.3/client_test.go000066400000000000000000000210061510560755200153600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package turn import ( "net" "runtime" "testing" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func buildMsg( transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter, ) []stun.Setter { return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...) } func createListeningTestClient(t *testing.T, loggerFactory logging.LoggerFactory) (*Client, net.PacketConn, bool) { t.Helper() conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) c, err := NewClient(&ClientConfig{ Conn: conn, Software: "TEST SOFTWARE", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, c.Listen()) return c, conn, true } func createListeningTestClientWithSTUNServ(t *testing.T, loggerFactory logging.LoggerFactory) ( // nolint:lll *Client, net.PacketConn, bool, ) { t.Helper() conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) addr := "stun1.l.google.com:19302" c, err := NewClient(&ClientConfig{ STUNServerAddr: addr, Conn: conn, LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, c.Listen()) return c, conn, true } func TestClientWithSTUN(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") t.Run("SendBindingRequest", func(t *testing.T) { client, pc, ok := createListeningTestClientWithSTUNServ(t, loggerFactory) if !ok { return } defer client.Close() resp, err := client.SendBindingRequest() assert.NoError(t, err, "should succeed") log.Debugf("mapped-addr: %s", resp) assert.Equal(t, 0, client.trMap.Size(), "should be no transaction left") assert.NoError(t, pc.Close()) }) t.Run("SendBindingRequestTo Parallel", func(t *testing.T) { client, pc, ok := createListeningTestClient(t, loggerFactory) if !ok { return } defer client.Close() // Simple channel fo go routine start signaling started := make(chan struct{}) finished := make(chan struct{}) var err1 error to, err := net.ResolveUDPAddr("udp4", "stun1.l.google.com:19302") assert.NoError(t, err) // stun1.l.google.com:19302, more at https://gist.github.com/zziuni/3741933#file-stuns-L5 go func() { close(started) _, err1 = client.SendBindingRequestTo(to) close(finished) }() // Block until go routine is started to make two almost parallel requests <-started _, err = client.SendBindingRequestTo(to) assert.NoError(t, err) <-finished assert.NoErrorf(t, err1, "should succeed: %v", err) assert.NoError(t, pc.Close()) }) t.Run("NewClient should fail if Conn is nil", func(t *testing.T) { _, err := NewClient(&ClientConfig{ LoggerFactory: loggerFactory, }) assert.Error(t, err, "should fail") }) t.Run("SendBindingRequestTo timeout", func(t *testing.T) { c, pc, ok := createListeningTestClient(t, loggerFactory) if !ok { return } defer c.Close() to, err := net.ResolveUDPAddr("udp4", "127.0.0.1:9") assert.NoError(t, err) c.rto = 10 * time.Millisecond // Force short timeout _, err = c.SendBindingRequestTo(to) assert.NotNil(t, err) assert.NoError(t, pc.Close()) }) } // Create an allocation, and then delete all nonces // The subsequent Write on the allocation will cause a CreatePermission // which will be forced to handle a stale nonce response. func TestClientNonceExpiration(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "0.0.0.0:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, realm string, _ net.Addr) (key []byte, ok bool) { return GenerateAuthKey(username, realm, "pass"), true }, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "0.0.0.0", }, }, }, Realm: "pion.ly", }) assert.NoError(t, err) conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) // nolint: goconst addr := "127.0.0.1:3478" client, err := NewClient(&ClientConfig{ Conn: conn, STUNServerAddr: addr, TURNServerAddr: addr, Username: "foo", Password: "pass", }) assert.NoError(t, err) assert.NoError(t, client.Listen()) allocation, err := client.Allocate() assert.NoError(t, err) _, err = allocation.WriteTo([]byte{0x00}, &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}) assert.NoError(t, err) // Shutdown assert.NoError(t, allocation.Close()) assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) } // Create a TCP-based allocation and verify allocation can be created. func TestTCPClient(t *testing.T) { // Setup server tcpListener, err := net.Listen("tcp4", "0.0.0.0:13478") //nolint: gosec,noctx require.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, realm string, _ net.Addr) (key []byte, ok bool) { return GenerateAuthKey(username, realm, "pass"), true }, ListenerConfigs: []ListenerConfig{ { Listener: tcpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "0.0.0.0", }, }, }, Realm: "pion.ly", }) require.NoError(t, err) // Setup clients conn, err := net.Dial("tcp", "127.0.0.1:13478") // nolint: noctx require.NoError(t, err) serverAddr := "127.0.0.1:13478" client, err := NewClient(&ClientConfig{ Conn: NewSTUNConn(conn), STUNServerAddr: serverAddr, TURNServerAddr: serverAddr, Username: "foo", Password: "pass", }) require.NoError(t, err) require.NoError(t, client.Listen()) require.Equal(t, serverAddr, client.STUNServerAddr().String()) allocation, err := client.AllocateTCP() require.NoError(t, err) peerAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12345") require.NoError(t, err) require.NoError(t, client.CreatePermission(peerAddr)) var cid proto.ConnectionID = 5 transactionID := [stun.TransactionIDSize]byte{1, 2, 3} attrs := buildMsg( transactionID, stun.NewType(stun.MethodConnectionAttempt, stun.ClassIndication), cid, proto.PeerAddress{IP: peerAddr.IP, Port: peerAddr.Port}, ) msg, err := stun.Build(attrs...) require.NoError(t, err) require.NoError(t, client.handleSTUNMessage(msg.Raw, peerAddr)) // Shutdown require.NoError(t, allocation.Close()) require.NoError(t, conn.Close()) require.NoError(t, server.Close()) } func TestTCPClientWithoutAddress(t *testing.T) { // Setup server tcpListener, err := net.Listen("tcp4", "0.0.0.0:13478") //nolint: gosec,noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, realm string, _ net.Addr) (key []byte, ok bool) { // Sleep needed for sending retransmission. if runtime.GOOS == "windows" { time.Sleep(20 * time.Millisecond) } else { time.Sleep(time.Millisecond) } return GenerateAuthKey(username, realm, "pass"), true }, ListenerConfigs: []ListenerConfig{ { Listener: tcpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "0.0.0.0", }, }, }, Realm: "pion.ly", }) assert.NoError(t, err) // Test tcp client without turn server address with small RTO. conn, err := net.Dial("tcp", "127.0.0.1:13478") // nolint: noctx assert.NoError(t, err) client, err := NewClient(&ClientConfig{ Conn: NewSTUNConn(conn), Username: "foo", Password: "pass", RTO: time.Nanosecond, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) defer client.Close() _, err = client.Allocate() // Due to small RTO all retrasmissions fail. assert.ErrorIs(t, err, errAllRetransmissionsFailed) assert.NoError(t, conn.Close()) // Test tcp client without turn server with successful allocation. conn, err = net.Dial("tcp", "127.0.0.1:13478") // nolint: noctx assert.NoError(t, err) client, err = NewClient(&ClientConfig{ Conn: NewSTUNConn(conn), Username: "foo", Password: "pass", }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) // Shutdown assert.NoError(t, relayConn.Close()) assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) } turn-4.1.3/codecov.yml000066400000000000000000000007151510560755200146650ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" turn-4.1.3/errors.go000066400000000000000000000041341510560755200143620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import "errors" var ( errRelayAddressInvalid = errors.New("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic") errNoAvailableConns = errors.New("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed") errConnUnset = errors.New("turn: PacketConnConfig must have a non-nil Conn") errListenerUnset = errors.New("turn: ListenerConfig must have a non-nil Listener") errListeningAddressInvalid = errors.New("turn: RelayAddressGenerator has invalid ListeningAddress") errRelayAddressGeneratorUnset = errors.New("turn: RelayAddressGenerator in RelayConfig is unset") errMaxRetriesExceeded = errors.New("turn: max retries exceeded") errMaxPortNotZero = errors.New("turn: MaxPort must be not 0") errMinPortNotZero = errors.New("turn: MaxPort must be not 0") errNilConn = errors.New("turn: conn cannot not be nil") errTODO = errors.New("turn: TODO") errAlreadyListening = errors.New("turn: already listening") errFailedToClose = errors.New("turn: Server failed to close") errFailedToRetransmitTransaction = errors.New("turn: failed to retransmit transaction") errAllRetransmissionsFailed = errors.New("all retransmissions failed for") errChannelBindNotFound = errors.New("no binding found for channel") errSTUNServerAddressNotSet = errors.New("STUN server address is not set for the client") errOneAllocateOnly = errors.New("only one Allocate() caller is allowed") errAlreadyAllocated = errors.New("already allocated") errNonSTUNMessage = errors.New("non-STUN message from STUN server") errFailedToDecodeSTUN = errors.New("failed to decode STUN message") errUnexpectedSTUNRequestMessage = errors.New("unexpected STUN request message") errRelayAddressGeneratorNil = errors.New("RelayAddressGenerator is nil") ) turn-4.1.3/examples/000077500000000000000000000000001510560755200143335ustar00rootroot00000000000000turn-4.1.3/examples/README.md000066400000000000000000000165001510560755200156140ustar00rootroot00000000000000# Examples ## turn-server The `turn-server` directory contains 5 examples that show common Pion TURN usages. All of these except `lt-creds` take the following arguments. * -users : <username>=<password>[,<username>=<password>,...] pairs * -realm : Realm name (defaults to "pion.ly") * -port : Listening port (defaults to 3478) * -public-ip : IP that your TURN server is reachable on, for local development then can just be your local IP, avoid using `127.0.0.1` as some browsers discard from that IP. ```sh $ cd simple $ go build $ ./simple -public-ip 127.0.0.1 -users username=password,foo=bar ``` The five example servers are #### add-software-attribute This examples adds the SOFTWARE attribute with the value "CustomTURNServer" to every outbound STUN packet. This could be useful if you want to add debug info to your outbound packets. You could also use this same pattern to filter/modify packets if needed. #### log This example logs all inbound/outbound STUN packets. This could be useful if you want to store all inbound/outbound traffic or generate rich logs. You could also intercept these reads/writes if you want to filter traffic going to/from specific peers. #### simple This example is the most minimal invocation of a Pion TURN instance possible. It has no custom behavior, and could be a good starting place for running your own TURN server. #### simple-multithreaded A multithreaded version of the `simple` Pion TURN server, demonstrating how to scale a Pion UDP TURN server to multiple CPU cores. By default, Pion TURN servers use a single UDP socket that is shared across all clients, which limits Pion UDP/TURN servers to a single CPU thread. This example passes a configurable number of UDP sockets to the TURN server, which share the same local `address:port` pair using the `SO_REUSEPORT` socket option. This then lets the server to create a separate readloop to drain each socket. The OS kernel will distribute packets received on the `address:port` pair across the sockets by the IP 5-tuple, which makes sure that all packets of a TURN allocation will be correctly processed in a single readloop. #### tcp This example demonstrates listening on TCP. You could combine this example with `simple` and you will have a Pion TURN instance that is available via TCP and UDP. #### tls This example demonstrates listening on TLS. You could combine this example with `simple` and you will have a Pion TURN instance that is available via TLS and UDP. #### lt-creds This example shows how to use long term credentials. You can issue passwords that automatically expire, and you don't have the store them. The only downside is that you can't revoke a single username/password. You need to rotate the shared secret. Instead of `users` it has the follow arguments instead * -authSecret : Shared secret for the Long Term Credential Mechanism #### lt-cred-turn-rest This example shows how to use ephemeral credentials, generated by a REST API, with the user part formatted as `timestamp:username`. The REST API and TURN server use the same shared secret to compute the credentials. The timestamp part specifies when the credentials will expire. This mechanism is described in https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 * -authSecret : Shared secret for the ephemeral Credential Mechanism #### perm-filter This example demonstrates the use of a permission handler in the PION TURN server. The example implements a filtering policy that lets clients to connect back to their own host or server-reflexive address but will drop everything else. This will let the client ping-test through but will block essentially all other peer connection attempts. ## turn-client The `turn-client` directory contains 3 examples that show common Pion TURN usages. All of these examples except `tcp-alloc` take the following arguments. * -host : TURN server host * -ping : Run ping test * -port : Listening port (defaults to 3478) * -realm : Realm name (defaults to "pion.ly") * -user : <username>=<password> pair #### tcp Dials the requested TURN server via TCP #### udp Dials the requested TURN server via UDP ```sh $ cd udp $ go build $ ./udp -host -user=user=pass ``` By adding `-ping`, it will perform a ping test. (it internally creates a 'pinger' and send a UDP packet every second, 10 times then exits. ```sh $ go build ./turn-client -host -user=user=pass -ping ``` Following diagram shows what turn-client does: ``` +----------------+ | TURN Server | | | +---o--------o---+ TURN port /^ / ^\ 3478_/ | / | \_relayConn (*1) | / | | _/ | mappedAddr_ | / | ___external IP:port (*2) \|v |/ for pingerConn +---o--------o---+ (*3) | | NAT | | +----------------+ | | TURN ___ | | __pingerConn listen \| |/ (sends `ping` to relayConn) port +---o--------o---+ (conn) | turn-client | +----------------+ ``` > (*1) The relayConn actually lives in the local turn-client, but it acts as if it is > listening on the TURN server. In fact, relayConn.LocalAddr() returns a transport address > on which the TURN server is listening. > (*2) For relayConn to send/receive packet to/from (*3), you will need to give relayConn permission > to send/receive packet to/from the IP address. In the example code, this is done by sending a > packet, "Hello" (content does not matter), to the mappedAddr. (assuming the IP address of > mappedAddr and the external IP:port (*3) are the same) This process is known as > "UDP hole punching" and TURN server exhibits "Address-restricted" behavior. Once it is done, > packets coming from (*3) will be received by relayConn. #### tcp-alloc The `tcp-alloc` exemplifies how to create client TCP allocations and use them to exchange messages between peers. It simulates two clients and creates a TCP allocation for each. Then, both clients exchange their relayed addresses with each other through a signaling server. Finally, each client uses its TCP allocation and the relayed address of the other client to send and receive a single message. The `tcp-alloc` takes the following arguments: * -host : TURN server host * -port : Listening port (defaults to 3478) * -user : <username>=<password> pair * -realm : Realm name (defaults to "pion.ly") * -signaling : Run the signaling server To run the example: 1) Start one client and the signaling server used to exchange the relayed addresses: ```sh go build ./tcp-alloc -host -port -user= -signaling=true ``` 2) Start the other client without starting the signaling server: ```sh ./tcp-alloc -host -port -user= -signaling=false ``` A Coturn TURN server can be locally deployed and used for testing with the following command (this is a test configuration of the Coturn TURN server and should not be used for production): ```sh /bin/turnserver -lt-cred-mech -u -r pion.ly --allow-loopback-peers --cli-password= ``` >If using this Coturn TURN server deployment: >* turn-server-name : 127.0.0.1 >* port : 3478 turn-4.1.3/examples/lt-cred-generator/000077500000000000000000000000001510560755200176515ustar00rootroot00000000000000turn-4.1.3/examples/lt-cred-generator/README.md000066400000000000000000000014631510560755200211340ustar00rootroot00000000000000# Usage This command generates credentials used by the Long-Term Credential Mechanism Defined in [RFC5389-10.2](https://tools.ietf.org/search/rfc5389#section-10.2). The idea is to use the expiry time of the credential as the username, and let the password contain some cryptographic hash of a (server-side) shared-secret and the expiry time. ```bash export SECRET=somesecret # Build binaries (cd examples/lt-cred-generator && go build .) (cd examples/turn-server/lt-cred && go build .) (cd examples/turn-client/udp && go build .) # Start server ./examples/turn-server/lt-cred/lt-cred -public-ip=127.0.0.1 -authSecret=$SECRET # Start client using generated credentials ./examples/lt-cred-generator/lt-cred-generator -authSecret=$SECRET | xargs -I{} ./examples/turn-client/udp/udp -host=127.0.0.1 -ping -user={} ``` turn-4.1.3/examples/lt-cred-generator/main.go000066400000000000000000000023241510560755200211250ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a CLI tool for generating // long-term credentials. package main import ( "flag" "fmt" "log" "os" "time" "github.com/pion/turn/v4" ) // Outputs username & password according to the // Long-Term Credential Mechanism (RFC5389-10.2: https://tools.ietf.org/search/rfc5389#section-10.2) func main() { authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism") showHelp := flag.Bool("h", false, "Show usage") flag.Parse() if showHelp != nil && *showHelp { log.Println("Usage:") log.Println("$ lt-cred-generator | xargs go run examples/turn-client/udp/main.go -host localhost -ping=true -user=") return } if authSecret == nil || len(*authSecret) == 0 { log.Fatal("Missing -authSecret parameter") } u, p, _ := turn.GenerateLongTermCredentials(*authSecret, time.Minute) if _, err := fmt.Fprintf(os.Stdout, "%s=%s", u, p); err != nil { // For use with xargs log.Panicf("Failed to write to stdout: %s", err) } if _, err := fmt.Fprintf(os.Stderr, "\n"); err != nil { // Ignored by xargs log.Panicf("Failed to write to stderr: %s", err) } } turn-4.1.3/examples/stun-only-server/000077500000000000000000000000001510560755200176075ustar00rootroot00000000000000turn-4.1.3/examples/stun-only-server/main.go000066400000000000000000000025251510560755200210660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a simple TURN server package main import ( "flag" "log" "net" "os" "os/signal" "strconv" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that STUN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create STUN server listener: %s", err) } server, err := turn.NewServer(turn.ServerConfig{ // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-client/000077500000000000000000000000001510560755200165775ustar00rootroot00000000000000turn-4.1.3/examples/turn-client/tcp-alloc/000077500000000000000000000000001510560755200204555ustar00rootroot00000000000000turn-4.1.3/examples/turn-client/tcp-alloc/main.go000066400000000000000000000120711510560755200217310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN client with support for TCP package main import ( "bufio" "flag" "fmt" "log" "net" "strings" "github.com/pion/logging" "github.com/pion/turn/v4" ) func setupSignalingChannel(addrCh chan string, signaling bool, relayAddr string) { addr := "127.0.0.1:5000" if signaling { // nolint:nestif go func() { listener, err := net.Listen("tcp", addr) // nolint: noctx if err != nil { log.Panicf("Failed to create signaling server: %s", err) } defer listener.Close() //nolint:errcheck,gosec for { conn, err := listener.Accept() if err != nil { log.Panicf("Failed to accept: %s", err) } go func() { var message string message, err = bufio.NewReader(conn).ReadString('\n') if err != nil { log.Panicf("Failed to read from relayAddr: %s", err) } addrCh <- message[:len(message)-1] }() if _, err = fmt.Fprintf(conn, "%s\n", relayAddr); err != nil { log.Panicf("Failed to write relayAddr: %s", err) } } }() } else { conn, err := net.Dial("tcp", addr) // nolint: noctx if err != nil { log.Panicf("Error dialing: %s", err) } message, err := bufio.NewReader(conn).ReadString('\n') if err != nil { log.Panicf("Failed to read relayAddr: %s", err) } addrCh <- message[:len(message)-1] if _, err = fmt.Fprintf(conn, "%s\n", relayAddr); err != nil { log.Panicf("Failed to write relayAddr: %s", err) } } } func main() { //nolint:cyclop host := flag.String("host", "", "TURN Server name.") port := flag.Int("port", 3478, "Listening port.") user := flag.String("user", "", "A pair of username and password (e.g. \"user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") signaling := flag.Bool("signaling", false, "Whether to start signaling server otherwise connect") flag.Parse() if len(*host) == 0 { log.Panicf("'host' is required") } if len(*user) == 0 { log.Panicf("'user' is required") } // Dial TURN Server turnServerAddrStr := fmt.Sprintf("%s:%d", *host, *port) turnServerAddr, err := net.ResolveTCPAddr("tcp", turnServerAddrStr) if err != nil { log.Panicf("Failed to resolve TURN server address: %s", err) } conn, err := net.DialTCP("tcp", nil, turnServerAddr) if err != nil { log.Panicf("Failed to connect to TURN server: %s", err) } cred := strings.SplitN(*user, "=", 2) // Start a new TURN Client and wrap our net.Conn in a STUNConn // This allows us to simulate datagram based communication over a net.Conn cfg := &turn.ClientConfig{ STUNServerAddr: turnServerAddrStr, TURNServerAddr: turnServerAddrStr, Conn: turn.NewSTUNConn(conn), Username: cred[0], Password: cred[1], Realm: *realm, LoggerFactory: logging.NewDefaultLoggerFactory(), } client, err := turn.NewClient(cfg) if err != nil { log.Panicf("Failed to create TURN client: %s", err) } defer client.Close() // Start listening on the conn provided. err = client.Listen() if err != nil { log.Panicf("Failed to listen: %s", err) } // Allocate a relay socket on the TURN server. On success, it // will return a client.TCPAllocation which represents the remote // socket. allocation, err := client.AllocateTCP() if err != nil { log.Panicf("Failed to allocate: %s", err) } defer func() { if closeErr := allocation.Close(); closeErr != nil { log.Panicf("Failed to close connection: %s", closeErr) } }() log.Printf("relayed-address=%s", allocation.Addr()) // Learn the peers relay address via signaling channel addrCh := make(chan string, 5) setupSignalingChannel(addrCh, *signaling, allocation.Addr().String()) // Get peer address peerAddrStr := <-addrCh peerAddr, err := net.ResolveTCPAddr("tcp", peerAddrStr) if err != nil { log.Panicf("Failed to resolve peer address: %s", err) } log.Printf("Received peer address: %s", peerAddrStr) buf := make([]byte, 4096) var n int if *signaling { // nolint:nestif conn, err := allocation.DialTCP("tcp", nil, peerAddr) if err != nil { log.Panicf("Failed to dial: %s", err) } if _, err = conn.Write([]byte("hello!")); err != nil { log.Panicf("Failed to write: %s", err) } n, err = conn.Read(buf) if err != nil { log.Panicf("Failed to read from relay connection: %s", err) } if err := conn.Close(); err != nil { log.Panicf("Failed to close: %s", err) } } else { if err := client.CreatePermission(peerAddr); err != nil { log.Panicf("Failed to create permission: %s", err) } conn, err := allocation.AcceptTCP() if err != nil { log.Panicf("Failed to accept TCP connection: %s", err) } log.Printf("Accepted connection from: %s", conn.RemoteAddr()) n, err = conn.Read(buf) if err != nil { log.Panicf("Failed to read from relay conn: %s", err) } if _, err := conn.Write([]byte("hello back!")); err != nil { log.Panicf("Failed to write: %s", err) } if err := conn.Close(); err != nil { log.Panicf("Failed to close: %s", err) } } log.Printf("Read message: %s", string(buf[:n])) } turn-4.1.3/examples/turn-client/tcp/000077500000000000000000000000001510560755200173655ustar00rootroot00000000000000turn-4.1.3/examples/turn-client/tcp/main.go000066400000000000000000000105641510560755200206460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN client with support for TCP package main import ( "flag" "fmt" "log" "net" "strings" "time" "github.com/pion/logging" "github.com/pion/turn/v4" ) func main() { //nolint:cyclop host := flag.String("host", "", "TURN Server name.") port := flag.Int("port", 3478, "Listening port.") user := flag.String("user", "", "A pair of username and password (e.g. \"user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") ping := flag.Bool("ping", false, "Run ping test") flag.Parse() if len(*host) == 0 { log.Fatalf("'host' is required") } if len(*user) == 0 { log.Fatalf("'user' is required") } // Dial TURN Server turnServerAddr := fmt.Sprintf("%s:%d", *host, *port) conn, err := net.Dial("tcp", turnServerAddr) // nolint: noctx if err != nil { log.Panicf("Failed to connect to TURN server: %s", err) } cred := strings.SplitN(*user, "=", 2) // Start a new TURN Client and wrap our net.Conn in a STUNConn // This allows us to simulate datagram based communication over a net.Conn cfg := &turn.ClientConfig{ STUNServerAddr: turnServerAddr, TURNServerAddr: turnServerAddr, Conn: turn.NewSTUNConn(conn), Username: cred[0], Password: cred[1], Realm: *realm, LoggerFactory: logging.NewDefaultLoggerFactory(), } client, err := turn.NewClient(cfg) if err != nil { log.Panicf("Failed to create TURN client: %s", err) } defer client.Close() // Start listening on the conn provided. err = client.Listen() if err != nil { log.Panicf("Failed to listen: %s", err) } // Allocate a relay socket on the TURN server. On success, it // will return a net.PacketConn which represents the remote // socket. relayConn, err := client.Allocate() if err != nil { log.Panicf("Failed to allocate: %s", err) } defer func() { if closeErr := relayConn.Close(); closeErr != nil { log.Fatalf("Failed to close connection: %s", closeErr) } }() // The relayConn's local address is actually the transport // address assigned on the TURN server. log.Printf("relayed-address=%s", relayConn.LocalAddr().String()) // If you provided `-ping`, perform a ping test against the // relayConn we have just allocated. if *ping { err = doPingTest(client, relayConn) if err != nil { log.Panicf("Failed to ping: %s", err) } } } func doPingTest(client *turn.Client, relayConn net.PacketConn) error { //nolint:cyclop // Send BindingRequest to learn our external IP mappedAddr, err := client.SendBindingRequest() if err != nil { return err } // Set up pinger socket (pingerConn) pingerConn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx if err != nil { log.Panicf("Failed to listen: %s", err) } defer func() { if closeErr := pingerConn.Close(); closeErr != nil { log.Panicf("Failed to close connection: %s", closeErr) } }() // Punch a UDP hole for the relayConn by sending a data to the mappedAddr. // This will trigger a TURN client to generate a permission request to the // TURN server. After this, packets from the IP address will be accepted by // the TURN server. _, err = relayConn.WriteTo([]byte("Hello"), mappedAddr) if err != nil { return err } // Start read-loop on pingerConn go func() { buf := make([]byte, 1600) for { n, from, pingerErr := pingerConn.ReadFrom(buf) if pingerErr != nil { break } msg := string(buf[:n]) if sentAt, pingerErr := time.Parse(time.RFC3339Nano, msg); pingerErr == nil { rtt := time.Since(sentAt) log.Printf("%d bytes from from %s time=%d ms\n", n, from.String(), int(rtt.Seconds()*1000)) } } }() // Start read-loop on relayConn go func() { buf := make([]byte, 1600) for { n, from, readerErr := relayConn.ReadFrom(buf) if readerErr != nil { break } // Echo back if _, readerErr = relayConn.WriteTo(buf[:n], from); readerErr != nil { break } } }() time.Sleep(500 * time.Millisecond) // Send 10 packets from relayConn to the echo server for i := 0; i < 10; i++ { msg := time.Now().Format(time.RFC3339Nano) _, err = pingerConn.WriteTo([]byte(msg), relayConn.LocalAddr()) if err != nil { return err } // For simplicity, this example does not wait for the pong (reply). // Instead, sleep 1 second. time.Sleep(time.Second) } return nil } turn-4.1.3/examples/turn-client/udp/000077500000000000000000000000001510560755200173675ustar00rootroot00000000000000turn-4.1.3/examples/turn-client/udp/main.go000066400000000000000000000105621510560755200206460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN client using UDP package main import ( "flag" "fmt" "log" "net" "strings" "time" "github.com/pion/logging" "github.com/pion/turn/v4" ) func main() { //nolint:cyclop host := flag.String("host", "", "TURN Server name.") port := flag.Int("port", 3478, "Listening port.") user := flag.String("user", "", "A pair of username and password (e.g. \"user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") ping := flag.Bool("ping", false, "Run ping test") flag.Parse() if len(*host) == 0 { log.Fatalf("'host' is required") } if len(*user) == 0 { log.Fatalf("'user' is required") } cred := strings.SplitN(*user, "=", 2) // TURN client won't create a local listening socket by itself. conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx if err != nil { log.Panicf("Failed to listen: %s", err) } defer func() { if closeErr := conn.Close(); closeErr != nil { log.Panicf("Failed to close connection: %s", closeErr) } }() turnServerAddr := fmt.Sprintf("%s:%d", *host, *port) cfg := &turn.ClientConfig{ STUNServerAddr: turnServerAddr, TURNServerAddr: turnServerAddr, Conn: conn, Username: cred[0], Password: cred[1], Realm: *realm, LoggerFactory: logging.NewDefaultLoggerFactory(), } client, err := turn.NewClient(cfg) if err != nil { log.Panicf("Failed to create TURN client: %s", err) } defer client.Close() // Start listening on the conn provided. err = client.Listen() if err != nil { log.Panicf("Failed to listen: %s", err) } // Allocate a relay socket on the TURN server. On success, it // will return a net.PacketConn which represents the remote // socket. relayConn, err := client.Allocate() if err != nil { log.Panicf("Failed to allocate: %s", err) } defer func() { if closeErr := relayConn.Close(); closeErr != nil { log.Panicf("Failed to close connection: %s", closeErr) } }() // The relayConn's local address is actually the transport // address assigned on the TURN server. log.Printf("relayed-address=%s", relayConn.LocalAddr().String()) // If you provided `-ping`, perform a ping test against the // relayConn we have just allocated. if *ping { err = doPingTest(client, relayConn) if err != nil { log.Panicf("Failed to ping: %s", err) } } } func doPingTest(client *turn.Client, relayConn net.PacketConn) error { //nolint:cyclop // Send BindingRequest to learn our external IP mappedAddr, err := client.SendBindingRequest() if err != nil { return err } // Set up pinger socket (pingerConn) pingerConn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx if err != nil { log.Panicf("Failed to listen: %s", err) } defer func() { if closeErr := pingerConn.Close(); closeErr != nil { log.Panicf("Failed to close connection: %s", closeErr) } }() // Punch a UDP hole for the relayConn by sending a data to the mappedAddr. // This will trigger a TURN client to generate a permission request to the // TURN server. After this, packets from the IP address will be accepted by // the TURN server. _, err = relayConn.WriteTo([]byte("Hello"), mappedAddr) if err != nil { return err } // Start read-loop on pingerConn go func() { buf := make([]byte, 1600) for { n, from, pingerErr := pingerConn.ReadFrom(buf) if pingerErr != nil { break } msg := string(buf[:n]) if sentAt, pingerErr := time.Parse(time.RFC3339Nano, msg); pingerErr == nil { rtt := time.Since(sentAt) log.Printf("%d bytes from from %s time=%d ms\n", n, from.String(), int(rtt.Seconds()*1000)) } } }() // Start read-loop on relayConn go func() { buf := make([]byte, 1600) for { n, from, readerErr := relayConn.ReadFrom(buf) if readerErr != nil { break } // Echo back if _, readerErr = relayConn.WriteTo(buf[:n], from); readerErr != nil { break } } }() time.Sleep(500 * time.Millisecond) // Send 10 packets from relayConn to the echo server for i := 0; i < 10; i++ { msg := time.Now().Format(time.RFC3339Nano) _, err = pingerConn.WriteTo([]byte(msg), relayConn.LocalAddr()) if err != nil { return err } // For simplicity, this example does not wait for the pong (reply). // Instead, sleep 1 second. time.Sleep(time.Second) } return nil } turn-4.1.3/examples/turn-server/000077500000000000000000000000001510560755200166275ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/add-software-attribute/000077500000000000000000000000001510560755200232105ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/add-software-attribute/main.go000066400000000000000000000062761510560755200244760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server // adding a software attribute. package main import ( "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/stun/v3" "github.com/pion/turn/v4" ) // attributeAdder wraps a PacketConn and appends the SOFTWARE attribute to STUN packets. // This pattern could be used to capture/inspect/modify data as well. type attributeAdder struct { net.PacketConn } func (s *attributeAdder) WriteTo(payload []byte, addr net.Addr) (n int, err error) { if stun.IsMessage(payload) { m := &stun.Message{Raw: payload} if err = m.Decode(); err != nil { return } if err = stun.NewSoftware("CustomTURNServer").AddTo(m); err != nil { return } m.Encode() payload = m.Raw } return s.PacketConn.WriteTo(payload, addr) } func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: &attributeAdder{udpListener}, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP) RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/log/000077500000000000000000000000001510560755200174105ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/log/main.go000066400000000000000000000066131510560755200206710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server with logging. package main import ( "flag" "fmt" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/stun/v3" "github.com/pion/turn/v4" ) // stunLogger wraps a PacketConn and prints incoming/outgoing STUN packets // This pattern could be used to capture/inspect/modify data as well. type stunLogger struct { net.PacketConn } func (s *stunLogger) WriteTo(p []byte, addr net.Addr) (n int, err error) { if n, err = s.PacketConn.WriteTo(p, addr); err == nil && stun.IsMessage(p) { msg := &stun.Message{Raw: p} if err = msg.Decode(); err != nil { return } fmt.Printf("Outbound STUN: %s \n", msg.String()) } return } func (s *stunLogger) ReadFrom(p []byte) (n int, addr net.Addr, err error) { if n, addr, err = s.PacketConn.ReadFrom(p); err == nil && stun.IsMessage(p) { msg := &stun.Message{Raw: p} if err = msg.Decode(); err != nil { return } fmt.Printf("Inbound STUN: %s \n", msg.String()) } return } func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: &stunLogger{udpListener}, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP) RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/lt-cred-turn-rest/000077500000000000000000000000001510560755200221225ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/lt-cred-turn-rest/main.go000066400000000000000000000043331510560755200234000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2024 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server using // ephemeral credentials. package main import ( "flag" "log" "net" "os" "os/signal" "strconv" "syscall" "github.com/pion/logging" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*authSecret) == 0 { log.Fatalf("'authSecret' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages // and process them yourself. logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, AuthHandler: turn.LongTermTURNRESTAuthHandler(*authSecret, logger), // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP). RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface. Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/lt-cred/000077500000000000000000000000001510560755200201615ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/lt-cred/main.go000066400000000000000000000047471510560755200214500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server using // long-term credentials. package main import ( "flag" "log" "net" "os" "os/signal" "strconv" "syscall" "github.com/pion/logging" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*authSecret) == 0 { log.Fatalf("'authSecret' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages // and process them yourself. logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: turn.NewLongTermAuthHandler(*authSecret, logger), // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP). RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface. Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/perm-filter/000077500000000000000000000000001510560755200210555ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/perm-filter/main.go000066400000000000000000000066131510560755200223360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // This example demonstrates the use of a permission handler in the PION TURN server. The // permission handler implements a filtering policy that lets clients to connect back to their own // host or server-reflexive address but will filter out everything else. This will let the client // ping-test through but will block essentially all other peer connection attempts. package main import ( "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "strings" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP) RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface Address: "0.0.0.0", }, // allow peer connections only to the client's own (host or server-reflexive) IP PermissionHandler: func(clientAddr net.Addr, peerIP net.IP) bool { clientIP := strings.SplitN(clientAddr.String(), ":", 2) if clientIP[0] != peerIP.String() { log.Printf("Blocking request from client IP %s to peer %s", clientIP[0], peerIP.String()) return false } log.Printf("Admitting request from client IP %s to peer %s", clientIP[0], peerIP.String()) return true }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/port-range/000077500000000000000000000000001510560755200207055ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/port-range/main.go000066400000000000000000000051771510560755200221720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server with a // specified port range. package main import ( "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{ // Claim that we are listening on IP passed by user (This should be your Public IP) RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface Address: "0.0.0.0", MinPort: 50000, MaxPort: 55000, }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/simple-multithreaded/000077500000000000000000000000001510560755200227515ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/simple-multithreaded/main.go000066400000000000000000000073101510560755200242250ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build unix // Package main implements a multi-threaded TURN server package main import ( "context" "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/turn/v4" "golang.org/x/sys/unix" ) func main() { //nolint:cyclop publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") threadNum := flag.Int("thread-num", 1, "Number of server threads (defaults to 1)") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:"+strconv.Itoa(*port)) if err != nil { log.Fatalf("Failed to parse server address: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } // Create `numThreads` UDP listeners to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic // UDP listeners share the same local address:port with setting SO_REUSEPORT and the kernel // will load-balance received packets per the IP 5-tuple listenerConfig := &net.ListenConfig{ Control: func(network, address string, conn syscall.RawConn) error { // nolint: revive var operr error if err = conn.Control(func(fd uintptr) { operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) }); err != nil { return err } return operr }, } relayAddressGenerator := &turn.RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP(*publicIP), // Claim that we are listening on IP passed by user Address: "0.0.0.0", // But actually be listening on every interface } packetConnConfigs := make([]turn.PacketConnConfig, *threadNum) for i := 0; i < *threadNum; i++ { conn, listErr := listenerConfig.ListenPacket(context.Background(), addr.Network(), addr.String()) // nolint: noctx if listErr != nil { log.Fatalf("Failed to allocate UDP listener at %s:%s", addr.Network(), addr.String()) } packetConnConfigs[i] = turn.PacketConnConfig{ PacketConn: conn, RelayAddressGenerator: relayAddressGenerator, } log.Printf("Server %d listening on %s\n", i, conn.LocalAddr().String()) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: packetConnConfigs, }) if err != nil { log.Panicf("Failed to create TURN server: %s", err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panicf("Failed to close TURN server: %s", err) } } turn-4.1.3/examples/turn-server/simple/000077500000000000000000000000001510560755200201205ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/simple/Dockerfile000066400000000000000000000031611510560755200221130ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT FROM golang:alpine as builder ARG VERSION=master RUN apk add --no-cache git WORKDIR /build # Clone Source using GIT RUN git clone --branch=$VERSION --depth=1 https://github.com/pion/turn.git turn && rm -rf turn/.git WORKDIR /build/turn/examples/turn-server/simple # Download all the dependencies RUN go get -d -v ./... # Build static binary RUN CGO_ENABLED=0 go build -trimpath -ldflags="-w -s" -o turn-server main.go ##### main FROM alpine ARG BUILD_DATE ARG VCS_REF ARG VERSION=master LABEL org.label-schema.build-date="${BUILD_DATE}" \ org.label-schema.name="pion-turn" \ org.label-schema.description="A toolkit for building TURN clients and servers in Go" \ org.label-schema.usage="https://github.com/pion/turn#readme" \ org.label-schema.vcs-ref="${VCS_REF}" \ org.label-schema.vcs-url="https://github.com/pion/turn" \ org.label-schema.vendor="Sean-Der" \ org.label-schema.version="${VERSION}" \ maintainer="https://github.com/pion" ENV REALM localhost ENV USERS username=password ENV UDP_PORT 3478 ENV PUBLIC_IP 127.0.0.1 EXPOSE 3478 #EXPOSE 49152:65535/tcp #EXPOSE 49152:65535/udp USER nobody # Copy the executable COPY --from=builder /build/turn/examples/turn-server/simple/turn-server /usr/bin/ # Run the executable CMD turn-server -public-ip $PUBLIC_IP -users $USERS -realm $REALM -port $UDP_PORT # docker build -t pion-turn -f Dockerfile . # docker run --rm -e REALM="localhost" -e USERS="username=password" -e UDP_PORT="3478" -e PUBLIC_IP="127.0.0.1" -p 3478:3478 pion-turn turn-4.1.3/examples/turn-server/simple/docker-compose.yml000066400000000000000000000013231510560755200235540ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT version: "3.1" services: pion-turn: container_name: "pion-turn" image: pion-turn:${VERSION:-latest} build: context: ./ stdin_open: true environment: - VERSION=${PION_TURN_VERSION:-master} - REALM=${PION_TURN_REALM:-localhost} - USERS=${PION_TURN_USERS:-username=password} - PUBLIC_IP=${PION_TURN_PUBLIC_IP:-127.0.0.1} - UDP_PORT=${PION_TURN_UDP_PORT:-3478} network_mode: host ports: # STUN - "${PION_TURN_UDP_PORT:-3478}:${PION_TURN_UDP_PORT:-3478}" # TURN - "49152-65535:49152-65535" cap_add: - NET_ADMIN - NET_RAW turn-4.1.3/examples/turn-server/simple/main.go000066400000000000000000000050711510560755200213760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a simple TURN server package main import ( "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a UDP listener to pass into pion/turn // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // PacketConnConfigs is a list of UDP Listeners and the configuration around them PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ // Claim that we are listening on IP passed by user (This should be your Public IP) RelayAddress: net.ParseIP(*publicIP), // But actually be listening on every interface Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/tcp/000077500000000000000000000000001510560755200174155ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/tcp/main.go000066400000000000000000000046601510560755200206760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements an example TURN server supporting TCP package main import ( "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 3478, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } // Create a TCP listener to pass into pion/turn // pion/turn itself doesn't allocate any TCP listeners, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic tcpListener, err := net.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(*port)) // nolint: noctx if err != nil { log.Panicf("Failed to create TURN server listener: %s", err) } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // ListenerConfig is a list of Listeners and the configuration around them ListenerConfigs: []turn.ListenerConfig{ { Listener: tcpListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP(*publicIP), Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/examples/turn-server/tls/000077500000000000000000000000001510560755200174315ustar00rootroot00000000000000turn-4.1.3/examples/turn-server/tls/main.go000066400000000000000000000053631510560755200207130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a TURN server with TLS support package main import ( "crypto/tls" "flag" "log" "net" "os" "os/signal" "regexp" "strconv" "syscall" "github.com/pion/turn/v4" ) func main() { publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") port := flag.Int("port", 5349, "Listening port.") users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")") realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") certFile := flag.String("cert", "server.crt", "Certificate (defaults to \"server.crt\")") keyFile := flag.String("key", "server.key", "Key (defaults to \"server.key\")") flag.Parse() if len(*publicIP) == 0 { log.Fatalf("'public-ip' is required") } else if len(*users) == 0 { log.Fatalf("'users' is required") } cer, err := tls.LoadX509KeyPair(*certFile, *keyFile) if err != nil { log.Println(err) return } // Create a TLS listener to pass into pion/turn // pion/turn itself doesn't allocate any TLS listeners, but lets the user pass them in // this allows us to add logging, storage or modify inbound/outbound traffic tlsListener, err := tls.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(*port), &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cer}, }) if err != nil { log.Println(err) return } // Cache -users flag for easy lookup later // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey usersMap := map[string][]byte{} for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) } server, err := turn.NewServer(turn.ServerConfig{ Realm: *realm, // Set AuthHandler callback // This is called every time a user tries to authenticate with the TURN server // Return the key for that user, or false when no user is found AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive if key, ok := usersMap[username]; ok { return key, true } return nil, false }, // ListenerConfig is a list of Listeners and the configuration around them ListenerConfigs: []turn.ListenerConfig{ { Listener: tlsListener, RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP(*publicIP), Address: "0.0.0.0", }, }, }, }) if err != nil { log.Panic(err) } // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs if err = server.Close(); err != nil { log.Panic(err) } } turn-4.1.3/go.mod000066400000000000000000000010151510560755200136200ustar00rootroot00000000000000module github.com/pion/turn/v4 go 1.21 require ( github.com/pion/logging v0.2.4 github.com/pion/randutil v0.1.0 github.com/pion/stun/v3 v3.0.1 github.com/pion/transport/v3 v3.1.1 github.com/stretchr/testify v1.11.1 golang.org/x/sys v0.30.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pion/dtls/v3 v3.0.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) turn-4.1.3/go.sum000066400000000000000000000044541510560755200136570ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= turn-4.1.3/helpers_unix_test.go000066400000000000000000000002461510560755200166120ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build unix // +build unix package turn type Handle = int turn-4.1.3/helpers_windows_test.go000066400000000000000000000003161510560755200173170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build windows // +build windows package turn import ( "syscall" ) type Handle = syscall.Handle turn-4.1.3/internal/000077500000000000000000000000001510560755200143315ustar00rootroot00000000000000turn-4.1.3/internal/allocation/000077500000000000000000000000001510560755200164565ustar00rootroot00000000000000turn-4.1.3/internal/allocation/allocation.go000066400000000000000000000246671510560755200211510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package allocation contains all CRUD operations for allocations package allocation import ( "net" "sync" "sync/atomic" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/ipnet" "github.com/pion/turn/v4/internal/proto" ) type allocationResponse struct { transactionID [stun.TransactionIDSize]byte responseAttrs []stun.Setter } // Allocation is tied to a FiveTuple and relays traffic // use CreateAllocation and GetAllocation to operate. type Allocation struct { RelayAddr net.Addr Protocol Protocol TurnSocket net.PacketConn RelaySocket net.PacketConn fiveTuple *FiveTuple permissionsLock sync.RWMutex permissions map[string]*Permission channelBindingsLock sync.RWMutex channelBindings []*ChannelBind lifetimeTimer *time.Timer closed chan any username, realm string eventHandler EventHandler log logging.LeveledLogger // Some clients (Firefox or others using resiprocate's nICE lib) may retry allocation // with same 5 tuple when received 413, for compatible with these clients, // cache for response lost and client retry to implement 'stateless stack approach' // See: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 responseCache atomic.Value // *allocationResponse } // NewAllocation creates a new instance of NewAllocation. func NewAllocation( turnSocket net.PacketConn, fiveTuple *FiveTuple, eventHandler EventHandler, log logging.LeveledLogger, ) *Allocation { return &Allocation{ TurnSocket: turnSocket, fiveTuple: fiveTuple, permissions: make(map[string]*Permission, 64), closed: make(chan any), eventHandler: eventHandler, log: log, } } // GetPermission gets the Permission from the allocation. func (a *Allocation) GetPermission(addr net.Addr) *Permission { a.permissionsLock.RLock() defer a.permissionsLock.RUnlock() return a.permissions[ipnet.FingerprintAddr(addr)] } // AddPermission adds a new permission to the allocation. func (a *Allocation) AddPermission(perms *Permission) { fingerprint := ipnet.FingerprintAddr(perms.Addr) a.permissionsLock.RLock() existedPermission, ok := a.permissions[fingerprint] a.permissionsLock.RUnlock() if ok { existedPermission.refresh(permissionTimeout) return } perms.allocation = a a.permissionsLock.Lock() a.permissions[fingerprint] = perms a.permissionsLock.Unlock() if a.eventHandler.OnPermissionCreated != nil { if u, ok := perms.Addr.(*net.UDPAddr); ok { a.eventHandler.OnPermissionCreated(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr, a.fiveTuple.Protocol.String(), a.username, a.realm, a.RelayAddr, u.IP) } } perms.start(permissionTimeout) } // RemovePermission removes the net.Addr's fingerprint from the allocation's permissions. func (a *Allocation) RemovePermission(addr net.Addr) { a.permissionsLock.Lock() defer a.permissionsLock.Unlock() delete(a.permissions, ipnet.FingerprintAddr(addr)) if a.eventHandler.OnPermissionDeleted != nil { if u, ok := addr.(*net.UDPAddr); ok { a.eventHandler.OnPermissionDeleted(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr, a.fiveTuple.Protocol.String(), a.username, a.realm, a.RelayAddr, u.IP) } } } // ListPermissions returns the permissions associated with an allocation. func (a *Allocation) ListPermissions() []*Permission { ps := []*Permission{} a.permissionsLock.RLock() defer a.permissionsLock.RUnlock() for _, p := range a.permissions { ps = append(ps, p) } return ps } // AddChannelBind adds a new ChannelBind to the allocation, it also updates the // permissions needed for this ChannelBind. func (a *Allocation) AddChannelBind(chanBind *ChannelBind, lifetime time.Duration) error { // Check that this channel id isn't bound to another transport address, and // that this transport address isn't bound to another channel number. channelByNumber := a.GetChannelByNumber(chanBind.Number) if channelByNumber != a.GetChannelByAddr(chanBind.Peer) { return errSameChannelDifferentPeer } // Add or refresh this channel. if channelByNumber == nil { a.channelBindingsLock.Lock() defer a.channelBindingsLock.Unlock() chanBind.allocation = a a.channelBindings = append(a.channelBindings, chanBind) chanBind.start(lifetime) // Channel binds also refresh permissions. a.AddPermission(NewPermission(chanBind.Peer, a.log)) if a.eventHandler.OnChannelCreated != nil { a.eventHandler.OnChannelCreated(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr, a.fiveTuple.Protocol.String(), a.username, a.realm, a.RelayAddr, chanBind.Peer, uint16(chanBind.Number)) } } else { channelByNumber.refresh(lifetime) // Channel binds also refresh permissions. a.AddPermission(NewPermission(channelByNumber.Peer, a.log)) } return nil } // RemoveChannelBind removes the ChannelBind from this allocation by id. func (a *Allocation) RemoveChannelBind(number proto.ChannelNumber) bool { a.channelBindingsLock.Lock() defer a.channelBindingsLock.Unlock() for i := len(a.channelBindings) - 1; i >= 0; i-- { if a.channelBindings[i].Number == number { if a.eventHandler.OnChannelDeleted != nil { a.eventHandler.OnChannelDeleted(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr, a.fiveTuple.Protocol.String(), a.username, a.realm, a.RelayAddr, a.channelBindings[i].Peer, uint16(a.channelBindings[i].Number)) } a.channelBindings = append(a.channelBindings[:i], a.channelBindings[i+1:]...) return true } } return false } // GetChannelByNumber gets the ChannelBind from this allocation by id. func (a *Allocation) GetChannelByNumber(number proto.ChannelNumber) *ChannelBind { a.channelBindingsLock.RLock() defer a.channelBindingsLock.RUnlock() for _, cb := range a.channelBindings { if cb.Number == number { return cb } } return nil } // GetChannelByAddr gets the ChannelBind from this allocation by net.Addr. func (a *Allocation) GetChannelByAddr(addr net.Addr) *ChannelBind { a.channelBindingsLock.RLock() defer a.channelBindingsLock.RUnlock() for _, cb := range a.channelBindings { if ipnet.AddrEqual(cb.Peer, addr) { return cb } } return nil } // ListChannelBindings returns the channel bindings associated with an allocation. func (a *Allocation) ListChannelBindings() []*ChannelBind { cs := []*ChannelBind{} a.channelBindingsLock.RLock() defer a.channelBindingsLock.RUnlock() cs = append(cs, a.channelBindings...) return cs } // Refresh updates the allocations lifetime. func (a *Allocation) Refresh(lifetime time.Duration) { if !a.lifetimeTimer.Reset(lifetime) { a.log.Errorf("Failed to reset allocation timer for %v", a.fiveTuple) } } // SetResponseCache cache allocation response for retransmit allocation request. func (a *Allocation) SetResponseCache(transactionID [stun.TransactionIDSize]byte, attrs []stun.Setter) { a.responseCache.Store(&allocationResponse{ transactionID: transactionID, responseAttrs: attrs, }) } // GetResponseCache return response cache for retransmit allocation request. func (a *Allocation) GetResponseCache() (id [stun.TransactionIDSize]byte, attrs []stun.Setter) { if res, ok := a.responseCache.Load().(*allocationResponse); ok && res != nil { id, attrs = res.transactionID, res.responseAttrs } return } // Close closes the allocation. func (a *Allocation) Close() error { select { case <-a.closed: return nil default: } close(a.closed) a.lifetimeTimer.Stop() for _, p := range a.ListPermissions() { a.RemovePermission(p.Addr) p.lifetimeTimer.Stop() } for _, c := range a.ListChannelBindings() { a.RemoveChannelBind(c.Number) c.lifetimeTimer.Stop() } return a.RelaySocket.Close() } // https://tools.ietf.org/html/rfc5766#section-10.3 // When the server receives a UDP datagram at a currently allocated // relayed transport address, the server looks up the allocation // associated with the relayed transport address. The server then // checks to see whether the set of permissions for the allocation allow // the relaying of the UDP datagram as described in Section 8. // // If relaying is permitted, then the server checks if there is a // channel bound to the peer that sent the UDP datagram (see // Section 11). If a channel is bound, then processing proceeds as // described in Section 11.7. // // If relaying is permitted but no channel is bound to the peer, then // the server forms and sends a Data indication. The Data indication // MUST contain both an XOR-PEER-ADDRESS and a DATA attribute. The DATA // attribute is set to the value of the 'data octets' field from the // datagram, and the XOR-PEER-ADDRESS attribute is set to the source // transport address of the received UDP datagram. The Data indication // is then sent on the 5-tuple associated with the allocation. const rtpMTU = 1600 func (a *Allocation) packetHandler(manager *Manager) { buffer := make([]byte, rtpMTU) for { n, srcAddr, err := a.RelaySocket.ReadFrom(buffer) if err != nil { manager.DeleteAllocation(a.fiveTuple) return } a.log.Debugf("Relay socket %s received %d bytes from %s", a.RelaySocket.LocalAddr(), n, srcAddr) if channel := a.GetChannelByAddr(srcAddr); channel != nil { // nolint:nestif channelData := &proto.ChannelData{ Data: buffer[:n], Number: channel.Number, } channelData.Encode() if _, err = a.TurnSocket.WriteTo(channelData.Raw, a.fiveTuple.SrcAddr); err != nil { a.log.Errorf("Failed to send ChannelData from allocation %v %v", srcAddr, err) } } else if p := a.GetPermission(srcAddr); p != nil { udpAddr, ok := srcAddr.(*net.UDPAddr) if !ok { a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err) return } peerAddressAttr := proto.PeerAddress{IP: udpAddr.IP, Port: udpAddr.Port} dataAttr := proto.Data(buffer[:n]) msg, err := stun.Build( stun.TransactionID, stun.NewType(stun.MethodData, stun.ClassIndication), peerAddressAttr, dataAttr, ) if err != nil { a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err) return } a.log.Debugf("Relaying message from %s to client at %s", srcAddr, a.fiveTuple.SrcAddr) if _, err = a.TurnSocket.WriteTo(msg.Raw, a.fiveTuple.SrcAddr); err != nil { a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err) } } else { a.log.Infof("No Permission or Channel exists for %v on allocation %v", srcAddr, a.RelayAddr) } } } turn-4.1.3/internal/allocation/allocation_manager.go000066400000000000000000000142021510560755200226230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "fmt" "net" "sync" "time" "github.com/pion/logging" ) // ManagerConfig a bag of config params for Manager. type ManagerConfig struct { LeveledLogger logging.LeveledLogger AllocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error) AllocateConn func(network string, requestedPort int) (net.Conn, net.Addr, error) PermissionHandler func(sourceAddr net.Addr, peerIP net.IP) bool EventHandler EventHandler } type reservation struct { token string port int } // Manager is used to hold active allocations. type Manager struct { lock sync.RWMutex log logging.LeveledLogger allocations map[FiveTupleFingerprint]*Allocation reservations []*reservation allocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error) allocateConn func(network string, requestedPort int) (net.Conn, net.Addr, error) permissionHandler func(sourceAddr net.Addr, peerIP net.IP) bool EventHandler EventHandler } // NewManager creates a new instance of Manager. func NewManager(config ManagerConfig) (*Manager, error) { switch { case config.AllocatePacketConn == nil: return nil, errAllocatePacketConnMustBeSet case config.AllocateConn == nil: return nil, errAllocateConnMustBeSet case config.LeveledLogger == nil: return nil, errLeveledLoggerMustBeSet } return &Manager{ log: config.LeveledLogger, allocations: make(map[FiveTupleFingerprint]*Allocation, 64), allocatePacketConn: config.AllocatePacketConn, allocateConn: config.AllocateConn, permissionHandler: config.PermissionHandler, EventHandler: config.EventHandler, }, nil } // GetAllocation fetches the allocation matching the passed FiveTuple. func (m *Manager) GetAllocation(fiveTuple *FiveTuple) *Allocation { m.lock.RLock() defer m.lock.RUnlock() return m.allocations[fiveTuple.Fingerprint()] } // AllocationCount returns the number of existing allocations. func (m *Manager) AllocationCount() int { m.lock.RLock() defer m.lock.RUnlock() return len(m.allocations) } // Close closes the manager and closes all allocations it manages. func (m *Manager) Close() error { m.lock.Lock() defer m.lock.Unlock() for _, a := range m.allocations { if err := a.Close(); err != nil { return err } } return nil } // CreateAllocation creates a new allocation and starts relaying. func (m *Manager) CreateAllocation( fiveTuple *FiveTuple, turnSocket net.PacketConn, requestedPort int, lifetime time.Duration, username, realm string, ) (*Allocation, error) { switch { case fiveTuple == nil: return nil, errNilFiveTuple case fiveTuple.SrcAddr == nil: return nil, errNilFiveTupleSrcAddr case fiveTuple.DstAddr == nil: return nil, errNilFiveTupleDstAddr case turnSocket == nil: return nil, errNilTurnSocket case lifetime == 0: return nil, errLifetimeZero } if alloc := m.GetAllocation(fiveTuple); alloc != nil { return nil, fmt.Errorf("%w: %v", errDupeFiveTuple, fiveTuple) } alloc := NewAllocation(turnSocket, fiveTuple, m.EventHandler, m.log) alloc.username = username alloc.realm = realm conn, relayAddr, err := m.allocatePacketConn("udp4", requestedPort) if err != nil { return nil, err } alloc.RelaySocket = conn alloc.RelayAddr = relayAddr m.log.Debugf("Listening on relay address: %s", alloc.RelayAddr) alloc.lifetimeTimer = time.AfterFunc(lifetime, func() { m.DeleteAllocation(alloc.fiveTuple) }) m.lock.Lock() m.allocations[fiveTuple.Fingerprint()] = alloc m.lock.Unlock() if m.EventHandler.OnAllocationCreated != nil { m.EventHandler.OnAllocationCreated(fiveTuple.SrcAddr, fiveTuple.DstAddr, fiveTuple.Protocol.String(), username, realm, relayAddr, requestedPort) } go alloc.packetHandler(m) return alloc, nil } // DeleteAllocation removes an allocation. func (m *Manager) DeleteAllocation(fiveTuple *FiveTuple) { fingerprint := fiveTuple.Fingerprint() m.lock.Lock() allocation := m.allocations[fingerprint] delete(m.allocations, fingerprint) m.lock.Unlock() if allocation == nil { return } if err := allocation.Close(); err != nil { m.log.Errorf("Failed to close allocation: %v", err) } if m.EventHandler.OnAllocationDeleted != nil { m.EventHandler.OnAllocationDeleted(fiveTuple.SrcAddr, fiveTuple.DstAddr, fiveTuple.Protocol.String(), allocation.username, allocation.realm) } } // CreateReservation stores the reservation for the token+port. func (m *Manager) CreateReservation(reservationToken string, port int) { time.AfterFunc(30*time.Second, func() { m.lock.Lock() defer m.lock.Unlock() for i := len(m.reservations) - 1; i >= 0; i-- { if m.reservations[i].token == reservationToken { m.reservations = append(m.reservations[:i], m.reservations[i+1:]...) return } } }) m.lock.Lock() m.reservations = append(m.reservations, &reservation{ token: reservationToken, port: port, }) m.lock.Unlock() } // GetReservation returns the port for a given reservation if it exists. func (m *Manager) GetReservation(reservationToken string) (int, bool) { m.lock.RLock() defer m.lock.RUnlock() for _, r := range m.reservations { if r.token == reservationToken { return r.port, true } } return 0, false } // GetRandomEvenPort returns a random un-allocated udp4 port. func (m *Manager) GetRandomEvenPort() (int, error) { for i := 0; i < 128; i++ { conn, addr, err := m.allocatePacketConn("udp4", 0) if err != nil { return 0, err } udpAddr, ok := addr.(*net.UDPAddr) err = conn.Close() if err != nil { return 0, err } if !ok { return 0, errFailedToCastUDPAddr } if udpAddr.Port%2 == 0 { return udpAddr.Port, nil } } return 0, errFailedToAllocateEvenPort } // GrantPermission handles permission requests by calling the permission handler callback // associated with the TURN server listener socket. func (m *Manager) GrantPermission(sourceAddr net.Addr, peerIP net.IP) error { // No permission handler: open if m.permissionHandler == nil { return nil } if m.permissionHandler(sourceAddr, peerIP) { return nil } return errAdminProhibited } turn-4.1.3/internal/allocation/allocation_manager_test.go000066400000000000000000000141331510560755200236650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package allocation import ( "io" "math/rand" "net" "strings" "testing" "time" "github.com/pion/logging" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) func TestManager(t *testing.T) { tt := []struct { name string f func(*testing.T, net.PacketConn) }{ {"CreateInvalidAllocation", subTestCreateInvalidAllocation}, {"CreateAllocation", subTestCreateAllocation}, {"CreateAllocationDuplicateFiveTuple", subTestCreateAllocationDuplicateFiveTuple}, {"DeleteAllocation", subTestDeleteAllocation}, {"AllocationTimeout", subTestAllocationTimeout}, {"Close", subTestManagerClose}, {"GetRandomEvenPort", subTestGetRandomEvenPort}, } network := "udp4" turnSocket, err := net.ListenPacket(network, "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) for _, tc := range tt { f := tc.f t.Run(tc.name, func(t *testing.T) { f(t, turnSocket) }) } } // Test invalid Allocation creations. func subTestCreateInvalidAllocation(t *testing.T, turnSocket net.PacketConn) { t.Helper() m, err := newTestManager() assert.NoError(t, err) a, err := m.CreateAllocation(nil, turnSocket, 0, proto.DefaultLifetime, "", "") assert.Nil(t, a, "Illegally created allocation with nil FiveTuple") assert.Error(t, err, "Illegally created allocation with nil FiveTuple") a, err = m.CreateAllocation(randomFiveTuple(), nil, 0, proto.DefaultLifetime, "", "") assert.Nil(t, a, "Illegally created allocation with nil turnSocket") assert.Error(t, err, "Illegally created allocation with nil turnSocket") a, err = m.CreateAllocation(randomFiveTuple(), turnSocket, 0, 0, "", "") assert.Nil(t, a, "Illegally created allocation with 0 lifetime") assert.Error(t, err, "Illegally created allocation with 0 lifetime") } // Test valid Allocation creations. func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) { t.Helper() m, err := newTestManager() assert.NoError(t, err) fiveTuple := randomFiveTuple() a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "") assert.NotNil(t, a, "Failed to create allocation") assert.NoError(t, err, "Failed to create allocation") a = m.GetAllocation(fiveTuple) assert.NotNil(t, a, "Failed to get allocation right after creation") } // Test that two allocations can't be created with the same FiveTuple. func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket net.PacketConn) { t.Helper() m, err := newTestManager() assert.NoError(t, err) fiveTuple := randomFiveTuple() a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "") assert.NotNil(t, a, "Failed to create allocation") assert.NoError(t, err, "Failed to create allocation") a, err = m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "") assert.Nil(t, a, "Was able to create allocation with same FiveTuple twice") assert.Error(t, err, "Was able to create allocation with same FiveTuple twice") } func subTestDeleteAllocation(t *testing.T, turnSocket net.PacketConn) { t.Helper() manager, err := newTestManager() assert.NoError(t, err) fiveTuple := randomFiveTuple() a, err := manager.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "") assert.NotNil(t, a, "Failed to create allocation") assert.NoError(t, err, "Failed to create allocation") a = manager.GetAllocation(fiveTuple) assert.NotNil(t, a, "Failed to get allocation right after creation") manager.DeleteAllocation(fiveTuple) a = manager.GetAllocation(fiveTuple) assert.Nilf(t, a, "Failed to delete allocation %v", fiveTuple) } // Test that allocation should be closed if timeout. func subTestAllocationTimeout(t *testing.T, turnSocket net.PacketConn) { t.Helper() m, err := newTestManager() assert.NoError(t, err) allocations := make([]*Allocation, 5) lifetime := time.Second for index := range allocations { fiveTuple := randomFiveTuple() a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, lifetime, "", "") assert.NoErrorf(t, err, "Failed to create allocation with %v", fiveTuple) allocations[index] = a } // Make sure all allocations timeout time.Sleep(lifetime + time.Second) for _, alloc := range allocations { assert.True(t, isClose(alloc.RelaySocket), "Allocation relay socket should be closed if lifetime timeout") } } // Test for manager close. func subTestManagerClose(t *testing.T, turnSocket net.PacketConn) { t.Helper() manager, err := newTestManager() assert.NoError(t, err) allocations := make([]*Allocation, 2) a1, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Second, "", "") allocations[0] = a1 a2, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Minute, "", "") allocations[1] = a2 // Make a1 timeout time.Sleep(2 * time.Second) assert.NoError(t, manager.Close()) for _, alloc := range allocations { assert.True(t, isClose(alloc.RelaySocket), "Manager's allocations should be closed") } } func randomFiveTuple() *FiveTuple { // nolint return &FiveTuple{ SrcAddr: &net.UDPAddr{IP: nil, Port: rand.Int()}, DstAddr: &net.UDPAddr{IP: nil, Port: rand.Int()}, } } func newTestManager() (*Manager, error) { loggerFactory := logging.NewDefaultLoggerFactory() config := ManagerConfig{ LeveledLogger: loggerFactory.NewLogger("test"), AllocatePacketConn: func(string, int) (net.PacketConn, net.Addr, error) { conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx if err != nil { return nil, nil, err } return conn, conn.LocalAddr(), nil }, AllocateConn: func(string, int) (net.Conn, net.Addr, error) { return nil, nil, nil }, } return NewManager(config) } func isClose(conn io.Closer) bool { closeErr := conn.Close() return closeErr != nil && strings.Contains(closeErr.Error(), "use of closed network connection") } func subTestGetRandomEvenPort(t *testing.T, _ net.PacketConn) { t.Helper() m, err := newTestManager() assert.NoError(t, err) port, err := m.GetRandomEvenPort() assert.NoError(t, err) assert.True(t, port > 0) assert.True(t, port%2 == 0) } turn-4.1.3/internal/allocation/allocation_test.go000066400000000000000000000231031510560755200221700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package allocation import ( "fmt" "net" "sync" "testing" "time" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/ipnet" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) func TestAllocation(t *testing.T) { tt := []struct { name string f func(*testing.T) }{ {"GetPermission", subTestGetPermission}, {"AddPermission", subTestAddPermission}, {"RemovePermission", subTestRemovePermission}, {"AddChannelBind", subTestAddChannelBind}, {"GetChannelByNumber", subTestGetChannelByNumber}, {"GetChannelByAddr", subTestGetChannelByAddr}, {"RemoveChannelBind", subTestRemoveChannelBind}, {"Refresh", subTestAllocationRefresh}, {"Close", subTestAllocationClose}, {"packetHandler", subTestPacketHandler}, {"ResponseCache", subTestResponseCache}, } for _, tc := range tt { f := tc.f t.Run(tc.name, func(t *testing.T) { f(t) }) } } func subTestGetPermission(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) addr2, err := net.ResolveUDPAddr("udp", "127.0.0.1:3479") assert.NoError(t, err) addr3, err := net.ResolveUDPAddr("udp", "127.0.0.2:3478") assert.NoError(t, err) perms := &Permission{ Addr: addr, } perms2 := &Permission{ Addr: addr2, } perms3 := &Permission{ Addr: addr3, } alloc.AddPermission(perms) alloc.AddPermission(perms2) alloc.AddPermission(perms3) foundP1 := alloc.GetPermission(addr) assert.Equal(t, perms, foundP1, "Should keep the first one.") foundP2 := alloc.GetPermission(addr2) assert.Equal(t, perms, foundP2, "Second one should be ignored.") foundP3 := alloc.GetPermission(addr3) assert.Equal(t, perms3, foundP3, "Permission with another IP should be found") } func subTestAddPermission(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) p := &Permission{ Addr: addr, } alloc.AddPermission(p) assert.Equal(t, alloc, p.allocation, "Permission's allocation should be the adder.") foundPermission := alloc.GetPermission(p.Addr) assert.Equal(t, p, foundPermission) } func subTestRemovePermission(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) p := &Permission{ Addr: addr, } alloc.AddPermission(p) foundPermission := alloc.GetPermission(p.Addr) assert.Equal(t, p, foundPermission, "Got permission is not same as the the added.") alloc.RemovePermission(p.Addr) foundPermission = alloc.GetPermission(p.Addr) assert.Nil(t, foundPermission, "Got permission should be nil after removed.") } func subTestAddChannelBind(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) c := NewChannelBind(proto.MinChannelNumber, addr, nil) err = alloc.AddChannelBind(c, proto.DefaultLifetime) assert.Nil(t, err, "should succeed") assert.Equal(t, alloc, c.allocation, "allocation should be the caller.") c2 := NewChannelBind(proto.MinChannelNumber+1, addr, nil) err = alloc.AddChannelBind(c2, proto.DefaultLifetime) assert.NotNil(t, err, "should failed with conflicted peer address") addr2, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3479") c3 := NewChannelBind(proto.MinChannelNumber, addr2, nil) err = alloc.AddChannelBind(c3, proto.DefaultLifetime) assert.NotNil(t, err, "should fail with conflicted number.") } func subTestGetChannelByNumber(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) c := NewChannelBind(proto.MinChannelNumber, addr, nil) _ = alloc.AddChannelBind(c, proto.DefaultLifetime) existChannel := alloc.GetChannelByNumber(c.Number) assert.Equal(t, c, existChannel) notExistChannel := alloc.GetChannelByNumber(proto.MinChannelNumber + 1) assert.Nil(t, notExistChannel, "should be nil for not existed channel.") } func subTestGetChannelByAddr(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) c := NewChannelBind(proto.MinChannelNumber, addr, nil) _ = alloc.AddChannelBind(c, proto.DefaultLifetime) existChannel := alloc.GetChannelByAddr(c.Peer) assert.Equal(t, c, existChannel) addr2, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3479") notExistChannel := alloc.GetChannelByAddr(addr2) assert.Nil(t, notExistChannel, "should be nil for not existed channel.") } func subTestRemoveChannelBind(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478") assert.NoError(t, err) c := NewChannelBind(proto.MinChannelNumber, addr, nil) _ = alloc.AddChannelBind(c, proto.DefaultLifetime) alloc.RemoveChannelBind(c.Number) channelByNumber := alloc.GetChannelByNumber(c.Number) assert.Nil(t, channelByNumber) channelByAddr := alloc.GetChannelByAddr(c.Peer) assert.Nil(t, channelByAddr) } func subTestAllocationRefresh(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) var wg sync.WaitGroup wg.Add(1) alloc.lifetimeTimer = time.AfterFunc(proto.DefaultLifetime, func() { wg.Done() }) alloc.Refresh(0) wg.Wait() // LifetimeTimer has expired assert.False(t, alloc.lifetimeTimer.Stop()) } func subTestAllocationClose(t *testing.T) { t.Helper() network := "udp" l, err := net.ListenPacket(network, "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) alloc := NewAllocation(nil, nil, EventHandler{}, nil) alloc.RelaySocket = l // Add mock lifetimeTimer alloc.lifetimeTimer = time.AfterFunc(proto.DefaultLifetime, func() {}) // Add channel addr, err := net.ResolveUDPAddr(network, "127.0.0.1:3478") assert.NoError(t, err) c := NewChannelBind(proto.MinChannelNumber, addr, nil) _ = alloc.AddChannelBind(c, proto.DefaultLifetime) // Add permission alloc.AddPermission(NewPermission(addr, nil)) assert.Nil(t, alloc.Close(), "should succeed") assert.True(t, isClose(alloc.RelaySocket), "should be closed") } func subTestPacketHandler(t *testing.T) { t.Helper() network := "udp" manager, _ := newTestManager() // TURN server initialization turnSocket, err := net.ListenPacket(network, "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) // Client listener initialization clientListener, err := net.ListenPacket(network, "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) dataCh := make(chan []byte) // Client listener read data go func() { buffer := make([]byte, rtpMTU) for { n, _, err2 := clientListener.ReadFrom(buffer) if err2 != nil { return } dataCh <- buffer[:n] } }() alloc, err := manager.CreateAllocation(&FiveTuple{ SrcAddr: clientListener.LocalAddr(), DstAddr: turnSocket.LocalAddr(), }, turnSocket, 0, proto.DefaultLifetime, "", "") assert.NoError(t, err, "should succeed") peerListener1, err := net.ListenPacket(network, "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) peerListener2, err := net.ListenPacket(network, "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) // Add permission with peer1 address alloc.AddPermission(NewPermission(peerListener1.LocalAddr(), manager.log)) // Add channel with min channel number and peer2 address channelBind := NewChannelBind(proto.MinChannelNumber, peerListener2.LocalAddr(), manager.log) _ = alloc.AddChannelBind(channelBind, proto.DefaultLifetime) _, port, _ := ipnet.AddrIPPort(alloc.RelaySocket.LocalAddr()) relayAddrWithHostStr := fmt.Sprintf("127.0.0.1:%d", port) relayAddrWithHost, _ := net.ResolveUDPAddr(network, relayAddrWithHostStr) // Test for permission and data message targetText := "permission" _, _ = peerListener1.WriteTo([]byte(targetText), relayAddrWithHost) data := <-dataCh // Resolve stun data message assert.True(t, stun.IsMessage(data), "should be stun message") var msg stun.Message assert.NoError(t, stun.Decode(data, &msg), "decode data to stun message failed") var msgData proto.Data assert.NoError(t, msgData.GetFrom(&msg), "get data from stun message failed") assert.Equal(t, targetText, string(msgData), "get message doesn't equal the target text") // Test for channel bind and channel data targetText2 := "channel bind" _, _ = peerListener2.WriteTo([]byte(targetText2), relayAddrWithHost) data = <-dataCh // Resolve channel data assert.True(t, proto.IsChannelData(data), "should be channel data") channelData := proto.ChannelData{ Raw: data, } assert.NoError(t, channelData.Decode(), fmt.Sprintf("channel data decode with error: %v", err)) assert.Equal(t, channelBind.Number, channelData.Number, "get channel data's number is invalid") assert.Equal(t, targetText2, string(channelData.Data), "get data doesn't equal the target text.") // Listeners close _ = manager.Close() _ = clientListener.Close() _ = peerListener1.Close() _ = peerListener2.Close() } func subTestResponseCache(t *testing.T) { t.Helper() alloc := NewAllocation(nil, nil, EventHandler{}, nil) transactionID := [stun.TransactionIDSize]byte{1, 2, 3} responseAttrs := []stun.Setter{ &proto.Lifetime{ Duration: proto.DefaultLifetime, }, } alloc.SetResponseCache(transactionID, responseAttrs) cacheID, cacheAttr := alloc.GetResponseCache() assert.Equal(t, transactionID, cacheID) assert.Equal(t, responseAttrs, cacheAttr) } turn-4.1.3/internal/allocation/channel_bind.go000066400000000000000000000022421510560755200214110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" "time" "github.com/pion/logging" "github.com/pion/turn/v4/internal/proto" ) // ChannelBind represents a TURN Channel // See: https://tools.ietf.org/html/rfc5766#section-2.5 type ChannelBind struct { Peer net.Addr Number proto.ChannelNumber allocation *Allocation lifetimeTimer *time.Timer log logging.LeveledLogger } // NewChannelBind creates a new ChannelBind. func NewChannelBind(number proto.ChannelNumber, peer net.Addr, log logging.LeveledLogger) *ChannelBind { return &ChannelBind{ Number: number, Peer: peer, log: log, } } func (c *ChannelBind) start(lifetime time.Duration) { c.lifetimeTimer = time.AfterFunc(lifetime, func() { if !c.allocation.RemoveChannelBind(c.Number) { c.log.Errorf("Failed to remove ChannelBind for %v %x %v", c.Number, c.Peer, c.allocation.fiveTuple) } }) } func (c *ChannelBind) refresh(lifetime time.Duration) { if !c.lifetimeTimer.Reset(lifetime) { c.log.Errorf("Failed to reset ChannelBind timer for %v %x %v", c.Number, c.Peer, c.allocation.fiveTuple) } } turn-4.1.3/internal/allocation/channel_bind_test.go000066400000000000000000000024051510560755200224510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" "testing" "time" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) func TestChannelBind(t *testing.T) { c := newChannelBind(2 * time.Second) assert.Equalf(t, c, c.allocation.GetChannelByNumber(c.Number), "GetChannelByNumber(%d) shouldn't be nil after added to allocation", c.Number) } func TestChannelBindStart(t *testing.T) { c := newChannelBind(2 * time.Second) time.Sleep(3 * time.Second) assert.Nil(t, c.allocation.GetChannelByNumber(c.Number), "GetChannelByNumber(%d) should be nil after timeout", c.Number) } func TestChannelBindReset(t *testing.T) { c := newChannelBind(3 * time.Second) time.Sleep(2 * time.Second) c.refresh(3 * time.Second) time.Sleep(2 * time.Second) assert.NotNil(t, c.allocation.GetChannelByNumber(c.Number), "GetChannelByNumber(%d) shouldn't be nil after refresh", c.Number) } func newChannelBind(lifetime time.Duration) *ChannelBind { a := NewAllocation(nil, nil, EventHandler{}, nil) addr, _ := net.ResolveUDPAddr("udp", "0.0.0.0:0") c := &ChannelBind{ Number: proto.MinChannelNumber, Peer: addr, } _ = a.AddChannelBind(c, lifetime) return c } turn-4.1.3/internal/allocation/errors.go000066400000000000000000000025141510560755200203230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import "errors" var ( errAllocatePacketConnMustBeSet = errors.New("AllocatePacketConn must be set") errAllocateConnMustBeSet = errors.New("AllocateConn must be set") errLeveledLoggerMustBeSet = errors.New("LeveledLogger must be set") errSameChannelDifferentPeer = errors.New("you cannot use the same channel number with different peer") errNilFiveTuple = errors.New("allocations must not be created with nil FivTuple") errNilFiveTupleSrcAddr = errors.New("allocations must not be created with nil FiveTuple.SrcAddr") errNilFiveTupleDstAddr = errors.New("allocations must not be created with nil FiveTuple.DstAddr") errNilTurnSocket = errors.New("allocations must not be created with nil turnSocket") errLifetimeZero = errors.New("allocations must not be created with a lifetime of 0") errDupeFiveTuple = errors.New("allocation attempt created with duplicate FiveTuple") errFailedToCastUDPAddr = errors.New("failed to cast net.Addr to *net.UDPAddr") errFailedToAllocateEvenPort = errors.New("failed to allocate an even port") errAdminProhibited = errors.New("permission request administratively prohibited") ) turn-4.1.3/internal/allocation/event_handler.go000066400000000000000000000052401510560755200216240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" ) // EventHandler is a set of callbacks that the server will call at certain hook points during an // allocation's lifecycle. All events are reported with the context that identifies the allocation // triggering the event (source and destination address, protocol, username and realm used for // authenticating the allocation), plus additional callback specific parameters. It is OK to handle // only a subset of the callbacks. type EventHandler struct { // OnAuth is called after an authentication request has been processed with the TURN method // triggering the authentication request (either "Allocate", "Refresh" "CreatePermission", // or "ChannelBind"), and the verdict is the authentication result. OnAuth func(srcAddr, dstAddr net.Addr, protocol, username, realm string, method string, verdict bool) // OnAllocationCreated is called after a new allocation has been made. The relayAddr // argument specifies the relay address and requestedPort is the port requested by the // client (if any). OnAllocationCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, requestedPort int) // OnAllocationDeleted is called after an allocation has been removed. OnAllocationDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string) // OnAllocationError is called when the readloop hdndling an allocation exits with an // error with an error message. OnAllocationError func(srcAddr, dstAddr net.Addr, protocol, message string) // OnPermissionCreated is called after a new permission has been made to an IP address. OnPermissionCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, peer net.IP) // OnPermissionDeleted is called after a permission for a given IP address has been // removed. OnPermissionDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, peer net.IP) // OnChannelCreated is called after a new channel has been made. The relay address, the // peer address and the channel number can be used to uniquely identify the channel // created. OnChannelCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr, peer net.Addr, channelNumber uint16) // OnChannelDeleted is called after a channel has been removed from the server. The relay // address, the peer address and the channel number can be used to uniquely identify the // channel deleted. OnChannelDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr, peer net.Addr, channelNumber uint16) } turn-4.1.3/internal/allocation/five_tuple.go000066400000000000000000000033311510560755200211470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" ) // Protocol is an enum for relay protocol. type Protocol uint8 // Network protocols for relay. const ( UDP Protocol = iota TCP ) func (p Protocol) String() string { switch p { case UDP: return "UDP" case TCP: return "TCP" default: return "" } } // FiveTuple is the combination (client IP address and port, server IP // address and port, and transport protocol (currently one of UDP, // TCP, or TLS)) used to communicate between the client and the // server. The 5-tuple uniquely identifies this communication // stream. The 5-tuple also uniquely identifies the Allocation on // the server. type FiveTuple struct { Protocol SrcAddr, DstAddr net.Addr } // Equal asserts if two FiveTuples are equal. func (f *FiveTuple) Equal(b *FiveTuple) bool { return f.Fingerprint() == b.Fingerprint() } // FiveTupleFingerprint is a comparable representation of a FiveTuple. type FiveTupleFingerprint struct { srcIP, dstIP [16]byte srcPort, dstPort uint16 protocol Protocol } // Fingerprint is the identity of a FiveTuple. func (f *FiveTuple) Fingerprint() (fp FiveTupleFingerprint) { srcIP, srcPort := netAddrIPAndPort(f.SrcAddr) copy(fp.srcIP[:], srcIP) fp.srcPort = srcPort dstIP, dstPort := netAddrIPAndPort(f.DstAddr) copy(fp.dstIP[:], dstIP) fp.dstPort = dstPort fp.protocol = f.Protocol return } func netAddrIPAndPort(addr net.Addr) (net.IP, uint16) { switch a := addr.(type) { case *net.UDPAddr: return a.IP.To16(), uint16(a.Port) // nolint:gosec // G115 case *net.TCPAddr: return a.IP.To16(), uint16(a.Port) // nolint:gosec // G115 default: return nil, 0 } } turn-4.1.3/internal/allocation/five_tuple_test.go000066400000000000000000000026411510560755200222110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" "testing" "github.com/stretchr/testify/assert" ) func TestFiveTupleProtocol(t *testing.T) { udpExpect := Protocol(0) tcpExpect := Protocol(1) assert.Equal(t, UDP, udpExpect) assert.Equal(t, TCP, tcpExpect) } func TestFiveTupleEqual(t *testing.T) { srcAddr1, _ := net.ResolveUDPAddr("udp", "0.0.0.0:3478") srcAddr2, _ := net.ResolveUDPAddr("udp", "0.0.0.0:3479") dstAddr1, _ := net.ResolveUDPAddr("udp", "0.0.0.0:3480") dstAddr2, _ := net.ResolveUDPAddr("udp", "0.0.0.0:3481") tt := []struct { name string expect bool a *FiveTuple b *FiveTuple }{ { "Equal", true, &FiveTuple{UDP, srcAddr1, dstAddr1}, &FiveTuple{UDP, srcAddr1, dstAddr1}, }, { "DifferentProtocol", false, &FiveTuple{UDP, srcAddr1, dstAddr1}, &FiveTuple{TCP, srcAddr1, dstAddr1}, }, { "DifferentSrcAddr", false, &FiveTuple{UDP, srcAddr1, dstAddr1}, &FiveTuple{UDP, srcAddr2, dstAddr1}, }, { "DifferentDstAddr", false, &FiveTuple{UDP, srcAddr1, dstAddr1}, &FiveTuple{UDP, srcAddr1, dstAddr2}, }, } for _, tc := range tt { a := tc.a b := tc.b expect := tc.expect t.Run(tc.name, func(t *testing.T) { fact := a.Equal(b) assert.Equalf(t, expect, fact, "%v, %v equal check should be %t, but %t", a, b, expect, fact) }) } } turn-4.1.3/internal/allocation/permission.go000066400000000000000000000021131510560755200211720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package allocation import ( "net" "time" "github.com/pion/logging" ) const permissionTimeout = time.Duration(5) * time.Minute // Permission represents a TURN permission. TURN permissions mimic the address-restricted // filtering mechanism of NATs that comply with [RFC4787]. // See: https://tools.ietf.org/html/rfc5766#section-2.3 type Permission struct { Addr net.Addr allocation *Allocation lifetimeTimer *time.Timer log logging.LeveledLogger } // NewPermission create a new Permission. func NewPermission(addr net.Addr, log logging.LeveledLogger) *Permission { return &Permission{ Addr: addr, log: log, } } func (p *Permission) start(lifetime time.Duration) { p.lifetimeTimer = time.AfterFunc(lifetime, func() { p.allocation.RemovePermission(p.Addr) }) } func (p *Permission) refresh(lifetime time.Duration) { if !p.lifetimeTimer.Reset(lifetime) { p.log.Errorf("Failed to reset permission timer for %v %v", p.Addr, p.allocation.fiveTuple) } } turn-4.1.3/internal/client/000077500000000000000000000000001510560755200156075ustar00rootroot00000000000000turn-4.1.3/internal/client/allocation.go000066400000000000000000000114741510560755200202720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "errors" "fmt" "net" "sync" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/turn/v4/internal/proto" ) // AllocationConfig is a set of configuration params use by NewUDPConn and NewTCPAllocation. type AllocationConfig struct { Client Client RelayedAddr net.Addr ServerAddr net.Addr Integrity stun.MessageIntegrity Nonce stun.Nonce Username stun.Username Realm stun.Realm Lifetime time.Duration Net transport.Net Log logging.LeveledLogger } type allocation struct { client Client // Read-only relayedAddr net.Addr // Read-only serverAddr net.Addr // Read-only permMap *permissionMap // Thread-safe integrity stun.MessageIntegrity // Read-only username stun.Username // Read-only realm stun.Realm // Read-only _nonce stun.Nonce // Needs mutex x _lifetime time.Duration // Needs mutex x net transport.Net // Thread-safe refreshAllocTimer *PeriodicTimer // Thread-safe refreshPermsTimer *PeriodicTimer // Thread-safe readTimer *time.Timer // Thread-safe mutex sync.RWMutex // Thread-safe log logging.LeveledLogger // Read-only } func (a *allocation) setNonceFromMsg(msg *stun.Message) { // Update nonce var nonce stun.Nonce if err := nonce.GetFrom(msg); err == nil { a.setNonce(nonce) a.log.Debug("Refresh allocation: 438, got new nonce.") } else { a.log.Warn("Refresh allocation: 438 but no nonce.") } } func (a *allocation) refreshAllocation(lifetime time.Duration, dontWait bool) error { msg, err := stun.Build( stun.TransactionID, stun.NewType(stun.MethodRefresh, stun.ClassRequest), proto.Lifetime{Duration: lifetime}, a.username, a.realm, a.nonce(), a.integrity, stun.Fingerprint, ) if err != nil { return fmt.Errorf("%w: %s", errFailedToBuildRefreshRequest, err.Error()) } a.log.Debugf("Send refresh request (dontWait=%v)", dontWait) trRes, err := a.client.PerformTransaction(msg, a.serverAddr, dontWait) if err != nil { return fmt.Errorf("%w: %s", errFailedToRefreshAllocation, err.Error()) } if dontWait { a.log.Debug("Refresh request sent") return nil } a.log.Debug("Refresh request sent, and waiting response") res := trRes.Msg if res.Type.Class == stun.ClassErrorResponse { var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { if code.Code == stun.CodeStaleNonce { a.setNonceFromMsg(res) return errTryAgain } return err } return fmt.Errorf("%s", res.Type) //nolint:err113 } // Getting lifetime from response var updatedLifetime proto.Lifetime if err := updatedLifetime.GetFrom(res); err != nil { return fmt.Errorf("%w: %s", errFailedToGetLifetime, err.Error()) } a.setLifetime(updatedLifetime.Duration) a.log.Debugf("Updated lifetime: %d seconds", int(a.lifetime().Seconds())) return nil } func (a *allocation) refreshPermissions() error { addrs := a.permMap.addrs() if len(addrs) == 0 { a.log.Debug("No permission to refresh") return nil } if err := a.CreatePermissions(addrs...); err != nil { if errors.Is(err, errTryAgain) { return errTryAgain } a.log.Errorf("Fail to refresh permissions: %s", err) return err } a.log.Debug("Refresh permissions successful") return nil } func (a *allocation) onRefreshTimers(id int) { a.log.Debugf("Refresh timer %d expired", id) switch id { case timerIDRefreshAlloc: var err error lifetime := a.lifetime() // Limit the max retries on errTryAgain to 3 // when stale nonce returns, sencond retry should succeed for i := 0; i < maxRetryAttempts; i++ { err = a.refreshAllocation(lifetime, false) if !errors.Is(err, errTryAgain) { break } } if err != nil { a.log.Warnf("Failed to refresh allocation: %s", err) } case timerIDRefreshPerms: var err error for i := 0; i < maxRetryAttempts; i++ { err = a.refreshPermissions() if !errors.Is(err, errTryAgain) { break } } if err != nil { a.log.Warnf("Failed to refresh permissions: %s", err) } } } func (a *allocation) nonce() stun.Nonce { a.mutex.RLock() defer a.mutex.RUnlock() return a._nonce } func (a *allocation) setNonce(nonce stun.Nonce) { a.mutex.Lock() defer a.mutex.Unlock() a.log.Debugf("Set new nonce with %d bytes", len(nonce)) a._nonce = nonce } func (a *allocation) lifetime() time.Duration { a.mutex.RLock() defer a.mutex.RUnlock() return a._lifetime } func (a *allocation) setLifetime(lifetime time.Duration) { a.mutex.Lock() defer a.mutex.Unlock() a._lifetime = lifetime } turn-4.1.3/internal/client/binding.go000066400000000000000000000065731510560755200175630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "sync" "sync/atomic" "time" ) // Channel number: // // 0x4000 through 0x7FFF: These values are the allowed channel // numbers (16,383 possible values). const ( minChannelNumber uint16 = 0x4000 maxChannelNumber uint16 = 0x7fff ) type bindingState int32 const ( bindingStateIdle bindingState = iota bindingStateRequest bindingStateReady bindingStateRefresh bindingStateFailed ) type binding struct { number uint16 // Read-only st bindingState // Thread-safe (atomic op) addr net.Addr // Read-only mgr *bindingManager // Read-only muBind sync.Mutex // Thread-safe, for ChannelBind ops _refreshedAt time.Time // Protected by mutex mutex sync.RWMutex // Thread-safe } func (b *binding) setState(state bindingState) { atomic.StoreInt32((*int32)(&b.st), int32(state)) } func (b *binding) state() bindingState { return bindingState(atomic.LoadInt32((*int32)(&b.st))) } func (b *binding) setRefreshedAt(at time.Time) { b.mutex.Lock() defer b.mutex.Unlock() b._refreshedAt = at } func (b *binding) refreshedAt() time.Time { b.mutex.RLock() defer b.mutex.RUnlock() return b._refreshedAt } func (b *binding) ok() bool { state := b.state() return state == bindingStateReady || state == bindingStateRefresh } // Thread-safe binding map. type bindingManager struct { chanMap map[uint16]*binding addrMap map[string]*binding next uint16 mutex sync.RWMutex } func newBindingManager() *bindingManager { return &bindingManager{ chanMap: map[uint16]*binding{}, addrMap: map[string]*binding{}, next: minChannelNumber, } } func (mgr *bindingManager) assignChannelNumber() uint16 { n := mgr.next if mgr.next == maxChannelNumber { mgr.next = minChannelNumber } else { mgr.next++ } return n } func (mgr *bindingManager) create(addr net.Addr) *binding { mgr.mutex.Lock() defer mgr.mutex.Unlock() b := &binding{ number: mgr.assignChannelNumber(), addr: addr, mgr: mgr, _refreshedAt: time.Now(), } mgr.chanMap[b.number] = b mgr.addrMap[b.addr.String()] = b return b } func (mgr *bindingManager) findByAddr(addr net.Addr) (*binding, bool) { mgr.mutex.RLock() defer mgr.mutex.RUnlock() b, ok := mgr.addrMap[addr.String()] return b, ok } func (mgr *bindingManager) findByNumber(number uint16) (*binding, bool) { mgr.mutex.RLock() defer mgr.mutex.RUnlock() b, ok := mgr.chanMap[number] return b, ok } func (mgr *bindingManager) deleteByAddr(addr net.Addr) bool { mgr.mutex.Lock() defer mgr.mutex.Unlock() b, ok := mgr.addrMap[addr.String()] if !ok { return false } delete(mgr.addrMap, addr.String()) delete(mgr.chanMap, b.number) return true } func (mgr *bindingManager) deleteByNumber(number uint16) bool { mgr.mutex.Lock() defer mgr.mutex.Unlock() b, ok := mgr.chanMap[number] if !ok { return false } delete(mgr.addrMap, b.addr.String()) delete(mgr.chanMap, number) return true } func (mgr *bindingManager) size() int { mgr.mutex.RLock() defer mgr.mutex.RUnlock() return len(mgr.chanMap) } func (mgr *bindingManager) all() []*binding { mgr.mutex.RLock() defer mgr.mutex.RUnlock() list := make([]*binding, 0, len(mgr.chanMap)) for _, b := range mgr.chanMap { list = append(list, b) } return list } turn-4.1.3/internal/client/binding_test.go000066400000000000000000000046101510560755200206100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "testing" "github.com/stretchr/testify/assert" ) func TestBindingManager(t *testing.T) { t.Run("number assignment", func(t *testing.T) { bm := newBindingManager() var chanNum uint16 for i := uint16(0); i < 10; i++ { chanNum = bm.assignChannelNumber() assert.Equal(t, minChannelNumber+i, chanNum, "should match") } bm.next = uint16(0x7ff0) for i := uint16(0); i < 16; i++ { chanNum = bm.assignChannelNumber() assert.Equal(t, 0x7ff0+i, chanNum, "should match") } // Back to min chanNum = bm.assignChannelNumber() assert.Equal(t, minChannelNumber, chanNum, "should match") }) t.Run("method test", func(t *testing.T) { lo := net.IPv4(127, 0, 0, 1) count := 100 bm := newBindingManager() for i := 0; i < count; i++ { addr := &net.UDPAddr{IP: lo, Port: 10000 + i} b0 := bm.create(addr) b1, ok := bm.findByAddr(addr) assert.True(t, ok, "should succeed") b2, ok := bm.findByNumber(b0.number) assert.True(t, ok, "should succeed") assert.Equal(t, b0, b1, "should match") assert.Equal(t, b0, b2, "should match") } all := bm.all() for _, b := range all { found, ok := bm.findByNumber(b.number) assert.True(t, ok, "should exist") assert.Equal(t, b, found, "should match") } assert.Equal(t, count, len(all), "should match") assert.Equal(t, count, bm.size(), "should match") assert.Equal(t, count, len(bm.addrMap), "should match") for i := 0; i < count; i++ { addr := &net.UDPAddr{IP: lo, Port: 10000 + i} if i%2 == 0 { assert.True(t, bm.deleteByAddr(addr), "should return true") } else { assert.True(t, bm.deleteByNumber(minChannelNumber+uint16(i)), "should return true") // nolint:gosec // G115 } } assert.Equal(t, 0, bm.size(), "should match") assert.Equal(t, 0, len(bm.addrMap), "should match") assert.Equal(t, 0, len(bm.all()), "should match") }) t.Run("failure test", func(t *testing.T) { addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 7777} m := newBindingManager() var ok bool _, ok = m.findByAddr(addr) assert.False(t, ok, "should fail") _, ok = m.findByNumber(uint16(5555)) assert.False(t, ok, "should fail") ok = m.deleteByAddr(addr) assert.False(t, ok, "should fail") ok = m.deleteByNumber(uint16(5555)) assert.False(t, ok, "should fail") }) } turn-4.1.3/internal/client/client.go000066400000000000000000000010051510560755200174100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package client implements the API for a TURN client package client import ( "net" "github.com/pion/stun/v3" ) // Client is an interface for the public turn.Client in order to break cyclic dependencies. type Client interface { WriteTo(data []byte, to net.Addr) (int, error) PerformTransaction(msg *stun.Message, to net.Addr, dontWait bool) (TransactionResult, error) OnDeallocated(relayedAddr net.Addr) } turn-4.1.3/internal/client/client_test.go000066400000000000000000000016221510560755200204540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "github.com/pion/stun/v3" ) type mockClient struct { writeTo func(data []byte, to net.Addr) (int, error) performTransaction func(msg *stun.Message, to net.Addr, dontWait bool) (TransactionResult, error) onDeallocated func(relayedAddr net.Addr) } func (c *mockClient) WriteTo(data []byte, to net.Addr) (int, error) { if c.writeTo != nil { return c.writeTo(data, to) } return 0, nil } func (c *mockClient) PerformTransaction(msg *stun.Message, to net.Addr, dontWait bool) (TransactionResult, error) { if c.performTransaction != nil { return c.performTransaction(msg, to, dontWait) } return TransactionResult{}, errFake } func (c *mockClient) OnDeallocated(relayedAddr net.Addr) { if c.onDeallocated != nil { c.onDeallocated(relayedAddr) } } turn-4.1.3/internal/client/errors.go000066400000000000000000000030151510560755200174510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "errors" ) var ( errFake = errors.New("fake error") errTryAgain = errors.New("try again") errClosed = errors.New("use of closed network connection") errTCPAddrCast = errors.New("addr is not a TCP address") errUDPAddrCast = errors.New("addr is not a UDP address") errAlreadyClosed = errors.New("already closed") errDoubleLock = errors.New("try-lock is already locked") errTransactionClosed = errors.New("transaction closed") errWaitForResultOnNonResultTransaction = errors.New("WaitForResult called on non-result transaction") errFailedToBuildRefreshRequest = errors.New("failed to build refresh request") errFailedToRefreshAllocation = errors.New("failed to refresh allocation") errFailedToGetLifetime = errors.New("failed to get lifetime from refresh response") errInvalidTURNAddress = errors.New("invalid TURN server address") errUnexpectedSTUNRequestMessage = errors.New("unexpected STUN request message") ) type timeoutError struct { msg string } func newTimeoutError(msg string) error { return &timeoutError{ msg: msg, } } func (e *timeoutError) Error() string { return e.msg } func (e *timeoutError) Timeout() bool { return true } turn-4.1.3/internal/client/periodic_timer.go000066400000000000000000000031121510560755200211310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "sync" "time" ) // PeriodicTimerTimeoutHandler is a handler called on timeout. type PeriodicTimerTimeoutHandler func(timerID int) // PeriodicTimer is a periodic timer. type PeriodicTimer struct { id int interval time.Duration timeoutHandler PeriodicTimerTimeoutHandler stopFunc func() mutex sync.RWMutex } // NewPeriodicTimer create a new timer. func NewPeriodicTimer(id int, timeoutHandler PeriodicTimerTimeoutHandler, interval time.Duration) *PeriodicTimer { return &PeriodicTimer{ id: id, interval: interval, timeoutHandler: timeoutHandler, } } // Start starts the timer. func (t *PeriodicTimer) Start() bool { t.mutex.Lock() defer t.mutex.Unlock() // This is a noop if the timer is always running if t.stopFunc != nil { return false } cancelCh := make(chan struct{}) go func() { canceling := false for !canceling { timer := time.NewTimer(t.interval) select { case <-timer.C: t.timeoutHandler(t.id) case <-cancelCh: canceling = true timer.Stop() } } }() t.stopFunc = func() { close(cancelCh) } return true } // Stop stops the timer. func (t *PeriodicTimer) Stop() { t.mutex.Lock() defer t.mutex.Unlock() if t.stopFunc != nil { t.stopFunc() t.stopFunc = nil } } // IsRunning tests if the timer is running. // Debug purpose only. func (t *PeriodicTimer) IsRunning() bool { t.mutex.RLock() defer t.mutex.RUnlock() return (t.stopFunc != nil) } turn-4.1.3/internal/client/periodic_timer_test.go000066400000000000000000000026771510560755200222070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" ) func TestPeriodicTimer(t *testing.T) { t.Run("basic", func(t *testing.T) { timerID := 3 var nCbs uint64 rt := NewPeriodicTimer(timerID, func(id int) { atomic.AddUint64(&nCbs, 1) assert.Equal(t, timerID, id) }, 50*time.Millisecond) assert.False(t, rt.IsRunning(), "should not be running yet") ok := rt.Start() assert.True(t, ok, "should be true") assert.True(t, rt.IsRunning(), "should be running") time.Sleep(100 * time.Millisecond) ok = rt.Start() assert.False(t, ok, "start again is noop") time.Sleep(120 * time.Millisecond) rt.Stop() assert.False(t, rt.IsRunning(), "should not be running") assert.Equal( t, uint64(4), atomic.LoadUint64(&nCbs), "should be called 4 times (actual: %d)", atomic.LoadUint64(&nCbs), ) }) t.Run("stop inside handler", func(t *testing.T) { timerID := 4 var rt *PeriodicTimer rt = NewPeriodicTimer(timerID, func(id int) { assert.Equal(t, timerID, id) rt.Stop() }, 20*time.Millisecond) assert.False(t, rt.IsRunning(), "should not be running yet") ok := rt.Start() assert.True(t, ok, "should be true") assert.True(t, rt.IsRunning(), "should be running") time.Sleep(30 * time.Millisecond) assert.False(t, rt.IsRunning(), "should not be running") }) } turn-4.1.3/internal/client/permission.go000066400000000000000000000027711510560755200203350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "sync" "sync/atomic" "github.com/pion/turn/v4/internal/ipnet" ) type permState int32 const ( permStateIdle permState = iota permStatePermitted ) type permission struct { addr net.Addr st permState // Thread-safe (atomic op) mutex sync.RWMutex // Thread-safe } func (p *permission) setState(state permState) { atomic.StoreInt32((*int32)(&p.st), int32(state)) } func (p *permission) state() permState { return permState(atomic.LoadInt32((*int32)(&p.st))) } // Thread-safe permission map. type permissionMap struct { permMap map[string]*permission mutex sync.RWMutex } func (m *permissionMap) insert(addr net.Addr, p *permission) bool { m.mutex.Lock() defer m.mutex.Unlock() p.addr = addr m.permMap[ipnet.FingerprintAddr(addr)] = p return true } func (m *permissionMap) find(addr net.Addr) (*permission, bool) { m.mutex.RLock() defer m.mutex.RUnlock() p, ok := m.permMap[ipnet.FingerprintAddr(addr)] return p, ok } func (m *permissionMap) delete(addr net.Addr) { m.mutex.Lock() defer m.mutex.Unlock() delete(m.permMap, ipnet.FingerprintAddr(addr)) } func (m *permissionMap) addrs() []net.Addr { m.mutex.RLock() defer m.mutex.RUnlock() addrs := []net.Addr{} for _, p := range m.permMap { addrs = append(addrs, p.addr) } return addrs } func newPermissionMap() *permissionMap { return &permissionMap{ permMap: map[string]*permission{}, } } turn-4.1.3/internal/client/permission_test.go000066400000000000000000000046061510560755200213730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "sort" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestPermission(t *testing.T) { t.Run("Getter and setter", func(t *testing.T) { perm := &permission{} assert.Equal(t, permStateIdle, perm.state()) perm.setState(permStatePermitted) assert.Equal(t, permStatePermitted, perm.state()) }) } func TestPermissionMap(t *testing.T) { t.Run("Basic operations", func(t *testing.T) { pm := newPermissionMap() assert.NotNil(t, pm) assert.NotNil(t, pm.permMap) perm1 := &permission{st: permStateIdle} perm2 := &permission{st: permStatePermitted} perm3 := &permission{st: permStateIdle} udpAddr1, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5000") udpAddr2, _ := net.ResolveUDPAddr("udp", "5.6.7.8:8888") tcpAddr, _ := net.ResolveTCPAddr("tcp", "7.8.9.10:5000") assert.True(t, pm.insert(udpAddr1, perm1)) assert.Equal(t, 1, len(pm.permMap)) assert.True(t, pm.insert(udpAddr2, perm2)) assert.Equal(t, 2, len(pm.permMap)) assert.True(t, pm.insert(tcpAddr, perm3)) assert.Equal(t, 3, len(pm.permMap)) perms, ok := pm.find(udpAddr1) assert.True(t, ok) assert.Equal(t, perm1, perms) assert.Equal(t, permStateIdle, perms.st) perms, ok = pm.find(udpAddr2) assert.True(t, ok) assert.Equal(t, perm2, perms) assert.Equal(t, permStatePermitted, perms.st) perms, ok = pm.find(tcpAddr) assert.True(t, ok) assert.Equal(t, perm3, perms) assert.Equal(t, permStateIdle, perms.st) addrs := pm.addrs() ips := []net.IP{} for _, addr := range addrs { switch addr.(type) { case *net.UDPAddr: addr, err := net.ResolveUDPAddr(addr.Network(), addr.String()) assert.NoError(t, err) ips = append(ips, addr.IP) case *net.TCPAddr: addr, err := net.ResolveTCPAddr(addr.Network(), addr.String()) assert.NoError(t, err) ips = append(ips, addr.IP) } } assert.Equal(t, 3, len(ips)) sort.Slice(ips, func(i, j int) bool { return strings.Compare(ips[i].String(), ips[j].String()) < 0 }) assert.True(t, ips[0].Equal(udpAddr1.IP)) assert.True(t, ips[1].Equal(udpAddr2.IP)) assert.True(t, ips[2].Equal(tcpAddr.IP)) pm.delete(tcpAddr) assert.Equal(t, 2, len(pm.permMap)) pm.delete(udpAddr1) assert.Equal(t, 1, len(pm.permMap)) pm.delete(udpAddr2) assert.Equal(t, 0, len(pm.permMap)) }) } turn-4.1.3/internal/client/tcp_alloc.go000066400000000000000000000223751510560755200201070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "encoding/binary" "errors" "fmt" "math" "net" "time" "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/turn/v4/internal/proto" ) var ( _ transport.TCPListener = (*TCPAllocation)(nil) // Includes type check for net.Listener _ transport.Dialer = (*TCPAllocation)(nil) ) func noDeadline() time.Time { return time.Time{} } // TCPAllocation is an active TCP allocation on the TURN server // as specified by RFC 6062. // The allocation can be used to Dial/Accept relayed outgoing/incoming TCP connections. type TCPAllocation struct { connAttemptCh chan *connectionAttempt acceptTimer *time.Timer allocation } // NewTCPAllocation creates a new instance of TCPConn. func NewTCPAllocation(config *AllocationConfig) *TCPAllocation { alloc := &TCPAllocation{ connAttemptCh: make(chan *connectionAttempt, 10), acceptTimer: time.NewTimer(time.Duration(math.MaxInt64)), allocation: allocation{ client: config.Client, relayedAddr: config.RelayedAddr, serverAddr: config.ServerAddr, username: config.Username, realm: config.Realm, permMap: newPermissionMap(), integrity: config.Integrity, _nonce: config.Nonce, _lifetime: config.Lifetime, net: config.Net, log: config.Log, }, } alloc.log.Debugf("Initial lifetime: %d seconds", int(alloc.lifetime().Seconds())) alloc.refreshAllocTimer = NewPeriodicTimer( timerIDRefreshAlloc, alloc.onRefreshTimers, alloc.lifetime()/2, ) alloc.refreshPermsTimer = NewPeriodicTimer( timerIDRefreshPerms, alloc.onRefreshTimers, permRefreshInterval, ) if alloc.refreshAllocTimer.Start() { alloc.log.Debug("Started refreshAllocTimer") } if alloc.refreshPermsTimer.Start() { alloc.log.Debug("Started refreshPermsTimer") } return alloc } // Connect sends a Connect request to the turn server and returns a chosen connection ID. func (a *TCPAllocation) Connect(peer net.Addr) (proto.ConnectionID, error) { setters := []stun.Setter{ stun.TransactionID, stun.NewType(stun.MethodConnect, stun.ClassRequest), addr2PeerAddress(peer), a.username, a.realm, a.nonce(), a.integrity, stun.Fingerprint, } msg, err := stun.Build(setters...) if err != nil { return 0, err } a.log.Debugf("Send connect request (peer=%v)", peer) trRes, err := a.client.PerformTransaction(msg, a.serverAddr, false) if err != nil { return 0, err } res := trRes.Msg if res.Type.Class == stun.ClassErrorResponse { var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { return 0, fmt.Errorf("%s (error %s)", res.Type, code) //nolint // dynamic errors } return 0, fmt.Errorf("%s", res.Type) //nolint // dynamic errors } var cid proto.ConnectionID if err := cid.GetFrom(res); err != nil { return 0, err } a.log.Debugf("Connect request successful (cid=%v)", cid) return cid, nil } // Dial connects to the address on the named network. func (a *TCPAllocation) Dial(network, rAddrStr string) (net.Conn, error) { rAddr, err := net.ResolveTCPAddr(network, rAddrStr) if err != nil { return nil, err } return a.DialTCP(network, nil, rAddr) } // DialWithConn connects to the address on the named network with an already existing connection. // The provided connection must be an already connected TCP connection to the TURN server. func (a *TCPAllocation) DialWithConn(conn net.Conn, network, rAddrStr string) (*TCPConn, error) { rAddr, err := net.ResolveTCPAddr(network, rAddrStr) if err != nil { return nil, err } return a.DialTCPWithConn(conn, network, rAddr) } // DialTCP acts like Dial for TCP networks. func (a *TCPAllocation) DialTCP(network string, lAddr, rAddr *net.TCPAddr) (*TCPConn, error) { var rAddrServer *net.TCPAddr if addr, ok := a.serverAddr.(*net.TCPAddr); ok { rAddrServer = &net.TCPAddr{ IP: addr.IP, Port: addr.Port, } } else if addr, ok := a.serverAddr.(*net.UDPAddr); ok { rAddrServer = &net.TCPAddr{ IP: addr.IP, Port: addr.Port, } } else { return nil, errInvalidTURNAddress } conn, err := a.net.DialTCP(network, lAddr, rAddrServer) if err != nil { return nil, err } dataConn, err := a.DialTCPWithConn(conn, network, rAddr) if err != nil { conn.Close() //nolint:errcheck,gosec } return dataConn, err } // DialTCPWithConn acts like DialWithConn for TCP networks. func (a *TCPAllocation) DialTCPWithConn(conn net.Conn, _ string, rAddr *net.TCPAddr) (*TCPConn, error) { var err error // Check if we have a permission for the destination IP addr perm, ok := a.permMap.find(rAddr) if !ok { perm = &permission{} a.permMap.insert(rAddr, perm) } for i := 0; i < maxRetryAttempts; i++ { if err = a.createPermission(perm, rAddr); !errors.Is(err, errTryAgain) { break } } if err != nil { return nil, err } // Send connect request if haven't done so. cid, err := a.Connect(rAddr) if err != nil { return nil, err } tcpConn, ok := conn.(transport.TCPConn) if !ok { return nil, errTCPAddrCast } dataConn := &TCPConn{ TCPConn: tcpConn, ConnectionID: cid, remoteAddress: rAddr, allocation: a, } if err := a.BindConnection(dataConn, cid); err != nil { return nil, fmt.Errorf("failed to bind connection: %w", err) } return dataConn, nil } // BindConnection associates the provided connection. func (a *TCPAllocation) BindConnection(dataConn *TCPConn, cid proto.ConnectionID) error { //nolint:cyclop msg, err := stun.Build( stun.TransactionID, stun.NewType(stun.MethodConnectionBind, stun.ClassRequest), cid, a.username, a.realm, a.nonce(), a.integrity, stun.Fingerprint, ) if err != nil { return err } a.log.Debugf("Send connectionBind request (cid=%v)", cid) _, err = dataConn.Write(msg.Raw) if err != nil { return err } // Read exactly one STUN message, any data after belongs to the user b := make([]byte, stunHeaderSize) n, err := dataConn.Read(b) if n != stunHeaderSize { return errIncompleteTURNFrame } else if err != nil { return err } if !stun.IsMessage(b) { return errInvalidTURNFrame } datagramSize := binary.BigEndian.Uint16(b[2:4]) + stunHeaderSize raw := make([]byte, datagramSize) copy(raw, b) _, err = dataConn.Read(raw[stunHeaderSize:]) if err != nil { return err } res := &stun.Message{Raw: raw} if err = res.Decode(); err != nil { return fmt.Errorf("failed to decode STUN message: %w", err) } switch res.Type.Class { case stun.ClassErrorResponse: var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { return fmt.Errorf("%s (error %s)", res.Type, code) //nolint // dynamic errors } return fmt.Errorf("%s", res.Type) //nolint // dynamic errors case stun.ClassSuccessResponse: a.log.Debug("Successful connectionBind request") return nil default: return fmt.Errorf("%w: %s", errUnexpectedSTUNRequestMessage, res.String()) } } // Accept waits for and returns the next connection to the listener. func (a *TCPAllocation) Accept() (net.Conn, error) { return a.AcceptTCP() } // AcceptTCP accepts the next incoming call and returns the new connection. func (a *TCPAllocation) AcceptTCP() (transport.TCPConn, error) { addr, err := net.ResolveTCPAddr("tcp4", a.serverAddr.String()) if err != nil { return nil, err } tcpConn, err := a.net.DialTCP("tcp", nil, addr) if err != nil { return nil, err } dataConn, err := a.AcceptTCPWithConn(tcpConn) if err != nil { tcpConn.Close() //nolint:errcheck,gosec } return dataConn, err } // AcceptTCPWithConn accepts the next incoming call and returns the new connection. func (a *TCPAllocation) AcceptTCPWithConn(conn net.Conn) (*TCPConn, error) { select { case attempt := <-a.connAttemptCh: tcpConn, ok := conn.(transport.TCPConn) if !ok { return nil, errTCPAddrCast } dataConn := &TCPConn{ TCPConn: tcpConn, ConnectionID: attempt.cid, remoteAddress: attempt.from, allocation: a, } if err := a.BindConnection(dataConn, attempt.cid); err != nil { return nil, fmt.Errorf("failed to bind connection: %w", err) } return dataConn, nil case <-a.acceptTimer.C: return nil, &net.OpError{ Op: "accept", Net: a.Addr().Network(), Addr: a.Addr(), Err: newTimeoutError("i/o timeout"), } } } // SetDeadline sets the deadline associated with the listener. A zero time value disables the deadline. func (a *TCPAllocation) SetDeadline(t time.Time) error { var d time.Duration if t.Equal(noDeadline()) { d = time.Duration(math.MaxInt64) } else { d = time.Until(t) } a.acceptTimer.Reset(d) return nil } // Close releases the allocation // Any blocked Accept operations will be unblocked and return errors. // Any opened connection via Dial/Accept will be closed. func (a *TCPAllocation) Close() error { a.refreshAllocTimer.Stop() a.refreshPermsTimer.Stop() a.client.OnDeallocated(a.relayedAddr) return a.refreshAllocation(0, true /* dontWait=true */) } // Addr returns the relayed address of the allocation. func (a *TCPAllocation) Addr() net.Addr { return a.relayedAddr } // HandleConnectionAttempt is called by the TURN client // when it receives a ConnectionAttempt indication. func (a *TCPAllocation) HandleConnectionAttempt(from *net.TCPAddr, cid proto.ConnectionID) { a.connAttemptCh <- &connectionAttempt{ from: from, cid: cid, } } turn-4.1.3/internal/client/tcp_conn.go000066400000000000000000000024041510560755200177410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "errors" "net" "github.com/pion/transport/v3" "github.com/pion/turn/v4/internal/proto" ) var ( errInvalidTURNFrame = errors.New("data is not a valid TURN frame, no STUN or ChannelData found") errIncompleteTURNFrame = errors.New("data contains incomplete STUN or TURN frame") ) const ( stunHeaderSize = 20 ) var _ transport.TCPConn = (*TCPConn)(nil) // Includes type check for net.Conn // TCPConn wraps a transport.TCPConn and returns the allocations relayed // transport address in response to TCPConn.LocalAddress(). type TCPConn struct { transport.TCPConn remoteAddress *net.TCPAddr allocation *TCPAllocation ConnectionID proto.ConnectionID } type connectionAttempt struct { from *net.TCPAddr cid proto.ConnectionID } // LocalAddr returns the local network address. // The Addr returned is shared by all invocations of LocalAddr, so do not modify it. func (c *TCPConn) LocalAddr() net.Addr { return c.allocation.Addr() } // RemoteAddr returns the remote network address. // The Addr returned is shared by all invocations of RemoteAddr, so do not modify it. func (c *TCPConn) RemoteAddr() net.Addr { return c.remoteAddress } turn-4.1.3/internal/client/tcp_conn_test.go000066400000000000000000000122061510560755200210010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "testing" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) type dummyTCPConn struct { transport.TCPConn } func buildMsg( transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter, ) []stun.Setter { return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...) } func (c dummyTCPConn) Write(b []byte) (int, error) { return len(b), nil } func (c dummyTCPConn) Read(b []byte) (int, error) { transactionID := [stun.TransactionIDSize]byte{1, 2, 3} messageType := stun.MessageType{Method: stun.MethodConnectionBind, Class: stun.ClassSuccessResponse} attrs := buildMsg(transactionID, messageType) msg, err := stun.Build(attrs...) if err != nil { return 0, err } copy(b, msg.Raw) return len(msg.Raw), nil } func TestTCPConn(t *testing.T) { t.Run("Connect()", func(t *testing.T) { var cid proto.ConnectionID = 5 client := &mockClient{ performTransaction: func(msg *stun.Message, _ net.Addr, _ bool) (TransactionResult, error) { if msg.Type.Class == stun.ClassRequest && msg.Type.Method == stun.MethodConnect { msg, err := stun.Build( stun.TransactionID, stun.NewType(stun.MethodConnect, stun.ClassSuccessResponse), cid, ) assert.NoError(t, err) return TransactionResult{Msg: msg}, nil } return TransactionResult{}, errFake }, } addr := &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 1234, } pm := newPermissionMap() assert.True(t, pm.insert(addr, &permission{ st: permStatePermitted, })) loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") alloc := TCPAllocation{ allocation: allocation{ client: client, permMap: pm, log: log, }, } actualCid, err := alloc.Connect(addr) assert.NoError(t, err) assert.Equal(t, cid, actualCid) client = &mockClient{ performTransaction: func(msg *stun.Message, _ net.Addr, _ bool) (TransactionResult, error) { if msg.Type.Class == stun.ClassRequest && msg.Type.Method == stun.MethodConnect { msg, buildErr := stun.Build( stun.TransactionID, stun.NewType(stun.MethodConnect, stun.ClassErrorResponse), stun.ErrorCodeAttribute{Code: stun.CodeBadRequest}, ) assert.NoError(t, buildErr) return TransactionResult{Msg: msg}, nil } return TransactionResult{}, errFake }, } alloc = TCPAllocation{ allocation: allocation{ client: client, permMap: pm, log: log, }, } _, err = alloc.Connect(addr) assert.ErrorContains(t, err, "Connect error response", "error 400") }) t.Run("SetDeadline()", func(t *testing.T) { relayedAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:13478") assert.NoError(t, err) loggerFactory := logging.NewDefaultLoggerFactory() alloc := NewTCPAllocation(&AllocationConfig{ Client: &mockClient{}, Lifetime: time.Second, Log: loggerFactory.NewLogger("test"), RelayedAddr: relayedAddr, }) err = alloc.SetDeadline(time.Now()) assert.NoError(t, err) cid, err := alloc.AcceptTCPWithConn(nil) assert.Nil(t, cid) assert.Contains(t, err.Error(), "i/o timeout") }) t.Run("AcceptTCPWithConn()", func(t *testing.T) { relayedAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:13478") assert.NoError(t, err) loggerFactory := logging.NewDefaultLoggerFactory() alloc := NewTCPAllocation(&AllocationConfig{ Client: &mockClient{}, Lifetime: time.Second, Log: loggerFactory.NewLogger("test"), RelayedAddr: relayedAddr, }) from, err := net.ResolveTCPAddr("tcp", "127.0.0.1:11111") var cid proto.ConnectionID = 5 assert.NoError(t, err) alloc.HandleConnectionAttempt(from, cid) conn := dummyTCPConn{} dataConn, err := alloc.AcceptTCPWithConn(conn) assert.Equal(t, cid, dataConn.ConnectionID) assert.NoError(t, err) }) t.Run("DialWithConn()", func(t *testing.T) { relayedAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:13478") assert.NoError(t, err) var cid proto.ConnectionID = 5 loggerFactory := logging.NewDefaultLoggerFactory() client := &mockClient{ performTransaction: func(msg *stun.Message, _ net.Addr, _ bool) (TransactionResult, error) { typ := stun.NewType(stun.MethodConnect, stun.ClassSuccessResponse) if msg.Type.Method == stun.MethodCreatePermission { typ = stun.NewType(stun.MethodCreatePermission, stun.ClassSuccessResponse) } msg, buildErr := stun.Build( stun.TransactionID, typ, cid, ) assert.NoError(t, buildErr) return TransactionResult{Msg: msg}, nil }, } alloc := NewTCPAllocation(&AllocationConfig{ Client: client, Lifetime: time.Second, Log: loggerFactory.NewLogger("test"), RelayedAddr: relayedAddr, }) conn := dummyTCPConn{} dataConn, err := alloc.DialWithConn(conn, "tcp", "127.0.0.1:11111") assert.Equal(t, cid, dataConn.ConnectionID) assert.NoError(t, err) }) } turn-4.1.3/internal/client/transaction.go000066400000000000000000000103321510560755200204620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "net" "sync" "time" "github.com/pion/stun/v3" ) const ( maxRtxInterval time.Duration = 1600 * time.Millisecond ) // TransactionResult is a bag of result values of a transaction. type TransactionResult struct { Msg *stun.Message From net.Addr Retries int Err error } // TransactionConfig is a set of config params used by NewTransaction. type TransactionConfig struct { Key string Raw []byte To net.Addr Interval time.Duration IgnoreResult bool // True to throw away the result of this transaction (it will not be readable using WaitForResult) } // Transaction represents a transaction. type Transaction struct { Key string // Read-only Raw []byte // Read-only To net.Addr // Read-only nRtx int // Modified only by the timer thread interval time.Duration // Modified only by the timer thread timer *time.Timer // Thread-safe, set only by the creator, and stopper resultCh chan TransactionResult // Thread-safe mutex sync.RWMutex } // NewTransaction creates a new instance of Transaction. func NewTransaction(config *TransactionConfig) *Transaction { var resultCh chan TransactionResult if !config.IgnoreResult { resultCh = make(chan TransactionResult) } return &Transaction{ Key: config.Key, // Read-only Raw: config.Raw, // Read-only To: config.To, // Read-only interval: config.Interval, // Modified only by the timer thread resultCh: resultCh, // Thread-safe } } // StartRtxTimer starts the transaction timer. func (t *Transaction) StartRtxTimer(onTimeout func(trKey string, nRtx int)) { t.mutex.Lock() defer t.mutex.Unlock() t.timer = time.AfterFunc(t.interval, func() { t.mutex.Lock() t.nRtx++ nRtx := t.nRtx t.interval *= 2 if t.interval > maxRtxInterval { t.interval = maxRtxInterval } t.mutex.Unlock() onTimeout(t.Key, nRtx) }) } // StopRtxTimer stop the transaction timer. func (t *Transaction) StopRtxTimer() { t.mutex.Lock() defer t.mutex.Unlock() if t.timer != nil { t.timer.Stop() } } // WriteResult writes the result to the result channel. func (t *Transaction) WriteResult(res TransactionResult) bool { if t.resultCh == nil { return false } t.resultCh <- res return true } // WaitForResult waits for the transaction result. func (t *Transaction) WaitForResult() TransactionResult { if t.resultCh == nil { return TransactionResult{ Err: errWaitForResultOnNonResultTransaction, } } result, ok := <-t.resultCh if !ok { result.Err = errTransactionClosed } return result } // Close closes the transaction. func (t *Transaction) Close() { if t.resultCh != nil { close(t.resultCh) } } // Retries returns the number of retransmission it has made. func (t *Transaction) Retries() int { t.mutex.RLock() defer t.mutex.RUnlock() return t.nRtx } // TransactionMap is a thread-safe transaction map. type TransactionMap struct { trMap map[string]*Transaction mutex sync.RWMutex } // NewTransactionMap create a new instance of the transaction map. func NewTransactionMap() *TransactionMap { return &TransactionMap{ trMap: map[string]*Transaction{}, } } // Insert inserts a transaction to the map. func (m *TransactionMap) Insert(key string, tr *Transaction) bool { m.mutex.Lock() defer m.mutex.Unlock() m.trMap[key] = tr return true } // Find looks up a transaction by its key. func (m *TransactionMap) Find(key string) (*Transaction, bool) { m.mutex.RLock() defer m.mutex.RUnlock() tr, ok := m.trMap[key] return tr, ok } // Delete deletes a transaction by its key. func (m *TransactionMap) Delete(key string) { m.mutex.Lock() defer m.mutex.Unlock() delete(m.trMap, key) } // CloseAndDeleteAll closes and deletes all transactions. func (m *TransactionMap) CloseAndDeleteAll() { m.mutex.Lock() defer m.mutex.Unlock() for trKey, tr := range m.trMap { tr.Close() delete(m.trMap, trKey) } } // Size returns the length of the transaction map. func (m *TransactionMap) Size() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.trMap) } turn-4.1.3/internal/client/trylock.go000066400000000000000000000010571510560755200176300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "sync/atomic" ) // TryLock implement the classic "try-lock" operation. type TryLock struct { n int32 } // Lock tries to lock the try-lock. If successful, it returns true. // Otherwise, it returns false immediately. func (c *TryLock) Lock() error { if !atomic.CompareAndSwapInt32(&c.n, 0, 1) { return errDoubleLock } return nil } // Unlock unlocks the try-lock. func (c *TryLock) Unlock() { atomic.StoreInt32(&c.n, 0) } turn-4.1.3/internal/client/trylock_test.go000066400000000000000000000022541510560755200206670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestTryLock(t *testing.T) { t.Run("success case", func(t *testing.T) { cl := &TryLock{} testFunc := func() error { if err := cl.Lock(); err != nil { return err } defer cl.Unlock() return nil } err := testFunc() assert.NoError(t, err, "should succeed") assert.Equal(t, int32(0), cl.n, "should match") }) t.Run("failure case", func(t *testing.T) { cl := &TryLock{} testFunc := func() error { if err := cl.Lock(); err != nil { return err } defer cl.Unlock() time.Sleep(50 * time.Millisecond) return nil } var err1, err2 error doneCh1 := make(chan struct{}) doneCh2 := make(chan struct{}) go func() { err1 = testFunc() close(doneCh1) }() go func() { err2 = testFunc() close(doneCh2) }() <-doneCh1 <-doneCh2 // Either one of them should fail if err1 == nil { assert.Error(t, err2, "should fail") } else { assert.Error(t, err1, "should fail") } assert.Equal(t, int32(0), cl.n, "should match") }) } turn-4.1.3/internal/client/udp_conn.go000066400000000000000000000300511510560755200177420ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package client implements the API for a TURN client package client import ( "errors" "fmt" "io" "math" "net" "time" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/proto" ) const ( maxReadQueueSize = 1024 permRefreshInterval = 120 * time.Second bindingRefreshInterval = 5 * time.Minute bindingCheckInterval = 30 * time.Second maxRetryAttempts = 3 ) const ( timerIDRefreshAlloc int = iota timerIDRefreshPerms timerIDCheckBindings ) type inboundData struct { data []byte from net.Addr } // UDPConn is the implementation of the Conn and PacketConn interfaces for UDP network connections. // compatible with net.PacketConn and net.Conn. type UDPConn struct { bindingMgr *bindingManager // Thread-safe checkBindingsTimer *PeriodicTimer // Thread-safe readCh chan *inboundData // Thread-safe closeCh chan struct{} // Thread-safe allocation } // NewUDPConn creates a new instance of UDPConn. func NewUDPConn(config *AllocationConfig) *UDPConn { conn := &UDPConn{ bindingMgr: newBindingManager(), readCh: make(chan *inboundData, maxReadQueueSize), closeCh: make(chan struct{}), allocation: allocation{ client: config.Client, relayedAddr: config.RelayedAddr, serverAddr: config.ServerAddr, readTimer: time.NewTimer(time.Duration(math.MaxInt64)), permMap: newPermissionMap(), username: config.Username, realm: config.Realm, integrity: config.Integrity, _nonce: config.Nonce, _lifetime: config.Lifetime, net: config.Net, log: config.Log, }, } conn.log.Debugf("Initial lifetime: %d seconds", int(conn.lifetime().Seconds())) conn.refreshAllocTimer = NewPeriodicTimer( timerIDRefreshAlloc, conn.onRefreshTimers, conn.lifetime()/2, ) conn.refreshPermsTimer = NewPeriodicTimer( timerIDRefreshPerms, conn.onRefreshTimers, permRefreshInterval, ) conn.checkBindingsTimer = NewPeriodicTimer( timerIDCheckBindings, func(timerID int) { for _, bound := range conn.bindingMgr.all() { conn.maybeBind(bound) } }, bindingCheckInterval, ) if conn.refreshAllocTimer.Start() { conn.log.Debugf("Started refresh allocation timer") } if conn.refreshPermsTimer.Start() { conn.log.Debugf("Started refresh permission timer") } if conn.checkBindingsTimer.Start() { conn.log.Debugf("Started check bindings timer") } return conn } // ReadFrom reads a packet from the connection, // copying the payload into p. It returns the number of // bytes copied into p and the return address that // was on the packet. // It returns the number of bytes read (0 <= n <= len(p)) // and any error encountered. Callers should always process // the n > 0 bytes returned before considering the error err. // ReadFrom can be made to time out and return // an Error with Timeout() == true after a fixed time limit; // see SetDeadline and SetReadDeadline. func (c *UDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { for { select { case ibData := <-c.readCh: n := copy(p, ibData.data) if n < len(ibData.data) { return 0, nil, io.ErrShortBuffer } return n, ibData.from, nil case <-c.readTimer.C: return 0, nil, &net.OpError{ Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: newTimeoutError("i/o timeout"), } case <-c.closeCh: return 0, nil, &net.OpError{ Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: errClosed, } } } } func (a *allocation) createPermission(perm *permission, addr net.Addr) error { perm.mutex.Lock() defer perm.mutex.Unlock() if perm.state() == permStateIdle { // Punch a hole! (this would block a bit..) if err := a.CreatePermissions(addr); err != nil { a.permMap.delete(addr) return err } perm.setState(permStatePermitted) } return nil } // WriteTo writes a packet with payload to addr. // WriteTo can be made to time out and return // an Error with Timeout() == true after a fixed time limit; // see SetDeadline and SetWriteDeadline. // On packet-oriented connections, write timeouts are rare. func (c *UDPConn) WriteTo(payload []byte, addr net.Addr) (int, error) { //nolint:gocognit,cyclop var err error _, ok := addr.(*net.UDPAddr) if !ok { return 0, errUDPAddrCast } // Check if we have a permission for the destination IP addr perm, ok := c.permMap.find(addr) if !ok { perm = &permission{} c.permMap.insert(addr, perm) } for i := 0; i < maxRetryAttempts; i++ { // c.createPermission() would block, per destination IP (, or perm), // until the perm state becomes "requested". Purpose of this is to // guarantee the order of packets (within the same perm). // Note that CreatePermission transaction may not be complete before // all the data transmission. This is done assuming that the request // will be most likely successful and we can tolerate some loss of // UDP packet (or reorder), inorder to minimize the latency in most cases. if err = c.createPermission(perm, addr); !errors.Is(err, errTryAgain) { break } } if err != nil { return 0, err } // Bind channel bound, ok := c.bindingMgr.findByAddr(addr) if !ok { bound = c.bindingMgr.create(addr) } //nolint:nestif if !bound.ok() { // Try to establish an initial binding with the server. // Writes still occur via indications meanwhile. c.maybeBind(bound) // Send data using SendIndication peerAddr := addr2PeerAddress(addr) var msg *stun.Message msg, err = stun.Build( stun.TransactionID, stun.NewType(stun.MethodSend, stun.ClassIndication), proto.Data(payload), peerAddr, stun.Fingerprint, ) if err != nil { return 0, err } return c.client.WriteTo(msg.Raw, c.serverAddr) } // Binding is ready beyond this point, so send over it. _, err = c.sendChannelData(payload, bound.number) if err != nil { return 0, err } return len(payload), nil } // Close closes the connection. // Any blocked ReadFrom or WriteTo operations will be unblocked and return errors. func (c *UDPConn) Close() error { c.refreshAllocTimer.Stop() c.refreshPermsTimer.Stop() c.checkBindingsTimer.Stop() select { case <-c.closeCh: return errAlreadyClosed default: close(c.closeCh) } c.client.OnDeallocated(c.relayedAddr) return c.refreshAllocation(0, true /* dontWait=true */) } // LocalAddr returns the local network address. func (c *UDPConn) LocalAddr() net.Addr { return c.relayedAddr } // SetDeadline sets the read and write deadlines associated // with the connection. It is equivalent to calling both // SetReadDeadline and SetWriteDeadline. // // A deadline is an absolute time after which I/O operations // fail with a timeout (see type Error) instead of // blocking. The deadline applies to all future and pending // I/O, not just the immediately following call to ReadFrom or // WriteTo. After a deadline has been exceeded, the connection // can be refreshed by setting a deadline in the future. // // An idle timeout can be implemented by repeatedly extending // the deadline after successful ReadFrom or WriteTo calls. // // A zero value for t means I/O operations will not time out. func (c *UDPConn) SetDeadline(t time.Time) error { return c.SetReadDeadline(t) } // SetReadDeadline sets the deadline for future ReadFrom calls // and any currently-blocked ReadFrom call. // A zero value for t means ReadFrom will not time out. func (c *UDPConn) SetReadDeadline(t time.Time) error { var d time.Duration if t.Equal(noDeadline()) { d = time.Duration(math.MaxInt64) } else { d = time.Until(t) } c.readTimer.Reset(d) return nil } // SetWriteDeadline sets the deadline for future WriteTo calls // and any currently-blocked WriteTo call. // Even if write times out, it may return n > 0, indicating that // some of the data was successfully written. // A zero value for t means WriteTo will not time out. func (c *UDPConn) SetWriteDeadline(time.Time) error { // Write never blocks. return nil } func addr2PeerAddress(addr net.Addr) proto.PeerAddress { var peerAddr proto.PeerAddress switch a := addr.(type) { case *net.UDPAddr: peerAddr.IP = a.IP peerAddr.Port = a.Port case *net.TCPAddr: peerAddr.IP = a.IP peerAddr.Port = a.Port } return peerAddr } // CreatePermissions Issues a CreatePermission request for the supplied addresses // as described in https://datatracker.ietf.org/doc/html/rfc5766#section-9 func (a *allocation) CreatePermissions(addrs ...net.Addr) error { setters := []stun.Setter{ stun.TransactionID, stun.NewType(stun.MethodCreatePermission, stun.ClassRequest), } for _, addr := range addrs { setters = append(setters, addr2PeerAddress(addr)) } setters = append(setters, a.username, a.realm, a.nonce(), a.integrity, stun.Fingerprint) msg, err := stun.Build(setters...) if err != nil { return err } trRes, err := a.client.PerformTransaction(msg, a.serverAddr, false) if err != nil { return err } res := trRes.Msg if res.Type.Class == stun.ClassErrorResponse { var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { if code.Code == stun.CodeStaleNonce { a.setNonceFromMsg(res) return errTryAgain } turnError := &stun.TurnError{ StunMessageType: res.Type, ErrorCodeAttr: code, } return turnError } return fmt.Errorf("%s", res.Type) //nolint // dynamic errors } return nil } // HandleInbound passes inbound data in UDPConn. func (c *UDPConn) HandleInbound(data []byte, from net.Addr) { // Copy data copied := make([]byte, len(data)) copy(copied, data) select { case c.readCh <- &inboundData{data: copied, from: from}: default: c.log.Warnf("Receive buffer full") } } // FindAddrByChannelNumber returns a peer address associated with the // channel number on this UDPConn. func (c *UDPConn) FindAddrByChannelNumber(chNum uint16) (net.Addr, bool) { b, ok := c.bindingMgr.findByNumber(chNum) if !ok { return nil, false } return b.addr, true } func (c *UDPConn) maybeBind(bound *binding) { bind := func() { var err error for i := 0; i < maxRetryAttempts; i++ { if err = c.bind(bound); !errors.Is(err, errTryAgain) { break } } if err != nil { c.log.Warnf("Failed to bind channel %d: %s", bound.number, err) bound.setState(bindingStateFailed) return } bound.setRefreshedAt(time.Now()) bound.setState(bindingStateReady) } // Block only callers with the same binding until // the binding transaction has been complete bound.muBind.Lock() defer bound.muBind.Unlock() state := bound.state() switch { case state == bindingStateIdle: bound.setState(bindingStateRequest) case state == bindingStateReady && time.Since(bound.refreshedAt()) > bindingRefreshInterval: bound.setState(bindingStateRefresh) default: return } // Establish binding with the server if eligible // with regard to cases right above. go bind() } func (c *UDPConn) bind(bound *binding) error { setters := []stun.Setter{ stun.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassRequest), addr2PeerAddress(bound.addr), proto.ChannelNumber(bound.number), c.username, c.realm, c.nonce(), c.integrity, stun.Fingerprint, } msg, err := stun.Build(setters...) if err != nil { return err } trRes, err := c.client.PerformTransaction(msg, c.serverAddr, false) if err != nil { c.bindingMgr.deleteByAddr(bound.addr) return err } res := trRes.Msg if res.Type.Class == stun.ClassErrorResponse { var code stun.ErrorCodeAttribute if err = code.GetFrom(res); err == nil { if code.Code == stun.CodeStaleNonce { c.setNonceFromMsg(res) return errTryAgain } } return fmt.Errorf("unexpected response type %s", res.Type) //nolint // dynamic errors } c.log.Debugf("Channel binding successful: %s %d", bound.addr, bound.number) // Success. return nil } func (c *UDPConn) sendChannelData(data []byte, chNum uint16) (int, error) { chData := &proto.ChannelData{ Data: data, Number: proto.ChannelNumber(chNum), } chData.Encode() _, err := c.client.WriteTo(chData.Raw, c.serverAddr) if err != nil { return 0, err } return len(data), nil } turn-4.1.3/internal/client/udp_conn_test.go000066400000000000000000000160451510560755200210100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package client import ( "errors" "net" "testing" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestUDPConn(t *testing.T) { makeConn := func(client *mockClient, bm *bindingManager) UDPConn { return UDPConn{ allocation: allocation{ client: client, log: logging.NewDefaultLoggerFactory().NewLogger("test"), }, bindingMgr: bm, } } staleNonceMsg := func() *stun.Message { return stun.MustBuild( stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), stun.CodeStaleNonce, stun.NewNonce("new-nonce-123"), ) } t.Run("maybeBind()", func(t *testing.T) { tests := []struct { name string initialState bindingState interimState bindingState finalState bindingState pastInterval bool shouldSucceed bool }{ {"idle -> request -> ready", bindingStateIdle, bindingStateRequest, bindingStateReady, false, true}, {"idle -> request -> failed", bindingStateIdle, bindingStateRequest, bindingStateFailed, false, false}, {"ready (stale) -> refresh -> ready", bindingStateReady, bindingStateRefresh, bindingStateReady, true, true}, {"ready (stale) -> refresh -> failed", bindingStateReady, bindingStateRefresh, bindingStateFailed, true, false}, // Noop cases: {"ready (noop)", bindingStateReady, bindingStateReady, bindingStateReady, false, true}, {"request (noop)", bindingStateRequest, bindingStateRequest, bindingStateRequest, false, true}, {"refresh (noop)", bindingStateRefresh, bindingStateRefresh, bindingStateRefresh, false, true}, {"failed (noop)", bindingStateFailed, bindingStateFailed, bindingStateFailed, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { unblock := make(chan struct{}) bm := newBindingManager() bound := bm.create(&net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}) conn := makeConn(&mockClient{ performTransaction: func(msg *stun.Message, addr net.Addr, dontWait bool) (TransactionResult, error) { <-unblock if tt.shouldSucceed { return TransactionResult{Msg: new(stun.Message)}, nil } return TransactionResult{Msg: staleNonceMsg()}, nil }, }, bm) bound.setState(tt.initialState) if tt.pastInterval { bound.setRefreshedAt(time.Now().Add(-(bindingRefreshInterval + 1*time.Minute))) } conn.maybeBind(bound) assert.Equal(t, tt.interimState, bound.state()) // Release barrier so inner bind() can move forward. close(unblock) assert.Eventually(t, func() bool { return bound.state() == tt.finalState }, 5*time.Second, 10*time.Millisecond) }) } }) t.Run("bind()", func(t *testing.T) { tests := []struct { name string transactionFn func(*stun.Message, net.Addr, bool) (TransactionResult, error) expectErr error expectBindingDeleted bool expectNonceChanged bool }{ { name: "PerformTransaction returns error", transactionFn: func(*stun.Message, net.Addr, bool) (TransactionResult, error) { return TransactionResult{}, errFake }, expectErr: errFake, expectBindingDeleted: true, }, { name: "ErrorResponse with CodeStaleNonce triggers nonce update", transactionFn: func(*stun.Message, net.Addr, bool) (TransactionResult, error) { return TransactionResult{Msg: staleNonceMsg()}, nil }, expectErr: errTryAgain, expectNonceChanged: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bm := newBindingManager() bound := bm.create(&net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}) conn := makeConn(&mockClient{performTransaction: tt.transactionFn}, bm) nonceT0 := conn.nonce() err := conn.bind(bound) if tt.expectErr == nil { assert.NoError(t, err) } else { assert.ErrorIs(t, err, tt.expectErr) } if tt.expectBindingDeleted { assert.Empty(t, bm.chanMap) assert.Empty(t, bm.addrMap) } nonceT1 := conn.nonce() if tt.expectNonceChanged { assert.NotEqual(t, nonceT0, nonceT1, "should change") assert.NotEmpty(t, nonceT1, "should be non-empty") } else { assert.Equal(t, nonceT0, nonceT1, "should remain unchanged") } }) } }) t.Run("WriteTo()", func(t *testing.T) { client := &mockClient{ performTransaction: func(*stun.Message, net.Addr, bool) (TransactionResult, error) { return TransactionResult{}, errFake }, writeTo: func(data []byte, _ net.Addr) (int, error) { return len(data), nil }, } addr := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 1234, } pm := newPermissionMap() assert.True(t, pm.insert(addr, &permission{ st: permStatePermitted, })) bm := newBindingManager() binding := bm.create(addr) binding.setState(bindingStateReady) conn := UDPConn{ allocation: allocation{ client: client, permMap: pm, }, bindingMgr: bm, } buf := []byte("Hello") n, err := conn.WriteTo(buf, addr) assert.NoError(t, err, "should fail") assert.Equal(t, len(buf), n) }) } func TestCreatePermissions(t *testing.T) { t.Run("CreatePermissions success", func(t *testing.T) { called := false client := &mockClient{ performTransaction: func(msg *stun.Message, addr net.Addr, _ bool) (TransactionResult, error) { called = true // Simulate a successful response res := stun.New() res.Type = stun.NewType(stun.MethodCreatePermission, stun.ClassSuccessResponse) return TransactionResult{Msg: res}, nil }, } a := &allocation{ client: client, serverAddr: &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 3478}, username: stun.NewUsername("user"), realm: stun.NewRealm("realm"), integrity: stun.NewShortTermIntegrity("pass"), _nonce: stun.NewNonce("nonce"), } addr := &net.UDPAddr{IP: net.IPv4(5, 6, 7, 8), Port: 12345} err := a.CreatePermissions(addr) assert.NoError(t, err) assert.True(t, called) }) t.Run("CreatePermissions error", func(t *testing.T) { client := &mockClient{ performTransaction: func(msg *stun.Message, addr net.Addr, _ bool) (TransactionResult, error) { res := stun.New() res.Type = stun.NewType(stun.MethodCreatePermission, stun.ClassErrorResponse) code := stun.ErrorCodeAttribute{ Code: stun.CodeForbidden, Reason: []byte("Forbidden"), } _ = code.AddTo(res) return TransactionResult{Msg: res}, nil }, } a := &allocation{ client: client, serverAddr: &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 3478}, username: stun.NewUsername("user"), realm: stun.NewRealm("realm"), integrity: stun.NewShortTermIntegrity("pass"), _nonce: stun.NewNonce("nonce"), } addr := &net.UDPAddr{IP: net.IPv4(5, 6, 7, 8), Port: 12345} err := a.CreatePermissions(addr) var turnErr *stun.TurnError assert.Error(t, err) assert.True(t, errors.As(err, &turnErr), "should return a TurnError") assert.Equal(t, stun.CodeForbidden, turnErr.ErrorCodeAttr.Code) }) } turn-4.1.3/internal/ipnet/000077500000000000000000000000001510560755200154505ustar00rootroot00000000000000turn-4.1.3/internal/ipnet/util.go000066400000000000000000000024231510560755200167550ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package ipnet contains helper functions around net and IP package ipnet import ( "errors" "net" ) var errFailedToCastAddr = errors.New("failed to cast net.Addr to *net.UDPAddr or *net.TCPAddr") // AddrIPPort extracts the IP and Port from a net.Addr. func AddrIPPort(a net.Addr) (net.IP, int, error) { aUDP, ok := a.(*net.UDPAddr) if ok { return aUDP.IP, aUDP.Port, nil } aTCP, ok := a.(*net.TCPAddr) if ok { return aTCP.IP, aTCP.Port, nil } return nil, 0, errFailedToCastAddr } // AddrEqual asserts that two net.Addrs are equal // Currently only supports UDP but will be extended in the future to support others. func AddrEqual(a, b net.Addr) bool { aUDP, ok := a.(*net.UDPAddr) if !ok { return false } bUDP, ok := b.(*net.UDPAddr) if !ok { return false } return aUDP.IP.Equal(bUDP.IP) && aUDP.Port == bUDP.Port } // FingerprintAddr generates a fingerprint from net.UDPAddr or net.TCPAddr's // which can be used for indexing maps. func FingerprintAddr(addr net.Addr) string { switch a := addr.(type) { case *net.UDPAddr: return a.IP.String() case *net.TCPAddr: // Do we really need this case? return a.IP.String() } return "" // Should never happen } turn-4.1.3/internal/proto/000077500000000000000000000000001510560755200154745ustar00rootroot00000000000000turn-4.1.3/internal/proto/addr.go000066400000000000000000000022571510560755200167430ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "fmt" "net" ) // Addr is ip:port. type Addr struct { IP net.IP Port int } // Network implements net.Addr. func (Addr) Network() string { return "turn" } // FromUDPAddr sets addr to UDPAddr. func (a *Addr) FromUDPAddr(n *net.UDPAddr) { a.IP = n.IP a.Port = n.Port } // Equal returns true if b == a. func (a Addr) Equal(b Addr) bool { if a.Port != b.Port { return false } return a.IP.Equal(b.IP) } // EqualIP returns true if a and b have equal IP addresses. func (a Addr) EqualIP(b Addr) bool { return a.IP.Equal(b.IP) } func (a Addr) String() string { return fmt.Sprintf("%s:%d", a.IP, a.Port) } // FiveTuple represents 5-TUPLE value. type FiveTuple struct { Client Addr Server Addr Proto Protocol } func (t FiveTuple) String() string { return fmt.Sprintf("%s->%s (%s)", t.Client, t.Server, t.Proto, ) } // Equal returns true if b == t. func (t FiveTuple) Equal(b FiveTuple) bool { if t.Proto != b.Proto { return false } if !t.Client.Equal(b.Client) { return false } if !t.Server.Equal(b.Server) { return false } return true } turn-4.1.3/internal/proto/addr_test.go000066400000000000000000000030211510560755200177700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "fmt" "net" "testing" "github.com/stretchr/testify/assert" ) func TestAddr_FromUDPAddr(t *testing.T) { u := &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 1234, } a := new(Addr) a.FromUDPAddr(u) assert.True(t, u.IP.Equal(a.IP)) assert.Equal(t, u.Port, a.Port) assert.Equal(t, u.String(), a.String()) assert.Equal(t, "turn", a.Network()) } func TestAddr_EqualIP(t *testing.T) { a := Addr{ IP: net.IPv4(127, 0, 0, 1), Port: 1337, } b := Addr{ IP: net.IPv4(127, 0, 0, 1), Port: 1338, } assert.False(t, a.Equal(b)) assert.True(t, a.EqualIP(b)) } func TestFiveTuple_Equal(t *testing.T) { for _, tc := range []struct { name string a, b FiveTuple v bool }{ { name: "blank", v: true, }, { name: "proto", a: FiveTuple{ Proto: ProtoUDP, }, }, { name: "server", a: FiveTuple{ Server: Addr{ Port: 100, }, }, }, { name: "client", a: FiveTuple{ Client: Addr{ Port: 100, }, }, }, } { assert.Equal(t, tc.v, tc.a.Equal(tc.b), "(%s) %s [%v!=%v] %s", tc.name, tc.a, tc.v, tc.b, tc.b) } } func TestFiveTuple_String(t *testing.T) { s := fmt.Sprint(FiveTuple{ Proto: ProtoUDP, Server: Addr{ Port: 100, IP: net.IPv4(127, 0, 0, 1), }, Client: Addr{ Port: 200, IP: net.IPv4(127, 0, 0, 1), }, }) assert.Equal(t, "127.0.0.1:200->127.0.0.1:100 (UDP)", s, "unexpected stringer output") } turn-4.1.3/internal/proto/chandata.go000066400000000000000000000070571510560755200175770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "bytes" "encoding/binary" "errors" "io" ) // ChannelData represents The ChannelData Message. // // See RFC 5766 Section 11.4. type ChannelData struct { Data []byte // Can be sub slice of Raw Length int // Ignored while encoding, len(Data) is used Number ChannelNumber Raw []byte } // Equal returns true if compareTo == c. func (c *ChannelData) Equal(compareTo *ChannelData) bool { if c == nil && compareTo == nil { return true } if c == nil || compareTo == nil { return false } if c.Number != compareTo.Number { return false } if len(c.Data) != len(compareTo.Data) { return false } return bytes.Equal(c.Data, compareTo.Data) } // Grow ensures that internal buffer will fit v more bytes and // increases it capacity if necessary. // // Similar to stun.Message.grow method. func (c *ChannelData) grow(v int) { n := len(c.Raw) + v for cap(c.Raw) < n { c.Raw = append(c.Raw, 0) } c.Raw = c.Raw[:n] } // Reset resets Length, Data and Raw length. func (c *ChannelData) Reset() { c.Raw = c.Raw[:0] c.Length = 0 c.Data = c.Data[:0] } // Encode encodes ChannelData Message to Raw. func (c *ChannelData) Encode() { c.Raw = c.Raw[:0] c.WriteHeader() c.Raw = append(c.Raw, c.Data...) padded := nearestPaddedValueLength(len(c.Raw)) if bytesToAdd := padded - len(c.Raw); bytesToAdd > 0 { for i := 0; i < bytesToAdd; i++ { c.Raw = append(c.Raw, 0) } } } const padding = 4 func nearestPaddedValueLength(l int) int { n := padding * (l / padding) if n < l { n += padding } return n } // WriteHeader writes channel number and length. func (c *ChannelData) WriteHeader() { if len(c.Raw) < channelDataHeaderSize { // Making WriteHeader call valid even when c.Raw // is nil or len(c.Raw) is less than needed for header. c.grow(channelDataHeaderSize) } // Early bounds check to guarantee safety of writes below. _ = c.Raw[:channelDataHeaderSize] binary.BigEndian.PutUint16(c.Raw[:channelDataNumberSize], uint16(c.Number)) binary.BigEndian.PutUint16(c.Raw[channelDataNumberSize:channelDataHeaderSize], uint16(len(c.Data)), // nolint:gosec // G115 ) } // ErrBadChannelDataLength means that channel data length is not equal // to actual data length. var ErrBadChannelDataLength = errors.New("channelData length != len(Data)") // Decode decodes The ChannelData Message from Raw. func (c *ChannelData) Decode() error { buf := c.Raw if len(buf) < channelDataHeaderSize { return io.ErrUnexpectedEOF } num := binary.BigEndian.Uint16(buf[:channelDataNumberSize]) c.Number = ChannelNumber(num) l := binary.BigEndian.Uint16(buf[channelDataNumberSize:channelDataHeaderSize]) c.Data = buf[channelDataHeaderSize:] c.Length = int(l) if !c.Number.Valid() { return ErrInvalidChannelNumber } if int(l) < len(c.Data) { c.Data = c.Data[:int(l)] } if int(l) > len(buf[channelDataHeaderSize:]) { return ErrBadChannelDataLength } return nil } const ( channelDataLengthSize = 2 channelDataNumberSize = channelDataLengthSize channelDataHeaderSize = channelDataLengthSize + channelDataNumberSize ) // IsChannelData returns true if buf looks like the ChannelData Message. func IsChannelData(buf []byte) bool { if len(buf) < channelDataHeaderSize { return false } if int(binary.BigEndian.Uint16(buf[channelDataNumberSize:channelDataHeaderSize])) > len(buf[channelDataHeaderSize:]) { return false } // Quick check for channel number. num := binary.BigEndian.Uint16(buf[0:channelNumberSize]) return isChannelNumberValid(num) } turn-4.1.3/internal/proto/chandata_test.go000066400000000000000000000111411510560755200206230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "bufio" "bytes" "encoding/hex" "io" "testing" "github.com/stretchr/testify/assert" ) func TestChannelData_Encode(t *testing.T) { chanData := &ChannelData{ Data: []byte{1, 2, 3, 4}, Number: MinChannelNumber + 1, } chanData.Encode() b := &ChannelData{} b.Raw = append(b.Raw, chanData.Raw...) assert.NoError(t, b.Decode()) assert.True(t, b.Equal(chanData)) assert.True(t, IsChannelData(b.Raw) && IsChannelData(chanData.Raw)) } func TestChannelData_Equal(t *testing.T) { for _, tc := range []struct { name string a, b *ChannelData value bool }{ { name: "nil", value: true, }, { name: "nil to non-nil", b: &ChannelData{}, }, { name: "equal", b: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3}, }, a: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3}, }, value: true, }, { name: "number", b: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3}, }, a: &ChannelData{ Number: MinChannelNumber + 1, Data: []byte{1, 2, 3}, }, }, { name: "length", b: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3}, }, a: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3, 4}, }, }, { name: "data", b: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 3}, }, a: &ChannelData{ Number: MinChannelNumber, Data: []byte{1, 2, 2}, }, }, } { assert.Equal(t, tc.value, tc.a.Equal(tc.b)) } } func TestChannelData_Decode(t *testing.T) { for _, tc := range []struct { name string buf []byte err error }{ { name: "nil", err: io.ErrUnexpectedEOF, }, { name: "small", buf: []byte{1, 2, 3}, err: io.ErrUnexpectedEOF, }, { name: "zeroes", buf: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, err: ErrInvalidChannelNumber, }, { name: "bad chan number", buf: []byte{63, 255, 0, 0, 0, 4, 0, 0, 1, 2, 3, 4}, err: ErrInvalidChannelNumber, }, { name: "bad length", buf: []byte{0x40, 0x40, 0x02, 0x23, 0x16, 0, 0, 0, 0, 0, 0, 0}, err: ErrBadChannelDataLength, }, } { m := &ChannelData{ Raw: tc.buf, } assert.ErrorIs(t, m.Decode(), tc.err) } } func TestChannelData_Reset(t *testing.T) { d := &ChannelData{ Data: []byte{1, 2, 3, 4}, Number: MinChannelNumber + 1, } d.Encode() buf := make([]byte, len(d.Raw)) copy(buf, d.Raw) d.Reset() d.Raw = buf assert.NoError(t, d.Decode()) } func TestIsChannelData(t *testing.T) { for _, tc := range []struct { name string buf []byte value bool }{ { name: "nil", }, { name: "small", buf: []byte{1, 2, 3, 4}, }, { name: "zeroes", buf: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }, } { assert.Equal(t, tc.value, IsChannelData(tc.buf)) } } func BenchmarkIsChannelData(b *testing.B) { buf := []byte{64, 0, 0, 0, 0, 4, 0, 0, 1, 2, 3} b.ReportAllocs() b.SetBytes(int64(len(buf))) for i := 0; i < b.N; i++ { IsChannelData(buf) } } func BenchmarkChannelData_Encode(b *testing.B) { d := &ChannelData{ Data: []byte{1, 2, 3, 4}, Number: MinChannelNumber + 1, } b.ReportAllocs() b.SetBytes(4 + channelDataHeaderSize) for i := 0; i < b.N; i++ { d.Encode() } } func BenchmarkChannelData_Decode(b *testing.B) { d := &ChannelData{ Data: []byte{1, 2, 3, 4}, Number: MinChannelNumber + 1, } d.Encode() buf := make([]byte, len(d.Raw)) copy(buf, d.Raw) b.ReportAllocs() b.SetBytes(4 + channelDataHeaderSize) for i := 0; i < b.N; i++ { d.Reset() d.Raw = buf assert.NoError(b, d.Decode()) } } func TestChromeChannelData(t *testing.T) { var ( r = bytes.NewReader(loadData(t, "02_chandata.hex")) s = bufio.NewScanner(r) data [][]byte messages []*ChannelData ) // Decoding hex data into binary. for s.Scan() { b, err := hex.DecodeString(s.Text()) assert.NoError(t, err) data = append(data, b) } // All hex streams decoded to raw binary format and stored in data slice. // Decoding packets to messages. for i, packet := range data { chanData := new(ChannelData) chanData.Raw = packet assert.NoError(t, chanData.Decode(), "Packet %d errored", i) encoded := &ChannelData{ Data: chanData.Data, Number: chanData.Number, } encoded.Encode() decoded := new(ChannelData) decoded.Raw = encoded.Raw assert.NoError(t, decoded.Decode()) assert.True(t, decoded.Equal(chanData)) messages = append(messages, chanData) } assert.Equal(t, 2, len(messages), "unexpected number of messages") } turn-4.1.3/internal/proto/chann.go000066400000000000000000000036771510560755200171270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "encoding/binary" "errors" "strconv" "github.com/pion/stun/v3" ) // ChannelNumber represents CHANNEL-NUMBER attribute. // // The CHANNEL-NUMBER attribute contains the number of the channel. // // RFC 5766 Section 14.1. type ChannelNumber uint16 // Encoded as uint16 func (n ChannelNumber) String() string { return strconv.Itoa(int(n)) } // 16 bits of uint + 16 bits of RFFU = 0. const channelNumberSize = 4 // AddTo adds CHANNEL-NUMBER to message. func (n ChannelNumber) AddTo(m *stun.Message) error { v := make([]byte, channelNumberSize) binary.BigEndian.PutUint16(v[:2], uint16(n)) // v[2:4] are zeroes (RFFU = 0) m.Add(stun.AttrChannelNumber, v) return nil } // GetFrom decodes CHANNEL-NUMBER from message. func (n *ChannelNumber) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrChannelNumber) if err != nil { return err } if err = stun.CheckSize(stun.AttrChannelNumber, len(v), channelNumberSize); err != nil { return err } _ = v[channelNumberSize-1] // Asserting length *n = ChannelNumber(binary.BigEndian.Uint16(v[:2])) // v[2:4] is RFFU and equals to 0. return nil } // See https://tools.ietf.org/html/rfc5766#section-11: // // 0x4000 through 0x7FFF: These values are the allowed channel // numbers (16,383 possible values). const ( MinChannelNumber = 0x4000 MaxChannelNumber = 0x7FFF ) // ErrInvalidChannelNumber means that channel number is not valid as by RFC 5766 Section 11. var ErrInvalidChannelNumber = errors.New("channel number not in [0x4000, 0x7FFF]") // isChannelNumberValid returns true if c in [0x4000, 0x7FFF]. func isChannelNumberValid(c uint16) bool { return c >= MinChannelNumber && c <= MaxChannelNumber } // Valid returns true if channel number has correct value that complies RFC 5766 Section 11 range. func (n ChannelNumber) Valid() bool { return isChannelNumberValid(uint16(n)) } turn-4.1.3/internal/proto/chann_test.go000066400000000000000000000047321510560755200201570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func BenchmarkChannelNumber(b *testing.B) { b.Run("AddTo", func(b *testing.B) { b.ReportAllocs() m := new(stun.Message) for i := 0; i < b.N; i++ { n := ChannelNumber(12) assert.NoError(b, n.AddTo(m)) m.Reset() } }) b.Run("GetFrom", func(b *testing.B) { m := new(stun.Message) assert.NoError(b, ChannelNumber(12).AddTo(m)) for i := 0; i < b.N; i++ { var n ChannelNumber assert.NoError(b, n.GetFrom(m)) } }) } func TestChannelNumber(t *testing.T) { t.Run("String", func(t *testing.T) { n := ChannelNumber(112) assert.Equal(t, "112", n.String()) }) t.Run("NoAlloc", func(t *testing.T) { stunMsg := &stun.Message{} allocated := wasAllocs(func() { // Case with ChannelNumber on stack. n := ChannelNumber(6) assert.NoError(t, n.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) n := ChannelNumber(12) nP := &n allocated = wasAllocs(func() { // On heap. assert.NoError(t, nP.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { stunMsg := new(stun.Message) chanNumber := ChannelNumber(6) assert.NoError(t, chanNumber.AddTo(stunMsg)) stunMsg.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(stunMsg.Raw) assert.NoError(t, err) var numDecoded ChannelNumber err = numDecoded.GetFrom(decoded) assert.NoError(t, err) assert.Equal(t, chanNumber, numDecoded) allocated := wasAllocs(func() { var num ChannelNumber assert.NoError(t, num.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) nHandle := new(ChannelNumber) assert.ErrorIs(t, nHandle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrChannelNumber, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(nHandle.GetFrom(m))) }) }) }) } func TestChannelNumber_Valid(t *testing.T) { for _, tc := range []struct { n ChannelNumber value bool }{ {MinChannelNumber - 1, false}, {MinChannelNumber, true}, {MinChannelNumber + 1, true}, {MaxChannelNumber, true}, {MaxChannelNumber + 1, false}, } { v := tc.n.Valid() assert.Equalf(t, tc.value, v, "unexpected: (%s) %v != %v", tc.n.String(), tc.value, v) } } turn-4.1.3/internal/proto/chrome_test.go000066400000000000000000000016311510560755200203400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "bufio" "bytes" "encoding/hex" "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestChromeAllocRequest(t *testing.T) { var ( r = bytes.NewReader(loadData(t, "01_chromeallocreq.hex")) s = bufio.NewScanner(r) data [][]byte messages []*stun.Message ) // Decoding hex data into binary. for s.Scan() { b, err := hex.DecodeString(s.Text()) assert.NoError(t, err) data = append(data, b) } // All hex streams decoded to raw binary format and stored in data slice. // Decoding packets to messages. for i, packet := range data { m := new(stun.Message) _, err := m.Write(packet) assert.NoErrorf(t, err, "Packet %d: %v", i, err) messages = append(messages, m) } assert.Equal(t, 4, len(messages), "unexpected number of messages") } turn-4.1.3/internal/proto/connection_id.go000066400000000000000000000020511510560755200206340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "encoding/binary" "github.com/pion/stun/v3" ) // ConnectionID represents CONNECTION-ID attribute. // // The CONNECTION-ID attribute uniquely identifies a peer data // connection. It is a 32-bit unsigned integral value. // // RFC 6062 Section 6.2.1. type ConnectionID uint32 const connectionIDSize = 4 // uint32: 4 bytes, 32 bits // AddTo adds CONNECTION-ID to message. func (c ConnectionID) AddTo(m *stun.Message) error { v := make([]byte, lifetimeSize) binary.BigEndian.PutUint32(v, uint32(c)) m.Add(stun.AttrConnectionID, v) return nil } // GetFrom decodes CONNECTION-ID from message. func (c *ConnectionID) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrConnectionID) if err != nil { return err } if err = stun.CheckSize(stun.AttrConnectionID, len(v), connectionIDSize); err != nil { return err } _ = v[connectionIDSize-1] // Asserting length *(*uint32)(c) = binary.BigEndian.Uint32(v) return nil } turn-4.1.3/internal/proto/data.go000066400000000000000000000015001510560755200167300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import "github.com/pion/stun/v3" // Data represents DATA attribute. // // The DATA attribute is present in all Send and Data indications. The // value portion of this attribute is variable length and consists of // the application data (that is, the data that would immediately follow // the UDP header if the data was been sent directly between the client // and the peer). // // RFC 5766 Section 14.4. type Data []byte // AddTo adds DATA to message. func (d Data) AddTo(m *stun.Message) error { m.Add(stun.AttrData, d) return nil } // GetFrom decodes DATA from message. func (d *Data) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrData) if err != nil { return err } *d = v return nil } turn-4.1.3/internal/proto/data_test.go000066400000000000000000000033741510560755200200020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func BenchmarkData(b *testing.B) { b.Run("AddTo", func(b *testing.B) { m := new(stun.Message) d := make(Data, 10) for i := 0; i < b.N; i++ { assert.NoError(b, d.AddTo(m)) m.Reset() } }) b.Run("AddToRaw", func(b *testing.B) { m := new(stun.Message) d := make([]byte, 10) // Overhead should be low. for i := 0; i < b.N; i++ { m.Add(stun.AttrData, d) m.Reset() } }) } func TestData(t *testing.T) { t.Run("NoAlloc", func(t *testing.T) { stunMsg := new(stun.Message) v := []byte{1, 2, 3, 4} allocated := wasAllocs(func() { // On stack. d := Data(v) assert.NoError(t, d.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) d := &Data{1, 2, 3, 4} allocated = wasAllocs(func() { // On heap. assert.NoError(t, d.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { m := new(stun.Message) data := Data{1, 2, 33, 44, 0x13, 0xaf} assert.NoError(t, data.AddTo(m)) m.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) var dataDecoded Data assert.NoError(t, dataDecoded.GetFrom(decoded)) assert.Equal(t, data, dataDecoded) allocated := wasAllocs(func() { var dataDecoded Data assert.NoError(t, dataDecoded.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) var handle Data assert.ErrorIs(t, handle.GetFrom(m), stun.ErrAttributeNotFound) }) }) }) } turn-4.1.3/internal/proto/dontfrag.go000066400000000000000000000023561510560755200176350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "github.com/pion/stun/v3" ) // DontFragmentAttr is a deprecated alias for DontFragment // Deprecated: Please use DontFragment. type DontFragmentAttr = DontFragment // DontFragment represents DONT-FRAGMENT attribute. // // This attribute is used by the client to request that the server set // the DF (Don't Fragment) bit in the IP header when relaying the // application data onward to the peer. This attribute has no value // part and thus the attribute length field is 0. // // RFC 5766 Section 14.8. type DontFragment struct{} const dontFragmentSize = 0 // AddTo adds DONT-FRAGMENT attribute to message. func (DontFragment) AddTo(m *stun.Message) error { m.Add(stun.AttrDontFragment, nil) return nil } // GetFrom decodes DONT-FRAGMENT from message. func (d *DontFragment) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrDontFragment) if err != nil { return err } return stun.CheckSize(stun.AttrDontFragment, len(v), dontFragmentSize) } // IsSet returns true if DONT-FRAGMENT attribute is set. func (DontFragment) IsSet(m *stun.Message) bool { _, err := m.Get(stun.AttrDontFragment) return err == nil } turn-4.1.3/internal/proto/dontfrag_test.go000066400000000000000000000014651510560755200206740ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestDontFragment(t *testing.T) { var dontFrag DontFragment t.Run("False", func(t *testing.T) { m := new(stun.Message) m.WriteHeader() assert.False(t, dontFrag.IsSet(m)) }) t.Run("AddTo", func(t *testing.T) { stunMsg := new(stun.Message) assert.NoError(t, dontFrag.AddTo(stunMsg)) stunMsg.WriteHeader() t.Run("IsSet", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(stunMsg.Raw) assert.NoError(t, err) assert.True(t, dontFrag.IsSet(stunMsg)) allocated := wasAllocs(func() { dontFrag.IsSet(stunMsg) }) assert.False(t, allocated) }) }) } turn-4.1.3/internal/proto/evenport.go000066400000000000000000000025041510560755200176660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import "github.com/pion/stun/v3" // EvenPort represents EVEN-PORT attribute. // // This attribute allows the client to request that the port in the // relayed transport address be even, and (optionally) that the server // reserve the next-higher port number. // // RFC 5766 Section 14.6. type EvenPort struct { // ReservePort means that the server is requested to reserve // the next-higher port number (on the same IP address) // for a subsequent allocation. ReservePort bool } func (p EvenPort) String() string { if p.ReservePort { return "reserve: true" } return "reserve: false" } const ( evenPortSize = 1 firstBitSet = (1 << 8) - 1 // 0b100000000 ) // AddTo adds EVEN-PORT to message. func (p EvenPort) AddTo(m *stun.Message) error { v := make([]byte, evenPortSize) if p.ReservePort { // Set first bit to 1. v[0] = firstBitSet } m.Add(stun.AttrEvenPort, v) return nil } // GetFrom decodes EVEN-PORT from message. func (p *EvenPort) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrEvenPort) if err != nil { return err } if err = stun.CheckSize(stun.AttrEvenPort, len(v), evenPortSize); err != nil { return err } if v[0]&firstBitSet > 0 { p.ReservePort = true } return nil } turn-4.1.3/internal/proto/evenport_test.go000066400000000000000000000031511510560755200207240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestEvenPort(t *testing.T) { t.Run("String", func(t *testing.T) { p := EvenPort{} assert.Equal(t, "reserve: false", p.String()) p.ReservePort = true assert.Equal(t, "reserve: true", p.String()) }) t.Run("False", func(t *testing.T) { m := new(stun.Message) p := EvenPort{ ReservePort: false, } assert.NoError(t, p.AddTo(m)) m.WriteHeader() decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) var port EvenPort assert.NoError(t, port.GetFrom(m)) assert.Equal(t, p, port) }) t.Run("AddTo", func(t *testing.T) { m := new(stun.Message) evenPortAttr := EvenPort{ ReservePort: true, } assert.NoError(t, evenPortAttr.AddTo(m)) m.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) port := EvenPort{} assert.NoError(t, port.GetFrom(decoded)) assert.Equalf(t, evenPortAttr, port, "Decoded %q, expected %q", port.String(), evenPortAttr.String()) allocated := wasAllocs(func() { assert.NoError(t, port.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) var handle EvenPort assert.ErrorIs(t, handle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrEvenPort, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(handle.GetFrom(m))) }) }) }) } turn-4.1.3/internal/proto/fuzz_test.go000066400000000000000000000054451510560755200200700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "encoding/binary" "errors" "fmt" "testing" "github.com/pion/stun/v3" ) type attr interface { stun.Getter stun.Setter } type attrs []struct { g attr t stun.AttrType } func (a attrs) pick(v byte) struct { g attr t stun.AttrType } { idx := int(v) % len(a) return a[idx] } func FuzzSetters(f *testing.F) { f.Fuzz(func(_ *testing.T, attrType byte, value []byte) { var ( m1 = &stun.Message{ Raw: make([]byte, 0, 2048), } m2 = &stun.Message{ Raw: make([]byte, 0, 2048), } m3 = &stun.Message{ Raw: make([]byte, 0, 2048), } ) attributes := attrs{ {new(ChannelNumber), stun.AttrChannelNumber}, {new(Lifetime), stun.AttrLifetime}, {new(XORPeerAddress), stun.AttrXORPeerAddress}, {new(Data), stun.AttrData}, {new(XORRelayedAddress), stun.AttrXORRelayedAddress}, {new(EvenPort), stun.AttrEvenPort}, {new(RequestedTransport), stun.AttrRequestedTransport}, {new(DontFragment), stun.AttrDontFragment}, {new(ReservationToken), stun.AttrReservationToken}, {new(ConnectionID), stun.AttrConnectionID}, {new(RequestedAddressFamily), stun.AttrRequestedAddressFamily}, } attr := attributes.pick(attrType) m1.WriteHeader() m1.Add(attr.t, value) if err := attr.g.GetFrom(m1); err != nil { if errors.Is(err, stun.ErrAttributeNotFound) { fmt.Println("unexpected 404") //nolint panic(err) //nolint } return } m2.WriteHeader() if err := attr.g.AddTo(m2); err != nil { fmt.Println("failed to add attribute to m2") //nolint panic(err) //nolint } m3.WriteHeader() v, err := m2.Get(attr.t) if err != nil { panic(err) //nolint } m3.Add(attr.t, v) if !m2.Equal(m3) { fmt.Println(m2, "not equal", m3) //nolint panic("not equal") //nolint } }) } func FuzzChannelData(f *testing.F) { channelData := &ChannelData{} f.Fuzz(func(_ *testing.T, data []byte) { channelData.Reset() if len(data) > channelDataHeaderSize { // Make sure the channel id is in the proper range if b := binary.BigEndian.Uint16(data[0:4]); b > 20000 { binary.BigEndian.PutUint16(data[0:4], MinChannelNumber-1) } else if b > 40000 { binary.BigEndian.PutUint16(data[0:4], MinChannelNumber+(MaxChannelNumber-MinChannelNumber)%b) } } channelData.Raw = append(channelData.Raw, data...) if channelData.Decode() != nil { return } channelData.Encode() if !channelData.Number.Valid() { return } d2 := &ChannelData{} d2.Raw = channelData.Raw if err := d2.Decode(); err != nil { panic(err) //nolint } }) } func FuzzIsChannelData(f *testing.F) { f.Fuzz(func(_ *testing.T, data []byte) { IsChannelData(data) }) } turn-4.1.3/internal/proto/lifetime.go000066400000000000000000000025771510560755200176340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "encoding/binary" "time" "github.com/pion/stun/v3" ) // DefaultLifetime in RFC 5766 is 10 minutes. // // RFC 5766 Section 2.2. const DefaultLifetime = time.Minute * 10 // Lifetime represents LIFETIME attribute. // // The LIFETIME attribute represents the duration for which the server // will maintain an allocation in the absence of a refresh. The value // portion of this attribute is 4-bytes long and consists of a 32-bit // unsigned integral value representing the number of seconds remaining // until expiration. // // RFC 5766 Section 14.2. type Lifetime struct { time.Duration } // Seconds in uint32. const lifetimeSize = 4 // 4 bytes, 32 bits // AddTo adds LIFETIME to message. func (l Lifetime) AddTo(m *stun.Message) error { v := make([]byte, lifetimeSize) binary.BigEndian.PutUint32(v, uint32(l.Seconds())) m.Add(stun.AttrLifetime, v) return nil } // GetFrom decodes LIFETIME from message. func (l *Lifetime) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrLifetime) if err != nil { return err } if err = stun.CheckSize(stun.AttrLifetime, len(v), lifetimeSize); err != nil { return err } _ = v[lifetimeSize-1] // Asserting length seconds := binary.BigEndian.Uint32(v) l.Duration = time.Second * time.Duration(seconds) return nil } turn-4.1.3/internal/proto/lifetime_test.go000066400000000000000000000037761510560755200206750ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "time" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func BenchmarkLifetime(b *testing.B) { b.Run("AddTo", func(b *testing.B) { b.ReportAllocs() m := new(stun.Message) for i := 0; i < b.N; i++ { l := Lifetime{time.Second} assert.NoError(b, l.AddTo(m)) m.Reset() } }) b.Run("GetFrom", func(b *testing.B) { m := new(stun.Message) assert.NoError(b, Lifetime{time.Minute}.AddTo(m)) for i := 0; i < b.N; i++ { l := Lifetime{} assert.NoError(b, l.GetFrom(m)) } }) } func TestLifetime(t *testing.T) { t.Run("String", func(t *testing.T) { l := Lifetime{time.Second * 10} assert.Equal(t, "10s", l.String()) }) t.Run("NoAlloc", func(t *testing.T) { stunMsg := &stun.Message{} allocated := wasAllocs(func() { // On stack. l := Lifetime{ Duration: time.Minute, } assert.NoError(t, l.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) l := &Lifetime{time.Second} allocated = wasAllocs(func() { // On heap. assert.NoError(t, l.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { m := new(stun.Message) lifetime := Lifetime{time.Second * 10} assert.NoError(t, lifetime.AddTo(m)) m.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) life := Lifetime{} assert.NoError(t, life.GetFrom(decoded)) assert.Equal(t, lifetime, life) allocated := wasAllocs(func() { assert.NoError(t, life.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) nHandle := new(Lifetime) assert.ErrorIs(t, nHandle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrLifetime, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(nHandle.GetFrom(m))) }) }) }) } turn-4.1.3/internal/proto/peeraddr.go000066400000000000000000000023341510560755200176130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "net" "github.com/pion/stun/v3" ) // PeerAddress implements XOR-PEER-ADDRESS attribute. // // The XOR-PEER-ADDRESS specifies the address and port of the peer as // seen from the TURN server. (For example, the peer's server-reflexive // transport address if the peer is behind a NAT.) // // RFC 5766 Section 14.3. type PeerAddress struct { IP net.IP Port int } func (a PeerAddress) String() string { return stun.XORMappedAddress(a).String() } // AddTo adds XOR-PEER-ADDRESS to message. func (a PeerAddress) AddTo(m *stun.Message) error { return stun.XORMappedAddress(a).AddToAs(m, stun.AttrXORPeerAddress) } // GetFrom decodes XOR-PEER-ADDRESS from message. func (a *PeerAddress) GetFrom(m *stun.Message) error { return (*stun.XORMappedAddress)(a).GetFromAs(m, stun.AttrXORPeerAddress) } // XORPeerAddress implements XOR-PEER-ADDRESS attribute. // // The XOR-PEER-ADDRESS specifies the address and port of the peer as // seen from the TURN server. (For example, the peer's server-reflexive // transport address if the peer is behind a NAT.) // // RFC 5766 Section 14.3. type XORPeerAddress = PeerAddress turn-4.1.3/internal/proto/peeraddr_test.go000066400000000000000000000012541510560755200206520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "net" "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestPeerAddress(t *testing.T) { // Simple tests because already tested in stun. a := PeerAddress{ IP: net.IPv4(111, 11, 1, 2), Port: 333, } t.Run("String", func(t *testing.T) { assert.Equal(t, "111.11.1.2:333", a.String()) }) m := new(stun.Message) assert.NoError(t, a.AddTo(m)) m.WriteHeader() decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) var aGot PeerAddress assert.NoError(t, aGot.GetFrom(decoded)) } turn-4.1.3/internal/proto/proto.go000066400000000000000000000022221510560755200171640ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package proto implements RFC 5766 Traversal Using Relays around NAT. package proto import ( "github.com/pion/stun/v3" ) // Default ports for TURN from RFC 5766 Section 4. const ( // DefaultPort for TURN is same as STUN. DefaultPort = stun.DefaultPort // DefaultTLSPort is for TURN over TLS and is same as STUN. DefaultTLSPort = stun.DefaultTLSPort ) // CreatePermissionRequest is shorthand for create permission request type. func CreatePermissionRequest() stun.MessageType { return stun.NewType(stun.MethodCreatePermission, stun.ClassRequest) } // AllocateRequest is shorthand for allocation request message type. func AllocateRequest() stun.MessageType { return stun.NewType(stun.MethodAllocate, stun.ClassRequest) } // SendIndication is shorthand for send indication message type. func SendIndication() stun.MessageType { return stun.NewType(stun.MethodSend, stun.ClassIndication) } // RefreshRequest is shorthand for refresh request message type. func RefreshRequest() stun.MessageType { return stun.NewType(stun.MethodRefresh, stun.ClassRequest) } turn-4.1.3/internal/proto/proto_test.go000066400000000000000000000012311510560755200202220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "io" "os" "path/filepath" "testing" ) const allocRuns = 10 // wasAllocs returns true if f allocates memory. func wasAllocs(f func()) bool { return testing.AllocsPerRun(allocRuns, f) > 0 } func loadData(tb testing.TB, name string) []byte { tb.Helper() name = filepath.Join("testdata", name) f, err := os.Open(name) // #nosec if err != nil { tb.Fatal(err) } defer func() { if errClose := f.Close(); errClose != nil { tb.Fatal(errClose) } }() v, err := io.ReadAll(f) if err != nil { tb.Fatal(err) } return v } turn-4.1.3/internal/proto/relayedaddr.go000066400000000000000000000022121510560755200203000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "net" "github.com/pion/stun/v3" ) // RelayedAddress implements XOR-RELAYED-ADDRESS attribute. // // It specifies the address and port that the server allocated to the // client. It is encoded in the same way as XOR-MAPPED-ADDRESS. // // RFC 5766 Section 14.5. type RelayedAddress struct { IP net.IP Port int } func (a RelayedAddress) String() string { return stun.XORMappedAddress(a).String() } // AddTo adds XOR-PEER-ADDRESS to message. func (a RelayedAddress) AddTo(m *stun.Message) error { return stun.XORMappedAddress(a).AddToAs(m, stun.AttrXORRelayedAddress) } // GetFrom decodes XOR-PEER-ADDRESS from message. func (a *RelayedAddress) GetFrom(m *stun.Message) error { return (*stun.XORMappedAddress)(a).GetFromAs(m, stun.AttrXORRelayedAddress) } // XORRelayedAddress implements XOR-RELAYED-ADDRESS attribute. // // It specifies the address and port that the server allocated to the // client. It is encoded in the same way as XOR-MAPPED-ADDRESS. // // RFC 5766 Section 14.5. type XORRelayedAddress = RelayedAddress turn-4.1.3/internal/proto/relayedaddr_test.go000066400000000000000000000012651510560755200213460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "net" "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestRelayedAddress(t *testing.T) { // Simple tests because already tested in stun. a := RelayedAddress{ IP: net.IPv4(111, 11, 1, 2), Port: 333, } t.Run("String", func(t *testing.T) { assert.Equal(t, "111.11.1.2:333", a.String()) }) m := new(stun.Message) assert.NoError(t, a.AddTo(m)) m.WriteHeader() decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) var aGot RelayedAddress assert.NoError(t, aGot.GetFrom(decoded)) } turn-4.1.3/internal/proto/reqfamily.go000066400000000000000000000032421510560755200200150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "errors" "github.com/pion/stun/v3" ) // RequestedAddressFamily represents the REQUESTED-ADDRESS-FAMILY Attribute as // defined in RFC 6156 Section 4.1.1. type RequestedAddressFamily byte const requestedFamilySize = 4 var errInvalidRequestedFamilyValue = errors.New("invalid value for requested family attribute") // GetFrom decodes REQUESTED-ADDRESS-FAMILY from message. func (f *RequestedAddressFamily) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrRequestedAddressFamily) if err != nil { return err } if err = stun.CheckSize(stun.AttrRequestedAddressFamily, len(v), requestedFamilySize); err != nil { return err } switch v[0] { case byte(RequestedFamilyIPv4), byte(RequestedFamilyIPv6): *f = RequestedAddressFamily(v[0]) default: return errInvalidRequestedFamilyValue } return nil } func (f RequestedAddressFamily) String() string { switch f { case RequestedFamilyIPv4: return "IPv4" case RequestedFamilyIPv6: return "IPv6" default: return "unknown" } } // AddTo adds REQUESTED-ADDRESS-FAMILY to message. func (f RequestedAddressFamily) AddTo(m *stun.Message) error { v := make([]byte, requestedFamilySize) v[0] = byte(f) // b[1:4] is RFFU = 0. // The RFFU field MUST be set to zero on transmission and MUST be // ignored on reception. It is reserved for future uses. m.Add(stun.AttrRequestedAddressFamily, v) return nil } // Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1. const ( RequestedFamilyIPv4 RequestedAddressFamily = 0x01 RequestedFamilyIPv6 RequestedAddressFamily = 0x02 ) turn-4.1.3/internal/proto/reqfamily_test.go000066400000000000000000000037441510560755200210630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestRequestedAddressFamily(t *testing.T) { t.Run("String", func(t *testing.T) { assert.Equal(t, "IPv4", RequestedFamilyIPv4.String()) assert.Equal(t, "IPv6", RequestedFamilyIPv6.String()) assert.Equal(t, "unknown", RequestedAddressFamily(0x04).String()) }) t.Run("NoAlloc", func(t *testing.T) { stunMsg := &stun.Message{} allocated := wasAllocs(func() { // On stack. r := RequestedFamilyIPv4 assert.NoError(t, r.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) requestFamilyAttr := new(RequestedAddressFamily) *requestFamilyAttr = RequestedFamilyIPv4 allocated = wasAllocs(func() { // On heap. assert.NoError(t, requestFamilyAttr.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { stunMsg := new(stun.Message) requestFamilyAddr := RequestedFamilyIPv4 assert.NoError(t, requestFamilyAddr.AddTo(stunMsg)) stunMsg.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(stunMsg.Raw) assert.NoError(t, err) var req RequestedAddressFamily assert.NoError(t, req.GetFrom(decoded)) assert.Equal(t, requestFamilyAddr, req) allocated := wasAllocs(func() { assert.NoError(t, requestFamilyAddr.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) var handle RequestedAddressFamily assert.ErrorIs(t, handle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrRequestedAddressFamily, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(handle.GetFrom(m))) m.Reset() m.Add(stun.AttrRequestedAddressFamily, []byte{5, 0, 0, 0}) assert.NotNil(t, handle.GetFrom(m), "should not error on unknown value") }) }) }) } turn-4.1.3/internal/proto/reqtrans.go000066400000000000000000000033421510560755200176640ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "strconv" "github.com/pion/stun/v3" ) // Protocol is IANA assigned protocol number. type Protocol byte const ( // ProtoTCP is IANA assigned protocol number for TCP. ProtoTCP Protocol = 6 // ProtoUDP is IANA assigned protocol number for UDP. ProtoUDP Protocol = 17 ) func (p Protocol) String() string { switch p { case ProtoTCP: return "TCP" case ProtoUDP: return "UDP" default: return strconv.Itoa(int(p)) } } // RequestedTransport represents REQUESTED-TRANSPORT attribute. // // This attribute is used by the client to request a specific transport // protocol for the allocated transport address. RFC 5766 only allows the use of // code point 17 (User Datagram Protocol). // // RFC 5766 Section 14.7. type RequestedTransport struct { Protocol Protocol } func (t RequestedTransport) String() string { return "protocol: " + t.Protocol.String() } const requestedTransportSize = 4 // AddTo adds REQUESTED-TRANSPORT to message. func (t RequestedTransport) AddTo(m *stun.Message) error { v := make([]byte, requestedTransportSize) v[0] = byte(t.Protocol) // b[1:4] is RFFU = 0. // The RFFU field MUST be set to zero on transmission and MUST be // ignored on reception. It is reserved for future uses. m.Add(stun.AttrRequestedTransport, v) return nil } // GetFrom decodes REQUESTED-TRANSPORT from message. func (t *RequestedTransport) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrRequestedTransport) if err != nil { return err } if err = stun.CheckSize(stun.AttrRequestedTransport, len(v), requestedTransportSize); err != nil { return err } t.Protocol = Protocol(v[0]) return nil } turn-4.1.3/internal/proto/reqtrans_test.go000066400000000000000000000036471510560755200207330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestRequestedTransport(t *testing.T) { t.Run("String", func(t *testing.T) { transAttr := RequestedTransport{ Protocol: ProtoUDP, } assert.Equal(t, "protocol: UDP", transAttr.String()) transAttr = RequestedTransport{ Protocol: ProtoTCP, } assert.Equal(t, "protocol: TCP", transAttr.String()) transAttr.Protocol = 254 assert.Equal(t, "protocol: 254", transAttr.String()) }) t.Run("NoAlloc", func(t *testing.T) { stunMsg := &stun.Message{} allocated := wasAllocs(func() { // On stack. r := RequestedTransport{ Protocol: ProtoUDP, } assert.NoError(t, r.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) r := &RequestedTransport{ Protocol: ProtoUDP, } allocated = wasAllocs(func() { // On heap. assert.NoError(t, r.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { m := new(stun.Message) transAttr := RequestedTransport{ Protocol: ProtoUDP, } assert.NoError(t, transAttr.AddTo(m)) m.WriteHeader() t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(m.Raw) assert.NoError(t, err) req := RequestedTransport{ Protocol: ProtoUDP, } assert.NoError(t, req.GetFrom(decoded)) assert.Equal(t, transAttr, req) allocated := wasAllocs(func() { assert.NoError(t, transAttr.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) var handle RequestedTransport assert.ErrorIs(t, handle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrRequestedTransport, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(handle.GetFrom(m))) }) }) }) } turn-4.1.3/internal/proto/rsrvtoken.go000066400000000000000000000024311510560755200200600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import "github.com/pion/stun/v3" // ReservationToken represents RESERVATION-TOKEN attribute. // // The RESERVATION-TOKEN attribute contains a token that uniquely // identifies a relayed transport address being held in reserve by the // server. The server includes this attribute in a success response to // tell the client about the token, and the client includes this // attribute in a subsequent Allocate request to request the server use // that relayed transport address for the allocation. // // RFC 5766 Section 14.9. type ReservationToken []byte const reservationTokenSize = 8 // 8 bytes // AddTo adds RESERVATION-TOKEN to message. func (t ReservationToken) AddTo(m *stun.Message) error { if err := stun.CheckSize(stun.AttrReservationToken, len(t), reservationTokenSize); err != nil { return err } m.Add(stun.AttrReservationToken, t) return nil } // GetFrom decodes RESERVATION-TOKEN from message. func (t *ReservationToken) GetFrom(m *stun.Message) error { v, err := m.Get(stun.AttrReservationToken) if err != nil { return err } if err = stun.CheckSize(stun.AttrReservationToken, len(v), reservationTokenSize); err != nil { return err } *t = v return nil } turn-4.1.3/internal/proto/rsrvtoken_test.go000066400000000000000000000032341510560755200211210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package proto import ( "testing" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) func TestReservationToken(t *testing.T) { t.Run("NoAlloc", func(t *testing.T) { stunMsg := &stun.Message{} tok := make([]byte, 8) allocated := wasAllocs(func() { // On stack. tk := ReservationToken(tok) assert.NoError(t, tk.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) tk := make(ReservationToken, 8) allocated = wasAllocs(func() { // On heap. assert.NoError(t, tk.AddTo(stunMsg)) stunMsg.Reset() }) assert.False(t, allocated) }) t.Run("AddTo", func(t *testing.T) { stunMsg := new(stun.Message) tk := make(ReservationToken, 8) tk[2] = 33 tk[7] = 1 assert.NoError(t, tk.AddTo(stunMsg)) stunMsg.WriteHeader() t.Run("HandleErr", func(t *testing.T) { badTk := ReservationToken{34, 45} assert.True(t, stun.IsAttrSizeInvalid(badTk.AddTo(stunMsg))) }) t.Run("GetFrom", func(t *testing.T) { decoded := new(stun.Message) _, err := decoded.Write(stunMsg.Raw) assert.NoError(t, err) var tok ReservationToken assert.NoError(t, tok.GetFrom(decoded)) assert.Equal(t, tk, tok) allocated := wasAllocs(func() { assert.NoError(t, tok.GetFrom(decoded)) }) assert.False(t, allocated) t.Run("HandleErr", func(t *testing.T) { m := new(stun.Message) var handle ReservationToken assert.ErrorIs(t, handle.GetFrom(m), stun.ErrAttributeNotFound) m.Add(stun.AttrReservationToken, []byte{1, 2, 3}) assert.True(t, stun.IsAttrSizeInvalid(handle.GetFrom(m))) }) }) }) } turn-4.1.3/internal/proto/testdata/000077500000000000000000000000001510560755200173055ustar00rootroot00000000000000turn-4.1.3/internal/proto/testdata/01_chromeallocreq.hex000066400000000000000000000014441510560755200233160ustar00rootroot00000000000000000300242112a442626b4a6849664c3630526863802f0016687474703a2f2f6c6f63616c686f73743a333030302f00000019000411000000 011300582112a442626b4a6849664c36305268630009001000000401556e617574686f72697a656400150010356130323039623563623830363130360014000b61312e63796465762e7275758022001a436f7475726e2d342e352e302e33202764616e204569646572272300 0003006c2112a442324e50695a437a4634535034802f0016687474703a2f2f6c6f63616c686f73743a333030302f000000190004110000000006000665726e61646f00000014000b61312e63796465762e7275000015001035613032303962356362383036313036000800145c8743f3b64bec0880cdd8d476d37b801a6c3d33 010300582112a442324e50695a437a4634535034001600080001fb922b1ab211002000080001adb2f49f38ae000d0004000002588022001a436f7475726e2d342e352e302e33202764616e204569646572277475000800145d7e85b767a519ffce91dbf0a96775e370db92e3 turn-4.1.3/internal/proto/testdata/01_chromeallocreq.hex.license000066400000000000000000000001421510560755200247310ustar00rootroot00000000000000SPDX-FileCopyrightText: 2023 The Pion community SPDX-License-Identifier: CC0-1.0turn-4.1.3/internal/proto/testdata/02_chandata.hex000066400000000000000000000024401510560755200220570ustar00rootroot0000000000000040000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a33776c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e001eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442 4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d0403023011310f300d06035504030c06576562525443301e170d3138303831313033353230305a170d3138303931313033353230305a3011310f300d06035504030c065765625254433059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469cfb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd74b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4bc9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a96e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd000000000000001300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e9a19240b3a68b95699546c044f863cf8349bc8046214fefd000000000000001400010116fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed06898c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b turn-4.1.3/internal/proto/testdata/02_chandata.hex.license000066400000000000000000000001421510560755200234750ustar00rootroot00000000000000SPDX-FileCopyrightText: 2023 The Pion community SPDX-License-Identifier: CC0-1.0turn-4.1.3/internal/proto/testdata/fuzz/000077500000000000000000000000001510560755200203035ustar00rootroot00000000000000turn-4.1.3/internal/proto/testdata/fuzz/FuzzChannelData/000077500000000000000000000000001510560755200233245ustar00rootroot00000000000000957e495ca04ae84d31a88d96966c784464d7ccd554158614bd24aa9be2c68c11000066400000000000000000000033661510560755200336740ustar00rootroot00000000000000turn-4.1.3/internal/proto/testdata/fuzz/FuzzChannelDatago test fuzz v1 []byte("@\x00\x00d\x00\x01\x00P!\x12\xa4BE71r/+2+nNzX\x00\x06\x00\tD7X4:3wlY\x00\x00\x00\xc0W\x00\x04\x00\x00\x03\xe7\x80*\x00\b\x1dQ6ڶ[\x16\x93\x00%\x00\x00\x00$\x00\x04n\x00\x1e\xff\x00\b\x00\x14e\xd1\x1a3\x0e\x10J\x9f_Y\x8a\xf4\xabƨ\x05\xf2`\x03π(\x00\x04k3DB@\x00\x02#\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x11\x01,\v\x00\x01 \x00\x01\x00\x00\x00\x00\x01 \x00\x01\x1d\x00\x01\x1a0\x82\x01\x160\x81\xbd\xa0\x03\x02\x01\x02\x02\t\x00\xaf\xe5(q4\v\xd1>0\n\x06\b*\x86H\xce=\x04\x03\x020\x111\x0f0\r\x06\x03U\x04\x03\f\x06WebRTC0\x1e\x17\r180811035200Z\x17\r180911035200Z0\x111\x0f0\r\x06\x03U\x04\x03\f\x06WebRTC0Y0\x13\x06\a*\x86H\xce=\x02\x01\x06\b*\x86H\xce=\x03\x01\a\x03B\x00\x04\x80\x80\xe3H\xbdAF\x9c\xfbz}\xf3\x16go\xd7*\x06!\x17e\xa5\n\x0f\v\aRl\x87-π\t>\xd5ʣ\xf5\xa4\nr]\xd7KA\xb7\x9b\xdd\x19\xeec\fS\x13\xc8`\x1di\x83(l\x87\"\xc10\n\x06\b*\x86H\xce=\x04\x03\x02\x03H\x000E\x02!\x00\xd1:\n\x13\x1b©\xf2z\xbd=LT\x7f~\xf1r\x99j\f\aU\xc7\a\xb6\xa3\xe0H\xd8v-\xed\x02 \x05_\xc8\x18(\x18\xa6D\xa3ӵ\xb1W0L\xc3\xf1B\x1f\xad\xb0bc\xbf\xb4Q\xcd(\xbeK\xc9\xee\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x12\x00-\x10\x00\x00!\x00\x02\x00\x00\x00\x00\x00! \xf7\xe2<\x97\xdfE\xa9n\x13\xcb>v\xb3~\xff^s\xe2\xae\xe0\xb6A])D=\v\xd2OW\x8b~\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x13\x00X\x0f\x00\x00L\x00\x03\x00\x00\x00\x00\x00L\x04\x03\x00H0F\x02!\x00\xfd\xbbt걬\xa1S.j\xc0\xab&}[\x83\xa2K\xb4\xd5\xd7\xd5\x04\x93n'\x85\xe6㈲\xbd\x02!\x00\xf6\xa4W\xb9\xed\xd9\xea\xd5*\x9d\x0e\x9a\x19$\v:h\xb9V\x99Tl\x04O\x86<\xf84\x9b\xc8\x04b\x14\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x14\x00\x01\x01\x16\xfe\xfd\x00\x01\x00\x00\x00\x00\x00\x04\x000\x00\x01\x00\x00\x00\x00\x00\x04\n\xae$!\xe7\xd5Ic*}\xef\x8e\xd0h\x98\xc3ŵ?[\x81*\x96:9\xabl\xdd0;y\xbd\xb27\xf31L\x1d\xa2\x1b")a50da1417367eb74b5567c8264828b991bd8cee6e445582860492f99f1b4d676000066400000000000000000000027651510560755200334750ustar00rootroot00000000000000turn-4.1.3/internal/proto/testdata/fuzz/FuzzChannelDatago test fuzz v1 []byte("@\x00\x02#\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x11\x01,\v\x00\x01 \x00\x01\x00\x00\x00\x00\x01 \x00\x01\x1d\x00\x01\x1a0\x82\x01\x160\x81\xbd\xa0\x03\x02\x01\x02\x02\t\x00\xaf\xe5(q4\v\xd1>0\n\x06\b*\x86H\xce=\x04\x03\x020\x111\x0f0\r\x06\x03U\x04\x03\f\x06WebRTC0\x1e\x17\r180811035200Z\x17\r180911035200Z0\x111\x0f0\r\x06\x03U\x04\x03\f\x06WebRTC0Y0\x13\x06\a*\x86H\xce=\x02\x01\x06\b*\x86H\xce=\x03\x01\a\x03B\x00\x04\x80\x80\xe3H\xbdAF\x9c\xfbz}\xf3\x16go\xd7*\x06!\x17e\xa5\n\x0f\v\aRl\x87-π\t>\xd5ʣ\xf5\xa4\nr]\xd7KA\xb7\x9b\xdd\x19\xeec\fS\x13\xc8`\x1di\x83(l\x87\"\xc10\n\x06\b*\x86H\xce=\x04\x03\x02\x03H\x000E\x02!\x00\xd1:\n\x13\x1b©\xf2z\xbd=LT\x7f~\xf1r\x99j\f\aU\xc7\a\xb6\xa3\xe0H\xd8v-\xed\x02 \x05_\xc8\x18(\x18\xa6D\xa3ӵ\xb1W0L\xc3\xf1B\x1f\xad\xb0bc\xbf\xb4Q\xcd(\xbeK\xc9\xee\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x12\x00-\x10\x00\x00!\x00\x02\x00\x00\x00\x00\x00! \xf7\xe2<\x97\xdfE\xa9n\x13\xcb>v\xb3~\xff^s\xe2\xae\xe0\xb6A])D=\v\xd2OW\x8b~\x16\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x13\x00X\x0f\x00\x00L\x00\x03\x00\x00\x00\x00\x00L\x04\x03\x00H0F\x02!\x00\xfd\xbbt걬\xa1S.j\xc0\xab&}[\x83\xa2K\xb4\xd5\xd7\xd5\x04\x93n'\x85\xe6㈲\xbd\x02!\x00\xf6\xa4W\xb9\xed\xd9\xea\xd5*\x9d\x0e\x9a\x19$\v:h\xb9V\x99Tl\x04O\x86<\xf84\x9b\xc8\x04b\x14\xfe\xfd\x00\x00\x00\x00\x00\x00\x00\x14\x00\x01\x01\x16\xfe\xfd\x00\x01\x00\x00\x00\x00\x00\x04\x000\x00\x01\x00\x00\x00\x00\x00\x04\n\xae$!\xe7\xd5Ic*}\xef\x8e\xd0h\x98\xc3ŵ?[\x81*\x96:9\xabl\xdd0;y\xbd\xb27\xf31L\x1d\xa2\x1b")c1c1d815dfbe98c11d89ee9fb0436ede58f3451a3a5338cd4e0f6b1122fd600f000066400000000000000000000000341510560755200342620ustar00rootroot00000000000000turn-4.1.3/internal/proto/testdata/fuzz/FuzzChannelDatago test fuzz v1 []byte("*") turn-4.1.3/internal/server/000077500000000000000000000000001510560755200156375ustar00rootroot00000000000000turn-4.1.3/internal/server/base36.go000066400000000000000000000024451510560755200172560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "math/big" "strings" ) // Base36 alphabet for encoding. const base36Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" // EncodeBase36 converts bytes to base36 string using big.Int for arbitrary length. func encodeBase36(data []byte) string { if len(data) == 0 { return "" } num := new(big.Int).SetBytes(data) if num.Cmp(big.NewInt(0)) == 0 { return "0" } base := big.NewInt(36) buf := make([]byte, 0, len(data)*2) remainder := new(big.Int) for num.Cmp(big.NewInt(0)) > 0 { num.DivMod(num, base, remainder) buf = append(buf, base36Alphabet[remainder.Int64()]) } for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { buf[i], buf[j] = buf[j], buf[i] } return string(buf) } // DecodeBase36 converts base36 string back to bytes using big.Int for arbitrary length. func decodeBase36(encoded string) []byte { if encoded == "" { return []byte{} } if encoded == "0" { return []byte{0} } num := big.NewInt(0) base := big.NewInt(36) for _, char := range strings.ToUpper(encoded) { digit := strings.IndexRune(base36Alphabet, char) if digit == -1 { return nil } num.Mul(num, base) num.Add(num, big.NewInt(int64(digit))) } return num.Bytes() } turn-4.1.3/internal/server/base36_test.go000066400000000000000000000026561510560755200203210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "strings" "testing" "github.com/stretchr/testify/assert" ) func TestBase36(t *testing.T) { for _, tt := range []struct { name string input []byte expected string }{ { name: "empty input", input: []byte{}, expected: "", }, { name: "zero byte", input: []byte{0}, expected: "0", }, { name: "single byte - 1", input: []byte{1}, expected: "1", }, { name: "single byte - 35", input: []byte{35}, expected: "Z", }, { name: "single byte - 36", input: []byte{36}, expected: "10", }, { name: "single byte - 255", input: []byte{255}, expected: "73", }, { name: "multiple bytes - hello", input: []byte("hello"), expected: "5PZCSZU7", }, { name: "multiple bytes - test", input: []byte("test"), expected: "WANEK4", }, { name: "complex text", input: []byte("long_complexTeXT wITH_space"), expected: "6XMY2Y5EIZEF867E5LXYHH2OVVURC1A852VPOAZP0L", }, } { t.Run(tt.name, func(t *testing.T) { encoded := encodeBase36(tt.input) assert.Equal(t, tt.expected, encoded) decoded := decodeBase36(encoded) assert.Equal(t, tt.input, decoded) decoded = decodeBase36(strings.ToLower(encoded)) assert.Equal(t, tt.input, decoded) }) } } turn-4.1.3/internal/server/errors.go000066400000000000000000000035671510560755200175150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import "errors" var ( errFailedToGenerateNonce = errors.New("failed to generate nonce") errInvalidNonce = errors.New("invalid nonce") errFailedToSendError = errors.New("failed to send error message") errNoSuchUser = errors.New("no such user exists") errUnexpectedClass = errors.New("unexpected class") errUnexpectedMethod = errors.New("unexpected method") errFailedToHandle = errors.New("failed to handle") errUnhandledSTUNPacket = errors.New("unhandled STUN packet") errUnableToHandleChannelData = errors.New("unable to handle ChannelData") errFailedToCreateSTUNPacket = errors.New("failed to create stun message from packet") errFailedToCreateChannelData = errors.New("failed to create channel data from packet") errRelayAlreadyAllocatedForFiveTuple = errors.New("relay already allocated for 5-TUPLE") errUnsupportedTransportProtocol = errors.New("RequestedTransport must be UDP or TCP") errNoDontFragmentSupport = errors.New("no support for DONT-FRAGMENT") errRequestWithReservationTokenAndEvenPort = errors.New("Request must not contain RESERVATION-TOKEN and EVEN-PORT") errNoAllocationFound = errors.New("no allocation found") errNoPermission = errors.New("unable to handle send-indication, no permission added") errShortWrite = errors.New("packet write smaller than packet") errNoSuchChannelBind = errors.New("no such channel bind") errFailedWriteSocket = errors.New("failed writing to socket") ) turn-4.1.3/internal/server/nonce.go000066400000000000000000000033361510560755200172750ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" "time" ) const ( nonceLifetime = time.Hour // See: https://tools.ietf.org/html/rfc5766#section-4 nonceLength = 40 nonceKeyLength = 64 ) // NewNonceHash creates a NonceHash. func NewNonceHash() (NonceManager, error) { key := make([]byte, nonceKeyLength) if _, err := rand.Read(key); err != nil { return nil, err } return &NonceHash{key}, nil } // NonceHash is used to create and verify nonces. type NonceHash struct { key []byte } // Generate a nonce. func (n *NonceHash) Generate() (string, error) { nonce := make([]byte, 8, nonceLength) binary.BigEndian.PutUint64(nonce, uint64(time.Now().UnixMilli())) // nolint:gosec // G115 hash := hmac.New(sha256.New, n.key) if _, err := hash.Write(nonce[:8]); err != nil { return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint } nonce = hash.Sum(nonce) return hex.EncodeToString(nonce), nil } // Validate checks that nonce is signed and is not expired. func (n *NonceHash) Validate(nonce string) error { b, err := hex.DecodeString(nonce) if err != nil || len(b) != nonceLength { return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint } if ts := time.UnixMilli(int64(binary.BigEndian.Uint64(b))); time.Since(ts) > nonceLifetime { // nolint:gosec // G115 return errInvalidNonce } hash := hmac.New(sha256.New, n.key) if _, err = hash.Write(b[:8]); err != nil { return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint } if !hmac.Equal(b[8:], hash.Sum(nil)) { return errInvalidNonce } return nil } turn-4.1.3/internal/server/nonce_test.go000066400000000000000000000014551510560755200203340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestNonceHash(t *testing.T) { t.Run("generated hashes validate", func(t *testing.T) { h, err := NewNonceHash() assert.NoError(t, err) nonce, err := h.Generate() assert.NoError(t, err) assert.NoError(t, h.Validate(nonce)) }) t.Run("generated short hashes validate", func(t *testing.T) { for i := 1; i <= 8; i++ { h, err := NewShortNonceHash(i * 4) assert.NoError(t, err, fmt.Sprintf("nonce init at size %d", i*4)) nonce, err := h.Generate() assert.NoError(t, err, fmt.Sprintf("generate at size %d", i*4)) assert.NoError(t, h.Validate(nonce), fmt.Sprintf("decode at size %d", i*4)) } }) } turn-4.1.3/internal/server/server.go000066400000000000000000000064441510560755200175040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package server implements the private API to implement a TURN server package server import ( "fmt" "net" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" ) // Request contains all the state needed to process a single incoming datagram. type Request struct { // Current Request State Conn net.PacketConn SrcAddr net.Addr Buff []byte // Server State AllocationManager *allocation.Manager NonceHash NonceManager // User Configuration AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool) // Quota Handler QuotaHandler func(username string, realm string, srcAddr net.Addr) (ok bool) Log logging.LeveledLogger Realm string ChannelBindTimeout time.Duration } // HandleRequest processes the give Request. func HandleRequest(r Request) error { r.Log.Debugf("Received %d bytes of udp from %s on %s", len(r.Buff), r.SrcAddr, r.Conn.LocalAddr()) if proto.IsChannelData(r.Buff) { return handleDataPacket(r) } return handleTURNPacket(r) } func handleDataPacket(req Request) error { req.Log.Debugf("Received DataPacket from %s", req.SrcAddr.String()) c := proto.ChannelData{Raw: req.Buff} if err := c.Decode(); err != nil { return fmt.Errorf("%w: %v", errFailedToCreateChannelData, err) //nolint:errorlint } err := handleChannelData(req, &c) if err != nil { err = fmt.Errorf("%w from %v: %v", errUnableToHandleChannelData, req.SrcAddr, err) //nolint:errorlint } return err } func handleTURNPacket(req Request) error { req.Log.Debug("Handling TURN packet") stunMsg := &stun.Message{Raw: append([]byte{}, req.Buff...)} if err := stunMsg.Decode(); err != nil { // nolint:errorlint return fmt.Errorf("%w: %v", errFailedToCreateSTUNPacket, err) } handler, err := getMessageHandler(stunMsg.Type.Class, stunMsg.Type.Method) if err != nil { // nolint:errorlint return fmt.Errorf( "%w %v-%v from %v: %v", errUnhandledSTUNPacket, stunMsg.Type.Method, stunMsg.Type.Class, req.SrcAddr, err, ) } err = handler(req, stunMsg) if err != nil { // nolint:errorlint return fmt.Errorf( "%w %v-%v from %v: %v", errFailedToHandle, stunMsg.Type.Method, stunMsg.Type.Class, req.SrcAddr, err, ) } return nil } func getMessageHandler(class stun.MessageClass, method stun.Method) ( // nolint:cyclop func(req Request, stunMsg *stun.Message) error, error, ) { switch class { case stun.ClassIndication: switch method { case stun.MethodSend: return handleSendIndication, nil default: return nil, fmt.Errorf("%w: %s", errUnexpectedMethod, method) } case stun.ClassRequest: switch method { case stun.MethodAllocate: return handleAllocateRequest, nil case stun.MethodRefresh: return handleRefreshRequest, nil case stun.MethodCreatePermission: return handleCreatePermissionRequest, nil case stun.MethodChannelBind: return handleChannelBindRequest, nil case stun.MethodBinding: return handleBindingRequest, nil default: return nil, fmt.Errorf("%w: %s", errUnexpectedMethod, method) } default: return nil, fmt.Errorf("%w: %s", errUnexpectedClass, class) } } turn-4.1.3/internal/server/short_nonce.go000066400000000000000000000075451510560755200205220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "fmt" "time" ) // NonceManager interface that both implementations satisfy. type NonceManager interface { Generate() (string, error) Validate(nonce string) error } const ( shortNonceLifetime = time.Hour // Same as original shortNonceKeyLength = 64 // Same as original shortNonceTimestampLen = 4 // 6 bytes for timestamp (minutes) - optimal size shortNonceMinHMACLen = 2 // Minimum HMAC length for security shortNonceMaxHMACLen = 32 // Maximum HMAC length (full SHA256) defaultNonceHMACLen = 12 // Default HMAC length ) // NewShortNonceHash creates a ShortNonceHash. The hmacLen argument specifies the number of HMAC // bytes to include (2-32 bytes). The total nonce size will be 4 + hmacLen bytes, default hmaclen // is 12 bytes. The 4 bytes timestamp gives about ~8000 years before nonces would start to repeat // (safe until year 10,135). func NewShortNonceHash(hmacLen int) (NonceManager, error) { if hmacLen == 0 { hmacLen = defaultNonceHMACLen } if hmacLen < shortNonceMinHMACLen || hmacLen > shortNonceMaxHMACLen { return nil, errFailedToGenerateNonce } key := make([]byte, shortNonceKeyLength) if _, err := rand.Read(key); err != nil { return nil, fmt.Errorf("%w: %w", errFailedToGenerateNonce, err) } return &ShortNonceHash{ key: key, hmacLen: hmacLen, }, nil } // ShortNonceHash is used to create and verify short nonces. type ShortNonceHash struct { key []byte hmacLen int } // Generate a short nonce (4 + hmacLen bytes encoded as base36). func (s *ShortNonceHash) Generate() (string, error) { timestampMinutes := time.Now().Unix() / 60 // Convert to bytes and trim to 4 bytes. This safely handles the conversion since we know // current values fit in 4 bytes until year 10,135. timestampBytes8 := make([]byte, 8) binary.BigEndian.PutUint64(timestampBytes8, uint64(timestampMinutes)) // nolint:gosec // G115 timestampBytes := timestampBytes8[4:] hash := hmac.New(sha256.New, s.key) if _, err := hash.Write(timestampBytes); err != nil { return "", fmt.Errorf("%w: %w", errFailedToGenerateNonce, err) } fullHMAC := hash.Sum(nil) truncatedHMAC := fullHMAC[:s.hmacLen] totalLen := shortNonceTimestampLen + s.hmacLen nonce := make([]byte, totalLen) copy(nonce[:shortNonceTimestampLen], timestampBytes) copy(nonce[shortNonceTimestampLen:], truncatedHMAC) return encodeBase36(nonce), nil } // Validate checks that nonce is signed and is not expired. func (s *ShortNonceHash) Validate(nonce string) error { nonceBytes := decodeBase36(nonce) if nonceBytes == nil { return errInvalidNonce } expectedLen := shortNonceTimestampLen + s.hmacLen if len(nonceBytes) != expectedLen { // Pad with leadnign zeros if leading zeros were stripped during encoding/decoding. if len(nonceBytes) < expectedLen { padded := make([]byte, expectedLen) copy(padded[expectedLen-len(nonceBytes):], nonceBytes) nonceBytes = padded } else { return errInvalidNonce } } timestampBytes := nonceBytes[:shortNonceTimestampLen] receivedHMAC := nonceBytes[shortNonceTimestampLen:] timestampMinutes := int64(binary.BigEndian.Uint32(timestampBytes)) // Check if nonce is expired (older than 1 hour). currentMinutes := time.Now().Unix() / 60 if currentMinutes < timestampMinutes { return errInvalidNonce } ageMinutes := currentMinutes - timestampMinutes if ageMinutes > 60 { return errInvalidNonce } // Recompute HMAC and compare. hash := hmac.New(sha256.New, s.key) if _, err := hash.Write(timestampBytes); err != nil { return fmt.Errorf("%w: %w", errInvalidNonce, err) } expectedHMAC := hash.Sum(nil)[:s.hmacLen] if !hmac.Equal(receivedHMAC, expectedHMAC) { return errInvalidNonce } return nil } turn-4.1.3/internal/server/stun.go000066400000000000000000000011401510560755200171530ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/ipnet" ) func handleBindingRequest(req Request, stunMsg *stun.Message) error { req.Log.Debugf("Received BindingRequest from %s", req.SrcAddr) ip, port, err := ipnet.AddrIPPort(req.SrcAddr) if err != nil { return err } attrs := buildMsg(stunMsg.TransactionID, stun.BindingSuccess, &stun.XORMappedAddress{ IP: ip, Port: port, }, stun.Fingerprint) return buildAndSend(req.Conn, req.SrcAddr, attrs...) } turn-4.1.3/internal/server/turn.go000066400000000000000000000356751510560755200171760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "fmt" "net" "github.com/pion/randutil" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/ipnet" "github.com/pion/turn/v4/internal/proto" ) const runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // See: https://tools.ietf.org/html/rfc5766#section-6.2 // . func handleAllocateRequest(req Request, stunMsg *stun.Message) error { //nolint:cyclop req.Log.Debugf("Received AllocateRequest from %s", req.SrcAddr) // 1. The server MUST require that the request be authenticated. This // authentication MUST be done using the long-term credential // mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2] // unless the client and server agree to use another mechanism through // some procedure outside the scope of this document. messageIntegrity, hasAuth, err := authenticateRequest(req, stunMsg, stun.MethodAllocate) if !hasAuth { return err } fiveTuple := &allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, } requestedPort := 0 reservationToken := "" badRequestMsg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest}, ) insufficientCapacityMsg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeInsufficientCapacity}, ) // 2. The server checks if the 5-tuple is currently in use by an // existing allocation. If yes, the server rejects the request with // a 437 (Allocation Mismatch) error. if alloc := req.AllocationManager.GetAllocation(fiveTuple); alloc != nil { id, attrs := alloc.GetResponseCache() if id != stunMsg.TransactionID { msg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeAllocMismatch}, ) return buildAndSendErr(req.Conn, req.SrcAddr, errRelayAlreadyAllocatedForFiveTuple, msg...) } // A retry allocation msg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassSuccessResponse), append(attrs, messageIntegrity)..., ) return buildAndSend(req.Conn, req.SrcAddr, msg...) } // 3. The server checks if the request contains a REQUESTED-TRANSPORT // attribute. If the REQUESTED-TRANSPORT attribute is not included // or is malformed, the server rejects the request with a 400 (Bad // Request) error. Otherwise, if the attribute is included but // specifies a protocol other that UDP/TCP, the server rejects the // request with a 442 (Unsupported Transport Protocol) error. var requestedTransport proto.RequestedTransport if err = requestedTransport.GetFrom(stunMsg); err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } else if requestedTransport.Protocol != proto.ProtoUDP && requestedTransport.Protocol != proto.ProtoTCP { msg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnsupportedTransProto}, ) return buildAndSendErr(req.Conn, req.SrcAddr, errUnsupportedTransportProtocol, msg...) } // 4. The request may contain a DONT-FRAGMENT attribute. If it does, // but the server does not support sending UDP datagrams with the DF // bit set to 1 (see Section 12), then the server treats the DONT- // FRAGMENT attribute in the Allocate request as an unknown // comprehension-required attribute. if stunMsg.Contains(stun.AttrDontFragment) { msg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnknownAttribute}, &stun.UnknownAttributes{stun.AttrDontFragment}, ) return buildAndSendErr(req.Conn, req.SrcAddr, errNoDontFragmentSupport, msg...) } // 5. The server checks if the request contains a RESERVATION-TOKEN // attribute. If yes, and the request also contains an EVEN-PORT // attribute, then the server rejects the request with a 400 (Bad // Request) error. Otherwise, it checks to see if the token is // valid (i.e., the token is in range and has not expired and the // corresponding relayed transport address is still available). If // the token is not valid for some reason, the server rejects the // request with a 508 (Insufficient Capacity) error. var reservationTokenAttr proto.ReservationToken if err = reservationTokenAttr.GetFrom(stunMsg); err == nil { var evenPort proto.EvenPort if err = evenPort.GetFrom(stunMsg); err == nil { return buildAndSendErr(req.Conn, req.SrcAddr, errRequestWithReservationTokenAndEvenPort, badRequestMsg...) } } // 6. The server checks if the request contains an EVEN-PORT attribute. // If yes, then the server checks that it can satisfy the request // (i.e., can allocate a relayed transport address as described // below). If the server cannot satisfy the request, then the // server rejects the request with a 508 (Insufficient Capacity) // error. var evenPort proto.EvenPort if err = evenPort.GetFrom(stunMsg); err == nil { var randomPort int randomPort, err = req.AllocationManager.GetRandomEvenPort() if err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, insufficientCapacityMsg...) } requestedPort = randomPort reservationToken, err = randutil.GenerateCryptoRandomString(8, runesAlpha) if err != nil { return err } } // Parse realm and username (already checked in authenticateRequest) realmAttr := &stun.Realm{} _ = realmAttr.GetFrom(stunMsg) usernameAttr := &stun.Username{} _ = usernameAttr.GetFrom(stunMsg) // 7. At any point, the server MAY choose to reject the request with a // 486 (Allocation Quota Reached) error if it feels the client is // trying to exceed some locally defined allocation quota. The // server is free to define this allocation quota any way it wishes, // but SHOULD define it based on the username used to authenticate // the request, and not on the client's transport address. if req.QuotaHandler != nil && !req.QuotaHandler(usernameAttr.String(), realmAttr.String(), req.SrcAddr) { quotaReachedMsg := buildMsg(stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeAllocQuotaReached}) return buildAndSend(req.Conn, req.SrcAddr, quotaReachedMsg...) } // 8. Also at any point, the server MAY choose to reject the request // with a 300 (Try Alternate) error if it wishes to redirect the // client to a different server. The use of this error code and // attribute follow the specification in [RFC5389]. lifetimeDuration := allocationLifeTime(stunMsg) alloc, err := req.AllocationManager.CreateAllocation( fiveTuple, req.Conn, requestedPort, lifetimeDuration, usernameAttr.String(), realmAttr.String(), ) if err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, insufficientCapacityMsg...) } // Once the allocation is created, the server replies with a success // response. // The success response contains: // * An XOR-RELAYED-ADDRESS attribute containing the relayed transport // address. // * A LIFETIME attribute containing the current value of the time-to- // expiry timer. // * A RESERVATION-TOKEN attribute (if a second relayed transport // address was reserved). // * An XOR-MAPPED-ADDRESS attribute containing the client's IP address // and port (from the 5-tuple). srcIP, srcPort, err := ipnet.AddrIPPort(req.SrcAddr) if err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } relayIP, relayPort, err := ipnet.AddrIPPort(alloc.RelayAddr) if err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } responseAttrs := []stun.Setter{ &proto.RelayedAddress{ IP: relayIP, Port: relayPort, }, &proto.Lifetime{ Duration: lifetimeDuration, }, &stun.XORMappedAddress{ IP: srcIP, Port: srcPort, }, } if reservationToken != "" { req.AllocationManager.CreateReservation(reservationToken, relayPort) responseAttrs = append(responseAttrs, proto.ReservationToken([]byte(reservationToken))) } msg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassSuccessResponse), append(responseAttrs, messageIntegrity)..., ) alloc.SetResponseCache(stunMsg.TransactionID, responseAttrs) return buildAndSend(req.Conn, req.SrcAddr, msg...) } func handleRefreshRequest(req Request, stunMsg *stun.Message) error { req.Log.Debugf("Received RefreshRequest from %s", req.SrcAddr) messageIntegrity, hasAuth, err := authenticateRequest(req, stunMsg, stun.MethodRefresh) if !hasAuth { return err } lifetimeDuration := allocationLifeTime(stunMsg) fiveTuple := &allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, } if lifetimeDuration != 0 { a := req.AllocationManager.GetAllocation(fiveTuple) if a == nil { return fmt.Errorf("%w %v:%v", errNoAllocationFound, req.SrcAddr, req.Conn.LocalAddr()) } a.Refresh(lifetimeDuration) } else { req.AllocationManager.DeleteAllocation(fiveTuple) } return buildAndSend( req.Conn, req.SrcAddr, buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodRefresh, stun.ClassSuccessResponse), []stun.Setter{ &proto.Lifetime{ Duration: lifetimeDuration, }, messageIntegrity, }..., )..., ) } func handleCreatePermissionRequest(req Request, stunMsg *stun.Message) error { req.Log.Debugf("Received CreatePermission from %s", req.SrcAddr) alloc := req.AllocationManager.GetAllocation(&allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, }) if alloc == nil { return fmt.Errorf("%w %v:%v", errNoAllocationFound, req.SrcAddr, req.Conn.LocalAddr()) } messageIntegrity, hasAuth, err := authenticateRequest(req, stunMsg, stun.MethodCreatePermission) if !hasAuth { return err } addCount := 0 if err := stunMsg.ForEach(stun.AttrXORPeerAddress, func(m *stun.Message) error { var peerAddress proto.PeerAddress if err := peerAddress.GetFrom(m); err != nil { return err } if err := req.AllocationManager.GrantPermission(req.SrcAddr, peerAddress.IP); err != nil { req.Log.Infof("permission denied for client %s to peer %s", req.SrcAddr, peerAddress.IP) return err } req.Log.Debugf("Adding permission for %s", fmt.Sprintf("%s:%d", peerAddress.IP, peerAddress.Port)) alloc.AddPermission(allocation.NewPermission( &net.UDPAddr{ IP: peerAddress.IP, Port: peerAddress.Port, }, req.Log, )) addCount++ return nil }); err != nil { addCount = 0 } respClass := stun.ClassSuccessResponse if addCount == 0 { respClass = stun.ClassErrorResponse } return buildAndSend( req.Conn, req.SrcAddr, buildMsg(stunMsg.TransactionID, stun.NewType(stun.MethodCreatePermission, respClass), []stun.Setter{messageIntegrity}...)..., ) } func handleSendIndication(req Request, stunMsg *stun.Message) error { req.Log.Debugf("Received SendIndication from %s", req.SrcAddr) alloc := req.AllocationManager.GetAllocation(&allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, }) if alloc == nil { return fmt.Errorf("%w %v:%v", errNoAllocationFound, req.SrcAddr, req.Conn.LocalAddr()) } dataAttr := proto.Data{} if err := dataAttr.GetFrom(stunMsg); err != nil { return err } peerAddress := proto.PeerAddress{} if err := peerAddress.GetFrom(stunMsg); err != nil { return err } msgDst := &net.UDPAddr{IP: peerAddress.IP, Port: peerAddress.Port} if perm := alloc.GetPermission(msgDst); perm == nil { return fmt.Errorf("%w: %v", errNoPermission, msgDst) } l, err := alloc.RelaySocket.WriteTo(dataAttr, msgDst) if err != nil { return fmt.Errorf("%w: %s", errFailedWriteSocket, err.Error()) } else if l != len(dataAttr) { return fmt.Errorf("%w %d != %d (expected)", errShortWrite, l, len(dataAttr)) } return err } func handleChannelBindRequest(req Request, stunMsg *stun.Message) error { req.Log.Debugf("Received ChannelBindRequest from %s", req.SrcAddr) alloc := req.AllocationManager.GetAllocation(&allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, }) if alloc == nil { return fmt.Errorf("%w %v:%v", errNoAllocationFound, req.SrcAddr, req.Conn.LocalAddr()) } badRequestMsg := buildMsg( stunMsg.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest}, ) messageIntegrity, hasAuth, err := authenticateRequest(req, stunMsg, stun.MethodChannelBind) if !hasAuth { return err } var channel proto.ChannelNumber if err = channel.GetFrom(stunMsg); err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } peerAddr := proto.PeerAddress{} if err = peerAddr.GetFrom(stunMsg); err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } if err = req.AllocationManager.GrantPermission(req.SrcAddr, peerAddr.IP); err != nil { req.Log.Infof("permission denied for client %s to peer %s", req.SrcAddr, peerAddr.IP) unauthorizedRequestMsg := buildMsg(stunMsg.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnauthorized}) return buildAndSendErr(req.Conn, req.SrcAddr, err, unauthorizedRequestMsg...) } req.Log.Debugf("Binding channel %d to %s", channel, peerAddr) err = alloc.AddChannelBind(allocation.NewChannelBind( channel, &net.UDPAddr{IP: peerAddr.IP, Port: peerAddr.Port}, req.Log, ), req.ChannelBindTimeout) if err != nil { return buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } return buildAndSend( req.Conn, req.SrcAddr, buildMsg(stunMsg.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassSuccessResponse), []stun.Setter{messageIntegrity}...)..., ) } func handleChannelData(req Request, channelData *proto.ChannelData) error { req.Log.Debugf("Received ChannelData from %s", req.SrcAddr) alloc := req.AllocationManager.GetAllocation(&allocation.FiveTuple{ SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP, }) if alloc == nil { return fmt.Errorf("%w %v:%v", errNoAllocationFound, req.SrcAddr, req.Conn.LocalAddr()) } channel := alloc.GetChannelByNumber(channelData.Number) if channel == nil { return fmt.Errorf("%w %x", errNoSuchChannelBind, uint16(channelData.Number)) } l, err := alloc.RelaySocket.WriteTo(channelData.Data, channel.Peer) if err != nil { return fmt.Errorf("%w: %s", errFailedWriteSocket, err.Error()) } else if l != len(channelData.Data) { return fmt.Errorf("%w %d != %d (expected)", errShortWrite, l, len(channelData.Data)) } return nil } turn-4.1.3/internal/server/turn_test.go000066400000000000000000000062141510560755200202200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package server import ( "net" "testing" "time" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) func TestAllocationLifeTime(t *testing.T) { t.Run("Parsing", func(t *testing.T) { lifetime := proto.Lifetime{ Duration: 5 * time.Second, } m := &stun.Message{} lifetimeDuration := allocationLifeTime(m) assert.Equal(t, proto.DefaultLifetime, lifetimeDuration, "Allocation lifetime should be default time duration") assert.NoError(t, lifetime.AddTo(m)) lifetimeDuration = allocationLifeTime(m) assert.Equal(t, lifetime.Duration, lifetimeDuration, "Allocation lifetime should be equal to the one set in the message") }) // If lifetime is bigger than maximumLifetime t.Run("Overflow", func(t *testing.T) { lifetime := proto.Lifetime{ Duration: maximumAllocationLifetime * 2, } m2 := &stun.Message{} _ = lifetime.AddTo(m2) lifetimeDuration := allocationLifeTime(m2) assert.Equal(t, proto.DefaultLifetime, lifetimeDuration) }) t.Run("DeletionZeroLifetime", func(t *testing.T) { conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) defer func() { assert.NoError(t, conn.Close()) }() logger := logging.NewDefaultLoggerFactory().NewLogger("turn") allocationManager, err := allocation.NewManager(allocation.ManagerConfig{ AllocatePacketConn: func(network string, _ int) (net.PacketConn, net.Addr, error) { con, listenErr := net.ListenPacket(network, "0.0.0.0:0") // nolint: noctx if err != nil { return nil, nil, listenErr } return con, con.LocalAddr(), nil }, AllocateConn: func(string, int) (net.Conn, net.Addr, error) { return nil, nil, nil }, LeveledLogger: logger, }) assert.NoError(t, err) nonceHash, err := NewShortNonceHash(0) assert.NoError(t, err) staticKey, err := nonceHash.Generate() assert.NoError(t, err) req := Request{ AllocationManager: allocationManager, NonceHash: nonceHash, Conn: conn, SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000}, Log: logger, AuthHandler: func(string, string, net.Addr) (key []byte, ok bool) { return []byte(staticKey), true }, } fiveTuple := &allocation.FiveTuple{SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP} _, err = req.AllocationManager.CreateAllocation(fiveTuple, req.Conn, 0, time.Hour, "", "") assert.NoError(t, err) assert.NotNil(t, req.AllocationManager.GetAllocation(fiveTuple)) m := &stun.Message{} assert.NoError(t, (proto.Lifetime{}).AddTo(m)) assert.NoError(t, (stun.MessageIntegrity(staticKey)).AddTo(m)) assert.NoError(t, (stun.Nonce(staticKey)).AddTo(m)) assert.NoError(t, (stun.Realm(staticKey)).AddTo(m)) assert.NoError(t, (stun.Username(staticKey)).AddTo(m)) assert.NoError(t, handleRefreshRequest(req, m)) assert.Nil(t, req.AllocationManager.GetAllocation(fiveTuple)) }) } turn-4.1.3/internal/server/util.go000066400000000000000000000107311510560755200171450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package server import ( "errors" "fmt" "net" "time" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/proto" ) const ( // See: https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation. maximumAllocationLifetime = time.Hour ) func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error { msg, err := stun.Build(attrs...) if err != nil { return err } _, err = conn.WriteTo(msg.Raw, dst) if errors.Is(err, net.ErrClosed) { return nil } return err } // Send a STUN packet and return the original error to the caller. func buildAndSendErr(conn net.PacketConn, dst net.Addr, err error, attrs ...stun.Setter) error { if sendErr := buildAndSend(conn, dst, attrs...); sendErr != nil { err = fmt.Errorf("%w %v %v", errFailedToSendError, sendErr, err) //nolint:errorlint } return err } func buildMsg( transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter, ) []stun.Setter { return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...) } func authenticateRequest(req Request, stunMsg *stun.Message, callingMethod stun.Method) ( stun.MessageIntegrity, bool, error, ) { respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) { nonce, err := req.NonceHash.Generate() if err != nil { return nil, false, err } return nil, false, buildAndSend(req.Conn, req.SrcAddr, buildMsg(stunMsg.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: responseCode}, stun.NewNonce(nonce), stun.NewRealm(req.Realm), )...) } if !stunMsg.Contains(stun.AttrMessageIntegrity) { return respondWithNonce(stun.CodeUnauthorized) } nonceAttr := &stun.Nonce{} usernameAttr := &stun.Username{} realmAttr := &stun.Realm{} badRequestMsg := buildMsg( stunMsg.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest}, ) // No Auth handler is set, server is running in STUN only mode // Respond with 400 so clients don't retry. if req.AuthHandler == nil { sendErr := buildAndSend(req.Conn, req.SrcAddr, badRequestMsg...) return nil, false, sendErr } if err := nonceAttr.GetFrom(stunMsg); err != nil { return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } // Assert Nonce is signed and is not expired. if err := req.NonceHash.Validate(nonceAttr.String()); err != nil { return respondWithNonce(stun.CodeStaleNonce) } if err := realmAttr.GetFrom(stunMsg); err != nil { return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } else if err := usernameAttr.GetFrom(stunMsg); err != nil { return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } ourKey, ok := req.AuthHandler(usernameAttr.String(), realmAttr.String(), req.SrcAddr) if !ok { return nil, false, buildAndSendErr( req.Conn, req.SrcAddr, fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()), badRequestMsg..., ) } if err := stun.MessageIntegrity(ourKey).Check(stunMsg); err != nil { genAuthEvent(req, stunMsg, callingMethod, false) return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...) } genAuthEvent(req, stunMsg, callingMethod, true) return stun.MessageIntegrity(ourKey), true, nil } func genAuthEvent(req Request, stunMsg *stun.Message, callingMethod stun.Method, verdict bool) { if req.AllocationManager.EventHandler.OnAuth == nil { return } realmAttr := &stun.Realm{} if err := realmAttr.GetFrom(stunMsg); err != nil { return } usernameAttr := &stun.Username{} if err := usernameAttr.GetFrom(stunMsg); err != nil { return } transportAttr := &proto.RequestedTransport{} if err := transportAttr.GetFrom(stunMsg); err != nil { transportAttr = &proto.RequestedTransport{Protocol: proto.ProtoUDP} } req.AllocationManager.EventHandler.OnAuth(req.SrcAddr, req.Conn.LocalAddr(), transportAttr.Protocol.String(), usernameAttr.String(), realmAttr.String(), callingMethod.String(), verdict) } func allocationLifeTime(m *stun.Message) time.Duration { lifetimeDuration := proto.DefaultLifetime var lifetime proto.Lifetime if err := lifetime.GetFrom(m); err == nil { if lifetime.Duration < maximumAllocationLifetime { lifetimeDuration = lifetime.Duration } } return lifetimeDuration } turn-4.1.3/lt_cred.go000066400000000000000000000071451510560755200144670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "crypto/hmac" "crypto/sha1" //nolint:gosec,gci "encoding/base64" "net" "strconv" "strings" "time" "github.com/pion/logging" ) // GenerateLongTermCredentials can be used to create credentials valid for [duration] time. func GenerateLongTermCredentials(sharedSecret string, duration time.Duration) (string, string, error) { t := time.Now().Add(duration).Unix() username := strconv.FormatInt(t, 10) password, err := longTermCredentials(username, sharedSecret) return username, password, err } // GenerateLongTermTURNRESTCredentials can be used to create credentials valid for [duration] time. func GenerateLongTermTURNRESTCredentials(sharedSecret string, user string, duration time.Duration) ( string, string, error, ) { t := time.Now().Add(duration).Unix() timestamp := strconv.FormatInt(t, 10) username := timestamp + ":" + user password, err := longTermCredentials(username, sharedSecret) return username, password, err } func longTermCredentials(username string, sharedSecret string) (string, error) { mac := hmac.New(sha1.New, []byte(sharedSecret)) _, err := mac.Write([]byte(username)) if err != nil { return "", err // Not sure if this will ever happen } password := mac.Sum(nil) return base64.StdEncoding.EncodeToString(password), nil } // NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials. // See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2 // . func NewLongTermAuthHandler(sharedSecret string, logger logging.LeveledLogger) AuthHandler { if logger == nil { logger = logging.NewDefaultLoggerFactory().NewLogger("turn") } return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { logger.Tracef("Authentication username=%q realm=%q srcAddr=%v", username, realm, srcAddr) t, err := strconv.Atoi(username) if err != nil { logger.Errorf("Invalid time-windowed username %q", username) return nil, false } if int64(t) < time.Now().Unix() { logger.Errorf("Expired time-windowed username %q", username) return nil, false } password, err := longTermCredentials(username, sharedSecret) if err != nil { logger.Error(err.Error()) return nil, false } return GenerateAuthKey(username, realm, password), true } } // LongTermTURNRESTAuthHandler returns a turn.AuthAuthHandler that can be used to authenticate // time-windowed ephemeral credentials generated by the TURN REST API as described in // https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 // // The supported format of is timestamp:username, where username is an arbitrary user id and the // timestamp specifies the expiry of the credential. func LongTermTURNRESTAuthHandler(sharedSecret string, logger logging.LeveledLogger) AuthHandler { if logger == nil { logger = logging.NewDefaultLoggerFactory().NewLogger("turn") } return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { logger.Tracef("Authentication username=%q realm=%q srcAddr=%v", username, realm, srcAddr) timestamp := strings.Split(username, ":")[0] t, err := strconv.Atoi(timestamp) if err != nil { logger.Errorf("Invalid time-windowed username %q", username) return nil, false } if int64(t) < time.Now().Unix() { logger.Errorf("Expired time-windowed username %q", username) return nil, false } password, err := longTermCredentials(username, sharedSecret) if err != nil { logger.Error(err.Error()) return nil, false } return GenerateAuthKey(username, realm, password), true } } turn-4.1.3/lt_cred_test.go000066400000000000000000000062301510560755200155200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package turn import ( "net" "testing" "time" "github.com/pion/logging" "github.com/stretchr/testify/assert" ) func TestLtCredMech(t *testing.T) { username := "1599491771" sharedSecret := "foobar" expectedPassword := "Tpz/nKkyvX/vMSLKvL4sbtBt8Vs=" //nolint:gosec actualPassword, _ := longTermCredentials(username, sharedSecret) assert.Equal(t, expectedPassword, actualPassword) } func TestNewLongTermAuthHandler(t *testing.T) { const sharedSecret = "HELLO_WORLD" serverListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: NewLongTermAuthHandler(sharedSecret, nil), PacketConnConfigs: []PacketConnConfig{ { PacketConn: serverListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, Realm: "pion.ly", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) username, password, err := GenerateLongTermCredentials(sharedSecret, time.Minute) assert.NoError(t, err) addr := "127.0.0.1:3478" client, err := NewClient(&ClientConfig{ STUNServerAddr: addr, TURNServerAddr: addr, Conn: conn, Username: username, Password: password, LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) client.Close() assert.NoError(t, relayConn.Close()) assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) } func TestLongTermTURNRESTAuthHandler(t *testing.T) { const sharedSecret = "HELLO_WORLD" serverListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: LongTermTURNRESTAuthHandler(sharedSecret, nil), PacketConnConfigs: []PacketConnConfig{ { PacketConn: serverListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, Realm: "pion.ly", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) username, password, err := GenerateLongTermTURNRESTCredentials(sharedSecret, "testuser", time.Minute) assert.NoError(t, err) client, err := NewClient(&ClientConfig{ STUNServerAddr: "127.0.0.1:3478", TURNServerAddr: "127.0.0.1:3478", Conn: conn, Username: username, Password: password, LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) client.Close() assert.NoError(t, relayConn.Close()) assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) } turn-4.1.3/relay_address_generator_none.go000066400000000000000000000027601510560755200207570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "fmt" "net" "strconv" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) // RelayAddressGeneratorNone returns the listener with no modifications. type RelayAddressGeneratorNone struct { // Address is passed to Listen/ListenPacket when creating the Relay Address string Net transport.Net } // Validate is called on server startup and confirms the RelayAddressGenerator is properly configured. func (r *RelayAddressGeneratorNone) Validate() error { if r.Net == nil { var err error r.Net, err = stdnet.NewNet() if err != nil { return fmt.Errorf("failed to create network: %w", err) } } if r.Address == "" { return errListeningAddressInvalid } return nil } // AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorNone) AllocatePacketConn(network string, requestedPort int) ( net.PacketConn, net.Addr, error, ) { conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort)) // nolint: noctx if err != nil { return nil, nil, err } return conn, conn.LocalAddr(), nil } // AllocateConn generates a new Conn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorNone) AllocateConn(string, int) (net.Conn, net.Addr, error) { return nil, nil, errTODO } turn-4.1.3/relay_address_generator_range.go000066400000000000000000000057201510560755200211130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "fmt" "net" "github.com/pion/randutil" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) // RelayAddressGeneratorPortRange can be used to only allocate connections inside a defined port range. // Similar to the RelayAddressGeneratorStatic a static ip address can be set. type RelayAddressGeneratorPortRange struct { // RelayAddress is the IP returned to the user when the relay is created RelayAddress net.IP // MinPort the minimum port to allocate MinPort uint16 // MaxPort the maximum (inclusive) port to allocate MaxPort uint16 // MaxRetries the amount of tries to allocate a random port in the defined range MaxRetries int // Rand the random source of numbers Rand randutil.MathRandomGenerator // Address is passed to Listen/ListenPacket when creating the Relay Address string Net transport.Net } // Validate is called on server startup and confirms the RelayAddressGenerator is properly configured. func (r *RelayAddressGeneratorPortRange) Validate() error { if r.Net == nil { var err error r.Net, err = stdnet.NewNet() if err != nil { return fmt.Errorf("failed to create network: %w", err) } } if r.Rand == nil { r.Rand = randutil.NewMathRandomGenerator() } if r.MaxRetries == 0 { r.MaxRetries = 10 } switch { case r.MinPort == 0: return errMinPortNotZero case r.MaxPort == 0: return errMaxPortNotZero case r.RelayAddress == nil: return errRelayAddressInvalid case r.Address == "": return errListeningAddressInvalid default: return nil } } // AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorPortRange) AllocatePacketConn( network string, requestedPort int, ) (net.PacketConn, net.Addr, error) { if requestedPort != 0 { conn, err := r.Net.ListenPacket(network, fmt.Sprintf("%s:%d", r.Address, requestedPort)) // nolint: noctx if err != nil { return nil, nil, err } relayAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, nil, errNilConn } relayAddr.IP = r.RelayAddress return conn, relayAddr, nil } for try := 0; try < r.MaxRetries; try++ { port := r.MinPort + uint16(r.Rand.Intn(int((r.MaxPort+1)-r.MinPort))) // nolint:gosec // G115 false positive conn, err := r.Net.ListenPacket(network, fmt.Sprintf("%s:%d", r.Address, port)) // nolint: noctx if err != nil { continue } relayAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, nil, errNilConn } relayAddr.IP = r.RelayAddress return conn, relayAddr, nil } return nil, nil, errMaxRetriesExceeded } // AllocateConn generates a new Conn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorPortRange) AllocateConn(string, int) (net.Conn, net.Addr, error) { return nil, nil, errTODO } turn-4.1.3/relay_address_generator_static.go000066400000000000000000000037521510560755200213110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "fmt" "net" "strconv" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) // RelayAddressGeneratorStatic can be used to return static IP address each time a relay is created. // This can be used when you have a single static IP address that you want to use. type RelayAddressGeneratorStatic struct { // RelayAddress is the IP returned to the user when the relay is created RelayAddress net.IP // Address is passed to Listen/ListenPacket when creating the Relay Address string Net transport.Net } // Validate is called on server startup and confirms the RelayAddressGenerator is properly configured. func (r *RelayAddressGeneratorStatic) Validate() error { if r.Net == nil { var err error r.Net, err = stdnet.NewNet() if err != nil { return fmt.Errorf("failed to create network: %w", err) } } switch { case r.RelayAddress == nil: return errRelayAddressInvalid case r.Address == "": return errListeningAddressInvalid default: return nil } } // AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorStatic) AllocatePacketConn( network string, requestedPort int, ) (net.PacketConn, net.Addr, error) { conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort)) // nolint: noctx if err != nil { return nil, nil, err } // Replace actual listening IP with the user requested one of RelayAddressGeneratorStatic relayAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, nil, errNilConn } relayAddr.IP = r.RelayAddress return conn, relayAddr, nil } // AllocateConn generates a new Conn to receive traffic on and the IP/Port // to populate the allocation response with. func (r *RelayAddressGeneratorStatic) AllocateConn(string, int) (net.Conn, net.Addr, error) { return nil, nil, errTODO } turn-4.1.3/renovate.json000066400000000000000000000001731510560755200152340ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } turn-4.1.3/server.go000066400000000000000000000143761510560755200143650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package turn contains the public API for pion/turn, a toolkit for building TURN clients and servers package turn import ( "errors" "fmt" "net" "time" "github.com/pion/logging" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" "github.com/pion/turn/v4/internal/server" ) const ( defaultInboundMTU = 1600 ) // Server is an instance of the Pion TURN Server. type Server struct { log logging.LeveledLogger authHandler AuthHandler quotaHandler QuotaHandler realm string channelBindTimeout time.Duration nonceHash server.NonceManager eventHandler EventHandler packetConnConfigs []PacketConnConfig listenerConfigs []ListenerConfig allocationManagers []*allocation.Manager inboundMTU int } // NewServer creates the Pion TURN server. func NewServer(config ServerConfig) (*Server, error) { //nolint:gocognit,cyclop if err := config.validate(); err != nil { return nil, err } loggerFactory := config.LoggerFactory if loggerFactory == nil { loggerFactory = logging.NewDefaultLoggerFactory() } mtu := defaultInboundMTU if config.InboundMTU != 0 { mtu = config.InboundMTU } nonceHash, err := server.NewShortNonceHash(0) if err != nil { return nil, err } server := &Server{ log: loggerFactory.NewLogger("turn"), authHandler: config.AuthHandler, quotaHandler: config.QuotaHandler, realm: config.Realm, channelBindTimeout: config.ChannelBindTimeout, packetConnConfigs: config.PacketConnConfigs, listenerConfigs: config.ListenerConfigs, nonceHash: nonceHash, inboundMTU: mtu, eventHandler: config.EventHandler, } if server.channelBindTimeout == 0 { server.channelBindTimeout = proto.DefaultLifetime } for _, cfg := range server.packetConnConfigs { am, err := server.createAllocationManager(cfg.RelayAddressGenerator, cfg.PermissionHandler) if err != nil { return nil, fmt.Errorf("failed to create AllocationManager: %w", err) } go func(cfg PacketConnConfig, am *allocation.Manager) { server.readLoop(cfg.PacketConn, am) if err := am.Close(); err != nil { server.log.Errorf("Failed to close AllocationManager: %s", err) } }(cfg, am) } for _, cfg := range server.listenerConfigs { am, err := server.createAllocationManager(cfg.RelayAddressGenerator, cfg.PermissionHandler) if err != nil { return nil, fmt.Errorf("failed to create AllocationManager: %w", err) } go func(cfg ListenerConfig, am *allocation.Manager) { server.readListener(cfg.Listener, am) if err := am.Close(); err != nil { server.log.Errorf("Failed to close AllocationManager: %s", err) } }(cfg, am) } return server, nil } // AllocationCount returns the number of active allocations. // It can be used to drain the server before closing. func (s *Server) AllocationCount() int { allocs := 0 for _, am := range s.allocationManagers { allocs += am.AllocationCount() } return allocs } // Close stops the TURN Server. // It cleans up any associated state and closes all connections it is managing. func (s *Server) Close() error { var errors []error for _, cfg := range s.packetConnConfigs { if err := cfg.PacketConn.Close(); err != nil { errors = append(errors, err) } } for _, cfg := range s.listenerConfigs { if err := cfg.Listener.Close(); err != nil { errors = append(errors, err) } } if len(errors) == 0 { return nil } err := errFailedToClose for _, e := range errors { err = fmt.Errorf("%s; close error (%w) ", err, e) //nolint:errorlint } return err } func (s *Server) readListener(l net.Listener, am *allocation.Manager) { for { conn, err := l.Accept() if err != nil { s.log.Debugf("Failed to accept: %s", err) return } go func() { s.readLoop(NewSTUNConn(conn), am) // Delete allocation am.DeleteAllocation(&allocation.FiveTuple{ Protocol: allocation.UDP, // fixed UDP SrcAddr: conn.RemoteAddr(), DstAddr: conn.LocalAddr(), }) if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { s.log.Errorf("Failed to close conn: %s", err) } }() } } type nilAddressGenerator struct{} func (n *nilAddressGenerator) Validate() error { return errRelayAddressGeneratorNil } func (n *nilAddressGenerator) AllocatePacketConn(string, int) (net.PacketConn, net.Addr, error) { return nil, nil, errRelayAddressGeneratorNil } func (n *nilAddressGenerator) AllocateConn(string, int) (net.Conn, net.Addr, error) { return nil, nil, errRelayAddressGeneratorNil } func (s *Server) createAllocationManager( addrGenerator RelayAddressGenerator, handler PermissionHandler, ) (*allocation.Manager, error) { if handler == nil { handler = DefaultPermissionHandler } if addrGenerator == nil { addrGenerator = &nilAddressGenerator{} } am, err := allocation.NewManager(allocation.ManagerConfig{ AllocatePacketConn: addrGenerator.AllocatePacketConn, AllocateConn: addrGenerator.AllocateConn, PermissionHandler: handler, EventHandler: s.eventHandler, LeveledLogger: s.log, }) if err != nil { return am, err } s.allocationManagers = append(s.allocationManagers, am) return am, err } func (s *Server) readLoop(conn net.PacketConn, allocationManager *allocation.Manager) { buf := make([]byte, s.inboundMTU) for { n, addr, err := conn.ReadFrom(buf) switch { case err != nil: s.log.Debugf("Exit read loop on error: %s", err) return case n >= s.inboundMTU: s.log.Debugf("Read bytes exceeded MTU, packet is possibly truncated") continue } if err := server.HandleRequest(server.Request{ Conn: conn, SrcAddr: addr, Buff: buf[:n], Log: s.log, AuthHandler: s.authHandler, QuotaHandler: s.quotaHandler, Realm: s.realm, AllocationManager: allocationManager, ChannelBindTimeout: s.channelBindTimeout, NonceHash: s.nonceHash, }); err != nil { if s.eventHandler.OnAllocationError != nil { s.eventHandler.OnAllocationError(addr, conn.LocalAddr(), allocation.UDP.String(), err.Error()) } s.log.Debugf("Failed to handle datagram: %v", err) } } } turn-4.1.3/server_config.go000066400000000000000000000131101510560755200156730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "crypto/md5" //nolint:gosec,gci "fmt" "net" "strings" "time" "github.com/pion/logging" "github.com/pion/turn/v4/internal/allocation" ) // RelayAddressGenerator is used to generate a RelayAddress when creating an allocation. // You can use one of the provided ones or provide your own. type RelayAddressGenerator interface { // Validate confirms that the RelayAddressGenerator is properly initialized Validate() error // Allocate a PacketConn (UDP) RelayAddress AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) // Allocate a Conn (TCP) RelayAddress AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) } // PermissionHandler is a callback to filter incoming CreatePermission and ChannelBindRequest // requests based on the client IP address and port and the peer IP address the client intends to // connect to. If the client is behind a NAT then the filter acts on the server reflexive // ("mapped") address instead of the real client IP address and port. Note that TURN permissions // are per-allocation and per-peer-IP-address, to mimic the address-restricted filtering mechanism // of NATs that comply with [RFC4787], see https://tools.ietf.org/html/rfc5766#section-2.3. type PermissionHandler func(clientAddr net.Addr, peerIP net.IP) (ok bool) // DefaultPermissionHandler is convince function that grants permission to all peers. func DefaultPermissionHandler(net.Addr, net.IP) (ok bool) { return true } // PacketConnConfig is a single net.PacketConn to listen/write on. // This will be used for UDP listeners. type PacketConnConfig struct { PacketConn net.PacketConn // When an allocation is generated the RelayAddressGenerator // creates the net.PacketConn and returns the IP/Port it is available at RelayAddressGenerator RelayAddressGenerator // PermissionHandler is a callback to filter peer addresses. Can be set as nil, in which // case the DefaultPermissionHandler is automatically instantiated to admit all peer // connections PermissionHandler PermissionHandler } func (c *PacketConnConfig) validate() error { if c.PacketConn == nil { return errConnUnset } if c.RelayAddressGenerator != nil { if err := c.RelayAddressGenerator.Validate(); err != nil { return err } } return nil } // ListenerConfig is a single net.Listener to accept connections on. // This will be used for TCP, TLS and DTLS listeners. type ListenerConfig struct { Listener net.Listener // When an allocation is generated the RelayAddressGenerator // creates the net.PacketConn and returns the IP/Port it is available at RelayAddressGenerator RelayAddressGenerator // PermissionHandler is a callback to filter peer addresses. Can be set as nil, in which // case the DefaultPermissionHandler is automatically instantiated to admit all peer // connections PermissionHandler PermissionHandler } func (c *ListenerConfig) validate() error { if c.Listener == nil { return errListenerUnset } if c.RelayAddressGenerator == nil { return errRelayAddressGeneratorUnset } return c.RelayAddressGenerator.Validate() } // AuthHandler is a callback used to handle incoming auth requests, // allowing users to customize Pion TURN with custom behavior. type AuthHandler func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) // GenerateAuthKey is a convenience function to easily generate keys in the format used by AuthHandler. func GenerateAuthKey(username, realm, password string) []byte { // #nosec h := md5.New() fmt.Fprint(h, strings.Join([]string{username, realm, password}, ":")) // nolint: errcheck return h.Sum(nil) } // EventHandler is a set of callbacks that the server will call at certain hook points during an // allocation's lifecycle. type EventHandler = allocation.EventHandler // QuotaHandler is a callback allows allocations to be rejected when a per-user quota is // exceeded. If the callback returns true the allocation request is accepted, otherwise it is // rejected and a 486 (Allocation Quota Reached) error is returned to the user. type QuotaHandler func(username, realm string, srcAddr net.Addr) (ok bool) // ServerConfig configures the Pion TURN Server. type ServerConfig struct { // PacketConnConfigs and ListenerConfigs are a list of all the turn listeners // Each listener can have custom behavior around the creation of Relays PacketConnConfigs []PacketConnConfig ListenerConfigs []ListenerConfig // LoggerFactory must be set for logging from this server. LoggerFactory logging.LoggerFactory // Realm sets the realm for this server Realm string // AuthHandler is a callback used to handle incoming auth requests, // allowing users to customize Pion TURN with custom behavior AuthHandler AuthHandler // QuotaHandler is a callback used to reject new allocations when a // per-user quota is exceeded. QuotaHandler QuotaHandler // EventHandlers is a set of callbacks for tracking allocation lifecycle. EventHandler EventHandler // ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes. ChannelBindTimeout time.Duration // Sets the server inbound MTU(Maximum transmition unit). Defaults to 1600 bytes. InboundMTU int } func (s *ServerConfig) validate() error { if len(s.PacketConnConfigs) == 0 && len(s.ListenerConfigs) == 0 { return errNoAvailableConns } for _, s := range s.PacketConnConfigs { if err := s.validate(); err != nil { return err } } for _, s := range s.ListenerConfigs { if err := s.validate(); err != nil { return err } } return nil } turn-4.1.3/server_test.go000066400000000000000000001170051510560755200154150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package turn import ( "errors" "fmt" "net" "sync/atomic" "syscall" "testing" "time" "github.com/pion/logging" "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" "github.com/stretchr/testify/assert" ) const ( timeout = 200 * time.Millisecond interval = 50 * time.Millisecond stunAddr = "1.2.3.4:3478" turnAddr = "1.2.3.4:3478" testAddr = "127.0.0.1:3478" ) type EventHandlerType int const ( unknownEvent EventHandlerType = iota onAuth onAllocationCreated onAllocationDeleted onAllocationError onPermissionCreated onPermissionDeleted onChannelCreated onChannelDeleted ) // EventHandlerArgs is a set of arguments passed from the low-level event callbacks to the server. type eventHandlerArgs struct { Type EventHandlerType srcAddr, dstAddr, relayAddr net.Addr protocol string username, realm, method, message, peer string verdict bool requestedPort int channelNumber uint16 } // mtuConn enforces a maxMTU limit and returns an error on overflow. type mtuConn struct { net.PacketConn mtu int } func (c *mtuConn) WriteTo(data []byte, addr net.Addr) (int, error) { if len(data) > c.mtu { return 0, errors.New("MTU violation") //nolint:err113 } return c.PacketConn.WriteTo(data, addr) } type mtuConnGenerator struct { RelayAddressGenerator mtu int } // mtuConnGenerator is a relay address generator that wraps the PacketConn returned by a base generator in an mtuConn. func (g *mtuConnGenerator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { conn, addr, err := g.RelayAddressGenerator.AllocatePacketConn(network, requestedPort) if err != nil { return nil, nil, err } return &mtuConn{conn, g.mtu}, addr, nil } // truncConn truncates send payloads to a given max length. type truncConn struct { net.PacketConn mtu int } func (c *truncConn) WriteTo(data []byte, addr net.Addr) (int, error) { if len(data) > c.mtu { return c.PacketConn.WriteTo(data[0:c.mtu], addr) } return c.PacketConn.WriteTo(data, addr) } type truncConnGenerator struct { RelayAddressGenerator mtu int } // truncConnGenerator is a relay address generator that wraps the PacketConn returned by a base // generator in a truncConn. func (g *truncConnGenerator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { conn, addr, err := g.RelayAddressGenerator.AllocatePacketConn(network, requestedPort) if err != nil { return nil, nil, err } return &truncConn{conn, g.mtu}, addr, nil } func TestServer(t *testing.T) { //nolint:maintidx lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() loggerFactory := logging.NewDefaultLoggerFactory() credMap := map[string][]byte{ "user": GenerateAuthKey("user", "pion.ly", "pass"), } t.Run("simple", func(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match") conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) client, err := NewClient(&ClientConfig{ Conn: conn, LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) _, err = client.SendBindingRequestTo(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3478}) assert.NoError(t, err, "should succeed") client.Close() assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) }) t.Run("default inboundMTU", func(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ LoggerFactory: loggerFactory, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, }) assert.NoError(t, err) assert.Equal(t, server.inboundMTU, defaultInboundMTU) assert.NoError(t, server.Close()) }) t.Run("Set inboundMTU", func(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ InboundMTU: 2000, LoggerFactory: loggerFactory, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, }) assert.NoError(t, err) assert.Equal(t, server.inboundMTU, 2000) assert.NoError(t, server.Close()) }) t.Run("Delete allocation on spontaneous TCP close", func(t *testing.T) { // Test whether allocation is properly deleted when client spontaneously closes the // TCP connection underlying it tcpListener, err := net.Listen("tcp4", testAddr) // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, ListenerConfigs: []ListenerConfig{ { Listener: tcpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "127.0.0.1", }, }, }, Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) // make sure we can reuse the client port dialer := &net.Dialer{ Control: func(_, _ string, conn syscall.RawConn) error { return conn.Control(func(descriptor uintptr) { _ = syscall.SetsockoptInt(Handle(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) }) }, } conn, err := dialer.Dial("tcp", testAddr) assert.NoError(t, err) clientAddr := conn.LocalAddr() serverAddr, err := net.ResolveTCPAddr("tcp4", testAddr) assert.NoError(t, err) client, err := NewClient(&ClientConfig{ STUNServerAddr: serverAddr.String(), TURNServerAddr: serverAddr.String(), Conn: NewSTUNConn(conn), Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) _, err = client.SendBindingRequestTo(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3478}) assert.NoError(t, err, "should succeed") relayConn, err := client.Allocate() assert.NoError(t, err) assert.NotNil(t, relayConn) fiveTuple := &allocation.FiveTuple{ Protocol: allocation.UDP, // Fixed UDP SrcAddr: clientAddr, DstAddr: serverAddr, } // Allocation exists assert.Len(t, server.allocationManagers, 1) assert.NotNil(t, server.allocationManagers[0].GetAllocation(fiveTuple)) // client.Close() // This should properly close the client and delete the allocation on the server assert.NoError(t, conn.Close()) // Let connection to properly close time.Sleep(100 * time.Millisecond) // to we still have the allocation on the server? assert.Nil(t, server.allocationManagers[0].GetAllocation(fiveTuple)) client.Close() // This should err: client connection has gone so we cannot send the Refresh(0) // message assert.Error(t, relayConn.Close()) assert.NoError(t, server.Close()) }) t.Run("Filter on client address and peer IP", func(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.IPv4(127, 0, 0, 1), Address: "127.0.0.1", }, PermissionHandler: func(src net.Addr, peer net.IP) bool { return src.String() == "127.0.0.1:54321" && peer.Equal(net.IPv4(127, 0, 0, 4)) }, }, }, Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) // Enforce correct client IP and port conn, err := net.ListenPacket("udp4", "127.0.0.1:54321") // nolint: noctx assert.NoError(t, err) client, err := NewClient(&ClientConfig{ STUNServerAddr: testAddr, TURNServerAddr: testAddr, Conn: conn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) whiteAddr, errA := net.ResolveUDPAddr("udp", "127.0.0.4:12345") assert.NoError(t, errA, "should succeed") blackAddr, errB1 := net.ResolveUDPAddr("udp", "127.0.0.5:12345") assert.NoError(t, errB1, "should succeed") // Explicit CreatePermission err = client.CreatePermission(whiteAddr) assert.NoError(t, err, "grant permission for whitelisted peer") err = client.CreatePermission(blackAddr) assert.ErrorContains(t, err, "error", "deny permission for blacklisted peer address") err = client.CreatePermission(whiteAddr, whiteAddr) assert.NoError(t, err, "grant permission for repeated whitelisted peer addresses") err = client.CreatePermission(blackAddr) assert.ErrorContains(t, err, "error", "deny permission for repeated blacklisted peer address") // Isn't this a corner case in the spec? err = client.CreatePermission(whiteAddr, blackAddr) assert.ErrorContains(t, err, "error", "deny permission for mixed whitelisted and blacklisted peers") // Implicit CreatePermission for ChannelBindRequests: WriteTo always tries to bind a channel _, err = relayConn.WriteTo([]byte("Hello"), whiteAddr) assert.NoError(t, err, "write to whitelisted peer address succeeds - 1") _, err = relayConn.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write to blacklisted peer address fails - 1") _, err = relayConn.WriteTo([]byte("Hello"), whiteAddr) assert.NoError(t, err, "write to whitelisted peer address succeeds - 2") _, err = relayConn.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write to blacklisted peer address fails - 2") _, err = relayConn.WriteTo([]byte("Hello"), whiteAddr) assert.NoError(t, err, "write to whitelisted peer address succeeds - 3") _, err = relayConn.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write to blacklisted peer address fails - 3") // Let the previous transaction terminate time.Sleep(200 * time.Millisecond) assert.NoError(t, relayConn.Close()) client.Close() assert.NoError(t, conn.Close()) // Enforce filtered source address conn2, err := net.ListenPacket("udp4", "127.0.0.1:12321") // nolint: noctx assert.NoError(t, err) client2, err := NewClient(&ClientConfig{ STUNServerAddr: testAddr, TURNServerAddr: testAddr, Conn: conn2, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client2.Listen()) relayConn2, err := client2.Allocate() assert.NoError(t, err) // Explicit CreatePermission err = client2.CreatePermission(whiteAddr) assert.ErrorContains(t, err, "error", "deny permission from filtered source to whitelisted peer") err = client2.CreatePermission(blackAddr) assert.ErrorContains(t, err, "error", "deny permission from filtered source to blacklisted peer") // Implicit CreatePermission for ChannelBindRequests: WriteTo always tries to bind a channel _, err = relayConn2.WriteTo([]byte("Hello"), whiteAddr) assert.ErrorContains(t, err, "error", "write from filtered source to whitelisted peer fails - 1") _, err = relayConn2.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write from filtered source to blacklisted peer fails - 1") _, err = relayConn2.WriteTo([]byte("Hello"), whiteAddr) assert.ErrorContains(t, err, "error", "write from filtered source to whitelisted peer fails - 2") _, err = relayConn2.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write from filtered source to blacklisted peer fails - 2") _, err = relayConn2.WriteTo([]byte("Hello"), whiteAddr) assert.ErrorContains(t, err, "error", "write from filtered source to whitelisted peer fails - 3") _, err = relayConn2.WriteTo([]byte("Hello"), blackAddr) assert.ErrorContains(t, err, "error", "write from filtered source to blacklisted peer fails - 3") // Let the previous transaction terminate time.Sleep(200 * time.Millisecond) assert.NoError(t, relayConn2.Close()) client2.Close() assert.NoError(t, conn2.Close()) assert.NoError(t, server.Close()) }) t.Run("Return error if payload exceeds peer connection MTU", func(t *testing.T) { udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) errMessage := atomic.Value{} errMessage.Store("") server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, EventHandler: allocation.EventHandler{ // used to report write errors from peer OnAllocationError: func(_, _ net.Addr, _, message string) { errMessage.Store(message) }, }, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &mtuConnGenerator{ RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.IPv4(127, 0, 0, 1), Address: "127.0.0.1", }, mtu: 1, // allow single-byte payload }, }, }, Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) addr := "127.0.0.1:3478" client, err := NewClient(&ClientConfig{ STUNServerAddr: addr, TURNServerAddr: addr, Conn: conn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) peerAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1") assert.NoError(t, err, "should succeed") _, err = relayConn.WriteTo([]byte("Hello"), peerAddr) assert.NoError(t, err, "mtu not enforced in client side") assert.Eventually(t, func() bool { return errMessage.Load() != "" }, timeout, interval) assert.Contains(t, errMessage.Load(), "MTU violation", "allocation error") // Let the previous transaction terminate time.Sleep(200 * time.Millisecond) assert.NoError(t, relayConn.Close()) client.Close() assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) }) t.Run("Return error if peer payload is truncated", func(t *testing.T) { errMessage := atomic.Value{} errMessage.Store("") udpListener, err := net.ListenPacket("udp4", "127.0.0.1:3478") // nolint: noctx assert.NoError(t, err) server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, EventHandler: allocation.EventHandler{ // used to report write errors from peer OnAllocationError: func(_, _ net.Addr, _, message string) { errMessage.Store(message) }, }, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &truncConnGenerator{ RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.IPv4(127, 0, 0, 1), Address: "127.0.0.1", }, mtu: 1, }, }, }, Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx assert.NoError(t, err) addr := "127.0.0.1:3478" client, err := NewClient(&ClientConfig{ STUNServerAddr: addr, TURNServerAddr: addr, Conn: conn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err) assert.NoError(t, client.Listen()) relayConn, err := client.Allocate() assert.NoError(t, err) peerAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1") assert.NoError(t, err, "should succeed") _, err = relayConn.WriteTo([]byte("Hello"), peerAddr) assert.NoError(t, err, "mtu not enforced in client side") assert.Eventually(t, func() bool { return errMessage.Load() != "" }, timeout, interval) assert.Contains(t, errMessage.Load(), "packet write smaller than packet 1 != 5 (expected)", "allocation error") // Let the previous transaction terminate time.Sleep(200 * time.Millisecond) assert.NoError(t, relayConn.Close()) client.Close() assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) }) } type VNet struct { wan *vnet.Router net0 *vnet.Net // net (0) on the WAN net1 *vnet.Net // net (1) on the WAN netL0 *vnet.Net // net (0) on the LAN server *Server } func (v *VNet) Close() error { if err := v.server.Close(); err != nil { return err } return v.wan.Stop() } func buildVNet(handler *EventHandler) (*VNet, error) { //nolint:cyclop loggerFactory := logging.NewDefaultLoggerFactory() if handler == nil { handler = &EventHandler{} } // WAN wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: loggerFactory, }) if err != nil { return nil, err } net0, err := vnet.NewNet(&vnet.NetConfig{ StaticIP: "1.2.3.4", // Will be assigned to eth0 }) if err != nil { return nil, err } err = wan.AddNet(net0) if err != nil { return nil, err } net1, err := vnet.NewNet(&vnet.NetConfig{ StaticIP: "1.2.3.5", // Will be assigned to eth0 }) if err != nil { return nil, err } err = wan.AddNet(net1) if err != nil { return nil, err } // LAN lan, err := vnet.NewRouter(&vnet.RouterConfig{ StaticIP: "5.6.7.8", // This router's external IP on eth0 CIDR: "192.168.0.0/24", NATType: &vnet.NATType{ MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, }, LoggerFactory: loggerFactory, }) if err != nil { return nil, err } netL0, err := vnet.NewNet(&vnet.NetConfig{}) if err != nil { return nil, err } if err = lan.AddNet(netL0); err != nil { return nil, err } if err = wan.AddRouter(lan); err != nil { return nil, err } if err = wan.Start(); err != nil { return nil, err } // Start server... credMap := map[string][]byte{"user": GenerateAuthKey("user", "pion.ly", "pass")} udpListener, err := net0.ListenPacket("udp4", "1.2.3.4:3478") // nolint: noctx if err != nil { return nil, err } if handler == nil { handler = &EventHandler{} } server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, Realm: "pion.ly", EventHandler: *handler, PacketConnConfigs: []PacketConnConfig{ { PacketConn: udpListener, RelayAddressGenerator: &RelayAddressGeneratorNone{ Address: "1.2.3.4", Net: net0, }, }, }, LoggerFactory: loggerFactory, }) if err != nil { return nil, err } // Register host names err = wan.AddHost("stun.pion.ly", "1.2.3.4") if err != nil { return nil, err } err = wan.AddHost("turn.pion.ly", "1.2.3.4") if err != nil { return nil, err } err = wan.AddHost("echo.pion.ly", "1.2.3.5") if err != nil { return nil, err } return &VNet{ wan: wan, net0: net0, net1: net1, netL0: netL0, server: server, }, nil } func testEventHandler(ch chan eventHandlerArgs, authCounter *atomic.Int32) *EventHandler { return &EventHandler{ OnAuth: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, method string, verdict bool, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onAuth, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, method: method, verdict: verdict, } } authCounter.Add(1) }, OnAllocationCreated: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, requestedPort int, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onAllocationCreated, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, relayAddr: relayAddr, requestedPort: requestedPort, } } }, OnAllocationDeleted: func(srcAddr, dstAddr net.Addr, protocol, username, realm string) { if ch != nil { ch <- eventHandlerArgs{ Type: onAllocationDeleted, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, } } }, OnAllocationError: func(srcAddr, dstAddr net.Addr, protocol, message string) { if ch != nil { ch <- eventHandlerArgs{ Type: onAllocationError, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, message: message, } } }, OnPermissionCreated: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, peer net.IP, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onPermissionCreated, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, relayAddr: relayAddr, peer: peer.String(), } } }, OnPermissionDeleted: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr net.Addr, peer net.IP, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onPermissionDeleted, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, relayAddr: relayAddr, peer: peer.String(), } } }, OnChannelCreated: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr, peer net.Addr, channelNumber uint16, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onChannelCreated, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, relayAddr: relayAddr, peer: peer.String(), channelNumber: channelNumber, } } }, OnChannelDeleted: func(srcAddr, dstAddr net.Addr, protocol, username, realm string, relayAddr, peer net.Addr, channelNumber uint16, ) { if ch != nil { ch <- eventHandlerArgs{ Type: onChannelDeleted, srcAddr: srcAddr, dstAddr: dstAddr, protocol: protocol, username: username, realm: realm, relayAddr: relayAddr, peer: peer.String(), channelNumber: channelNumber, } } }, } } func expectEvent(ch chan eventHandlerArgs) (eventHandlerArgs, bool) { select { case res := <-ch: return res, true case <-time.After(timeout): return eventHandlerArgs{}, false } } func checkAllocationEvent(t *testing.T, event eventHandlerArgs) { t.Helper() udpAddr, ok := event.srcAddr.(*net.UDPAddr) assert.True(t, ok) assert.Equal(t, udpAddr.IP.String(), "5.6.7.8") udpAddr, ok = event.dstAddr.(*net.UDPAddr) assert.True(t, ok) assert.Equal(t, udpAddr.IP.String(), "1.2.3.4") assert.Equal(t, "UDP", event.protocol) assert.Equal(t, "user", event.username) assert.Equal(t, "pion.ly", event.realm) } func checkAuthEvent(t *testing.T, event eventHandlerArgs, method string, verdict bool) { t.Helper() assert.Equal(t, onAuth, event.Type, "should receive an OnAuth event") checkAllocationEvent(t, event) assert.Equal(t, event.method, method) assert.Equal(t, event.verdict, verdict) } func TestServerVNet(t *testing.T) { //nolint:maintidx lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") t.Run("SendBindingRequest", func(t *testing.T) { v, err := buildVNet(nil) assert.NoError(t, err) defer func() { assert.NoError(t, v.Close()) }() lconn, err := v.netL0.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err, "should succeed") defer func() { assert.NoError(t, lconn.Close()) }() log.Debug("creating a client.") client, err := NewClient(&ClientConfig{ STUNServerAddr: stunAddr, Conn: lconn, LoggerFactory: loggerFactory, }) assert.NoError(t, err, "should succeed") assert.NoError(t, client.Listen(), "should succeed") defer client.Close() log.Debug("sending a binding request.") reflAddr, err := client.SendBindingRequest() assert.NoError(t, err) log.Debugf("mapped-address: %s", reflAddr) udpAddr, ok := reflAddr.(*net.UDPAddr) assert.True(t, ok) // The mapped-address should have IP address that was assigned // to the LAN router. assert.True(t, udpAddr.IP.Equal(net.IPv4(5, 6, 7, 8)), "should match") }) t.Run("AllocationLifecycle", func(t *testing.T) { counter := &atomic.Int32{} events := make(chan eventHandlerArgs, 5) defer close(events) virtNet, err := buildVNet(testEventHandler(events, counter)) assert.NoError(t, err) defer func() { assert.NoError(t, virtNet.Close()) }() lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err, "should succeed") defer func() { assert.NoError(t, lconn.Close()) }() log.Debug("creating a client.") client, err := NewClient(&ClientConfig{ TURNServerAddr: turnAddr, Conn: lconn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err, "should succeed") assert.NoError(t, client.Listen(), "should succeed") defer client.Close() log.Debug("sending an allocate request.") relayConn, err := client.Allocate() assert.NoError(t, err, "should succeed") event, ok := expectEvent(events) assert.True(t, ok, "should receive an event") checkAuthEvent(t, event, "Allocate", true) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onAllocationCreated, event.Type, "should receive an OnAllocationCreated event") checkAllocationEvent(t, event) assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String()) assert.Equal(t, 0, event.requestedPort) log.Debug("Sending test packet") peerAddr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 5), Port: 80} _, err = relayConn.WriteTo([]byte("test"), peerAddr) assert.NoError(t, err, "should succeed") event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") checkAuthEvent(t, event, "CreatePermission", true) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onPermissionCreated, event.Type, "should receive an OnPermissionCreated event") checkAllocationEvent(t, event) assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String()) assert.Equal(t, 0, event.requestedPort) assert.Equal(t, "1.2.3.5", event.peer) log.Debug("Forcing the creation of a channel") _, err = relayConn.WriteTo([]byte("test"), peerAddr) assert.NoError(t, err, "should succeed") event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") checkAuthEvent(t, event, "ChannelBind", true) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onChannelCreated, event.Type, "should receive an OnChannelCreated event") checkAllocationEvent(t, event) assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String()) // obtain the channel id a := virtNet.server.allocationManagers[0].GetAllocation(&allocation.FiveTuple{ Protocol: allocation.UDP, SrcAddr: event.srcAddr, DstAddr: event.dstAddr, }) assert.NotNil(t, a) channelBind := a.GetChannelByAddr(peerAddr) assert.NotNil(t, channelBind) assert.Equal(t, channelBind.Number, proto.ChannelNumber(event.channelNumber)) log.Debug("Closing relay connection") assert.NoError(t, relayConn.Close(), "relay conn close should succeed") event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onAuth, event.Type, "should receive an OnAuth event") checkAuthEvent(t, event, "Refresh", true) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onPermissionDeleted, event.Type, "should receive an OnPermissionDeleted event") checkAllocationEvent(t, event) assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String()) assert.Equal(t, "1.2.3.5", event.peer) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onChannelDeleted, event.Type, "should receive an OnChannelDeleted event") checkAllocationEvent(t, event) assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String()) assert.Equal(t, channelBind.Number, proto.ChannelNumber(event.channelNumber)) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onAllocationDeleted, event.Type, "should receive an OnAllocationDeleted event") checkAllocationEvent(t, event) assert.Eventually(t, func() bool { return counter.Load() == 4 }, timeout, interval) }) t.Run("AuthEventHandlerSuccess", func(t *testing.T) { counter := &atomic.Int32{} virtNet, err := buildVNet(testEventHandler(nil, counter)) assert.NoError(t, err) defer func() { assert.NoError(t, virtNet.Close()) }() lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err, "should succeed") defer func() { assert.NoError(t, lconn.Close()) }() log.Debug("creating a client.") client, err := NewClient(&ClientConfig{ TURNServerAddr: turnAddr, Conn: lconn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err, "should succeed") assert.NoError(t, client.Listen(), "should succeed") defer client.Close() log.Debug("sending an allocate request.") relayConn, err := client.Allocate() assert.NoError(t, err, "should succeed") log.Debug("Closing relay connection") assert.NoError(t, relayConn.Close(), "relay conn close should succeed") assert.Eventually(t, func() bool { return counter.Load() == 2 }, timeout, interval) }) t.Run("AuthEventHandlerFailure", func(t *testing.T) { counter := &atomic.Int32{} events := make(chan eventHandlerArgs, 5) defer close(events) virtNet, err := buildVNet(testEventHandler(events, counter)) assert.NoError(t, err) defer func() { assert.NoError(t, virtNet.Close()) }() lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err, "should succeed") defer func() { assert.NoError(t, lconn.Close()) }() log.Debug("creating a client.") client, err := NewClient(&ClientConfig{ TURNServerAddr: turnAddr, Conn: lconn, Username: "user", Password: "wrong-pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) assert.NoError(t, err, "should succeed") assert.NoError(t, client.Listen(), "should succeed") defer client.Close() log.Debug("sending an allocate request.") _, err = client.Allocate() assert.Error(t, err, "should not succeed") event, ok := expectEvent(events) assert.True(t, ok, "should receive an event") checkAuthEvent(t, event, "Allocate", false) event, ok = expectEvent(events) assert.True(t, ok, "should receive an event") assert.Equal(t, onAllocationError, event.Type, "should receive an OnAllocationError event") udpAddr, ok := event.srcAddr.(*net.UDPAddr) assert.True(t, ok) assert.Equal(t, udpAddr.IP.String(), "5.6.7.8") assert.Equal(t, "UDP", event.protocol) assert.Eventually(t, func() bool { return counter.Load() == 1 }, timeout, interval) }) } func TestConsumeSingleTURNFrame(t *testing.T) { type testCase struct { data []byte err error } cases := map[string]testCase{ "channel data": { data: []byte{0x40, 0x01, 0x00, 0x08, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, err: nil, }, "partial data less than channel header": { data: []byte{1}, err: errIncompleteTURNFrame, }, "partial stun message": { data: []byte{0x0, 0x16, 0x02, 0xDC, 0x21, 0x12, 0xA4, 0x42, 0x0, 0x0, 0x0}, err: errIncompleteTURNFrame, }, "stun message": { data: []byte{ 0x00, 0x16, 0x00, 0x02, 0x21, 0x12, 0xA4, 0x42, 0xf7, 0x43, 0x81, 0xa3, 0xc9, 0xcd, 0x88, 0x89, 0x70, 0x58, 0xac, 0x73, 0x00, 0x00, }, }, } for name, cs := range cases { c := cs t.Run(name, func(t *testing.T) { n, e := consumeSingleTURNFrame(c.data) assert.Equal(t, c.err, e) if e == nil { assert.Equal(t, len(c.data), n) } }) } } func TestSTUNOnly(t *testing.T) { serverAddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:3478") assert.NoError(t, err) serverConn, err := net.ListenPacket(serverAddr.Network(), serverAddr.String()) // nolint: noctx assert.NoError(t, err) defer serverConn.Close() //nolint:errcheck server, err := NewServer(ServerConfig{ PacketConnConfigs: []PacketConnConfig{{ PacketConn: serverConn, }}, Realm: "pion.ly", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) defer server.Close() //nolint:errcheck conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) client, err := NewClient(&ClientConfig{ Conn: conn, STUNServerAddr: testAddr, TURNServerAddr: testAddr, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) assert.NoError(t, client.Listen()) defer client.Close() reflAddr, err := client.SendBindingRequest() assert.NoError(t, err) _, ok := reflAddr.(*net.UDPAddr) assert.True(t, ok) _, err = client.Allocate() assert.Equal(t, err.Error(), "Allocate error response (error 400: )") assert.NoError(t, conn.Close()) } func TestQuotaReached(t *testing.T) { serverAddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:3478") assert.NoError(t, err) serverConn, err := net.ListenPacket(serverAddr.Network(), serverAddr.String()) // nolint: noctx assert.NoError(t, err) defer serverConn.Close() //nolint:errcheck credMap := map[string][]byte{"user": GenerateAuthKey("user", "pion.ly", "pass")} server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false //nolint:nlreturn }, QuotaHandler: func(_, _ string, _ net.Addr) (ok bool) { return false }, Realm: "pion.ly", PacketConnConfigs: []PacketConnConfig{{ PacketConn: serverConn, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "0.0.0.0", }, }}, LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) defer server.Close() //nolint:errcheck conn, err := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx assert.NoError(t, err) client, err := NewClient(&ClientConfig{ Conn: conn, STUNServerAddr: testAddr, TURNServerAddr: testAddr, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) assert.NoError(t, client.Listen()) defer client.Close() _, err = client.Allocate() assert.Equal(t, err.Error(), "Allocate error response (error 486: )") } func RunBenchmarkServer(b *testing.B, clientNum int) { //nolint:cyclop b.Helper() loggerFactory := logging.NewDefaultLoggerFactory() credMap := map[string][]byte{ "user": GenerateAuthKey("user", "pion.ly", "pass"), } testSeq := []byte("benchmark-data") // Setup server serverAddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:3478") if err != nil { b.Fatalf("Failed to resolve server address: %s", err) } serverConn, err := net.ListenPacket(serverAddr.Network(), serverAddr.String()) // nolint: noctx if err != nil { b.Fatalf("Failed to allocate server listener at %s:%s", serverAddr.Network(), serverAddr.String()) } defer serverConn.Close() //nolint:errcheck server, err := NewServer(ServerConfig{ AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return pw, true } return nil, false }, PacketConnConfigs: []PacketConnConfig{{ PacketConn: serverConn, RelayAddressGenerator: &RelayAddressGeneratorStatic{ RelayAddress: net.ParseIP("127.0.0.1"), Address: "0.0.0.0", }, }}, Realm: "pion.ly", LoggerFactory: loggerFactory, }) if err != nil { b.Fatalf("Failed to start server: %s", err) } defer server.Close() //nolint:errcheck // Create a sink sinkAddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:65432") if err != nil { b.Fatalf("Failed to resolve sink address: %s", err) } sink, err := net.ListenPacket(sinkAddr.Network(), sinkAddr.String()) // nolint: noctx if err != nil { b.Fatalf("Failed to allocate sink: %s", err) } defer sink.Close() //nolint:errcheck go func() { buf := make([]byte, 1600) for { // Ignore "use of closed network connection" errors if _, _, listenErr := sink.ReadFrom(buf); listenErr != nil { return } // Do not care about received data } }() // Setup client(s) clients := make([]net.PacketConn, clientNum) for i := 0; i < clientNum; i++ { clientConn, listenErr := net.ListenPacket("udp4", "0.0.0.0:0") // nolint: noctx if listenErr != nil { b.Fatalf("Failed to allocate socket for client %d: %s", i+1, err) } defer clientConn.Close() //nolint:errcheck client, err := NewClient(&ClientConfig{ STUNServerAddr: testAddr, TURNServerAddr: testAddr, Conn: clientConn, Username: "user", Password: "pass", Realm: "pion.ly", LoggerFactory: loggerFactory, }) if err != nil { b.Fatalf("Failed to start client %d: %s", i+1, err) } defer client.Close() if listenErr := client.Listen(); listenErr != nil { b.Fatalf("Client %d cannot listen: %s", i+1, listenErr) } // Create an allocation turnConn, err := client.Allocate() if err != nil { b.Fatalf("Client %d cannot create allocation: %s", i+1, err) } defer turnConn.Close() //nolint:errcheck clients[i] = turnConn } // Run benchmark for j := 0; j < b.N; j++ { for i := 0; i < clientNum; i++ { if _, err := clients[i].WriteTo(testSeq, sinkAddr); err != nil { b.Fatalf("Client %d cannot send to TURN server: %s", i+1, err) } } } } // BenchmarkServer will benchmark the server with multiple simultaneous client connections. func BenchmarkServer(b *testing.B) { for i := 1; i <= 4; i++ { b.Run(fmt.Sprintf("client_num_%d", i), func(b *testing.B) { RunBenchmarkServer(b, i) }) } } turn-4.1.3/stun_conn.go000066400000000000000000000066611510560755200150630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package turn import ( "encoding/binary" "errors" "net" "time" "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/proto" ) var ( errInvalidTURNFrame = errors.New("data is not a valid TURN frame, no STUN or ChannelData found") errIncompleteTURNFrame = errors.New("data contains incomplete STUN or TURN frame") ) // STUNConn wraps a net.Conn and implements // net.PacketConn by being STUN aware and // packetizing the stream. type STUNConn struct { nextConn net.Conn buff []byte } const ( stunHeaderSize = 20 channelDataLengthSize = 2 channelDataNumberSize = channelDataLengthSize channelDataHeaderSize = channelDataLengthSize + channelDataNumberSize channelDataPadding = 4 ) // Given a buffer give the last offset of the TURN frame // If the buffer isn't a valid STUN or ChannelData packet, // or the length doesn't match return false. func consumeSingleTURNFrame(b []byte) (int, error) { // Too short to determine if ChannelData or STUN if len(b) < 9 { return 0, errIncompleteTURNFrame } var datagramSize uint16 switch { case stun.IsMessage(b): datagramSize = binary.BigEndian.Uint16(b[2:4]) + stunHeaderSize case proto.ChannelNumber(binary.BigEndian.Uint16(b[0:2])).Valid(): datagramSize = binary.BigEndian.Uint16(b[channelDataNumberSize:channelDataHeaderSize]) if paddingOverflow := (datagramSize + channelDataPadding) % channelDataPadding; paddingOverflow != 0 { datagramSize = (datagramSize + channelDataPadding) - paddingOverflow } datagramSize += channelDataHeaderSize case len(b) < stunHeaderSize: return 0, errIncompleteTURNFrame default: return 0, errInvalidTURNFrame } if len(b) < int(datagramSize) { return 0, errIncompleteTURNFrame } return int(datagramSize), nil } // ReadFrom implements ReadFrom from net.PacketConn. func (s *STUNConn) ReadFrom(payload []byte) (n int, addr net.Addr, err error) { // First pass any buffered data from previous reads n, err = consumeSingleTURNFrame(s.buff) if errors.Is(err, errInvalidTURNFrame) { return 0, nil, err } else if err == nil { copy(payload, s.buff[:n]) s.buff = s.buff[n:] return n, s.nextConn.RemoteAddr(), nil } // Then read from the nextConn, appending to our buff n, err = s.nextConn.Read(payload) if err != nil { return 0, nil, err } s.buff = append(s.buff, append([]byte{}, payload[:n]...)...) return s.ReadFrom(payload) } // WriteTo implements WriteTo from net.PacketConn. func (s *STUNConn) WriteTo(payload []byte, _ net.Addr) (n int, err error) { return s.nextConn.Write(payload) } // Close implements Close from net.PacketConn. func (s *STUNConn) Close() error { return s.nextConn.Close() } // LocalAddr implements LocalAddr from net.PacketConn. func (s *STUNConn) LocalAddr() net.Addr { return s.nextConn.LocalAddr() } // SetDeadline implements SetDeadline from net.PacketConn. func (s *STUNConn) SetDeadline(t time.Time) error { return s.nextConn.SetDeadline(t) } // SetReadDeadline implements SetReadDeadline from net.PacketConn. func (s *STUNConn) SetReadDeadline(t time.Time) error { return s.nextConn.SetReadDeadline(t) } // SetWriteDeadline implements SetWriteDeadline from net.PacketConn. func (s *STUNConn) SetWriteDeadline(t time.Time) error { return s.nextConn.SetWriteDeadline(t) } // NewSTUNConn creates a STUNConn. func NewSTUNConn(nextConn net.Conn) *STUNConn { return &STUNConn{nextConn: nextConn} }