Несколько дней назад у Хавьера был простой сценарий оболочки, который он разместил в нашем внутреннем чате. Его цель состояла в том, чтобы получить все диапазоны IP-адресов для страны в рамках подготовки к отпечатку с https://ipinfo.io/ (в качестве примера возьмем PL). Учитывая, что это связано с извлечением нескольких веб-страниц, мне было интересно узнать, какой будет наиболее эффективный подход к этому в оболочке. Честно говоря, сама проблема, вытягивание данных с сайта или сбор BGP-маршрутов, меня не интересовала, я хотел посмотреть, как наиболее эффективно сделать массовый HTTP enum с помощью curl.

Как и в случае со всеми сценариями оболочки, его первоначальный подход имел некоторые… проблемы, и потребовалось более 4 часов, чтобы извлечь все данные, мы опустим эту версию и используем следующую версию, которую Хавьер, Роган и я придумали в качестве основы:

seq 1 3 \
| xargs -I% curl -s "https://ipinfo.io/countries/pl/%" \
| grep -oE "AS[0-9]{1,9}" \
| sort -u \
| xargs -I% curl -s "https://ipinfo.io/%" \
| grep -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' \
| sort -u

Сценарий:

  1. Выбирает три страницы: https://ipinfo.io/countries/pl/1, https://ipinfo.io/countries/pl/2 и https://ipinfo.io/countries/pl/3. »
  2. Затем grep извлекает номера AS маршрутизации, например. АС5617
  3. У каждого из них есть свои данные, например. https://ipinfo.io/AS5617
  4. Затем они анализируются для адресов CIDR, например. 178.42.0.0/15.

Если вы не знакомы с xargs, это просто позволяет вам выполнить что-то через ввод, например:

> ls
foo bar baz
> file *
foo: empty
bar: empty
baz: empty
> ls | xargs file
foo: empty
bar: empty
baz: empty
> ls | xargs echo file
file foo bar baz
> ls | xargs -L1 echo file
file foo
file bar
file baz

-L1 просто говорит выполнять его для каждого элемента, это необходимо понять позже.

Тестовая среда

Я не хотел продолжать извлекать сотни мегабайт данных из ipinfo, поэтому я вытащил все данные на свою машину и передал их с помощью http-сервера npm. Это означает, что я просто заменил ссылки на https://127.0.0.1:8080/. Это также означало, что я мог устранить несоответствия, появившиеся при перемещении данных через Интернет. В целом, 5 706 файлов представляли собой 276 МБ необработанных байтов. Подавляющее большинство из них были страницами AS, средний размер которых составлял 49 КБ, причем самый большой из них составлял 892 КБ, а самый маленький - 12 КБ.

Для записи выполнения скрипта я использовал утилиту времени Unix. Для записи сетевого трафика я использовал tshark. Я также использовал tshark для создания статистики с помощью сводки «-z conv,ip».

Итак, наш базовый прогон, выполненный на моем MBP, выглядит так, с важной сводкой в ​​конце.

seq 1 3  0.00s user 0.00s system 44% cpu 0.010 total
xargs -I% curl -s "https://127.0.0.1:8080/%"  0.02s user 0.02s system 40% cpu 0.108 total
grep --color -oE "AS[0-9]{1,9}"  0.03s user 0.00s system 33% cpu 0.108 total
sort -u  0.01s user 0.01s system 14% cpu 0.113 total
xargs -I% curl -s "https://127.0.0.1:8080/%"  35.07s user 30.07s system 50% cpu 2:07.74 total
grep --color -Eo '[0-9\.]{7,15}\/[0-9]{1,2}'  16.09s user 0.19s system 12% cpu 2:07.74 total
sort -u  0.15s user 0.06s system 0% cpu 2:07.94 total
2:07.94 total time
94 764 frames
295 926 465 bytes

Вы можете видеть, что это заняло более 2 минут, сгенерировало 94k кадров и 282M данных.

Подход 1: параллелизм

Многократная обработка проблемы часто является самым простым способом ускорить что-то. Это можно просто сделать, передав переключатель -P в xargs выше. Использование произвольного значения 20 параллельных процессов дает следующий код:

time seq 1 3\                     
| xargs -P20 -I% curl -s "https://127.0.0.1:8080/%" \
| grep -oE "AS[0-9]{1,9}" \
| sort -u \                  
| xargs -P20 -I% curl -s "https://127.0.0.1:8080/%" \
| grep -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' \
| sort -u

Это дает следующую статистику:

seq 1 3  0.00s user 0.00s system 46% cpu 0.009 total
xargs -P20 -I% curl -s "https://127.0.0.1:8080/%"  0.03s user 0.03s system 83% cpu 0.068 total
grep --color -oE "AS[0-9]{1,9}"  0.03s user 0.00s system 56% cpu 0.068 total
sort -u  0.01s user 0.00s system 22% cpu 0.073 total
xargs -P20 -I% curl -s "https://127.0.0.1:8080/%"  48.46s user 46.47s system 304% cpu 31.160 total
grep --color -Eo '[0-9\.]{7,15}\/[0-9]{1,2}'  23.50s user 0.58s system 77% cpu 31.163 total
sort -u  0.16s user 0.05s system 0% cpu 31.337 total
31.337 total time
94 777 frames
296 116 785 bytes

Отлично, это сокращает время до четверти исходного и не должно влиять на количество кадров или переданных байтов, но на самом деле этот материал немного менее детерминирован.

Подход 2: Конвейерная обработка

Большинство HTTP-серверов поддерживают конвейерную обработку, когда несколько запросов выполняются и обслуживаются в рамках одного и того же TCP-соединения. Это избавляет от необходимости каждый раз устанавливать и разрывать новое TCP-соединение.

Роган нашел изящный подход к конвейерной обработке с помощью curl с помощью параметра конфигурации, результирующий код выглядит следующим образом:

time curl -s --config \
  <(for x in \
    $(curl -s --config \
      <(for i in `seq 1 3`
          do echo "url=https://127.0.0.1:8080/$i"
        done) \
      | grep -oE "AS[0-9]{1,9}" \
      | sort -u)
    do echo "url=https://127.0.0.1:8080/$x"
  done) \
| grep -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' \
| sort -u

Это создает список url=… с символами новой строки между ними и передает его для скручивания в виде файла конфигурации с перенаправлением оболочки ‹.

Это немного менее читабельно, и большая часть времени выполнения скрыта в первом процессе curl, но давайте проверим, как он работает:

curl -s --config 0.69s user 0.60s system 9% cpu 14.336 total
grep --color -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' 13.97s user 0.10s system 98% cpu 14.339 total
sort -u 0.14s user 0.05s system 1% cpu 14.522 total
14.522 total time
44 867 frames
292 995 529 bytes

Вау, это вдвое меньше времени и больше половины количества кадров по сравнению с многопроцессорной версией. Однако это всего на 1% меньше, если смотреть на общее количество байтов. Таким образом, дополнительные пакеты действительно создают большие накладные расходы на обработку, несмотря на то, что они не приводят к значительному увеличению объема данных.

Подход 3: Гибрид

Затем я хотел, чтобы мы могли объединить оба подхода, что привело бы к некоторым забавным сценариям. Вот результат:

for x in $(for i in $(seq 1 3)
      do echo "https://127.0.0.1:8080/$i"
    done \
    | xargs -L1 -P3 curl -s \
    | grep -oE "AS[0-9]{1,9}" \
    | sort -u)
  do echo "https://127.0.0.1:8080/$x"
done \
  | xargs -L287 -P20 curl -s \
  |grep -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' \
  | sort -u

Это не использует изящный трюк Рогана –config для curl, а просто передает URL-адреса в качестве входных данных и контролирует, сколько URL-адресов будет передано с помощью -L. Я думаю, что это немного более читабельно. Учитывая, что первый цикл извлекает три страницы, имеет смысл делать это одновременно, следовательно, -L1 -P3. Для последнего я взял 5 703 страницы, которые нужно было извлечь, и разделил их на 20 процессов из предыдущих, что дало мне 287.

Это дает следующие результаты:

for x in ; do; echo "https://127.0.0.1:8080/$x"; done 0.13s user 0.06s system 128% cpu 0.146 total
xargs -L287 -P20 curl -s 0.83s user 1.16s system 13% cpu 14.541 total
grep --color -Eo '[0-9\.]{7,15}\/[0-9]{1,2}' 13.64s user 0.28s system 95% cpu 14.543 total
sort -u 0.13s user 0.05s system 1% cpu 14.704 total
14.704 total time
48 318 frames
293 187 882 bytes

Таким образом, это приводит к почти такой же производительности, как и конвейерная версия, с несколькими тысячами дополнительных кадров и несколькими сотнями дополнительных килобайт.

Вывод

+-------------+---------+----------+-----------+--------+
|    Test     |  Base   | Parallel | Pipelined | Hybrid |
+-------------+---------+----------+-----------+--------+
| Time m:s.ms | 2:07.94 | 31.337   | 14.522    | 14.704 |
| Frames      | 94 764  | 94 777   | 44 867    | 48 318 |
| Data M      | 282.21  | 282.39   | 279.42    | 279.60 |
+-------------+---------+----------+-----------+--------+

Если вам нужно сделать много небольших HTTP-запросов, используйте конвейерную обработку curl для максимальной скорости и пропускной способности. Если вам требуется быстрое ускорение для обычного сценария оболочки, xargs -P — ваш друг. Вы также можете использовать параллельную обработку (которая оказалась намного медленнее в этих тестах), что позволит вам выполнять работу на нескольких хостах. Надеюсь, вы также почерпнули немного шелл-скрипт-фу.

Первоначально опубликовано на странице https://sensepost.com/blog/2018/efficient-http-scripting-in-the-shell/