Zabezpečte svůj DNS server

24. 7. 2012
Doba čtení: 8 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
V poslední době se často hovoří o zneužívání DNS serverů ke kybernetickým útokům. Většina doporučení ohledně zabezpečení doporučuje pouze vypnutí DNS serveru. Co když ale jeho služby potřebujete? V článku si představíme praktickou realizaci zabezpečení DNS serveru pomocí linuxového netfiltru.

Zesilující DNS útok

Služba DNS je jedna z mála základních internetových služeb, které jsou provozovány nad transportním protokolem UDP. Protože UDP nenavazuje žádné spojení, je jím možné posílat data protistraně, aniž ta by o tom věděla, a mohla to nějak ovlivnit. Dokonce je velice snadné odeslat UDP paket se zfalšovanou adresou odesílatele.

Protože odpovědi DNS serveru jsou obvykle řádově větší než dotazy, dají se veřejné DNS servery využít jako spolehlivé zesilovače. Prostě jim pošlete dotaz s falešnou adresou odesílatele, mířící ve skutečnosti na oběť vašeho útoku. DNS server na něj odpoví a vaše ruce zůstanou čisté. Když se zeptáte několika serverů a každého zásobujete několika sty dotazy každou sekundu, dokážete na cílový server poslat desítky i stovky Mbit/s zbytečného provozu, který může kompletně zahltit linku k serveru.

Schéma zesilujícího DNS útoku.
Autor: Martin Haller

Schéma zesilujícího DNS útoku. Zdroj: Lupa.cz

DNSSEC bohužel nepomůže

Aby byl zesilovací faktor co největší, musíte vymyslet takový dotaz, který ze serveru vyloudí co největší odpověď. Jako ideální se jeví typ dotazu ANY, kterým se DNS serveru ptáme na všechno, co o daném jméně ví. A zeptáme se nejlépe na nějaké jméno, které je začátkem zóny (tzv. apex). Máme jistotu, že dostaneme SOA záznam, několik NS a MX záznamů a často i nějaké A, či AAAA záznamy. To ilustruje následující snímek Wiresharku. Na jednoduchý dotaz zabírající 78 bajtů přišla odpověď dlouhá 3,4 kilobajtů.

V tomto konkrétním případě (se kterým jsem se v praxi setkal) je každý paket zesílen přibližně 44×. Můžeme to připisovat chybě v nastavení domény, která obsahuje evidentně zbytečně mnoho TXT záznamů. Ale i perfektně nastavenou doménu je možné zneužít k zesílení s faktorem 5 až 15. Je-li doména chráněna DNSSECem, máme jistotu, že dosáhneme velkého zesílení použitím dotazu na DNSKEY, ke kterému server navíc přidá podpisy.

BCP38, obrana nejúčinnější

Tou hlavní ochranou před podobnými druhy útoků by mělo být filtrování zdrojových adres. Zjednodušeně řečeno, provozovatel každé koncové sítě by na hraničních směrovačích měl propouštět ven do internetu pouze takové zdrojové adresy, které jsou koncové síti přiděleny. Podrobněji se tomu věnuje doporučení IETF BCP 38. Problém je, že takové filtrování je na odpovědnosti každého provozovatele koncové sítě. Na úrovni tranzitní sítě, kde je směrování dynamické a klienti jsou připojeni v různých místech, se podobné filtrování realizuje velmi obtížně. Je tedy potřeba smířit se s tím, že vždy bude existovat nějaké místo na internetu, odkud bude možné posílat falšované pakety, které nikdo nezahodí.

Vypnutí nepoužívaných DNS serverů

Další možností, jak zmírnit následky zesilujícího útoku, je omezit počet DNS serverů. Na internetu je provozována spousta otevřených rekurzivních serverů, ochotných komukoli přeložit jakékoli jméno. U velké části z nich přitom rekurzivní funkce není používána a zůstala zapnutá jen proto, že jde o výchozí konfiguraci DNS serveru. Právě na tyto případy dlouhodobě upozorňuje jak CZ.NIC, tak i bezpečnostní tým CSIRT.cz, jako třeba v nedávné zprávičce.

Odkazované texty bohužel vyvolávají dojem, že otevřené rekurzivní nameservery jsou jediným způsobem, jak službu DNS zneužít, a jejich eliminací bude internet záchráněn. Ve skutečnosti se otevřené rekurzivní DNS servery používají k útokům proto, že jsou k dispozici. Pokud k dispozici nebudou, útočníci se velice rychle přeorientují na autoritativní servery, které z principu funkce musí být schopny odpovědět komukoli. Tím nechci bagatelizovat snahu zmíněných institucí o omezení počtu otevřených serverů, což je jistě prospěšná činnost, jen bych byl opatrný ohledně očekávaných výsledků takové akce.

Omezení četnosti dotazů pomocí hashlimit

Pokud veřejný DNS server provozovat musíte (a je jedno, jestli autoritativní nebo rekurzivní), je vhodné omezit počet dotazů, které dokáže obsloužit. Pro útoky je totiž typické, že stejná adresa (ve skutečnosti jde o adresu oběti) posílá serveru stovky dotazů za sekundu. Omezení by se mělo vztahovat na danou IP adresu, aby překročením povoleného limitu nebyli omezeni ostatní uživatelé serveru. Přesně tohle umí modul linuxového netfiltru zvaný hashlimit. Stačí umístit do iptables následující pravidla:

# iptables -N dns_udp
# iptables -A INPUT -p udp --dport 53 -j dns_udp
# iptables -A dns_udp -m hashlimit --hashlimit-above 25/sec \
           --hashlimit-burst 100 --hashlimit-mode srcip \
           --hashlimit-name DoS-DNS -j DROP
# iptables -A dns_udp -j ACCEPT

Prvním příkazem jsme si pro větší přehlednost založili nový řetězec dns_udp a druhým příkazem do něj poslali veškerý příchozí provoz na port udp/53. V nově vzniklém řetězci nejprve voláme hashlimit, a pokud ten paket nezahodí, v dalším kroku jej přijmeme.

Nastavení hashlimitu si můžeme vysvětlit na klasickém příkladu s děravým vědrem. Pro každou možnou zdrojovou IP adresu má hashlimit připraveno vědro s kapacitou 100 paketů (označeno jako burst), ve kterém je díra taková, že z něj každou sekundu 25 paketů vyteče (hodnota above). Jak pakety přicházejí, hashlimit vyhledá podle zdrojové adresy příslušné vědro a vhodí do něj paket. Pokud se mu to povede, nic se neděje a paket pokračuje k dalšímu pravidlu, kterým je přijat. Pokud je vědro plné a paket není kam uložit, je zahozen. Stav věder je možné kontrolovat ve speciálním souboru  /proc/net/ipt_hashlimit/DoS-DNS.

9 1.1.1.1:0->0.0.0.0:0 128 128000 1280
9 2.2.2.2:0->0.0.0.0:0 118528 128000 1280
9 3.3.3.3:0->0.0.0.0:0 128000 128000 1280

Význam sloupců je zleva stáří vědra (9 je nejmladší), zdrojová a cílová IP adresa a port, aktuální stav vědra v jakýchsi fiktivních jednotkách, maximální kapacita vědra a velikost jednoho paketu. Oproti předchozímu výkladu je zde logika opačná, vědro je v základním stavu plné a s přicházejícími pakety ubývá. Jakmile je jeho stav nižší, než hodnota velikosti jednoho paketu, jsou pakety z dané adresy zahazovány.

Hashlimit je poměrně univerzální nástroj, kromě použitého módu, kdy má každá IP adresa své vědro, dokáže také pracovat se sdíleným vědrem pro určitý společný prefix. To se může hodit třeba u IPv6, kde by mohl útočník generovat pro každý dotaz jinou adresu z 64bitového prefixu. To docílíme přidáním volby --hashlimit-srcmask 64, samozřejmě při použití  ip6tables.

Ještě větší restrikce pro dotazy typu ANY

Jak už jsem napsal, dotazy typu ANY jsou dnes nejčastějším způsobem zneužívání DNS serverů. Zároveň se tento typ dotazů běžně nepoužívá, tedy snad kromě ladění DNS serverů správci. Proto také některé DNS servery (například Knot DNS) nabízejí možnost na dotazy typu ANY neodpovídat a stejně se chová i známá služba Google Public DNS. Úplné zakázání těchto dotazů ale ztěžuje ladění, nehledě na to, že jak bylo ukázáno, útočníci mají už teď mnoho jiných možností, jak vylákat z DNS serverů dostatečné zesílení. Bylo by tedy dobré, kdybychom mohli dotazy typu ANY nějakým způsobem klasifikovat ve firewallu a aplikovat pro ně odlišná, přísnější pravidla.

Problém je, že tvůrci protokolu DNS moc nepočítali s tím, že by někdo chtěl třídit pakety na základě typu dotazu, takže typ dotazu umístili nikoli na pevný ofset, ale na plovoucí umístění až za poptávané jméno, jak je vidět na obrázku. Klasifikování pomocí obecného modulu u32 je možné pouze pro konkrétní jména, nikoli pro všechny typy dotazů.

Naštěstí už podobný problém řešil (byť za účelem jakési antispamové ochrany) Bartłomiej Korupczyński, a tak existuje modul do netfiltru zvaný xt_dns, který dokáže klasifikovat UDP DNS dotazy podle druhu. Autor zvolil poměrně nestandardní, ale funkční řešení, kdy polohu pole s typem dotazu neodměřuje od začátku, ale od konce paketu, kde je ofset konstantní. Tím se vyhnul potřebě procházet jméno po jménu, a filtr je tedy rychlý. Jedinou nevýhodou filtru je, že nedokáže klasifikovat DNS dotazy používající rozšíření EDNS0. Takové dotazy na konci obsahují ještě pole OPT, specifikující rozšířené volby:

Právě dotazy s EDNS0  jsou mezi útočníky hojně rozšířeny, protože povolují serveru posílat odpověď větší než 512 bajtů. Z toho důvodu jsem modul xt_dns forknul a přidal do něj podporu i pro dotazy s EDNS0. Původně jsem používal stejný nestandardní způsob nalezení pole s typem dotazu odečítáním od konce paketu, pak jsem jej ale přepsal na verzi s klasickým procházením od začátku. Ta je robustnější a kromě EDNS0 si poradí i s pakety, které mají na konci nadbytečné bajty (například z důvodu zarovnání).

Po nainstalování modulu zbývá doplnit firewall o další limit pro dotazy typu ANY. Abychom mohli na počítadlech netfiltru sledovat funkčnost filtru, vytvoříme pro tyto dotazy další samostatný řetězec:

# iptables -N dns_udp
# iptables -N dns_any
# iptables -A INPUT -p udp --dport 53 -j dns_udp
# iptables -A dns_udp -m hashlimit --hashlimit-above 25/sec \
           --hashlimit-burst 100 --hashlimit-mode srcip \
           --hashlimit-name DoS-DNS -j DROP
# iptables -A dns_udp -m dns --dns-query ANY -j dns_any
# iptables -A dns_udp -j ACCEPT
# iptables -A dns_any -m hashlimit --hashlimit-above 1/sec \
           --hashlimit-burst 10 --hashlimit-mode srcip \
           --hashlimit-name DNS-ANY -j DROP
# iptables -A dns_any -j ACCEPT

Sledovat účinnost filtru můžeme například následujícím příkazem. Ten každou sekundu zobrazí stav počítadel v obou řetězcích a dále vypíše stav hashlimitu pro dotazy typu  ANY.

hacking_tip

watch -n1 'iptables -L dns_udp -xv && iptables -L dns_any -xv &&
           cat /proc/net/ipt_hashlimit/DNS-ANY'

Závěr

Popsaná implementace filtrování dokáže nejen zmírnit následky DoS útoků, ale také ochránit uživatele před sebou samými. Při náhodném sledování provozu na DNS serveru ještě bez filtrování jsem pomocí nástroje iftop odhalil zacyklený počítač, generující více než 400 totožných dotazů za sekundu. Server k němu několik dní ochotně posílal odpovědi rychlostí 10 Mbit/s. Přitom běžný provoz daného serveru se pohybuje kolem rychlosti 1 Mbit/s pro stovky klientů dohromady. Po upozornění majitele serveru provoz ustal, je tedy zřejmé, že nešlo o zfalšovanou adresu odesílatele.

Uvedený postup je plně použitelný i pro IPv6 provoz, stačí vyměnit iptables za ip6tables. Ačkoli při současném provozu IPv6 k naplňování limitů nedochází, štěstí přeje připraveným.

Autor článku

Ondřej Caletka vystudoval obor Telekomunikační technika na ČVUT a dnes pracuje ve vzdělávacím oddělení RIPE NCC, mezinárodní asociaci koordinující internetové sítě.

'; document.getElementById('preroll-iframe').onload = function () { setupIframe(); } prerollContainer = document.getElementsByClassName('preroll-container-iframe')[0]; } function setupIframe() { prerollDocument = document.getElementById('preroll-iframe').contentWindow.document; let el = prerollDocument.createElement('style'); prerollDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:20px;right:25px}"; videoContent = prerollDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('PREROLL sound allowed'); // setUpIMA(true); videoContent.volume = 1; videoContent.muted = false; setUpIMA(); }).catch(function () { console.log('PREROLL sound forbidden'); videoContent.volume = 0; videoContent.muted = true; setUpIMA(); }); } } function setupDimensions() { prerollWidth = Math.min(iinfoPrerollPosition.offsetWidth, 480); prerollHeight = Math.min(iinfoPrerollPosition.offsetHeight, 320); } function setUpIMA() { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Preroll advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = prerollWidth; // adsRequest.linearAdSlotHeight = prerollHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. prerollDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( prerollDocument.getElementById('adContainer'), videoContent); } function unmutePrerollAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } } function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(prerollWidth, prerollHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } function onAdEvent(adEvent) { const ad = adEvent.getAd(); console.log('Preroll event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: if (!ad.isLinear()) { videoContent.play(); } prerollDocument.getElementById('adContainer').style.width = '100%'; prerollDocument.getElementById('adContainer').style.maxWidth = '640px'; prerollDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); if (ad.isLinear()) { intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } prerollDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (prerollLastError === 303) { playYtVideo(); } break; case google.ima.AdEvent.Type.COMPLETE: if (ad.isLinear()) { clearInterval(intervalTimer); } playYtVideo(); break; } } function onAdError(adErrorEvent) { console.log(adErrorEvent.getError()); prerollLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { playYtVideo(); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoPrerollPosition.remove(); playPrerollAd(); } else { return false; } adVolume = 1; return true; } function onContentPauseRequested() { videoContent.pause(); } function onContentResumeRequested() { videoContent.play(); } function onActiveView() { if (prerollContainer) { const containerOffset = prerollContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (prerollPaused) { adsManager.resume(); prerollPaused = false; } return true; } else { if (!prerollPaused) { adsManager.pause(); prerollPaused = true; } } } return false; } function playYtVideo() { iinfoPrerollPosition.remove(); youtubeIframe.style.display = 'block'; youtubeIframe.src += '&autoplay=1&mute=1'; } }
'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »