Telegrama em movimento: parte 1, analisando o esquema

O desejo de escrever um cliente de alta qualidade para meu mensageiro favorito on go amadureceu há muito tempo, mas apenas um mês atrás eu decidi que a hora havia chegado e eu tinha qualificações suficientes para isso.





O desenvolvimento ainda está em andamento (e totalmente de código aberto), mas o caminho fascinante já passou de uma completa falta de compreensão do protocolo para um cliente relativamente estável. Em uma série de artigos, explicarei quais desafios enfrentei e como lidei com eles. As técnicas que apliquei podem ser úteis ao desenvolver um cliente para qualquer protocolo binário com um esquema.






Digite o idioma

Vamos começar com Type Language ou TL, um esquema de descrição de protocolo. Não vou me aprofundar na descrição do formato, o Habré já tem sua análise, vou falar apenas brevemente sobre ele. É um pouco semelhante ao gRPC e descreve o esquema de interação entre o cliente e o servidor: uma estrutura de dados e um conjunto de métodos.





Aqui está um exemplo de uma descrição de tipo:





error#1fbadfee code:int32 message:string = Error;
      
      



Aqui 1fbadfee



este é o id do tipo, error



seu nome, código e mensagem são campos, e Error



este é o nome da classe.





Os métodos são descritos da mesma maneira, apenas em vez de um nome de tipo, haverá um nome de método e, em vez de uma classe - um tipo de resultado:





sendPM#3faceff text:string habrauser:string = Error;	
      
      



Isso significa que o método sendPM



recebe argumentos text



e habrauser



, e retorna Error



, variantes (construtores) que foram descritos anteriormente, por exemplo error#1fbadfee



.





Para começar a trabalhar com um protocolo, você precisa aprender de alguma forma como analisar seu esquema. Existem duas maneiras: usar um analisador genérico ou escrever ad-hoc , ou seja, um analisador especializado para um protocolo específico. Para o primeiro caminho, existe o particípio , que, à primeira vista, é um bom analisador go generalizado, por meio do qual se poderia descrever a gramática. Decidi escolher o caminho ad-hoc e essa escolha valeu a pena.





Dados de teste

, , , . : , , .





, . Definition



, :





func TestDefinition(t *testing.T) {
	for _, tt := range []struct {
		Case       string
		Input      string
		String     string
		Definition Definition
	}{
		{
			Case:  "inputPhoneCall",
			Input: "inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall",
			Definition: Definition{
				ID:   0x1e36fded,
				Name: "inputPhoneCall",
				Params: []Parameter{
					{
						Name: "id",
						Type: bareLong,
					},
					{
						Name: "access_hash",
						Type: bareLong,
					},
				},
				Type: Type{Name: "InputPhoneCall"},
			},
		},
    // ...
  } {
		t.Run(tt.Case, func(t *testing.T) {
			var d Definition
			if err := d.Parse(tt.Input); err != nil {
				t.Fatal(err)
			}
			require.Equal(t, tt.Definition, d)
		})
  } 
}  
      
      



, Flag



( , ), .





, , . :





	t.Run("Error", func(t *testing.T) {
		for _, invalid := range []string{
			"=0",
			"0 :{.0?InputFi00=0",
		} {
			t.Run(invalid, func(t *testing.T) {
				var d Definition
				if err := d.Parse(invalid); err == nil {
					t.Error("should error")
				}
			})
		}
	})
      
      



testdata

. _testdata



: , , go .





Sample.tl _testdata :





func TestParseSample(t *testing.T) {
	data, err := ioutil.ReadFile(filepath.Join("_testdata", "Sample.tl"))
	if err != nil {
		t.Fatal(err)
	}
	schema, err := Parse(bytes.NewReader(data))
	if err != nil {
		t.Fatal(err)
	}
  // ...
}
      
      



go , , filepath.Join



-.





(golden)

"golden files". , . , ( -update



). , . goldie .





func TestParser(t *testing.T) {
	for _, v := range []string{
		"td_api.tl",
		"telegram_api.tl",
		"telegram_api_header.tl",
		"layer.tl",
	} {
		t.Run(v, func(t *testing.T) {
			data, err := ioutil.ReadFile(filepath.Join("_testdata", v))
			if err != nil {
				t.Fatal(err)
			}
			schema, err := Parse(bytes.NewReader(data))
			if err != nil {
				t.Fatal(err)
			}
			t.Run("JSON", func(t *testing.T) {
				g := goldie.New(t,
					goldie.WithFixtureDir(filepath.Join("_golden", "parser", "json")),
					goldie.WithDiffEngine(goldie.ColoredDiff),
					goldie.WithNameSuffix(".json"),
				)
				g.AssertJson(t, v, schema)
			})
		})
	}
}
      
      



, json ( json). -update



, , _golden



.





(, json ) , .





Decode-Encode-Decode

, , decode-encode-decode, .





String() string



:





// Annotation represents an annotation comment, like //@name value.
type Annotation struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

func (a Annotation) String() string {
	var b strings.Builder
	b.WriteString("//")
	b.WriteRune('@')
	b.WriteString(a.Name)
	b.WriteRune(' ')
	b.WriteString(a.Value)
	return b.String()
}
      
      



, strings.Builder, String()



.





, , .





Fuzzing

() . , , (coverage-guided fuzzing). go go-fuzz . ( ) , . , syzkaller, go, Linux .





, , , , .





, Definition:





// +build fuzz

package tl

import "fmt"

func FuzzDefinition(data []byte) int {
	var d Definition
	if err := d.Parse(string(data)); err != nil {
		return 0
	}

	var other Definition
	if err := other.Parse(d.String()); err != nil {
		fmt.Printf("input: %s\n", string(data))
		fmt.Printf("parsed: %#v\n", d)
		panic(err)
	}

	return 1
}

      
      



, .





Decode-encode-decode-encode

We need to go deeper. :













  1. (2)





  2. (3)





  3. (4) (2)





(4) (2) , .. - . , .





go-fuzz

Denial of Service , .. OOM. , go-fuzz , , .





corpus, , ( crashers, , , ). crashers , 0, . , , corpus , .





, go, , , .





, , , - . (STUN, TURN, SDP, MTProto, ...) .





, - . , , ( ) Telegram go:









  • ( )









  • Teste de comunicação de rede (unidade, e2e)





  • Trabalho de teste com efeitos colaterais (tempo, tempos limite, PRNG)





  • CI, ou configurar o pipeline de modo que o botão Merge não seja assustador de pressionar





E também quero agradecer mais aos participantes do projeto que aderiram ao projeto, sem eles seria muito mais difícil.








All Articles