summaryrefslogtreecommitdiff
path: root/org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org
blob: a14d7fe5b97108a5c57e64d9b403f84c8aa0df59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#+options: toc:nil

#+begin_export html
---
layout: default
title: Démarrer un serveur avant de connaître son adresse IP
excerpt: Lorsque vous démarrez votre serveur sur un réseau dynamique, il est possible qu’il démarre avant que l’adresse IP soit connue. Que faire ?
---
#+end_export

C’est un problème qui impacte tous les bidouilleurs à domicile. C’est
déjà suffisamment complexe de configurer sa Bidule Box, mais un
nouveau problème intervient. À la maison, vous avez sans doute une
adresse IP dynamique. C’est-à-dire que si vous débranchez votre
routeur et que vous le rebranchez, vous obtiendrez une nouvelle
adresse IP. Sur ma connexion Escrorange, toutefois, il semble que ce
ne soit le cas que pour IPv4. Malheureusement, cela ne change rien au
problème finalement : lorsque mon ordi démarre, il y a un court laps
de temps pendant lequel le réseau est initialisé, mais je n’ai pas
encore d’adresse IP. Ou alors, je ne les ai pas encore toutes.

Si mon service réseau décide de démarrer à ce moment-là, je me
retrouve bien embarassé : il ne sera tout simplement pas accessible
(ou seulement partiellement), et le problème ne sera peut-être jamais
détecté.

La solution retenue par systemd [[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/systemd-network-targets-and-services_configuring-and-managing-networking][consiste à définir une cible
=network-online.target=]], qui ne sera activée que quand la négociation
des adresses sera terminée. Comme noté sur le site de Red Hat, cette
solution n’est utile que pour le démarrage du système. En cas de panne
temporaire du réseau sans redémarrage de la machine, il n’y aura pas
de redémarrage du service. Malgré cette limitation, la solution
s’applique à tous les services nécessitant de se lier au réseau. Mais
peut-être est-il possible de concevoir mieux chaque service, afin que
le problème ne se pose pas ?

Nous allons aujourd’hui voir comment il est possible de concevoir le
service réseau de sorte à ce qu’il puisse réagir à la présence d’une
nouvelle adresse IP à lier, ou à la disparition d’une adresse
utilisée.

* Dites bonjour :)
Nous allons concevoir un serveur très simple : il lui est donné comme
configuration un nom de domaine et un nom de service, et il se liera à
toutes les adresses connues de ce domaine, selon le protocole TCP,
dont le port correspond au service. Lorsqu’un client se connectera, on
enverra "Bonjour :)" et on terminera la connexion.

Comme on s’attend à ce qu’il y ait plusieurs adresses IP à lier, il y
aura donc plusieurs sockets à surveiller en même temps. Il y a de
bonnes API pour faire cela, comme par exemple [[https://en.wikipedia.org/wiki/Epoll][epoll]], mais nous allons
nous contenter de la fonction =select= car elle est disponible
directement en guile.

#+name: classe-serveur
#+caption: classe-serveur : définition de la classe serveur
#+begin_src scheme :eval no
  (define-class <serveur> ()
    (sockets #:init-value '())
    (hôte #:init-keyword #:hôte #:init-thunk gethostname)
    (service #:init-keyword #:service #:init-value "12345"))
#+end_src

Le nom de l’hôte et le nom du service sont des constantes pour cette
classe. En effet, il serait trop complexe de gérer plusieurs noms
d’hôtes différents. La valeur par défaut du nom d’hôte est le nom de
l’ordinateur (que l’on considère comme dynamique, même s’il serait
assez incongru qu’il change sans que les adresses IP associées
suivent). La valeur par défaut du service, en revanche, est
arbitraire, et c’est au développeur de la fixer.

* Relier le serveur en cas de besoin
Nous pouvons définir une méthode, =relier=, qui s’occupera de
récupérer la liste des adresses IP pour l’hôte, afin de fermer les
sockets obsolètes et d’en lier de nouvelles pour les nouvelles
adresses. Il ne faudrait pas fermer les sockets qui sont toujours
valides, autrement les connexions en attente seraient rejetées.

Par choix, nous allons éviter de faire trop de mutations dans notre
API. La fonction retournera donc deux valeurs : un nouveau serveur, et
la liste des sockets obsolètes. Charge à l’utilisateur de l’API de les
fermer.

#+name: fonction-relier
#+caption: fonction-relier : mettre à jour la liste des sockets à lier
#+begin_src scheme :eval no
  (define-method (relier (serveur <serveur>))
    (define (index adresse)
      ;; retourne une valeur d’index pour adresse, comparable avec
      ;; equal?
      (list
       (sockaddr:fam adresse)
       (sockaddr:path adresse)
       (sockaddr:addr adresse)
       (sockaddr:port adresse)))
    (define tcp
      (protoent:proto (getprotobyname "tcp")))
    (let ((sockets-existantes
           (make-hash-table))
          (nouveau-serveur (shallow-clone serveur)))
      (for-each
       (lambda (socket)
         (let ((adresse (getsockname socket)))
           (hash-set! sockets-existantes
                      ;; On ne peut pas utiliser les adresses telles
                      ;; quelles, sinon equal? ne fonctionnera pas.
                      (index adresse)
                      socket)))
       (slot-ref serveur 'sockets))
      (slot-set!
       nouveau-serveur 'sockets
       (map
        (lambda (info-adresse)
          (let ((adresse (addrinfo:addr info-adresse)))
            (let ((existante
                   (hash-ref sockets-existantes
                             (index adresse))))
              (if existante
                  (begin
                    (hash-remove! sockets-existantes (index adresse))
                    existante)
                  ;; Nouvelle adresse
                  (let ((s (socket (addrinfo:fam info-adresse)
                                   (addrinfo:socktype info-adresse)
                                   (addrinfo:protocol info-adresse))))
                    (bind s adresse)
                    (listen s 10)
                    s)))))
        (getaddrinfo (slot-ref serveur 'hôte)
                     (slot-ref serveur 'service)
                     (logior AI_PASSIVE)
                     0 ;; Famille
                     SOCK_STREAM ;; Avec connexion
                     tcp)))
      (values nouveau-serveur
              (map cdr (hash-map->list cons sockets-existantes)))))

#+end_src

La fonction construit une table de hachage qui retient la liste des
sockets, indexée par l’adresse de la socket. Puisque les adresses IP
sont converties en nombre par guile, la fonction de comparaison
=equal?= permet de retrouver les adresses nécessaires pour satisfaire
=getaddrinfo=.

Par souci de facilité, nous allons appeler directement la fonction
=relier= dans la méthode d’initialisation de la classe de serveur.

#+name: initialisation-serveur
#+caption: initialisation-serveur : initialisation de la classe <serveur>
#+begin_src scheme :eval no
  (define-method (initialize (object <serveur>) initargs)
    (next-method)
    (receive (serveur _)
        (relier object)
      (slot-set! object 'sockets
                 (slot-ref serveur 'sockets))))
#+end_src

On commence bien sûr par appeler =(next-method)=, de sorte à définir
les champs hôte et service grâce à la méthode d’initialisation par
défaut, qui ne se fonde que sur la définition des slots. Il est
possible de détourner cette garantie en utilisant de l’héritage
multiple, mais nous supposerons que l’initialisation pour la classe
fille d’un héritage multiple ne fait pas directement appel à
=(next-method)=, ce qui fera disparaître le problème.

* Utilisons =select=

Comme nous allons utiliser la fonction select, nous devons être en
mesure d’obtenir la liste des ports ou descripteurs de fichiers à
surveiller. On peut simplement retourner la valeur du champ sockets
ici.

#+name: fonction-ports
#+caption: fonction-ports : retourne la liste des ports à surveiller
#+begin_src scheme :eval no
  (define-method (ports (serveur <serveur>))
    (values
     ;; Liste des ports dont on doit surveiller la lecture
     (slot-ref serveur 'sockets)
     ;; Liste des ports dont on attend l’écriture
     '()
     ;; Liste des ports dont on attend une erreur
     '()))
#+end_src

Enfin, lorsque la fonction select aura indiqué un port disponible en
lecture, nous devons agir. En l’occurence, il faut accepter le nouveau
client, lui envoyer le bonjour, et fermer la socket du client. En
revanche, si la socket ne fait pas partie du serveur, il ne faut rien
faire.

#+name: fonction-prête
#+caption: fonction-prête : agit lorsqu’un port est prêt
#+begin_src scheme :eval no
  (define-method (prête (serveur <serveur>) socket direction)
    (when (port? socket)
      (set! socket (port->fdes socket)))
    (for-each
     (lambda (socket-serveur)
       (when (and (eq? direction 'lire)
                  (eqv? (port->fdes socket-serveur) socket))
         (let ((client (accept socket-serveur)))
           (let ((port (car client))
                 (adresse (cdr client)))
             (format port "Bonjour ~a :)\n"
                     (inet-ntop (sockaddr:fam adresse)
                                (sockaddr:addr adresse)))
             (close-port port)))))
     (slot-ref serveur 'sockets))
    serveur)
#+end_src

* Le programme à exécuter
Nous pouvons maintenant exécuter [[file:../../../code/bonjour.scm]] :

#+begin_src scheme :eval no :tangle ../bonjour.scm :noweb no-export #:shebang "#!/usr/local/bin/guile"
  (use-modules (oop goops) (ice-9 receive) (ice-9 match)
               (srfi srfi-19))

  <<classe-serveur>>
  <<fonction-relier>>
  <<initialisation-serveur>>
  <<fonction-ports>>
  <<fonction-prête>>

  (define (main serveur)
    (format #t "~a : le serveur lie les adresses suivantes : ~a\n"
            (date->string (current-date))
            (map
             (lambda (socket)
               (let ((adresse (getsockname socket)))
                 (let ((adresse
                        (inet-ntop (sockaddr:fam adresse) (sockaddr:addr adresse)))
                       (port
                        (sockaddr:port adresse)))
                   (format #f "[~a]:~a" adresse port))))
             (slot-ref serveur 'sockets)))
    (receive (read write except) (ports serveur)
      (match (select read write except 60)
        ((read write except)
         (let traiter ((serveur serveur)
                       (tâches
                        (append
                            (map (lambda (socket)
                                   `(,socket lire))
                                 read)
                            (map (lambda (socket)
                                   `(,socket écrire))
                                 write)
                            (map (lambda (socket)
                                   `(,socket exception))
                                 except))))
           (match tâches
             (()
              (receive (serveur à-fermer) (relier serveur)
                (for-each close-port à-fermer)
                (main serveur)))
             (((socket direction) tâches-restantes ...)
              (traiter (prête serveur socket direction)
                       tâches-restantes))))))))

  (main (make <serveur>))
#+end_src

Ce programme vérifie toutes les 60 secondes (dans le pire des cas) que
les adresses à lier sont à jour, et sert tous les clients qui se
présentent en leur répondant « Bonjour :) ».

Il y a beaucoup de choses à améliorer. Pour commencer, il faudrait
utiliser autre chose que =select=, qui a une complexité temporelle
linéaire et qui ne gère qu’un nombre limité de clients. Nous avons
aussi une complexité linéaire pour vérifier si la socket est rattachée
au serveur, et donc quadratique pour un appel récursif de =main=. Il
faudrait utiliser =epoll=, ou une bibliothèque de gestion du réseau
plus haut niveau comme la GLib, et revoir notre code. Il faudrait
également utiliser getopt-long (en internationalisant les noms longs
d’options) pour choisir le nom d’hôte à lier. Enfin, exécuter
=getaddrinfo= à chaque fois que l’on sert un groupe de clients n’est
pas très pertinent, et attendre 60 secondes peut être long. Pour
détecter les situations où nous devrions relier le serveur, il va
falloir utiliser l’API Netlink, ce que nous ferons une prochaine fois.