/** * DomainFront Relay + Full Tunnel — Google Apps Script * * FOUR modes: * 1. Single relay: POST { k, m, u, h, b, ct, r } → { s, h, b } * 2. Batch relay: POST { k, q: [{m,u,h,b,ct,r}, ...] } → { q: [{s,h,b}, ...] } * 3. Tunnel: POST { k, t, h, p, sid, d } → { sid, d, eof } * 4. Tunnel batch: POST { k, t:"batch", ops:[...] } → { r: [...] } * Batch ops include TCP (`connect`, `data`) and UDP (`udp_open`, * `udp_data`) tunnel-node operations. * * CHANGE THESE TO YOUR OWN VALUES! */ const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET"; const TUNNEL_SERVER_URL = "https://YOUR_TUNNEL_NODE_URL"; const TUNNEL_AUTH_KEY = "YOUR_TUNNEL_AUTH_KEY"; // Active-probing defense. When false (production default), bad AUTH_KEY // requests get a decoy HTML page that looks like a placeholder Apps // Script web app instead of the JSON `{"e":"unauthorized"}` body. This // makes the deployment indistinguishable from a forgotten-but-public // Apps Script project to active scanners that POST malformed payloads // looking for proxy endpoints. // // Set to `true` during initial setup if a misconfigured client is // hitting "unauthorized" and you want the explicit JSON error to debug // — then flip back to false before the deployment is widely shared. // (Inspired by #365 Section 3, mhrv-rs v1.8.0+.) const DIAGNOSTIC_MODE = false; // Connection-level + IP-leak request headers we strip before forwarding // to the destination. UrlFetchApp rejects most of the connection-level // names anyway, but we also drop the `X-Forwarded-*` / `Forwarded` / // `Via` family so that a misconfigured upstream proxy on the user side // can't leak the user's real IP through the relay path. Mirrors // upstream `masterking32/MasterHttpRelayVPN@3094288`. const SKIP_HEADERS = { host: 1, connection: 1, "content-length": 1, "transfer-encoding": 1, "proxy-connection": 1, "proxy-authorization": 1, "priority": 1, te: 1, "x-forwarded-for": 1, "x-forwarded-host": 1, "x-forwarded-proto": 1, "x-forwarded-port": 1, "x-real-ip": 1, "forwarded": 1, "via": 1, }; // Methods we consider safe to replay if `UrlFetchApp.fetchAll()` raises. // GET/HEAD/OPTIONS are idempotent per RFC 9110; POST/PUT/PATCH/DELETE // can have side-effects so we surface the error instead of silently // re-firing them. const SAFE_REPLAY_METHODS = { GET: 1, HEAD: 1, OPTIONS: 1 }; // Compiled once to avoid re-parsing per request in the relay hot path. const URL_RE = /^https?:\/\//i; // HTML body for the bad-auth decoy. Mimics a minimal Apps Script-style // placeholder page — no proxy-shaped JSON, nothing distinctive enough // for a probe to fingerprint as a tunnel endpoint. const DECOY_HTML = 'Web App' + '

The script completed but did not return anything.

' + ''; function _decoyOrError(jsonBody) { if (DIAGNOSTIC_MODE) return _json(jsonBody); return ContentService .createTextOutput(DECOY_HTML) .setMimeType(ContentService.MimeType.HTML); } // Edge DNS cache. Plain UDP/53 queries normally traverse the full // client → GAS → tunnel-node → public resolver path, and the // trans-Atlantic round-trip dominates first-hop latency. When // ENABLE_EDGE_DNS_CACHE is true, _doTunnelBatch intercepts udp_open // ops with port=53, serves the reply from CacheService on a hit, or // does its own DoH lookup on a miss from inside Google's network. // Cache hits never reach the tunnel-node. // // Safety property: parse errors, refused qtypes, and "every DoH resolver // failed" return null from _edgeDnsResolve and the op falls through to // the existing tunnel-node forward path. CacheService failures (transient // quota, getAll exceptions, oversize keys) are softer: the per-batch // cache lookup is skipped and no put happens, but DoH still runs from // inside Google's network. The per-op outcome degrades to "uncached // forward via DoH" rather than "forwarded all the way to the tunnel-node". // Set ENABLE_EDGE_DNS_CACHE=false to disable the whole feature and route // all DNS through the tunnel as before. const ENABLE_EDGE_DNS_CACHE = true; // DoH endpoints tried in order on cache miss. All speak RFC 8484 // over GET. Apps Script's outbound network peers well to all three. const EDGE_DNS_RESOLVERS = [ "https://1.1.1.1/dns-query", "https://dns.google/dns-query", "https://dns.quad9.net/dns-query", ]; // CacheService bounds: 6h max TTL, 100KB per value, ~1000 keys, 250-char keys. const EDGE_DNS_MIN_TTL_S = 30; const EDGE_DNS_MAX_TTL_S = 21600; // 6h CacheService ceiling // Used for NXDOMAIN/SERVFAIL and the rare "no answer + no SOA in authority" // case. NOERROR/NODATA replies normally carry an SOA, and per RFC 2308 §5 // we honor that SOA's TTL via _dnsMinTtl (the positive path). const EDGE_DNS_NEG_TTL_S = 45; const EDGE_DNS_CACHE_PREFIX = "edns:"; // CacheService rejects keys longer than 250 chars. Names approaching the // 253-char DNS limit + prefix + qtype digits can exceed that, so keys // over this length get switched to a SHA-256-hashed form (see // _edgeDnsPrepare) rather than skipping the cache entirely. const EDGE_DNS_MAX_KEY_LEN = 240; // qtypes we refuse to cache and pass through to the tunnel-node: // 255 = ANY (resolvers handle it more correctly than we would) const EDGE_DNS_REFUSE_QTYPES = { 255: 1 }; // ========================== Entry point ========================== function doPost(e) { try { var req = JSON.parse(e.postData.contents); if (req.k !== AUTH_KEY) return _decoyOrError({ e: "unauthorized" }); // Tunnel mode if (req.t) return _doTunnel(req); // Batch relay mode if (Array.isArray(req.q)) return _doBatch(req.q); // Single relay mode return _doSingle(req); } catch (err) { // Parse failures of the request body are also probe-shaped — a real // mhrv-rs client never sends invalid JSON. Decoy for the same reason. return _decoyOrError({ e: String(err) }); } } // ========================== Tunnel mode ========================== function _doTunnel(req) { // Batch tunnel: { k, t:"batch", ops:[...] } if (req.t === "batch") { return _doTunnelBatch(req); } // Single tunnel op var payload = { k: TUNNEL_AUTH_KEY }; switch (req.t) { case "connect": payload.op = "connect"; payload.host = req.h; payload.port = req.p; break; case "connect_data": payload.op = "connect_data"; payload.host = req.h; payload.port = req.p; if (req.d) payload.data = req.d; break; case "data": payload.op = "data"; payload.sid = req.sid; if (req.d) payload.data = req.d; break; case "close": payload.op = "close"; payload.sid = req.sid; break; default: // Structured `code` lets the Rust client detect version skew // without substring-matching the error text. Must match // CODE_UNSUPPORTED_OP in tunnel_client.rs and tunnel-node/src/main.rs. return _json({ e: "unknown tunnel op: " + req.t, code: "UNSUPPORTED_OP" }); } var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel", { method: "post", contentType: "application/json", payload: JSON.stringify(payload), muteHttpExceptions: true, followRedirects: true, }); if (resp.getResponseCode() !== 200) { return _json({ e: "tunnel node HTTP " + resp.getResponseCode() }); } return ContentService.createTextOutput(resp.getContentText()) .setMimeType(ContentService.MimeType.JSON); } // Batch tunnel: forward all ops in one request to /tunnel/batch. // When ENABLE_EDGE_DNS_CACHE is true, udp_open/port=53 ops are served // locally where possible and only the remainder is forwarded. // // Edge-DNS resolution runs in two passes so the CacheService backend // is hit exactly once for the whole batch: // pass 1: parse each candidate's question and collect cache keys // one cache.getAll(keys) call serves every hit // pass 2: resolve each candidate (cache hit → synth; miss → DoH; null // → tunnel-node forward) // On a 5-DNS-query batch, this collapses 5 serial cache.get round trips // into one cache.getAll round trip. function _doTunnelBatch(req) { var ops = (req && req.ops) || []; // Feature off: byte-identical to the pre-feature behavior. if (!ENABLE_EDGE_DNS_CACHE) { return _doTunnelBatchForward(ops); } var results = new Array(ops.length); // sparse: filled by edge-DNS hits var forwardOps = []; var forwardIdx = []; // Pass 1: route non-DNS ops to forward, parse DNS candidates. var candidates = []; // [{ i, prep }, ...] for (var i = 0; i < ops.length; i++) { var op = ops[i]; if (op && op.op === "udp_open" && op.port === 53 && op.d) { var prep = _edgeDnsPrepare(op); if (prep) { candidates.push({ i: i, prep: prep }); continue; } } forwardOps.push(op); forwardIdx.push(i); } // One batched cache lookup for every DNS candidate. CacheService.getAll // returns a {key: value} map populated only for hits; missing keys are // simply absent. Any failure (transient quota, backend hiccup) returns // an empty map so each candidate falls through to its own DoH attempt // with no cached put either — the safe degradation path. var cacheMap = {}; var cache = null; if (candidates.length > 0) { try { cache = CacheService.getScriptCache(); var keys = new Array(candidates.length); for (var c = 0; c < candidates.length; c++) { keys[c] = candidates[c].prep.key; } cacheMap = cache.getAll(keys) || {}; } catch (_) { cacheMap = {}; cache = null; } } // Pass 2: resolve each candidate. cacheMap doubles as the in-batch dedup // table — a successful DoH writes its encoded reply back into cacheMap // so a later candidate with the same qname/qtype hits without re-DoH. // On null (cache miss + DoH all failed), append to the forward path so // the tunnel-node still gets a chance. for (var c = 0; c < candidates.length; c++) { var cand = candidates[c]; var synth = _edgeDnsResolve( cand.prep, cacheMap[cand.prep.key] || null, cache, cacheMap); if (synth) { results[cand.i] = synth; } else { forwardOps.push(ops[cand.i]); forwardIdx.push(cand.i); } } // All ops served locally — no tunnel-node round-trip. if (forwardOps.length === 0) { return _json({ r: results }); } // Nothing was served locally — forward verbatim, no splice needed. if (forwardOps.length === ops.length) { return _doTunnelBatchForward(ops); } // Partial: forward the un-served ops and splice results back in place. var resp = _doTunnelBatchFetch(forwardOps); if (resp.error) return _json({ e: resp.error }); if (resp.r.length !== forwardOps.length) { // Tunnel-node version skew — bail explicitly rather than silently // route TCP responses to UDP sids. return _json({ e: "tunnel batch length mismatch" }); } return _json({ r: _spliceTunnelResults(forwardIdx, resp.r, results) }); } // Verbatim forward: no splice, response passed through unchanged. function _doTunnelBatchForward(ops) { var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel/batch", { method: "post", contentType: "application/json", payload: JSON.stringify({ k: TUNNEL_AUTH_KEY, ops: ops }), muteHttpExceptions: true, followRedirects: true, }); if (resp.getResponseCode() !== 200) { return _json({ e: "tunnel batch HTTP " + resp.getResponseCode() }); } return ContentService.createTextOutput(resp.getContentText()) .setMimeType(ContentService.MimeType.JSON); } // Forward + parse for the splice path. Returns { r:[...] } on success or // { error: "..." } on any failure. function _doTunnelBatchFetch(ops) { var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel/batch", { method: "post", contentType: "application/json", payload: JSON.stringify({ k: TUNNEL_AUTH_KEY, ops: ops }), muteHttpExceptions: true, followRedirects: true, }); if (resp.getResponseCode() !== 200) { return { error: "tunnel batch HTTP " + resp.getResponseCode() }; } try { var parsed = JSON.parse(resp.getContentText()); return { r: (parsed && parsed.r) || [] }; } catch (err) { return { error: "tunnel batch parse error" }; } } // Pure helper: writes forwardedResults[j] into allResults[forwardIdx[j]] // for each j. Returns the mutated allResults so callers can chain. Pure // function — testable without the GAS runtime. function _spliceTunnelResults(forwardIdx, forwardedResults, allResults) { for (var j = 0; j < forwardIdx.length; j++) { allResults[forwardIdx[j]] = forwardedResults[j]; } return allResults; } // ========================== HTTP relay mode ========================== function _doSingle(req) { if (!req.u || typeof req.u !== "string" || !URL_RE.test(req.u)) { return _json({ e: "bad url" }); } var opts = _buildOpts(req); var resp = UrlFetchApp.fetch(req.u, opts); return _json({ s: resp.getResponseCode(), h: _respHeaders(resp), b: Utilities.base64Encode(resp.getContent()), }); } function _doBatch(items) { var fetchArgs = []; var fetchIndex = []; var fetchMethods = []; var errorMap = {}; for (var i = 0; i < items.length; i++) { var item = items[i]; if (!item || typeof item !== "object") { errorMap[i] = "bad item"; continue; } if (!item.u || typeof item.u !== "string" || !URL_RE.test(item.u)) { errorMap[i] = "bad url"; continue; } try { var opts = _buildOpts(item); opts.url = item.u; fetchArgs.push(opts); fetchIndex.push(i); fetchMethods.push(String(item.m || "GET").toUpperCase()); } catch (buildErr) { errorMap[i] = String(buildErr); } } // fetchAll() runs all requests in parallel inside Google. If it // throws as a whole (e.g. one URL violates UrlFetchApp limits and // poisons the whole batch), degrade to per-item fetch so a single // bad request does not zero out the entire batch's responses. // Mirrors upstream `masterking32/MasterHttpRelayVPN@3094288`. var responses = []; if (fetchArgs.length > 0) { try { responses = UrlFetchApp.fetchAll(fetchArgs); } catch (fetchAllErr) { responses = []; for (var j = 0; j < fetchArgs.length; j++) { try { if (!SAFE_REPLAY_METHODS[fetchMethods[j]]) { errorMap[fetchIndex[j]] = "batch fetchAll failed; unsafe method not replayed"; responses[j] = null; continue; } var fallbackReq = fetchArgs[j]; var fallbackUrl = fallbackReq.url; var fallbackOpts = {}; for (var key in fallbackReq) { if ( Object.prototype.hasOwnProperty.call(fallbackReq, key) && key !== "url" ) { fallbackOpts[key] = fallbackReq[key]; } } responses[j] = UrlFetchApp.fetch(fallbackUrl, fallbackOpts); } catch (singleErr) { errorMap[fetchIndex[j]] = String(singleErr); responses[j] = null; } } } } var results = []; var rIdx = 0; for (var i = 0; i < items.length; i++) { if (Object.prototype.hasOwnProperty.call(errorMap, i)) { results.push({ e: errorMap[i] }); } else { var resp = responses[rIdx++]; if (!resp) { results.push({ e: "fetch failed" }); } else { results.push({ s: resp.getResponseCode(), h: _respHeaders(resp), b: Utilities.base64Encode(resp.getContent()), }); } } } return _json({ q: results }); } // ========================== Helpers ========================== function _buildOpts(req) { var opts = { method: (req.m || "GET").toLowerCase(), muteHttpExceptions: true, followRedirects: req.r !== false, validateHttpsCertificates: true, escaping: false, }; if (req.h && typeof req.h === "object") { var headers = {}; for (var k in req.h) { if (req.h.hasOwnProperty(k) && !SKIP_HEADERS[k.toLowerCase()]) { headers[k] = req.h[k]; } } opts.headers = headers; } if (req.b) { opts.payload = Utilities.base64Decode(req.b); if (req.ct) opts.contentType = req.ct; } return opts; } // Lazy module-level cache of the runtime feature check; reset between GAS // executions but reused across all responses inside a single execution // (batches of 50+ make this matter). var _hasGetAllHeaders = null; function _respHeaders(resp) { if (_hasGetAllHeaders === null) { _hasGetAllHeaders = (typeof resp.getAllHeaders === "function"); } if (_hasGetAllHeaders) { try { return resp.getAllHeaders(); } catch (err) {} } return resp.getHeaders(); } // `doGet` is what active scanners hit first (HTTP GET probes are cheaper // than POSTs). We use ContentService here so the response body is the // raw HTML we wrote — `HtmlService.createHtmlOutput` would wrap it in // a `goog.script.init` sandbox iframe, which the Rust client would then // see if it ever GET-followed a redirect back onto /macros/.../exec // (decoy/no-json error path). ContentService keeps the doGet response // indistinguishable from a forgotten static-HTML web app. function doGet(e) { return ContentService .createTextOutput(DECOY_HTML) .setMimeType(ContentService.MimeType.HTML); } function _json(obj) { return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType( ContentService.MimeType.JSON ); } // ========================== Edge DNS helpers ========================== // Phase-1 helper: parses a udp_open op into the data needed for both the // batched cache lookup and the eventual resolve. Returns {bytes, q, key} // on success, or null for unparseable/refused ops so the caller can route // them to the tunnel-node forward path. // // Long qnames that would exceed CacheService's 250-char key limit fall back // to a SHA-256-hashed key under a separate `edns:h:` namespace. The // 256-bit digest makes accidental collisions astronomically unlikely, and // the distinct namespace prevents short-name keys from colliding with // hashed long-name keys. function _edgeDnsPrepare(op) { try { var bytes = Utilities.base64Decode(op.d); if (!bytes || bytes.length < 12) return null; var q = _dnsParseQuestion(bytes); if (!q) return null; if (EDGE_DNS_REFUSE_QTYPES[q.qtype]) return null; var key = EDGE_DNS_CACHE_PREFIX + q.qtype + ":" + q.qname; if (key.length > EDGE_DNS_MAX_KEY_LEN) { key = EDGE_DNS_CACHE_PREFIX + "h:" + q.qtype + ":" + _sha256Hex(q.qname); } return { bytes: bytes, q: q, key: key }; } catch (_) { return null; } } // Phase-2 helper: given a prepared op and an optional pre-fetched cache // value, returns a synthesized batch-result {sid, pkts, eof} on success, // or null on any failure so the caller can forward to the tunnel-node. // // `cache` is the CacheService handle reused across the batch (or null // if CacheService is unavailable, in which case DoH still runs // but no put). // `localMap` is an optional in-batch lookup table (typically the same // object returned by cache.getAll). When DoH succeeds, the // encoded reply is written back to localMap[prep.key] so that // a later candidate in the same batch with the same qname/qtype // hits without a second DoH round-trip. function _edgeDnsResolve(prep, cachedReplyB64, cache, localMap) { try { if (cachedReplyB64) { try { var hit = Utilities.base64Decode(cachedReplyB64); if (hit && hit.length >= 12) { // Rewrite txid to match this query (RFC 1035 §4.1.1). Returns a // copy so the cached bytes themselves are never mutated. var rewritten = _dnsRewriteTxid(hit, prep.q.txid); return { sid: "edns-cache", pkts: [Utilities.base64Encode(rewritten)], eof: true, }; } } catch (_) { /* corrupt cache entry — fall through to DoH */ } } for (var i = 0; i < EDGE_DNS_RESOLVERS.length; i++) { var reply = _edgeDnsDoh(EDGE_DNS_RESOLVERS[i], prep.bytes); if (!reply) continue; var rcode = reply[3] & 0x0F; var ttl; if (rcode === 2 || rcode === 3) { ttl = EDGE_DNS_NEG_TTL_S; } else { var minTtl = _dnsMinTtl(reply); ttl = (minTtl === null) ? EDGE_DNS_NEG_TTL_S : minTtl; if (ttl < EDGE_DNS_MIN_TTL_S) ttl = EDGE_DNS_MIN_TTL_S; if (ttl > EDGE_DNS_MAX_TTL_S) ttl = EDGE_DNS_MAX_TTL_S; } // Encode once and reuse for both the persistent cache and the // in-batch dedup map. The reply bytes carry the resolver-echoed // txid; any future hit rewrites it to that request's txid. var encoded = (cache || localMap) ? Utilities.base64Encode(reply) : null; if (cache) { try { cache.put(prep.key, encoded, ttl); } catch (_) { // >100KB value or transient quota — still return the live answer. } } if (localMap) { localMap[prep.key] = encoded; } // The DoH reply already echoes our query's txid; rewrite defensively // in case a resolver mangles it. var fixed = _dnsRewriteTxid(reply, prep.q.txid); return { sid: "edns-doh", pkts: [Utilities.base64Encode(fixed)], eof: true, }; } return null; } catch (err) { return null; } } // Hex-encodes the SHA-256 of a UTF-8 string. Used to keep long-qname cache // keys under CacheService's 250-char limit. 64 hex chars is well below the // cap and survives any future bumps to EDGE_DNS_MAX_KEY_LEN. SHA-256 over // MD5 here is just future-proofing — the hash isn't security-sensitive // (cache namespace only), but SHA-256 avoids any "why MD5?" discussion. function _sha256Hex(s) { var d = Utilities.computeDigest( Utilities.DigestAlgorithm.SHA_256, s, Utilities.Charset.UTF_8); var hex = ""; for (var i = 0; i < d.length; i++) { var b = d[i] & 0xFF; hex += (b < 16 ? "0" : "") + b.toString(16); } return hex; } // Single DoH GET against `url`. Returns the reply as a byte array, or null // on any failure (HTTP non-200, network error, malformed body). function _edgeDnsDoh(url, queryBytes) { try { var dns = Utilities.base64EncodeWebSafe(queryBytes).replace(/=+$/, ""); var resp = UrlFetchApp.fetch(url + "?dns=" + dns, { method: "get", muteHttpExceptions: true, followRedirects: true, headers: { accept: "application/dns-message" }, }); if (resp.getResponseCode() !== 200) return null; var body = resp.getContent(); if (!body || body.length < 12) return null; return body; } catch (err) { return null; } } // Returns { txid, qname, qtype } from a DNS wire-format query. // qname is lowercased and dot-joined (no trailing dot). Null on malformed. function _dnsParseQuestion(bytes) { if (bytes.length < 12) return null; var qdcount = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); // RFC ambiguity: multi-question queries are essentially unused in // practice and would mis-key the cache (we'd cache a multi-answer reply // under only the first question). Bail and let the tunnel-node handle it. if (qdcount !== 1) return null; var off = 12; var labels = []; var nameLen = 0; while (off < bytes.length) { var len = bytes[off] & 0xFF; if (len === 0) { off++; break; } if ((len & 0xC0) !== 0) return null; // questions don't use compression if (len > 63) return null; off++; if (off + len > bytes.length) return null; var label = ""; for (var i = 0; i < len; i++) { var c = bytes[off + i] & 0xFF; if (c >= 0x41 && c <= 0x5A) c += 0x20; // ASCII lowercase label += String.fromCharCode(c); } labels.push(label); off += len; nameLen += len + 1; if (nameLen > 255) return null; } if (off + 4 > bytes.length) return null; var qtype = ((bytes[off] & 0xFF) << 8) | (bytes[off + 1] & 0xFF); return { txid: ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF), qname: labels.join("."), qtype: qtype, }; } // Walks the DNS reply's answer + authority sections and returns the min RR // TTL, or null if there are no RRs (caller treats null as "use neg TTL"). // Returns null on any malformed input. function _dnsMinTtl(bytes) { if (bytes.length < 12) return null; var qdcount = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); var ancount = ((bytes[6] & 0xFF) << 8) | (bytes[7] & 0xFF); var nscount = ((bytes[8] & 0xFF) << 8) | (bytes[9] & 0xFF); var off = 12; for (var q = 0; q < qdcount; q++) { off = _dnsSkipName(bytes, off); if (off < 0 || off + 4 > bytes.length) return null; off += 4; } var min = null; var rrTotal = ancount + nscount; for (var r = 0; r < rrTotal; r++) { off = _dnsSkipName(bytes, off); if (off < 0 || off + 10 > bytes.length) return null; // 2B type, 2B class, 4B TTL, 2B rdlength var ttl = ((bytes[off + 4] & 0xFF) * 0x1000000) + (((bytes[off + 5] & 0xFF) << 16) | ((bytes[off + 6] & 0xFF) << 8) | (bytes[off + 7] & 0xFF)); // RFC 2181: TTLs are 32-bit unsigned; values with the top bit set are // treated as 0. Multiplying the high byte (instead of <<24) avoids V8 // sign-extension and keeps `ttl` in [0, 2^32). if (ttl < 0 || ttl > 0x7FFFFFFF) ttl = 0; if (min === null || ttl < min) min = ttl; var rdlen = ((bytes[off + 8] & 0xFF) << 8) | (bytes[off + 9] & 0xFF); off += 10 + rdlen; if (off > bytes.length) return null; } return min; } // Advances past a DNS name (sequence of labels or 16-bit pointer). // Returns the new offset, or -1 on malformed input. function _dnsSkipName(bytes, off) { while (off < bytes.length) { var len = bytes[off] & 0xFF; if (len === 0) return off + 1; if ((len & 0xC0) === 0xC0) { if (off + 2 > bytes.length) return -1; return off + 2; // pointer terminates the name in-place } if ((len & 0xC0) !== 0) return -1; // reserved label type if (len > 63) return -1; off += 1 + len; } return -1; } // Returns a copy of `bytes` with the first 2 bytes overwritten by the // big-endian 16-bit transaction id. Coerces to signed-byte range so the // result round-trips through Utilities.base64Encode regardless of whether // the runtime exposes bytes as signed Java int8 or unsigned JS numbers. // // Always copies — the cache-safety invariant (callers can hand in a buffer // they may reuse, e.g. a CacheService string round-tripped through decode) // is enforced here rather than via per-call-site reasoning. The copy is // cheap (~100 bytes for a typical DNS reply) compared to the surrounding // base64 encode/decode work. function _dnsRewriteTxid(bytes, txid) { var out = []; for (var i = 0; i < bytes.length; i++) out.push(bytes[i]); var hi = (txid >> 8) & 0xFF; var lo = txid & 0xFF; out[0] = hi > 127 ? hi - 256 : hi; out[1] = lo > 127 ? lo - 256 : lo; return out; }