ISP Column - May 2013
A column on things Internet
APNIC Labs IPv6 Measurement System
May 2013
George Michaelson,
Geoff Huston
For some years now at APNIC Labs we've been conducting a measurement
exercise intended to measure the extent to which IPv6 is being deployed
in the Internet. This is not a measurement of IPv6 traffic volumes, nor
of IPv6 routes, nor of IPv6-capable servers. This is a measurement of the
Ipv6 capabilities of devices connected to the Internet, and is intended
to answer the question: what proportion of devices on the Internet are
capable of supporting an IPv6 connection?We've often been asked about our
measurement methodology, and this article is intended to describe in some
detail how we perform this measurement.
General Approach
Using Flash and JavaScript, clients’ web browsers are inducted into a
measurement of their capabilities to use IPv6, based on the scripted
fetch of a set of ‘invisible’ 1x1 pixel images. Each test is intended to
isolate a particular capability of the client, in so far as if the client
successfully fetches the object associated with the test then the client
is considered to be capable in that aspect.
We have a set of five basic properties that are available in the test:
IPv4-only, IPv6-only, Dual Stack, Dual Stack with unresponsive IPv6 and
Dual Stack with unresponsive IPv4.
We are interested in the behaviour of the DNS transport as well as in the
behaviour of the HTTP transport.
The URL of each of the images is constructed using labels that describe
the object's transport properties in DNS resolution and the HTTP
transport for the web fetch. For example, a URL of
http://xxx.r6.td.labs.apnic.net/1x1.png describes a web object that is
only accessible using IPv6, (‘r6’) and the domain name itself is served
by authoritative name servers that can respond to DNS resolver queries
made over both IPv4 and IPv6 (‘td’), while a URL of
http://xxx.rd.t6.labs.apnic.net/1x1.png describes a dual stack web object
(‘rd’) whose domain name is only served by authoritative name servers
that are reachable only on IPv6 (‘t6’). The complete name structure of
the various tests is provided in the following table:
Behaviour Value
DNS t
HTTP r
IPv4-only 4
IPv6-only 6
Dual Stack d
Dual Stack, unresponsive IPv6 x
Dual Stack, unresponsive IPv4 z
In order to ensure that each client is forced to perform both the DNS
lookups and the web object fetches from the experiment’s servers, and not
use locally cached values, we make use of dynamic name generation and
wildcard DNS capabilities to generate a unique string as part of the
object's name. This unique string is used in the DNS part of the URL, and
is also used as an argument to the resource name part of the URL. Each
client is served with a unique name value, and all the tests presented to
the client share the same name value so that at the server end we can
match the operations that were performed in the context of each test
instance. As these domain name components map to a wildcard in the DNS
zone, it does not increase the complexity or time taken to perform DNS
resolution. The components of this unique string value include the time
of day (seconds since 1 January 1970 00:00 UTC), a random number, and
experiment version information.
The time taken by the client to fetch each URL is recorded by the
client-side script.
The set of URLs concludes with a “result” URL. This URL is triggered
either when all the other URLs have been loaded, or when a local timer
expires. The fetch of this “result” URL includes, as arguments to the GET
command, the results (and individual timer values) of the fetch
operations of all the other URLs. The default value of this timer for
result generation is 10 seconds.
As well as getting the client to perform self-timing of the experiment,
we also direct all traffic associated with the experiment (the
authoritative DNS name servers that will receive the DNS queries and the
web servers that will receive the HTTP fetches) to a server that is
logging all traffic. We perform logging at the DNS, HTTP and packet
level. These logs provide server-side information on the nature of that
clients capabilities such as the client-resolver relationship, apparent
RTT in DNS and web fetch, IPv4 and IPv6 capability, MTU, and TCP
connection failure rates.
Client-side Code
We use two forms of encoding of this method: Flash and JavaScript. They
are used in different experiment contexts.
Flash permits embedding of the measurement in advertising channels using
flash media for image ads. This channel delivers large volumes of unique
clients who can be targeted by keyword, or economy, or exclude specific
IP ranges.
There are a number of weaknesses of Flash, most notably being that Flash
code is not loaded on some popular mobile platforms, including Apple’s
mobile platforms. It’s also been observed that the Flash engine does not
appear to perform consistent client-side timer measurements, probably due
to a more complex internal object scheduler within the Flash engine. We
have also observed that the Flash engine does not preserve fetch order,
so that the order of objects to fetch generated by the Flash action
script is not necessarily the order in which the Flash engine will
perform the fetches. The most common permutation is that the Flash engine
reverses the object fetch order as it retrieves the set of objects.
JavaScript permits embedding of the measurement in specific host
websites. There are two variants of this script. One is where the
JavaScript is directly inserted into the host web page, and the other
form is as a user-defined code extension to Google's Analytics code. In
the latter case the web administrator can use the Analytics reports to
view the IPv6 capabilities of the site's visitors in addition to the
other Analytics reports. The website does not itself have to be IPv6
enabled: the tests cause the client to interact with our experiment
servers and the IPv6 capability is measured between he client and these
servers. In this case there is no control over who performs the test: the
test is performed by all end clients who visit the site where the
JavaScript is embedded.
JavaScript appears to be more widely supported than Flash. However,
because JavaScript uses code embedded in web sites, the number and
diversity of clients being testing in this manner depends on the visitor
profile of the hosting web. Many web sites have a large volume of repeat
clients, so the tested client population of the JavaScript test appears
to record a particular profile of capability (for example, we have
observed an anomalously high proportion of IPv6 capability in the clients
who use APNIC's whois web service). The JavaScript code can also be
configured via cookies not to re-sample a particular client within a
certain period (the cookie has a default retry value of 24 hours), in
order to counter, to some extent, measurement bias generated by repeat
visitors to the site.
The original versions of the test code explicitly enumerated the
individual URL tests to be executed. There is a more recent variant of
both the Flash and JavaScript codes that includes a runtime configuration
server. In this variant of the test code, the client will initially
perform a fetch from a configuration server. This server will return the
set of URLs to be used for the test. This allows the parameters of the
test to be varied in the fly without having to reload the JavaScript that
was embedded in the web page, or without re-submitting the ad with the
embedded Flash script.
Server Configuration
We use three servers for this experiment, One is located in Australia,
one in Germany and one in the United States. One server is a Linux-based
host, while the other two use FreeBSD as their host OS.
The servers use Apache for the web server, Bind for the DNS server, and
tcpdump for the packet capture. They are also configured with a local
Teredo server, and a local 6to4 relay.
Where possible, we use 16 different addresses in both IPv4 and IPv6.
When running these tests on a highly visited web page, or using a high
volume ad campaign, we have noted that there can be relatively large peak
demands for web fetches on our web servers. The experiment’s webservers
need sufficient capacity to handle hundreds of queries per second, which
means using a system configuration that has thousands of pre-forked http
daemons and kernel configuration support for thousands of open/active TCP
sessions. This also requires servers with large memory configuration.
Sufficient disk is required to ensure tcpdump and server logs can be held
for a continuous cycle of experiments.
Post processing is currently performed in a central log archive, to
integrate all sources of experiment data into a collated experiment log,
which is then post processed on a daily basis.
DNS configuration
The DNS part of the experiment configuration depends on the ‘wildcard’
DNS record. All zones which serve terminal fully qualified domain names
have a wildcard record which maps any name under that domain to the IPv4
or IPv6 address for the head server.
For the current experiments in both DNS and IPv6 capability, 4 distinct
subdomains of DNS are registered under a single prefix:
f.labs.apnic.net Experiments in Asia/Oceania
g.labs.apnic.net Experiments in the Americas
h.labs.apnic.net Experiments in Europe/Middle-East/Africa
i.labs.apnic.net Testing, future expansion
The master server generates an experiment set for the client based on a
basic geo-location mapping of the client's address to a geographic
region. This is done as a rudimentary load balancing exercise, and, more
importantly, to minimize the round trip time between the server and the
client, and thereby avoid, to some extent, retransmits and timeouts at
the client side while performing the experiment. The mapping of address
to region is intentionally quite coarse, and some traffic inevitably goes
to a distant head server, but this does not appear to have had a
significant impact on the measurement outcomes.
The parent domains are provisioned to have identical sub-domains, which
characterize DNS transport by the listed NS delegations. A domain which
is only delegated to IPv6 DNS servers cannot be successfully resolved by
a DNS resolver which does not have access to IPv6 transport. Consequently
the client should not be told the experiment’s IP address. A DNS resolver
which is dual stacked may be fetched over either IPv4 DNS transport, or
IPv6 DNS transport.
$TTL 1d
$ORIGIN .
f.labs.apnic.net IN SOA dns4f.labs.apnic.net. ggm.apnic.net. (
2013051101 ; Serial
3600 ; Refresh
900 ; Retry
3600000 ; Expire
3600 ) ; Minimum
IN NS dns4f.labs.apnic.net.
IN A 203.133.248.22
IN AAAA 2401:2000:6660::22
; sub delegations which feed off f.labs, served by NS *in* f.labs
$ORIGIN f.labs.apnic.net.
dns0 A 203.133.248.22
dns A 203.133.248.22
AAAA 2401:2000:6660::22
;
; dual stack
dnsd A 203.133.248.23
AAAA 2401:2000:6660::23
dnsrd A 203.133.248.24
AAAA 2401:2000:6660::24
;
; 4 only
dns4 A 203.133.248.25
dnsr4 A 203.133.248.26
;
; 6 only
dns6 AAAA 2401:2000:6660::27
dnsr6 AAAA 2401:2000:6660::28
;
; reachable on 4, not on 6
dnsx A 203.133.248.29
AAAA 6000:666::29
dnsrx A 203.133.248.30
AAAA 6000:666::30
;
; reachable on 6, not on 4
dnsz A 7.0.0.31
AAAA 2401:2000:6660::31
dnsrz A 7.0.0.32
AAAA 2401:2000:6660::32
;
dnssec NS dns0
dualstack NS dnsrd
ipv4bad NS dnsrd
ipv4only NS dnsrd
ipv6bad NS dnsrd
ipv6badbadbad NS dnsrd
ipv6only NS dnsrd
t4 NS dns4
t6 NS dns6
td NS dnsd
tx NS dnsx
tz NS dnsz
v6trans NS dns6
www A 203.133.248.18
AAAA 2401:2000:6660::18
*.results IN A 203.133.248.18
results IN A 203.133.248.18
As shown in the DNS zone file above, f.labs.apnic.net has 5
subdomains:
t4.f.labs.apnic.net DNS NS is on IPv4 only
t6.f.labs.apnic.net DNS NS is on IPv6 only
td.f.labs.apnic.net DNS NS is dual-stacked
tx.f.labs.apnic.net DNS NS is dual-stacked, but IPv6 is unreachable
tz.f.labs.apnic.net DNS NS is dual-stacked, but IPv4 is unreachable
Separately, results.g.labs.apnic.net and *.results.g.labs.apnic.net
(a wildcard) are defined as an IPv4 A record.
Within each subdomain(t4, t6,td, tx and tz) a further family of
subdomains are defined. For example, the following is the zone file
for t4.f.labs.apnic.net:
$TTL 1d
$ORIGIN .
t4.f.labs.apnic.net IN SOA t4.f.labs.apnic.net. ggm.apnic.net. (
2013051101 ; Serial
3h ; Refresh
1h ; Retry
1w ; Expire
3h ) ; Neg. cache TTL
A 203.133.248.25
AAAA 2401:2000:6660::25
;
; name servers
;
NS dns4.f.labs.apnic.net.
;
; zone contents
;
$ORIGIN t4.f.labs.apnic.net.
;
v4 A 203.133.248.25
v6 AAAA 2401:2000:6660::25
;
rd NS dnsr4.f.labs.apnic.net.
r4 NS dnsr4.f.labs.apnic.net.
r6 NS dnsr4.f.labs.apnic.net.
rx NS dnsr4.f.labs.apnic.net.
rz NS dnsr4.f.labs.apnic.net.
This zone has 5 sub-delegations (rd, r4, r6, rx and rz) each of
which is defined to have a single name server.
r4.t4.g.labs.apnic.net IPv4 NS, resources are reachable on IPv4 only
r6.t4.g.labs.apnic.net IPv4 NS, resources are reachable on IPv6 only
rd.t4.g.labs.apnic.net IPv4 NS, resources are reachable on dual-stack
rx.t4.g.labs.apnic.net IPv4 NS, resources define IPv4/IPv6 but IPv6 is unreachable
rz.t4.g.labs.apnic.net IPv4 NS, resources define IPv4/IPv6 but IPv4 is unreachable
For example, the r4.t4.f.labs.apnic.net subdomain uses the following
zone file:
$TTL 1d
$ORIGIN .
r4.t4.f.labs.apnic.net IN SOA r4.t4.f.labs.apnic.net. ggm.apnic.net. (
2013051101 ; Serial
3h ; Refresh
1h ; Retry
1w ; Expire
3h ) ; Neg. cache TTL
A 203.133.248.26
;
; name servers
;
NS dnsr4.f.labs.apnic.net.
;
; zone contents
;
$ORIGIN r4.t4.f.labs.apnic.net.
v4 5 IN A 203.133.248.26
;
; wildcard
;
* 5 IN A 203.133.248.26
Therefore from this delegation chain, an experiment configuration
server can request a client to fetch an experiment such as:
http://t10000.u8738132781.s1367808039.i333.v6024.r4.t4.f.labs.apnic.net/1x1.png.
The t10000.u8738132781.s1367808039.i333.v6024 part is all matched by
the wildcard, under the r4.t4.f.labs.apnic.net domain.
$ dig +short a t10000.u8738132781.s1367808039.i333.v6024.r4.t4.f.labs.apnic.net.
203.133.248.26
$ dig +short aaaa t10000.u8738132781.s1367808039.i333.v6024.r4.t4.f.labs.apnic.net.
(no answer)
$ dig +short r4.t4.f.labs.apnic.net. IN NS
dnsr4.f.labs.apnic.net
$ dig +short dns4.f.labs.apnic.net. IN A
203.133.248.25
$ dig +short dns4.f.labs.apnic.net. IN AAAA
(no answer)
With 5 t* subdomains and 5 r* subdomains a total of 25 domains have
to be populated, each slightly different, respecting the NS and
A/AAAA combinations which have to apply to that experiment.
We operate the experiment’s servers with 11 discrete BIND processes,
each listening to a different IPv4 and IPv6 address. One server is
used for the parent domains. One sever is used for t4, one for t6,
one for td, one for tx and one for tz. One server is used for all
subdomains that include the r4 forms (r4.t4, r4.t6, r4.td, r4.tx,
r4.tz). One is used for all r6 forms, one for rd, one for rx and one
for rz. This separation of parent and child in the DNS servers
ensures the integrity of the IP behaviours in the DNS, as within
this structure of authoritative server separation, the authoritative
name server for the parent is unable to answer questions that can be
resolved by the authoritative name server for the child.
Web server and Client code
The Apache webserver needs to be configured to accept all local IP
bindings and use ‘virtual server’ configuration to service them. In
the simple configuration model we use the ability of the Apache
httpd 2.2 to define a default virtual server, which captures all
otherwise un-defined instances. This framework is suitable to be the
handler for all incoming 1x1.png requests.
Apache configuration
" print cgi.escape(reason) print "
Try Again" print "
" os.unlink(fn) sys.exit() cgitb.enable() form = cgi.FieldStorage() # Get filename here. fileitem = form['user_file'] advertID = form['advertID'].value # Test if the file was uploaded if fileitem.filename: # strip leading path from file name to avoid # directory traversal attacks filename = os.path.basename(fileitem.filename) fn = '/tmp/' + filename fh = open(fn, 'wb') fh.write(fileitem.file.read()) fh.close() siz=os.path.getsize(fn) if siz > MAXSIZ: fail("Image too big: 35kb limit (%dkb)" % (siz/1024)) try: im = Image.open(fn) except IOError: fail("Bad image") wid,hei = im.size widhei=str(wid)+'x'+str(hei) if widhei not in sizes: fail("Image not correct size: %s not in %s" % (widhei, str(sizes))) # retained in case we do static tests at some stage but currently useless #tests = [t for t in ("rd.td", "r4.td", "r6.td", "rd.t6") # if form.getvalue(t, "off") == "on"] template_process( open("/var/www/v6ad/URLTemplate.hx", "r"), open("/var/www/v6ad/out/URLMain.hx", "w"), { '##DEBUG##': "false", '##LISTURL##': "http://results.h.labs.apnic.net/measureipv6id.cgi?advertID="+advertID, '##LISTURL2##': "http://results.g.labs.apnic.net/measureipv6id.cgi?advertID="+advertID, '##TIMEOUT##': "10000", '##BORDER##': "0", '##COLOR##': "ffffff", "##SCRIPTID##": str(advertID), '##USECOOKIE##': "false", '##RATELIMIT##': str(24 * 3600), '##PARAM##': "clickTAG" } ) template_process( open("/var/www/v6ad/Template.swfml", "r"), open("/var/www/v6ad/out/imagelib.swfml", "w"), { '##WIDTH##': str(wid), '##HEIGHT##': str(hei), '##IMAGE##': fn } ) # Construct imagelib.swf working = "/var/www/v6ad/out" swfmill = ("/usr/local/bin/swfmill", "simple", "imagelib.swfml", "imagelib.swf") proc = Popen(swfmill, stdout=PIPE, stderr=STDOUT, cwd=working) output, err = proc.communicate() if proc.poll(): fail("Building SWF image library failed: " + output) # Construct v6test.swf haxe = ("/usr/local/bin/haxe", "-swf-version", "10", "-swf-header", "%d:%d:12:FFFFFF" % (wid, hei), "-swf9", "urltest.swf", "-main", "URLMain", "-swf-lib", "imagelib.swf") proc = Popen(haxe, stdout=PIPE, stderr=STDOUT, cwd=working) output, err = proc.communicate() if proc.poll(): fail("Building final output SWF failed: " + output) # make the test harness template_process( open("/var/www/v6ad/TemplateHarness.html", "r"), open("/var/www/v6ad/out/index.html", "w"), { '##WIDTH##': str(wid), '##HEIGHT##': str(hei), '##IMAGE##': "urltest.swf" } ) # clean up the tempfile os.unlink(fn) swf = open("/var/www/v6ad/out/urltest.swf", "rb") swfbytes = swf.read() swfsize = len(swfbytes) print "Content-Type: application/octet-stream" print "Content-Length:", swfsize print "Content-Disposition: attachment; filename=urltest.swf" print print swfbytes Appendix 2: the JavaScript. The default javascript, ipprototest.js, is as follows: // GPLv3 // $Id: ipprototest.js 27701 2011-01-26 15:21:50Z eaben $ // 2011 Hacked on by Byron Ellacott, APNIC // 2011 Hacked on by George Michaelson, APNIC // Written by Emile Aben, RIPE NCC // Code inspired by Sander Steffann's IPv6test at http://v6test.max.nl/ (function() { var __ipprototest; IPProtoTest = function (opts) { if ( this instanceof IPProtoTest ) { this._version = '10i'; this._done = false; this.userId = ''; this.timeout = 10000; // 10 seconds this.noCheckInterval = 86400000; // 1 day this.domainSuffix = 'labs.apnic.net'; this.testSet = ['r4.td','rd.td','r6.td']; this.testSetLen = this.testSet.length; // shortcut this.randomize = false; // disable shuffling this.dotunnels = false; // disable tunnel testing this.dov6literal = true; // disable tunnel testing this.dov6dns = true; // disable tunnel testing this.docookies = true; // enable cookie time testing this.sampling = 1; // sampling frequency. 1 == disable // eg 2 == 50% 20 == 5% 100 == 1% this._testsComplete = 0; this._now = new Date(); // keep track of init-time this._testTime = this._now.getTime(); this._cookieExpire = new Date(this._testTime + this.noCheckInterval ); this._testId = Math.floor(Math.random()*Math.pow(2,31)); this._result = {}; // sets this._cookie_last_run etc. vars // and set results from previous run (if available) this.parseCookies(); //TODO compare the testset to set tested in cookies? // override defaults if ( opts instanceof Object) { for ( prop in opts ) { //safeguard is now done in the IPProtoTest(opt) call at the end of this file this[prop] = opts[prop]; } if ( this.dov6dns ) { this.testSet.push('rd.t6'); } if ( this.dov6literal ) { this.testSet.push('v6lit'); } if ( this.dotunnels ) { this.testSet.push('v6stf'); this.testSet.push('v6ter'); } this.testSetLen = this.testSet.length; if ( this.randomize ) { this.shuffle( this.testSet ); } } // make object accessible for callbacks __ipprototest = this; // determine if tests need to be done if ( !this.docookies || !this._cookie_last_run ) { if ( this.sampling > 1 && Math.random() > 1/this.sampling ) { // not running test, but setting the cookie if (this.docookies) this.setCookie('__ipprototest_last_run',this._testTime); return this; } else { this.startTest(); } } return this; } else return new IPProtoTest(opts); }; // public functions IPProtoTest.prototype.doGAQ=function() { var _gaq = window._gaq || []; // assuming google default if (this.GAQ instanceof Object) { // .. but allow override _gaq = this.GAQ; } //TODO document this var ipv4 = this._result.r4td ? 'yes' : 'no'; var ipv6 = this._result.r6td ? 'yes' : 'no'; var dual = this._result.rdtd ? 'yes' : 'no'; if (this.dov6dns) { var v6dns = this._result.rdt6 ? 'yes' : 'no'; } if (this.dov6literal) { var v6lit = this._result.v6lit ? 'yes' : 'no'; } if (this.dotunnels) { var v6stf = this._result.v6stf ? 'yes' : 'no'; var v6ter = this._result.v6ter ? 'yes' : 'no'; } var summary = ((ipv4 == 'yes' ? 1 : 0) + (ipv6 == 'yes' ? 2 : 0) + (dual == 'yes' ? 4 : 0)); if (this.dov6dns) { summary += ((v6dns == 'yes' ? 8 : 0)); } if (this.dov6literal) { summary += ((v6lit == 'yes' ? 16 : 0)); } if (this.dotunnels) { summary += ((v6stf == 'yes' ? 32 : 0) + (v6ter == 'yes' ? 64 : 0)); } // Normalize v4val to 0 if the v4 test fails. // Normalize all other test times to relative to v4, if v4 worked // If a test fails, set to zero. // If v4 failed, set all tests to zero. // This ensures zero cases, and no v4 do not contribute to avg times var v4val = this._result.r4td ? this._result.r4td : 0; var v6val = this._result.r6td ? (this._result.r4td ? (this._result.r6td - v4val) : 0) : 0; var duval = this._result.rdtd ? (this._result.r4td ? (this._result.rdtd - v4val) : 0) : 0; if (this.dov6dns) { var dnval = this._result.v6dns ? (this._result.r4td ? (this._result.v6dns - v4val) : 0) : 0; } if (this.dov6literal) { var lival = this._result.v6lit ? (this._result.r4td ? (this._result.v6lit - v4val) : 0) : 0; } if (this.dotunnels) { var stval = this._result.v6stf ? (this._result.r4td ? (this._result.v6stf - v4val) : 0) : 0; var teval = this._result.v6ter ? (this._result.r4td ? (this._result.v6ter - v4val) : 0) : 0; } _gaq.push(['_trackEvent', 'ipprototest', 'ipv4:' + ipv4, 'Tested', 0]); _gaq.push(['_trackEvent', 'ipprototest', 'ipv6:' + ipv6, 'Tested', v6val]); _gaq.push(['_trackEvent', 'ipprototest', 'dual:' + dual, 'Tested', duval]); if (this.dov6dns) { _gaq.push(['_trackEvent', 'ipprototest', 'v6lit:' + v6lit, 'Tested', lival]); } if (this.dov6literal) { _gaq.push(['_trackEvent', 'ipprototest', 'v6dns:' + v6dns, 'Tested', dnval]); } if (this.dotunnels) { _gaq.push(['_trackEvent', 'ipprototest', 'v6stf:' + v6stf, 'Tested', stval]); _gaq.push(['_trackEvent', 'ipprototest', 'v6ter:' + v6ter, 'Tested', teval]); } // push summary line, this users testset. _gaq.push(['_trackEvent', 'ipprototest', 'summary:' + summary]); }; IPProtoTest.prototype.onFinishInit=function() { if ( this.GAQ ) { // do Google analytics this.doGAQ(); } }; IPProtoTest.prototype.finishTest=function() { // cancel the timeout clearTimeout( __ipprototest._timeoutEvent ); if (! __ipprototest._done ) { // report back results var pfx = __ipprototest.getTestPfx(); for (var t_idx=0; t_idx < __ipprototest.testSetLen; t_idx++) { var test = __ipprototest.testSet[t_idx]; test = test.replace(/\./g,''); pfx += [ 'z', test , '-' , __ipprototest._result[ test ] ? __ipprototest._result[ test ] : 'null', '.' ].join(''); } var imgURL = [ 'http://', pfx, 'results.', __ipprototest.domainSuffix, '/1x1.png?', pfx ].join(''); var req=document.createElement('img'); req.src = imgURL; // loads it if(__ipprototest.docookies) __ipprototest.setCookie('__ipprototest_last_run', __ipprototest._testTime); // do all the stuff that needs to be done when // results are in __ipprototest.onFinishInit(); __ipprototest._done = true; // if there is a callback function, invoke it if ( __ipprototest.callback instanceof Function ) __ipprototest.callback(__ipprototest._result); } }; IPProtoTest.prototype.getTestPfx = function() { return [ 't', this.timeout, '.', 'u', this._testTime , '.', 's', this._testId , '.', 'i', this.userId , '.', 'v', this._version , '.' ].join(''); }; IPProtoTest.prototype.startTest = function() { var testPfx = this.getTestPfx(); var testPath='/1x1.png?'+testPfx; for(var i=0;i