Рубрики
Без рубрики

Подделать системные часы для одного приложения с помощью libfaketime

Небольшая предыстория Мой вариант использования был довольно специфичным – я хотел провести несколько сквозных работ… С пометкой “Учебник”, “докер”, “java”, “тестирование”.

Мой вариант использования был довольно специфичным – я хотел провести несколько сквозных тестов Java-приложения, работающего в контейнере Docker на базе CentOS. Фрагмент кода, который я хотел протестировать, полагался на сравнение дат:

if (happenedYesterday(event)) {
    foo();
} else {
    bar();
}

Учитывая, что я не смог изменить тестовые данные, проще всего было каким-то образом заставить приложение думать, что это вчера , заставить его создать событие , затем восстановите исходную дату и заставьте приложение вызвать приведенный выше фрагмент кода.

Поскольку я хотел бы показать, как я перешел от идеи к рабочему решению, мне нужно предоставить способ воспроизвести все ошибки, которые я допустил в рамках этого упражнения – мне нужна среда, максимально приближенная к оригиналу. Насколько это возможно. Чтобы достичь этого, а также чтобы примеры были как можно более простыми, я буду использовать fabric8/java-centos-openjdk8-jdk Docker image.

Давайте запустим контейнер из неизмененного изображения и откроем его оболочку:

[me@pc ~]$ docker run --name centos -d -it fabric8/java-centos-openjdk8-jdk /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]#

ПРИМЕЧАНИЕ: -u 0 аргумент приводит к тому, что команда регистрируется в контейнере как root (0 – идентификатор пользователя root)

Во всех примерах в этом посте я буду использовать pc как в [me@pc ~]$ для указания команд, вызываемых на моем локальном компьютере, и centos как в [root@centos/]# для команд, вызываемых внутри контейнера.

В моем наивном подходе я думал, что этот шаг будет таким же простым, как выполнение одной из этих команд:

[root@centos /]# date -s "15 Oct 2019 19:05"
date: cannot set date: Operation not permitted
Tue Oct 15 19:05:00 UTC 2019
[root@centos /]# hwclock --set --date "15 Oct 2019 19:05"
hwclock: Cannot access the Hardware Clock via any known method.
hwclock: Use the --debug option to see the details of our search for an access method.

К сожалению, это было не так.

Я пытался найти некоторые обходные пути, но, насколько я понимаю, Docker повторно использует часы хост-компьютера, поэтому переопределение даты в контейнере либо невыполнимо, либо нелегко выполнимо 1 2 3 . Поскольку я всего лишь случайный пользователь Docker, я не хотел копать глубже. Однако, когда я искал обходной путь, я наткнулся на другой способ изменить время – |/libfaketime .

libfaketime – это библиотека, которая способна “переопределять” системные вызовы, используемые приложениями для получения текущей даты или времени. Затем он может предоставить поддельное значение для этих вызовов. Более того, вам не нужно изменять строку существующего кода или добавлять ее в список зависимостей вашего приложения – она будет прозрачной. Поскольку я не гуру Linux, использование этой библиотеки сначала показалось мне оболочкой для моего java-приложения, хотя это не так, как это работает.

Установка проста – вы берете исходный код библиотеки и запускаете make install в корневом каталоге извлеченных исходных текстов. Это приведет к созданию множества файлов в /usr/local/lib/faketime . Чтобы автоматизировать этот процесс, я создал следующий файл Dockerfile:

FROM fabric8/java-centos-openjdk8-jdk

USER root

RUN yum -y groupinstall 'Development Tools' && \
    yum -y install make unzip wget && \
    mkdir faketime && \
    cd faketime && \
    wget https://github.com/wolfcw/libfaketime/archive/master.zip && \
    unzip master.zip && \
    cd libfaketime-master && \
    make install

ENTRYPOINT /bin/bash

Теперь должна быть возможность запустить все это и протестировать библиотеку:

[me@pc ~]$ docker build -t centos .
...
[me@pc ~]$ docker run --name centos -d -it centos /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]# date
Wed Feb 19 17:16:34 UTC 2020
[root@centos /]# LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME="-15d" date
Tue Feb  4 17:16:49 UTC 2020

Поскольку путь к libfaketime указан в переменной LD_PRELOAD , переменная ПОДДЕЛЬНОЕ ВРЕМЯ установлена на -15 дней , вызов date отобразил дату 15 дней назад. Для меня самая интересная часть – это часть LD_PRELOAD .

Переменная LD_PRELOAD позволяет указать пути к библиотекам, которые должны быть загружены до загрузки любых других библиотек. Что важно, все символы (например, функции), содержащиеся в предварительно загруженных библиотеках имеют приоритет над символами из библиотек, загруженных впоследствии 4 5 6 .

Это означает, что если программа использует функцию foo() из библиотеки A и библиотека A связан во время выполнения, можно указать путь к библиотеке B содержащий другую реализацию foo() в переменной LD_PRELOAD . В результате, когда программа ссылается на foo() в своем исходном коде, будет вызвана реализация из библиотеки B .

libfaketime заменяет символы, связанные с взаимодействием с системными часами, используя механизм предварительной загрузки.

Учитывая все знания, собранные до сих пор, пришло время подделать дату в Java-приложении.

Прежде всего – чтобы протестировать библиотеку, я создал простое приложение, которое печатает текущую дату и время каждую секунду, навсегда. Вот код:

import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
  private static final int NO_DELAY = 0;

  public static void main(String[] args) {
    Runnable printCurrentDateTime = () -> System.out.println(LocalDateTime.now());

    Executors
      .newSingleThreadScheduledExecutor()
      .scheduleAtFixedRate(printCurrentDateTime, NO_DELAY, 1, TimeUnit.SECONDS);
  }
}

Затем, чтобы протестировать библиотеку с приложением, я снова запустил контейнер CentOS с томом, установленным в каталог, в котором находился класс java.

[me@pc ~]$ docker run --name centos -v /home/test/:/app -d -it centos /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]# cd /app
[root@centos app] javac Main.java
[root@centos app] java Main
2020-02-20T21:53:36.132
2020-02-20T21:53:37.113
2020-02-20T21:53:38.113
^C
[root@centos app] LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME="-15d" FAKETIME_DONT_FAKE_MONOTONIC=1 java Main
2020-02-05T21:55:51.787
2020-02-05T21:55:52.766
2020-02-05T21:55:53.765
^C

Он работает, как и ожидалось, хотя я добавил в команду еще одну переменную: FAKETIME_DONT_FAKE_MONOTONIC=1/| . Из README libfaketime :

Приложения на основе Java/JVM работают, но вам нужно передать дополнительный аргумент (FAKETIME_DONT_FAKE_MONOTONIC). Подробности см. в разделе “Основы использования” ниже. Без этого аргумента команда java обычно зависает.

Действительно, без него даже эта крошечная программа зависала после печати первой даты. Должен признаться, я потратил некоторое время на его отладку только потому, что пропустил чтение readme.

Чтобы сократить команды, LD_PRELOAD и Значения FAKETIME_DONT_FAKE_MONOTONIC могут быть указаны в качестве значений среды образа Docker. Я опустил FACETIME потому что этот, скорее всего, изменится.

...

USER root

ENV LD_PRELOAD /usr/local/lib/faketime/libfaketime.so.1
ENV FAKETIME_DONT_FAKE_MONOTONIC 1

RUN ...

После этих изменений должна быть возможность запустить FAKETIME="-15d" java Main для отображения того же вывода, что и раньше.

Можно указать значение DATETIME |/в файле вместо переменной. Это позволяет изменить значение в любой момент. libfaketime подберет его через десять секунд 7 .

Это можно сделать в масштабах всей системы или только для пользователя. Я опишу последнее.

Файл должен быть назван .faketimerc и он должен быть помещен в домашнюю директорию. Он должен содержать только значение переменной FAKE TIME , точно так же, как это:

-15d

Запуск java Main в командной оболочке теперь должен отображать ожидаемый результат. Пока запущенная программа продолжает работать, в другом сеансе командной строки мы можем ввести echo -10d > ~/.faketimerc . Результат должен измениться через десять секунд.

Указание относительного смещения – не единственный способ потратить время. Вот еще несколько вариантов:

  • различные умножители смещения: во всех примерах используется “-16d”, но вместо “d” также может быть “m”, “h”, “y” или ничего в течение секунд; смещение может быть установлено в прошлом ( -10d ) или в будущем ( +10d ) > ПРИМЕРЫ: > * -120 отстает на 120 секунд > * +2 часа – это 2 часа в будущем > * +1 год – это 1 год в будущем
  • “”начало с’ дата: FAKETIME="@2020-12-24 20:30:00" , где часы начнут тикать с этой даты для каждого нового процесса , но можно настроить его так, чтобы вместо этого часы продолжали тикать 7
  • абсолютная дата: FAKETIME="2020-12-24 20:30:00" отобразит фиксированное значение, как если бы время остановилось в этот момент

Это далеко не все функции, которые поддерживает libfaketime. Я предлагаю бегло просмотреть список функций в readme, просто чтобы ознакомиться с возможностями – на случай, если вам когда-нибудь понадобится использовать какую-либо из них.

  • Я буду использовать libfaketime в случаях, подобных этому
  • LD_PRELOAD механизм может использоваться на машинах Linux для замены фрагментов кода без изменения исходного кода
  • Я должен был прочитать дружественное руководство раньше, чтобы сэкономить время
  1. https://forums.docker.com/t/is-it-possible-to-change-time-dynamically-in-docker-container/56787/5

  2. https://forums.docker.com/t/change-containers-year/58880/3

  3. https://stackoverflow.com/a/29561602

  4. http://www.goldsborough.me/c/low-level/kernel/2016/08/29/16-48-53-the_-ld_preload-_trick/

  5. https://blog.fpmurphy.com/2012/09/all-about-ld_preload.html

  6. https://rafalcieslak.wordpress.com/2013/04/02/dynamic-linker-tricks-using-ld_preload-to-cheat-inject-features-and-investigate-programs/

  7. https://github.com/wolfcw/libfaketime

Оригинал: “https://dev.to/maciejtoporowicz/fake-the-system-clock-for-a-single-application-with-libfaketime-4oge”