5,94 Meter Docker-Bild mit Telegramm MTProxy

Wie jeder bereits gehört hat , hat Telegram Ende Mai den offiziellen MTProto Proxy-Server (auch bekannt als MTProxy) gestartet, der in der folgenden Sprache geschrieben ist . Im Jahr 2018 gibt es ohne Docker nicht viel, da es im Zero-Config-Format von demselben „offiziellen“ Weg begleitet wird. Alles wäre in Ordnung, aber drei "Aber" trübten den Eindruck der Veröffentlichung ein wenig: Das Bild wiegt> 130 MB (es gibt ein ziemlich pralles Debian, nicht das übliche Alpin), aufgrund der "Null-Konfiguration" ist es nicht immer bequem konfiguriert (nur durch die Umgebungseinstellungen). und die Jungs haben die Kampagne vergessen, legen die Docker-Datei aus.



TL; DR Wir werden ein praktisch 1-in-1-offizielles Docking-Image auf Alpenbasis mit einer Größe von 5,94 MB erstellen und hier (und hier im Dockerfile) ablegen. Unterwegs werden wir herausfinden, wie Sie sich manchmal mit Zangen und einer Datei mit der Alpine-Software anfreunden können, und wir werden ein bisschen in der Größe spielen, ausschließlich zum Spaß.

Bildinhalt


Noch einmal, wegen was ist die ganze Aufregung? Mal sehen, was das offizielle Bild mit dem Befehl history darstellt :

$ docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" telegrammessenger/proxy 

Ebenen werden jeweils von unten nach oben gelesen:



Das dickste ist das Debian Jessie, von dem das Originalbild geerbt wurde. Wir müssen es zuerst loswerden (alpin: 3,6, zum Vergleich: 3,97 MB). gefolgt von Abmessungen sind Locken und frische Zertifikate. Um zu verstehen, was die beiden anderen Dateien und das Verzeichnis bedeuten, schauen wir uns den Befehl run an und ersetzen CMD durch bash (auf diese Weise können Sie das gestartete Image umgehen, sich näher kennenlernen, bestimmte Fragmente ausführen und etwas Nützliches kopieren):

 $ docker run -it --rm telegrammessenger/proxy /bin/bash 

Jetzt können wir das Bild des Vorfalls problemlos wiederherstellen - so etwas wie das verlorene offizielle Dockerfile sah aus wie:

 FROM debian:jessie-20180312 RUN set -eux \ && apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY ./mtproto-proxy /usr/local/bin RUN mkdir /data COPY ./secret/ /etc/telegram/ COPY ./run.sh /run.sh CMD ["/bin/sh", "-c", "/bin/bash /run.sh"] 

Wenn mtproto-proxy ein kompilierter Server ist, enthält der geheime Ordner nur die Datei "Hallo-Entdecker - wie geht es dir?" Mit dem AES-Verschlüsselungsschlüssel (siehe Serverbefehle, dort gibt es übrigens eine offizielle Empfehlung, den Schlüssel über die API abzurufen, aber so ausgedrückt wahrscheinlich, um die Situation zu vermeiden, in der die API ebenfalls blockiert ist) und run.sh alle Vorbereitungen zum Starten des Proxys trifft.

Der Inhalt der ursprünglichen run.sh
 #!/bin/bash if [ ! -z "$DEBUG" ]; then set -x; fi mkdir /data 2>/dev/null >/dev/null RANDOM=$(printf "%d" "0x$(head -c4 /dev/urandom | od -t x1 -An | tr -d ' ')") if [ -z "$WORKERS" ]; then WORKERS=2 fi echo "####" echo "#### Telegram Proxy" echo "####" echo SECRET_CMD="" if [ ! -z "$SECRET" ]; then echo "[+] Using the explicitly passed secret: '$SECRET'." elif [ -f /data/secret ]; then SECRET="$(cat /data/secret)" echo "[+] Using the secret in /data/secret: '$SECRET'." else if [[ ! -z "$SECRET_COUNT" ]]; then if [[ ! ( "$SECRET_COUNT" -ge 1 && "$SECRET_COUNT" -le 16 ) ]]; then echo "[F] Can generate between 1 and 16 secrets." exit 5 fi else SECRET_COUNT="1" fi echo "[+] No secret passed. Will generate $SECRET_COUNT random ones." SECRET="$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')" for pass in $(seq 2 $SECRET_COUNT); do SECRET="$SECRET,$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')" done fi if echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}(,[0-9a-fA-F]{32}){,15}$'; then SECRET="$(echo "$SECRET" | tr '[:upper:]' '[:lower:]')" SECRET_CMD="-S $(echo "$SECRET" | sed 's/,/ -S /g')" echo -- "$SECRET_CMD" > /data/secret_cmd echo "$SECRET" > /data/secret else echo '[F] Bad secret format: should be 32 hex chars (for 16 bytes) for every secret; secrets should be comma-separated.' exit 1 fi if [ ! -z "$TAG" ]; then echo "[+] Using the explicitly passed tag: '$TAG'." fi TAG_CMD="" if [[ ! -z "$TAG" ]]; then if echo "$TAG" | grep -qE '^[0-9a-fA-F]{32}$'; then TAG="$(echo "$TAG" | tr '[:upper:]' '[:lower:]')" TAG_CMD="-P $TAG" else echo '[!] Bad tag format: should be 32 hex chars (for 16 bytes).' echo '[!] Continuing.' fi fi curl -s https://core.telegram.org/getProxyConfig -o /etc/telegram/backend.conf || { echo '[F] Cannot download proxy configuration from Telegram servers.' exit 2 } CONFIG=/etc/telegram/backend.conf IP="$(curl -s -4 "https://digitalresistance.dog/myIp")" INTERNAL_IP="$(ip -4 route get 8.8.8.8 | grep '^8\.8\.8\.8\s' | grep -Po 'src\s+\d+\.\d+\.\d+\.\d+' | awk '{print $2}')" if [[ -z "$IP" ]]; then echo "[F] Cannot determine external IP address." exit 3 fi if [[ -z "$INTERNAL_IP" ]]; then echo "[F] Cannot determine internal IP address." exit 4 fi echo "[*] Final configuration:" I=1 echo "$SECRET" | tr ',' '\n' | while read S; do echo "[*] Secret $I: $S" echo "[*] tg:// link for secret $I auto configuration: tg://proxy?server=${IP}&port=443&secret=${S}" echo "[*] t.me link for secret $I: https://t.me/proxy?server=${IP}&port=443&secret=${S}" I=$(($I+1)) done [ ! -z "$TAG" ] && echo "[*] Tag: $TAG" || echo "[*] Tag: no tag" echo "[*] External IP: $IP" echo "[*] Make sure to fix the links in case you run the proxy on a different port." echo echo '[+] Starting proxy...' sleep 1 exec /usr/local/bin/mtproto-proxy -p 2398 -H 443 -M "$WORKERS" -C 60000 --aes-pwd /etc/telegram/hello-explorers-how-are-you-doing -u root $CONFIG --allow-skip-dh --nat-info "$INTERNAL_IP:$IP" $SECRET_CMD $TAG_CMD 

Montage


Unter CentOS 7 MTProxy auf Habré, das bereits gesammelt wurde , werden wir versuchen, ein Bild unter Alpine zu sammeln und Megabyte, Werbung, 130 im resultierenden Docker-Bild zu speichern.

Eine Besonderheit von Alpine Linux ist die Verwendung von Mussl anstelle von Glibc. Beide sind Standard-C-Bibliotheken. Musl ist winzig (es hat kein Fünftel des „Standards“), aber Volumen und Leistung (zumindest versprochen) entscheiden, wenn es um Docker geht. Und es ist nicht rassistisch korrekt, Glibc auf Alpine zu setzen, Onkel Jakub Jirutka wird es zum Beispiel nicht verstehen .

Wir werden auch Docker einbauen, um Abhängigkeiten zu isolieren und Freiheit zum Experimentieren zu gewinnen. Erstellen Sie also eine neue Docker-Datei:

 FROM alpine:3.6 RUN apk add --no-cache git make gcc musl-dev linux-headers openssl-dev RUN git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources RUN cd /mtproxy/sources \ && make -j$(getconf _NPROCESSORS_ONLN) 

Von den Abhängigkeiten wird git nützlich sein (und nicht nur zum Klonen des offiziellen Repositorys, die make-Datei hängt das sha-Commit an die Version an), make-, gcc- und Header-Dateien (der Mindestsatz wurde empirisch ermittelt). Wir klonen den Master-Zweig nur mit einer Tiefe von 1 Commit (wir brauchen definitiv keinen Verlauf). Versuchen wir, beim Kompilieren mit dem Schalter -j alle Hostressourcen zu nutzen. Ich habe es absichtlich in eine größere Anzahl von Ebenen aufgeteilt, um ein bequemes Caching während des Wiederaufbaus zu erhalten (normalerweise gibt es viele davon).

Wir werden den Befehl build ausführen (im Verzeichnis mit der Docker-Datei):

 $ docker build -t mtproxy:test . 

Und hier ist das erste Problem:

 In file included from ./net/net-connections.h:34:0, from mtproto/mtproto-config.c:44: ./jobs/jobs.h:234:23: error: field 'rand_data' has incomplete type struct drand48_data rand_data; ^~~~~~~~~ 

Tatsächlich werden alle nachfolgenden damit verbunden. Erstens schwört der Compiler für diejenigen, die mit sich selbst nicht vertraut sind, auf das Fehlen einer Deklaration der Struktur drand48_data. Zweitens bewerteten musl-Entwickler threadsichere Zufallsfunktionen (mit dem Postfix _r) und alles, was mit ihnen verbunden ist (einschließlich Strukturen). Und die Entwickler von Telegram haben sich wiederum nicht darum gekümmert, für Systeme zu kompilieren, in denen random_r und seine Gegenstücke nicht implementiert sind (in vielen Betriebssystembibliotheken können Sie sehen, dass das HAVE_RANDOM_R-Flag oder das willkürliche + Vorhandensein oder Fehlen dieser Funktionsgruppe normalerweise im Autokonfigurator berücksichtigt wird).

Nun, jetzt werden wir definitiv glibc installieren? Nein! Wir werden das, was wir brauchen, von glibc auf ein Minimum kopieren und einen Patch für die MTProxy-Quellen erstellen.

Zusätzlich zu Problemen mit random_r haben wir ein Problem mit der Backtrace-Funktion (execinfo.h), mit der im Falle einer Ausnahme die Stack-Backtrace ausgegeben wird: Sie könnten versuchen, sie durch die Implementierung von libunwind zu ersetzen, aber es lohnt sich nicht, da der Aufruf durch Überprüfen auf gerahmt wurde __GLIBC__.

Random_compat.patch Patch-Inhalt
 Index: jobs/jobs.h IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- jobs/jobs.h (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ jobs/jobs.h (date 1527996954000) @@ -28,6 +28,8 @@ #include "net/net-msg.h" #include "net/net-timers.h" +#include "common/randr_compat.h" + #define __joblocked #define __jobref Index: common/server-functions.c IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/server-functions.c (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ common/server-functions.c (date 1527998325000) @@ -35,7 +35,9 @@ #include <arpa/inet.h> #include <assert.h> #include <errno.h> +#ifdef __GLIBC__ #include <execinfo.h> +#endif #include <fcntl.h> #include <getopt.h> #include <grp.h> @@ -168,6 +170,7 @@ } void print_backtrace (void) { +#ifdef __GLIBC__ void *buffer[64]; int nptrs = backtrace (buffer, 64); kwrite (2, "\n------- Stack Backtrace -------\n", 33); @@ -178,6 +181,7 @@ kwrite (2, s, strlen (s)); kwrite (2, "\n", 1); } +#endif } pthread_t debug_main_pthread_id; Index: common/randr_compat.h IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/randr_compat.h (date 1527998264000) +++ common/randr_compat.h (date 1527998264000) @@ -0,0 +1,72 @@ +/* + The GNU C Library is free software. See the file COPYING.LIB for copying + conditions, and LICENSES for notices about a few contributions that require + these additional notices to be distributed. License copyright years may be + listed using range notation, eg, 2000-2011, indicating that every year in + the range, inclusive, is a copyrightable year that would otherwise be listed + individually. +*/ + +#pragma once + +#include <endian.h> +#include <pthread.h> + +struct drand48_data { + unsigned short int __x[3]; /* Current state. */ + unsigned short int __old_x[3]; /* Old state. */ + unsigned short int __c; /* Additive const. in congruential formula. */ + unsigned short int __init; /* Flag for initializing. */ + unsigned long long int __a; /* Factor in congruential formula. */ +}; + +union ieee754_double +{ + double d; + + /* This is the IEEE 754 double-precision format. */ + struct + { +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned int negative:1; + unsigned int exponent:11; + /* Together these comprise the mantissa. */ + unsigned int mantissa0:20; + unsigned int mantissa1:32; +#endif /* Big endian. */ +#if __BYTE_ORDER == __LITTLE_ENDIAN + /* Together these comprise the mantissa. */ + unsigned int mantissa1:32; + unsigned int mantissa0:20; + unsigned int exponent:11; + unsigned int negative:1; +#endif /* Little endian. */ + } ieee; + + /* This format makes it easier to see if a NaN is a signalling NaN. */ + struct + { +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned int negative:1; + unsigned int exponent:11; + unsigned int quiet_nan:1; + /* Together these comprise the mantissa. */ + unsigned int mantissa0:19; + unsigned int mantissa1:32; +#else + /* Together these comprise the mantissa. */ + unsigned int mantissa1:32; + unsigned int mantissa0:19; + unsigned int quiet_nan:1; + unsigned int exponent:11; + unsigned int negative:1; +#endif + } ieee_nan; +}; + +#define IEEE754_DOUBLE_BIAS 0x3ff /* Added to exponent. */ + +int drand48_r (struct drand48_data *buffer, double *result); +int lrand48_r (struct drand48_data *buffer, long int *result); +int mrand48_r (struct drand48_data *buffer, long int *result); +int srand48_r (long int seedval, struct drand48_data *buffer); \ No newline at end of file Index: Makefile IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- Makefile (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ Makefile (date 1527998107000) @@ -40,6 +40,7 @@ DEPENDENCE_NORM := $(subst ${OBJ}/,${DEP}/,$(patsubst %.o,%.d,${OBJECTS})) LIB_OBJS_NORMAL := \ + ${OBJ}/common/randr_compat.o \ ${OBJ}/common/crc32c.o \ ${OBJ}/common/pid.o \ ${OBJ}/common/sha1.o \ Index: common/randr_compat.c IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/randr_compat.c (date 1527998213000) +++ common/randr_compat.c (date 1527998213000) @@ -0,0 +1,120 @@ +/* + The GNU C Library is free software. See the file COPYING.LIB for copying + conditions, and LICENSES for notices about a few contributions that require + these additional notices to be distributed. License copyright years may be + listed using range notation, eg, 2000-2011, indicating that every year in + the range, inclusive, is a copyrightable year that would otherwise be listed + individually. +*/ + +#include <stddef.h> +#include "common/randr_compat.h" + +int __drand48_iterate (unsigned short int xsubi[3], struct drand48_data *buffer) { + uint64_t X; + uint64_t result; + + /* Initialize buffer, if not yet done. */ + if (!buffer->__init == 0) + { + buffer->__a = 0x5deece66dull; + buffer->__c = 0xb; + buffer->__init = 1; + } + + /* Do the real work. We choose a data type which contains at least + 48 bits. Because we compute the modulus it does not care how + many bits really are computed. */ + + X = (uint64_t) xsubi[2] << 32 | (uint32_t) xsubi[1] << 16 | xsubi[0]; + + result = X * buffer->__a + buffer->__c; + + xsubi[0] = result & 0xffff; + xsubi[1] = (result >> 16) & 0xffff; + xsubi[2] = (result >> 32) & 0xffff; + + return 0; +} + +int __erand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, double *result) { + union ieee754_double temp; + + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Construct a positive double with the 48 random bits distributed over + its fractional part so the resulting FP number is [0.0,1.0). */ + + temp.ieee.negative = 0; + temp.ieee.exponent = IEEE754_DOUBLE_BIAS; + temp.ieee.mantissa0 = (xsubi[2] << 4) | (xsubi[1] >> 12); + temp.ieee.mantissa1 = ((xsubi[1] & 0xfff) << 20) | (xsubi[0] << 4); + + /* Please note the lower 4 bits of mantissa1 are always 0. */ + *result = temp.d - 1.0; + + return 0; +} + +int __nrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) { + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Store the result. */ + if (sizeof (unsigned short int) == 2) + *result = xsubi[2] << 15 | xsubi[1] >> 1; + else + *result = xsubi[2] >> 1; + + return 0; +} + +int __jrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) { + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Store the result. */ + *result = (int32_t) ((xsubi[2] << 16) | xsubi[1]); + + return 0; +} + +int drand48_r (struct drand48_data *buffer, double *result) { + return __erand48_r (buffer->__x, buffer, result); +} + +int lrand48_r (struct drand48_data *buffer, long int *result) { + /* Be generous for the arguments, detect some errors. */ + if (buffer == NULL) + return -1; + + return __nrand48_r (buffer->__x, buffer, result); +} + +int mrand48_r (struct drand48_data *buffer, long int *result) { + /* Be generous for the arguments, detect some errors. */ + if (buffer == NULL) + return -1; + + return __jrand48_r (buffer->__x, buffer, result); +} + +int srand48_r (long int seedval, struct drand48_data *buffer) { + /* The standards say we only have 32 bits. */ + if (sizeof (long int) > 4) + seedval &= 0xffffffffl; + + buffer->__x[2] = seedval >> 16; + buffer->__x[1] = seedval & 0xffffl; + buffer->__x[0] = 0x330e; + + buffer->__a = 0x5deece66dull; + buffer->__c = 0xb; + buffer->__init = 1; + + return 0; +} \ No newline at end of file 

Legen Sie es in den Ordner ./patches und ändern Sie unsere Docker-Datei ein wenig, um den Patch im laufenden Betrieb anzuwenden:

 FROM alpine:3.6 COPY ./patches /mtproxy/patches RUN apk add --no-cache --virtual .build-deps \ git make gcc musl-dev linux-headers openssl-dev \ && git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources \ && cd /mtproxy/sources \ && patch -p0 -i /mtproxy/patches/randr_compat.patch \ && make -j$(getconf _NPROCESSORS_ONLN) \ && cp /mtproxy/sources/objs/bin/mtproto-proxy /mtproxy/ \ && rm -rf /mtproxy/{sources,patches} \ && apk add --no-cache --virtual .rundeps libcrypto1.0 \ && apk del .build-deps 

Jetzt wird zumindest die zusammengesetzte mtproto-proxy-Binärdatei gestartet, und wir können fortfahren.

Freigabe


Es ist Zeit, die ursprüngliche run.sh in docker-entrypoint.sh umzuwandeln. Meiner Meinung nach ist dies logisch, wenn die „obligatorische Bindung“ in ENTRYPOINT eingeht (sie kann immer von außen überladen werden) und die Argumente für den Start der Docker-Anwendung in CMD maximal passen (+ Umgebungsvariablen als Zweitbesetzung).

Wir könnten Bash und Full Grep in unser alpines Bild einbauen (ich werde es später erklären), um Kopfschmerzen zu vermeiden und den Originalcode so zu verwenden, wie er ist, aber es wird unser Miniaturbild zur Schande aufblasen, so dass wir einen echten Bonsai, seine Mutter, wachsen lassen.

Beginnen wir mit dem Shebang und ersetzen Sie #!/bin/bash durch #!/bin/sh . Die Standardeinstellung für alpine Asche ist in der Lage, fast die gesamte Bash-Syntax zu verarbeiten, aber wir stoßen immer noch auf ein Problem: Aus unbekannten Gründen lehnte er es ab, Klammern unter einer der Bedingungen zu akzeptieren. Daher werden wir sie durch Umkehren der Vergleichslogik erweitern:



Jetzt warten wir auf einen Showdown mit grep, der sich in der Busybox-Lieferung geringfügig von der üblichen unterscheidet (und übrigens viel langsamer ist, denken Sie bei Ihren Projekten daran). Erstens versteht er den Ausdruck {,15} , er muss {0,15} explizit angeben. Zweitens unterstützt es nicht das -P Flag (Perl-Stil), sondern verdaut den Ausdruck leise, wenn Extended (-E) aktiviert ist.

In unseren Abhängigkeiten bleibt nur Curl (es macht keinen Sinn, es durch wget aus der Busybox zu ersetzen) und libcrypto (es reicht aus, direkt openssl ist in dieser Assembly überhaupt nicht erforderlich).

Ein wunderschöner mehrstufiger Build wurde vor einigen Jahren in Docker veröffentlicht. Er ist beispielsweise ideal für Go-Anwendungen oder für Situationen, in denen die Montage kompliziert ist und Artefakte einfacher von Bild zu Bild übertragen können als für die endgültige Bereinigung. Wir werden es verwenden, um unsere Bonsai zu pflanzen, dies wird ein wenig sparen.

 FROM alpine:3.6 #         ( ) RUN apk add --no-cache --virtual .build-deps \ # ... ,    && make -j$(getconf _NPROCESSORS_ONLN) FROM alpine:3.6 #   ,  ,   WORKDIR /mtproxy COPY --from=0 /mtproxy/sources/objs/bin/mtproto-proxy . #          #          

Bonsai sollte Bonsai sein - entfernen Sie die libcrypto-Installation. Beim Erstellen benötigten wir die Header-Dateien aus dem openssl-dev-Paket, die in den Abhängigkeiten libcrypto aufrufen und unsere ausführbare Datei auf die Verwendung von libcrypto.so.1.0.0 ausrichten. Dies ist jedoch die einzige Abhängigkeit. Außerdem ist sie in Alpine vorinstalliert (in Version 3.6 ist sie libcrypto.so.41, 3.7 - libcrypto.so.42, sie befindet sich in / lib /). Sie schimpfen jetzt mit mir, dies ist nicht der zuverlässigste Weg, aber es lohnt sich und wir fügen der vorhandenen Version immer noch einen Symlink hinzu (wenn Sie einen besseren Weg haben, akzeptiere ich gerne PR).

Letzter Schliff und Ergebnis:

Docker Hub
Github

Für Ratschläge und Beiträge wäre ich dankbar.

Source: https://habr.com/ru/post/de413881/


All Articles