Image Docker de 5,94 mètres avec Telegram MTProxy

Comme tout le monde l'a déjà entendu , fin mai, Telegram a lancé le serveur officiel MTProto Proxy (alias MTProxy), écrit dans la langue suivante . En 2018, il n'y a pas grand-chose où sans Docker, car il est accompagné de la même manière «officielle» au format zero-config. Tout irait bien, mais trois "mais" ont gâché un peu l'impression de la sortie: l'image pèse> 130 Mo (il y a une Debian plutôt dodue, pas l'Alpine habituelle), en raison de la "zero-config", elle n'est pas toujours configurée de manière pratique (uniquement par les paramètres d'environnement) et les gars ont oublié la campagne, disposer le Dockerfile.



TL; DR Nous allons créer une image Docker officielle alpin pratiquement 1 en 1 de 5,94 Mo et la mettre ici (et le Dockerfile ici ); en cours de route, nous découvrirons comment vous pouvez parfois vous lier d'amitié avec le logiciel Alpine à l'aide de pinces et d'un fichier, et nous jouerons un peu en taille, exclusivement pour le plaisir.

Contenu de l'image


Encore une fois, à cause de tout ce bruit? Voyons ce que l'image officielle représente avec la commande history :

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

Les couches sont lues de bas en haut, respectivement:



La plus épaisse est la Debian Jessie, dont l'image d'origine est héritée, il faut d'abord s'en débarrasser (alpine: 3,6, à titre de comparaison, elle pèse 3,97 Mo); suivis des dimensions sont les certificats curl et frais. Pour comprendre ce que signifient les deux autres fichiers et le répertoire, nous allons regarder à l'intérieur en utilisant la commande run , en remplaçant CMD par bash (cela vous permettra de vous déplacer dans l'image lancée, de vous connaître de plus près, d'exécuter certains fragments, de copier quelque chose d'utile):

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

Maintenant, nous pouvons facilement restaurer l'image de l'incident - quelque chose comme le Dockerfile officiel perdu ressemblait à:

 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"] 

Lorsque mtproto-proxy est un serveur compilé, le dossier secret contient uniquement le fichier hello-explorers-how-are-you-do avec la clé de chiffrement AES (voir les commandes du serveur, là, soit dit en passant, il y a une recommandation officielle pour obtenir la clé via l'API, mais le mettre comme ça) probablement pour éviter la situation où l'API est également bloquée), et run.sh fait toutes les préparations pour démarrer le proxy.

Le contenu du run.sh d'origine
 #!/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 

Assemblage


Sous CentOS 7 MTProxy sur Habré déjà collecté , nous allons essayer de collecter une image sous Alpine et d'économiser mégaoctets, publicités, 130 dans l'image docker résultante.

Une caractéristique distinctive d'Alpine Linux est l'utilisation de musl au lieu de glibc. Les deux sont des bibliothèques C standard. Musl est minuscule (il n'a pas un cinquième de la «norme»), mais le volume et les performances (promis au moins) décident quand il s'agit de Docker. Et mettre de la glibc sur Alpine n'est pas racialement correct, l'oncle Jakub Jirutka ne comprendra pas , par exemple.

Nous allons également intégrer Docker pour isoler les dépendances et gagner en liberté pour l'expérimentation, alors créez un nouveau Dockerfile:

 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) 

Parmi les dépendances, git sera utile (et pas seulement pour le clonage du référentiel officiel, le fichier make attachera le commit sha à la version), les fichiers make, gcc et header (l'ensemble minimum a été obtenu empiriquement). Nous clonons uniquement la branche principale avec une profondeur de 1 commit (nous n'avons certainement pas besoin de l'historique). Eh bien, essayons d'utiliser toutes les ressources de l'hôte lors de la compilation avec le commutateur -j. Je l'ai délibérément divisé en un plus grand nombre de couches afin d'obtenir une mise en cache pratique lors de la reconstruction (généralement il y en a beaucoup).

Nous exécuterons la commande build (se trouvant dans le répertoire avec le Dockerfile):

 $ docker build -t mtproxy:test . 

Et voici le premier problème:

 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; ^~~~~~~~~ 

En fait, tous les suivants seront connectés avec lui. Premièrement, pour ceux qui ne se connaissent pas, le compilateur jure en fait de l'absence de déclaration de la structure drand48_data. Deuxièmement, les développeurs de musl ont obtenu des scores sur les fonctions aléatoires thread-safe (avec le suffixe _r) et sur tout ce qui y est lié (y compris les structures). Et les développeurs de Telegram, à leur tour, n'ont pas pris la peine de compiler pour des systèmes où random_r et ses homologues ne sont pas implémentés (dans de nombreuses bibliothèques de système d'exploitation, vous pouvez voir le drapeau HAVE_RANDOM_R ou son arbitraire + la présence ou l'absence de ce groupe de fonctions est généralement prise en compte dans l'auto-configurateur).

Eh bien, maintenant, nous allons définitivement installer la glibc? Non! Nous allons copier ce dont nous avons besoin de la glibc au minimum et créer un patch pour les sources MTProxy.

En plus des problèmes avec random_r, nous saisissons un problème avec la fonction backtrace (execinfo.h), qui est utilisée pour sortir la trace de la pile en cas d'exception: vous pouvez essayer de la remplacer par l'implémentation de libunwind, mais cela n'en vaut pas la peine, car l'appel a été cadré en vérifiant __GLIBC__.

Contenu du correctif Random_compat.patch
 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 

Mettez-le dans le dossier ./patches et modifiez un peu notre Dockerfile pour appliquer le patch à la volée:

 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 

Maintenant, le binaire mtproto-proxy assemblé est au moins lancé, et nous pouvons continuer.

Liquidation


Il est temps de transformer le run.sh d'origine en docker-entrypoint.sh. À mon avis, cela est logique lorsque la «liaison obligatoire» entre dans ENTRYPOINT (elle peut toujours être surchargée de l'extérieur), et les arguments pour lancer l'application dockerized s'adaptent au maximum en CMD (+ variables d'environnement comme sous-étude).

Nous pourrions installer bash et full grep dans notre image alpine (je l'expliquerai plus tard) pour éviter les maux de tête et utiliser le code d'origine tel quel, mais cela gonflera notre image miniature pour faire honte, donc nous ferons grandir un vrai, sa mère, bonsaï.

Commençons par le shebang, remplacez #!/bin/bash par #!/bin/sh . La valeur par défaut pour les cendres alpines est capable de digérer presque toute la syntaxe bash «telle quelle», mais nous rencontrons toujours un problème - pour des raisons inconnues, il a refusé d'accepter les parenthèses dans l'une des conditions, nous allons donc l'étendre en inversant la logique de comparaison:



Nous attendons maintenant une confrontation avec grep, qui dans la livraison busybox est légèrement différente de celle habituelle (et, en passant, beaucoup plus lente, gardez à l'esprit dans vos projets). Premièrement, il ne comprend pas l'expression {,15} , il devra spécifier explicitement {0,15} . Deuxièmement, il ne prend pas en charge l'indicateur -P (style perl), mais digère discrètement l'expression lorsque étendu (-E) est activé.

Dans nos dépendances, il ne reste que curl (il ne sert à rien de le remplacer par wget de busybox) et libcrypto (cela suffit, directement openssl n'est pas du tout nécessaire dans cet assemblage).

Une belle construction à plusieurs étapes est apparue dans Docker il y a quelques années, elle est idéale, par exemple, pour les applications Go ou pour les situations où l'assemblage est compliqué et plus facile de transférer des artefacts d'image en image que de faire le nettoyage final. Nous allons l'utiliser pour planter notre bonsaï, cela économisera un peu.

 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 devrait être bonsai - débarrassez-vous de l'installation de libcrypto. Lors de la construction, nous avions besoin des fichiers d'en-tête du paquet openssl-dev, qui dans les dépendances afficheront libcrypto et notre exécutable sera orienté vers l'utilisation de libcrypto.so.1.0.0. Mais c'est la seule dépendance, en plus, elle est préinstallée dans Alpine (dans la version 3.6 c'est libcrypto.so.41, 3.7 - libcrypto.so.42, c'est dans / lib /). Ils me grondent maintenant, ce n'est pas le moyen le plus fiable, mais ça vaut le coup et nous ajoutons toujours un lien symbolique vers la version existante (si vous avez une meilleure façon, j'accepterai volontiers PR).

Touche finale et résultat:

Docker hub
Github

Je serais reconnaissant pour tout conseil et contribution.

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


All Articles