Imagem do docker de 5,94 metros com Telegram MTProxy

Como todos já ouviram , no final de maio, o Telegram lançou o servidor oficial MTProto Proxy (também conhecido como MTProxy), escrito no seguinte idioma . Em 2018, não há muito onde sem o Docker, porque é acompanhado pela mesma maneira "oficial" no formato de configuração zero. Tudo ficaria bem, mas três "buts" estragaram um pouco a impressão do lançamento: a imagem pesa> 130 Mb (existe um Debian bastante rechonchudo, não o Alpine usual), devido ao "zero-config" nem sempre é convenientemente configurado (apenas pelas configurações do ambiente) e os caras esqueceram a campanha, esquematizaram o Dockerfile.



TL; DR Criaremos uma imagem de docker praticamente alpina oficial 1 em 1 de 5,94 MB de tamanho e a colocaremos aqui (e o Dockerfile aqui ); ao longo do caminho, descobriremos como às vezes você pode fazer amizade com o software Alpine usando pinças e um arquivo, e teremos um pouco de tamanho, exclusivamente por diversão.

Conteúdo da imagem


Mais uma vez, por que toda essa confusão? Vamos ver o que a imagem oficial representa com o comando history :

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

As camadas são lidas de baixo para cima, respectivamente:



A mais grossa é a Debian Jessie, da qual a imagem original é herdada. Temos que nos livrar dela antes de tudo (alpine: 3.6, para comparação, pesa 3.97MB); seguidos por dimensões são curl e certificados novos. Para entender o que os outros dois arquivos e o diretório significam, examinaremos o interior usando o comando run , substituindo o CMD pelo bash (isso permitirá que você percorra a imagem lançada, se conheça mais de perto, execute certos fragmentos, copie algo útil):

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

Agora podemos restaurar facilmente a imagem do incidente - algo como o Dockerfile oficial perdido parecia:

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

Onde mtproto-proxy é um servidor compilado, a pasta secreta contém apenas o arquivo hello-explorers-how-you-doing com a chave de criptografia AES (consulte os comandos do servidor, aliás, há uma recomendação oficial para obter a chave pela API, mas coloque-a assim) provavelmente para evitar a situação em que a API também está bloqueada) e o run.sh faz todos os preparativos para iniciar o proxy.

O conteúdo do run.sh original
 #!/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 

Assembléia


No CentOS 7 MTProxy em Habré já coletado , tentaremos coletar uma imagem no Alpine e economizar megabytes, comerciais, 130 na imagem do docker resultante.

Uma característica distintiva do Alpine Linux é o uso de musl em vez de glibc. Ambas são bibliotecas C padrão. Musl é pequeno (não possui um quinto do "padrão"), mas volume e desempenho (prometidos pelo menos) decidem quando se trata do Docker. E colocar glibc no Alpine não é racialmente correto, o tio Jakub Jirutka não entenderá , por exemplo.

Também criaremos docker para isolar dependências e obter liberdade para experimentação; portanto, crie um novo 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) 

Das dependências, o git será útil (e não apenas para clonar o repositório oficial, o arquivo make anexará o sha commit à versão), make, gcc e arquivos de cabeçalho (o conjunto mínimo foi obtido empiricamente). Apenas clonamos o ramo principal com uma profundidade de 1 confirmação (definitivamente não precisamos de histórico). Bem, vamos tentar utilizar todos os recursos do host ao compilar com a opção -j. Eu o dividi deliberadamente em um número maior de camadas para obter um cache conveniente durante a reconstrução (geralmente existem muitas).

Executaremos o comando build (estando no diretório com o Dockerfile):

 $ docker build -t mtproxy:test . 

E aqui está o primeiro problema:

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

Na verdade, todos os subsequentes serão conectados a ele. Primeiro, para aqueles que não estão familiarizados, o compilador jura pela falta de uma declaração da estrutura drand48_data. Em segundo lugar, os desenvolvedores de musl obtiveram pontuação em funções aleatórias seguras para threads (com o _r postfix) e em tudo relacionado a elas (incluindo estruturas). E os desenvolvedores do Telegram, por sua vez, não se preocuparam em compilar para sistemas onde o random_r e suas contrapartes não são implementadas (em muitas bibliotecas de SO, você pode ver o sinalizador HAVE_RANDOM_R ou sua presença arbitrária ou presença ou ausência desse grupo de funções geralmente é levada em consideração no auto-configurador).

Bem, agora vamos definitivamente instalar o glibc? Não! Copiaremos o mínimo necessário do glibc e faremos um patch para as fontes MTProxy.

Além dos problemas com o random_r, pegamos um problema com a função backtrace (execinfo.h), que é usada para gerar o backtrace da pilha em caso de uma exceção: você pode tentar substituí-lo pela implementação do libunwind, mas não vale a pena, porque a chamada foi enquadrada pela verificação de __GLIBC__.

Conteúdo do patch 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 

Coloque-o na pasta ./patches e modifique um pouco o nosso Dockerfile para aplicar o patch rapidamente:

 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 

Agora, o binário mtproto-proxy montado é pelo menos iniciado e podemos seguir em frente.

Liquidação


É hora de transformar o run.sh original em docker-entrypoint.sh. Na minha opinião, isso é lógico quando a "ligação obrigatória" entra em ENTRYPOINT (ela sempre pode ser sobrecarregada de fora), e os argumentos para iniciar o aplicativo dockerized se ajustam ao máximo em CMD (+ variáveis ​​de ambiente como um subestudo).

Poderíamos instalar o bash e o grep completo em nossa imagem alpina (explicarei mais adiante) para evitar dores de cabeça e usar o código original como está, mas isso inflará nossa imagem em miniatura para desonrar, para que possamos cultivar um bonsai real, sua mãe.

Vamos começar com o shebang, substitua #!/bin/bash por #!/bin/sh . O padrão para as cinzas alpinas é capaz de digerir quase toda a sintaxe "como está" do bash, mas ainda encontramos um problema - por razões desconhecidas, ele se recusou a aceitar parênteses em uma das condições, portanto, expandimos invertendo a lógica de comparação:



Agora, estamos aguardando um confronto com grep, que na entrega do busybox é um pouco diferente do usual (e, a propósito, muito mais lento, lembre-se de seus projetos). Primeiro, ele não entende a expressão {,15} , ele precisará especificar explicitamente {0,15} . Em segundo lugar, ele não suporta o sinalizador -P (estilo perl), mas digere silenciosamente a expressão quando estendido (-E) está ativado.

O que resta em nossas dependências é apenas curl (não faz sentido substituí-lo pelo wget do busybox) e libcrypto (basta, o openssl em si não é necessário neste assembly).

Uma bela construção de vários estágios apareceu no Docker há alguns anos; é ideal, por exemplo, para aplicativos Go ou para situações em que a montagem é complicada e mais fácil de transferir artefatos de imagem para imagem do que para fazer a limpeza final. Vamos usá-lo para plantar o nosso bonsai, isso vai economizar um pouco.

 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 . #          #          

Os bonsais devem ser bonsais - livre-se da instalação da libcrypto. Ao compilar, precisávamos dos arquivos de cabeçalho do pacote openssl-dev, que nas dependências abrirão a libcrypto e nosso executável será orientado para o uso da libcrypto.so.1.0.0. Mas essa é a única dependência, além disso, é pré-instalada no Alpine (na versão 3.6, é libcrypto.so.41, 3.7 - libcrypto.so.42, está em / lib /). Eles me repreendem agora, essa não é a maneira mais confiável, mas vale a pena e ainda adicionamos um link simbólico à versão existente (se você tiver uma maneira melhor, aceitarei com prazer o PR).

Toques finais e resultado:

Hub Docker
Github

Ficaria muito grato por qualquer conselho e contribuição.

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


All Articles