Как настроить передачу IP-адресов

Прокси-серверы конфиденциальности в браузере и настройки защиты IP-адресов могут скрывать IP-адреса клиентов в сторонних запросах объявлений к Google Менеджеру рекламы. Маскирование IP-адресов может снизить точность геотаргетинга и релевантность объявлений. Поскольку ваш веб-сервер или сеть доставки контента (CDN) взаимодействует с пользователем в собственном контексте, ваш сервер получает незамаскированный IP-адрес пользователя.

Функция передачи IP-адресов издателем позволяет зафиксировать незамаскированный IPv4- или IPv6-адрес пользователя на вашем сервере, зашифровать его с помощью гибридного шифрования с открытым ключом (HPKE) и передать зашифрованные полезные данные тегу Google Publisher Tag (GPT) на клиенте.

В этой статье рассказывается, как интегрировать передачу IP-адресов с тегом GPT.

Как реализовать рабочий процесс на стороне сервера

Чтобы не раскрывать IP-адрес пользователя в виде обычного текста сторонним скриптам в браузере, выполните следующие действия на веб-сервере или пограничном сервере CDN:

  • Записывать все IP-адреса.
  • Получите открытый ключ.
  • Выполните шифрование HPKE.

Как получить IP-адрес клиента

Извлеките незамаскированный IPv4- или IPv6-адрес клиента из входящего запроса страницы на веб-сервере или в CDN.

  • Прямые подключения. Считывается адрес удаленного сокета входящего HTTP-подключения.
  • За CDN, балансировщиком нагрузки или обратным прокси-сервером. Считывайте заголовок IP-адреса клиента из доверенного пограничного прокси-сервера, например заголовок X-Forwarded-For или заголовок IP-адреса клиента, относящийся к CDN. Доверяйте только тем заголовкам переадресации, которые добавляет или переопределяет ваша инфраструктура прокси-серверов. Если заголовок X-Forwarded-For содержит несколько адресов, разделенных запятыми, выберите IP-адрес клиента, добавленный доверенным пограничным прокси-сервером.

Менеджер рекламы поддерживает следующие адреса:

  • IPv4, например 203.0.113.42.
  • IPv6, например 2001:db8::8a2e:370:7334.

Передавайте только строку IP-адреса без номеров портов, скобок, префиксов CIDR и пробелов.

Получение и кеширование открытого ключа

Google публикует активный ключ шифрования HPKE в виде набора веб-ключей JSON (JWKS) RFC 8037 в следующей конечной точке:

https://www.gstatic.com/real-ip/ip_enc_key.json

Запрос GET к этой конечной точке возвращает объект JSON, массив keys которого содержит один активный объект ключа. Используйте ключ в свойстве x этого объекта, чтобы зашифровать IP-адрес клиента.

В следующем примере показано, как получить открытый ключ и извлечь свойство x в случае успеха или вызвать исключение в случае неудачи.

Java

private static final String KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json";

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * <p>In production, cache the returned public key in memory and refresh it at least once every 24
 * hours rather than fetching it on every incoming request.
 *
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
public static String fetchPublicKey() throws IOException, InterruptedException {
  return fetchPublicKey(KEY_ENDPOINT_URL);
}

public static String fetchPublicKey(String endpointUrl) throws IOException, InterruptedException {
  HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
  HttpRequest request =
      HttpRequest.newBuilder()
          .uri(URI.create(endpointUrl))
          .timeout(Duration.ofSeconds(5))
          .GET()
          .build();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() < 200 || response.statusCode() >= 300) {
    throw new IOException("Failed to fetch public key, HTTP status: " + response.statusCode());
  }
  JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
  JsonArray keys = json.getAsJsonArray("keys");
  if (keys == null || keys.isEmpty()) {
    throw new IOException("No keys found in response from " + endpointUrl);
  }
  JsonObject keyObj = keys.get(0).getAsJsonObject();
  if (!keyObj.has("x")) {
    throw new IOException("Key payload missing 'x' property: " + keyObj);
  }
  return keyObj.get("x").getAsString();
}

Python

KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json"


def fetch_public_key(url: str = KEY_ENDPOINT_URL) -> str:
  """Fetches the active X25519 public key from Google's key endpoint.

  In production, cache the returned public key in memory and refresh it at
  least once every 24 hours rather than fetching it on every incoming request.

  Args:
    url: URL of the public key endpoint.

  Returns:
    The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
  """
  with urllib.request.urlopen(url, timeout=5) as response:
    data = json.loads(response.read().decode("utf-8"))
  keys = data.get("keys", [])
  if not keys:
    raise ValueError(f"No keys found in response from {url}")
  key_payload = keys[0].get("x")
  if not key_payload:
    raise ValueError(f"Key payload missing 'x' property: {keys[0]}")
  return key_payload


JavaScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param {string=} url URL of the public key endpoint.
 * @return {!Promise<string>} The unpadded Base64URL-encoded 32-byte X25519
 *     public key ('x').
 */
async function fetchPublicKey(url = KEY_ENDPOINT_URL) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = await response.json();
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

TypeScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

interface JwkKey {
  kty?: string;
  crv?: string;
  x?: string;
  kid?: string;
}

interface KeyEndpointResponse {
  keys?: JwkKey[];
}

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param url URL of the public key endpoint.
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
export async function fetchPublicKey(
  url: string = KEY_ENDPOINT_URL,
): Promise<string> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = (await response.json()) as KeyEndpointResponse;
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

Если запрос выполнен успешно, ответ содержит массив keys с объектом ключа, у которого есть свойство x:

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "X25519",
      "x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
      "kid": "EZMLFw"
    }
  ]
}

В таблице ниже описаны свойства объекта key в массиве keys.

Свойство Описание
crv Идентификатор эллиптической кривой. Всегда X25519.
kid Идентификатор ключа в кодировке base64url без заполнения, который меняется при ротации ключа Google.
kty Тип ключа. Всегда OKP (пара ключей октетов).
x Недополненный 32-байтовый открытый ключ X25519 в кодировке base64url, используемый для шифрования.

Кешируйте полученный открытый ключ в памяти на сервере и обновляйте его не реже одного раза в 24 часа или чаще в соответствии с заголовком ответа HTTP Cache-Control. Не запрашивайте открытый ключ при каждом входящем запросе страницы. Google сохраняет замененные закрытые ключи. Полезные нагрузки, зашифрованные открытым ключом, который был кеширован в течение предыдущих 24 часов, продолжают расшифровываться без перебоев во время ротации ключей.

Шифрование IP-адреса

Зашифруйте байтовое представление строки IP-адреса клиента в кодировке UTF-8, используя HPKE (RFC 9180) и декодированный 32-байтовый открытый ключ X25519 x, полученный на этапе Получение и кеширование открытого ключа.

В примере ниже показано, как зашифровать строку IP-адреса.

Java

В этом примере используется Tink (com.google.crypto.tink:tink 1.16.0 или более поздней версии):

static {
  try {
    HybridConfig.register();
  } catch (GeneralSecurityException e) {
    throw new ExceptionInInitializerError(e);
  }
}

/**
 * Encrypts an IP address using HPKE (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
@AccessesPartialKey
public static String encryptIp(String pubXB64Url, String ipStr) throws GeneralSecurityException {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  byte[] pubBytes = Base64.getUrlDecoder().decode(pubXB64Url);
  if (pubBytes.length != 32) {
    throw new IllegalArgumentException(
        "Public key must be exactly 32 bytes, got " + pubBytes.length);
  }

  // 2. Configure HPKE parameters (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM, NO_PREFIX).
  HpkeParameters parameters =
      HpkeParameters.builder()
          .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
          .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
          .setAeadId(HpkeParameters.AeadId.AES_256_GCM)
          .setVariant(HpkeParameters.Variant.NO_PREFIX)
          .build();

  HpkePublicKey publicKey =
      HpkePublicKey.create(parameters, Bytes.copyFrom(pubBytes), /* idRequirement= */ null);

  // 3. Encrypt the UTF-8 IP address bytes. With Variant.NO_PREFIX, Tink
  //    outputs raw (enc || ciphertext_with_tag).
  KeysetHandle handle =
      KeysetHandle.newBuilder()
          .addEntry(KeysetHandle.importKey(publicKey).withRandomId().makePrimary())
          .build();

  HybridEncrypt encryptor = handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class);
  byte[] payload =
      encryptor.encrypt(ipStr.getBytes(StandardCharsets.UTF_8), /* contextInfo= */ new byte[0]);

  // 4. Return unpadded Base64URL-encoded payload.
  return Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
}

Python

В этом примере используется pyhpke:

def encrypt_ip(pub_x_b64url: str, ip_str: str) -> str:
  """Encrypts an IP address using HPKE (RFC 9180).

  Args:
    pub_x_b64url: Base64URL-encoded 32-byte X25519 public key ('x').
    ip_str: Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").

  Returns:
    Unpadded Base64URL-encoded payload (enc || ciphertext).
  """
  # 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  pub_bytes = base64.urlsafe_b64decode(
      pub_x_b64url + "=" * (-len(pub_x_b64url) % 4)
  )
  if len(pub_bytes) != 32:
    raise ValueError(
        f"Public key must be exactly 32 bytes, got {len(pub_bytes)}"
    )

  # 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  #    AES256_GCM).
  suite = CipherSuite.new(
      KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM
  )
  public_key = suite.kem.deserialize_public_key(pub_bytes)

  # 3. Create a fresh sender context per encryption (you may cache 'suite' and
  #    'public_key', but never reuse 'sender' across multiple seal() calls) and
  #    package (enc || ciphertext).
  enc, sender = suite.create_sender_context(public_key)
  ciphertext = sender.seal(ip_str.encode("utf-8"))

  # 4. Return unpadded Base64URL-encoded payload.
  return base64.urlsafe_b64encode(enc + ciphertext).decode("utf-8").rstrip("=")


JavaScript

В этом примере используется @hpke/core:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param {string} pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param {string} ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return {!Promise<string>} Unpadded Base64URL-encoded payload
 *     (enc || ciphertext).
 */
async function encryptIp(pubXB64Url, ipStr) {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

TypeScript

В этом примере используется @hpke/core:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
export async function encryptIp(
  pubXB64Url: string,
  ipStr: string,
): Promise<string> {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

Настройте библиотеку HPKE, используя следующий набор шифров и параметры:

Параметр Значение
Механизм инкапсуляции ключей (KEM) DHKEM(X25519, HKDF-SHA256) (0x0020)
Функция формирования ключа (KDF) HKDF-SHA256 (0x0001)
Аутентифицированное шифрование со связанными данными (AEAD) AES-256-GCM (0x0002)
Режим Базовый (0x00)
Информация о приложении (info) Пусто ("")
Связанные данные (aad) Пусто ("")

Зашифрованные выходные данные должны быть представлены в виде необработанного объединения 32-байтового инкапсулированного временного открытого ключа enc и зашифрованного текста AEAD, включая 16-байтовый тег аутентификации GCM, без префикса Tink или пользовательского заголовка. Закодируйте результат в виде строки base64url без заполнения.

Зашифруйте каждую полезную нагрузку как отдельное однократное сообщение HPKE с новой парой временных ключей. Если ваша библиотека HPKE использует контекст отправителя с отслеживанием состояния, создавайте новый контекст для каждого шифрования, а не используйте его повторно для нескольких сообщений. Шифруйте IP-адрес клиента для каждого запроса страницы или кешируйте полученную строку зашифрованного текста base64url для каждого IP-адреса на короткое время жизни (TTL).

Как реализовать клиентский рабочий процесс

После того как ваш сервер сгенерирует зашифрованный IP-адрес в формате base64url, передайте его тегу GPT на стороне клиента, прежде чем запрашивать объявления.

Передача зашифрованного IP-адреса в тег GPT

Передайте зашифрованную строку IP-адреса в метод googletag.setConfig, используя свойство ipAddress в PageSettingsConfig.

В примере ниже зашифрованная полезная нагрузка IP-адреса, созданная сервером, вставляется в метод googletag.setConfig перед включением сервисов и показом рекламного места.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Publisher-provided IP example</title>
    <script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous" async></script>
    <script>
      window.googletag = window.googletag || {cmd: []};

      // Inject the base64url-encoded encrypted IP generated by your server.
      const encryptedClientIp = 'ENCRYPTED_IP_PAYLOAD';

      googletag.cmd.push(function() {
        // Configure the encrypted IP before enabling services.
        googletag.setConfig({
          ipAddress: encryptedClientIp,
        });

        googletag
            .defineSlot('/6355419/Travel/Europe/France/Paris', [300, 250], 'banner-ad')
            .addService(googletag.pubads());
        googletag.enableServices();
      });
    </script>
  </head>
  <body>
    <div id="banner-ad" style="width: 300px; height: 250px;"></div>
    <script>
      googletag.cmd.push(function() {
        googletag.display('banner-ad');
      });
    </script>
  </body>
</html>

Замените ENCRYPTED_IP_PAYLOAD строкой, закодированной в base64url без заполнения, которую создает ваш сервер.

Чтобы удалить ранее настроенный IP-адрес на странице, задайте для свойства ipAddress значение null:

googletag.setConfig({ ipAddress: null });

Проверьте интеграцию

Чтобы убедиться, что тег GPT отправляет зашифрованные данные IP-адреса, а Менеджер рекламы расшифровывает их, используйте инструменты разработчика в браузере и Google Консоль для издателей.

Инструменты разработчика в браузере

  1. Откройте инструменты разработчика.
  2. Выберите вкладку Сеть.
  3. Фильтр для запросов объявлений в /gampad/ads.
  4. Убедитесь, что URL запроса содержит непустой параметр запроса ips. Поскольку тег GPT дополнительно кодирует значение ipAddress перед отправкой запроса объявления, параметр ips не совпадает с зашифрованной строкой.

Google Консоль для издателей

  1. Чтобы открыть консоль, следуйте инструкциям в руководстве по Google Консоли для издателей.
  2. Выберите вкладку Запрос страницы.
  3. Убедитесь, что предупреждения ipAddress не появляются. Если с полезной нагрузкой возникнет проблема, в консоли появится одно из следующих предупреждений:

    • Если значение, переданное свойству ipAddress, не является допустимой строкой base64 или base64url длиной от 67 до 124 символов, тег GPT игнорирует это значение, опускает параметр ips в запросе объявления и регистрирует следующее предупреждение:

      Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.
      
    • Если Менеджер рекламы не может расшифровать предоставленные полезные данные IP или если расшифрованный текст не является действительным адресом IPv4 или IPv6, в консоли регистрируется следующее предупреждение и выполняется возврат к IP-адресу сетевого запроса:

      Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).