net.fenyo.mail4hotspot.dns.DnsListener.java Source code

Java tutorial

Introduction

Here is the source code for net.fenyo.mail4hotspot.dns.DnsListener.java

Source

// (c) Alexandre Fenyo 2012, 2013, 2014, 2015, 2016 - alex@fenyo.net - http://fenyo.net - GPLv3 licensed

package net.fenyo.mail4hotspot.dns;

import net.fenyo.mail4hotspot.service.*;
import net.fenyo.mail4hotspot.tools.GeneralException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;

import org.springframework.beans.factory.*;

import org.apache.commons.lang3.ArrayUtils;

@Component("dnsListener")
public class DnsListener implements InitializingBean, DisposableBean, Runnable {
    protected final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(getClass());

    private DatagramSocket socket;

    final private int DATAGRAMMAXSIZE = 10000;
    final private int DNSFORMAT_ID_MSB = 0;
    final private int DNSFORMAT_ID_LSB = DNSFORMAT_ID_MSB + 1;
    final private int DNSFORMAT_FLAGS_MSB = DNSFORMAT_ID_LSB + 1;
    final private int DNSFORMAT_FLAGS_LSB = DNSFORMAT_FLAGS_MSB + 1;
    final private int DNSFORMAT_QDCOUNT_MSB = DNSFORMAT_FLAGS_LSB + 1;
    final private int DNSFORMAT_QDCOUNT_LSB = DNSFORMAT_QDCOUNT_MSB + 1;
    final private int DNSFORMAT_ANCOUNT_MSB = DNSFORMAT_QDCOUNT_LSB + 1;
    final private int DNSFORMAT_ANCOUNT_LSB = DNSFORMAT_ANCOUNT_MSB + 1;
    final private int DNSFORMAT_NSCOUNT_MSB = DNSFORMAT_ANCOUNT_LSB + 1;
    final private int DNSFORMAT_NSCOUNT_LSB = DNSFORMAT_NSCOUNT_MSB + 1;
    final private int DNSFORMAT_ARCOUNT_MSB = DNSFORMAT_NSCOUNT_LSB + 1;
    final private int DNSFORMAT_ARCOUNT_LSB = DNSFORMAT_ARCOUNT_MSB + 1;
    final private int DNSFORMAT_DATAOFFSET = DNSFORMAT_ARCOUNT_LSB + 1;
    // tun.vpnoverdns.com
    private String DNSDOMAIN;
    private int DNSPORT;
    private boolean INITIALUSERS;
    private boolean INITIALIPS;

    @Autowired
    private AdvancedServices advancedServices;

    @Autowired
    private GeneralServices generalServices;

    private Thread thread = null;

    private class VersionAndDomain {
        public String version;
        public String domain;
    }

    public DnsListener() {
    }

    public String getCreateInitialUsers() {
        return INITIALUSERS ? "true" : "false";
    }

    public void setCreateInitialUsers(final String initialusers) {
        INITIALUSERS = initialusers.toLowerCase().equals("true") ? true : false;
    }

    public String getCreateInitialIps() {
        return INITIALIPS ? "true" : "false";
    }

    public void setCreateInitialIps(final String initialips) {
        INITIALIPS = initialips.toLowerCase().equals("true") ? true : false;
    }

    public String getDnsDomain() {
        return DNSDOMAIN;
    }

    public void setDnsDomain(final String dnsdomain) {
        DNSDOMAIN = dnsdomain;
    }

    public int getDnsPort() {
        return DNSPORT;
    }

    public void setDnsPort(final int dnsport) {
        DNSPORT = dnsport;
    }

    private String getLabel(final String query_string, final int index) {
        final String labels[] = query_string.split("\\.");
        if (index > labels.length - 1)
            return null;
        return labels[labels.length - index - 1];
    }

    private VersionAndDomain getVersionAndDomain(final byte query[]) throws GeneralException {
        VersionAndDomain vad = new VersionAndDomain();

        final StringBuffer tmpstr = new StringBuffer();
        int offset = DNSFORMAT_DATAOFFSET;
        while (query[offset] != 0) {
            tmpstr.append(new String(query, offset + 1, query[offset], Charset.forName("US-ASCII")));
            tmpstr.append(".");
            offset += query[offset] + 1;
        }
        if (!tmpstr.toString().toLowerCase().endsWith(DNSDOMAIN + "."))
            throw new GeneralException("bad domain: " + tmpstr.toString());
        vad.domain = tmpstr.substring(0, tmpstr.length() - DNSDOMAIN.length() - 5).toString();
        // version must be 2 char length beginning with 'v'
        vad.version = tmpstr.substring(tmpstr.length() - DNSDOMAIN.length() - 4,
                tmpstr.length() - DNSDOMAIN.length() - 2 - 3 + 2 + 1).toString();
        if (vad.version.length() != 2 || !vad.version.substring(0, 1).toLowerCase().equals("v"))
            throw new GeneralException("bad version: " + tmpstr.toString());
        return vad;
    }

    // pour tester la taille max :
    // dig @192.168.0.205 cn-00000192.id-00000000.v0.mail4hotspot.fenyo.net. a
    // apparemment, en interrogeant via gw, la limite est  183 octets inclus
    // idem depuis RSI

    // c'est truncated en UDP donc ressai en TCP
    // <fenyo@gw> dig @192.168.0.5 cn-00000183.id-00000000.v0.mail4hotspot.fenyo.net. a
    // ;; Truncated, retrying in TCP mode.
    // A NOTER : par DNS de Free, jusqu' 78 inclus, a passe en UDP, sinon, en TCP et ca marche plus. EN EFFET,  partir de 79, le datagram fait dpasse 512 octets

    // DONC limiter au max  64 octets par rponse : cf constante MAX_REPLY_LEN dans le code flex : DnsQuerier.as
    // quoique le calcul plutt exact est le suivant et engage  rduire MAX_REPLY_LEN  32 :
    // paquet le plus long : c'est une rponse (car dans le cas d'une rponse, il y a plusieurs RRs)
    // il doit contenir plusieurs RRs donc c'est des :
    // retry-XXX.ln-XXX.rd-XXXXXXXX.id-XXXXXXXX.v0.mail4hotspot.fenyo.net. :
    // la chane fait 57 octets avec le "." final si pas dans un retry, sinon 67 octets, mais n'est pas rpte car c'est une rfrence vers elle qui est mise dans chaque RR
    // le paquet contient :
    // - entete IP : 20 octets
    // - entete UDP : 8 octets
    // - entete DNS (transaction Id + flags) : 4 octets
    // - 1 query : 57 + 1 (zro dlimiteur de fin - nota : en dbut de chaque segment il y a la longueur du segment)
    //           = 58 ou 68 avec des retries
    // - des RRs de rponse faisant chacun 16 octets pour transporter 4 octets utiles
    // - des RRs d'infos complmentaires (34 octets au total dans mon test via ethereal) :
    //              - mail4hotspot.fenyo.net IN NS ns1.fenyo.net. : 18 octets
    //              - ns1.fenyo.net          IN A  88.170.235.198 : 16 octets
    // donc : taille du datagramme IP :
    //           sans retries : 124 + (nb de RRs de rponse) * 16
    //           avec retries : 134 + (nb de RRs de rponse) * 16
    // or il y a 3 octets utiles par RR de rponse (le 4ime indique la position dans la rponse car les RRs sont mlangs pour le loadbalancing DNS)
    // donc pour MAX_REPLY_LEN octets utiles  envoyer du serveur vers le client, il y a :
    //           sans retries : 124 + (MAX_REPLY_LEN / 3) * 16 octets dans le datagramme IP
    //           avec retries : 134 + (MAX_REPLY_LEN / 3) * 16 octets dans le datagramme IP
    // donc si MAX_REPLY_LEN = 64, cela fait :
    //           sans retries : 486 octets par datagramme < 512 octets => reste 26 octets pour des RRs complmentaires
    //           avec retries : 496 octets par datagramme < 512 octets => reste 16 octets pour des RRs complmentaires
    // donc c'est trs dangereux car trop petit si on rajoute par ex des secondaires supplmentaires pour mail4hotspot.fenyo.net
    // => on prend MAX_REPLY_LEN = 32, ce qui fait gagner 176 octets supplmentaires de marge

    // maj 12/8/2012 :
    //   avec 64 c'est 2x plus rapide, on prend 64
    //   les captures wireshark sont dans mail4hotspot - general\captures\MAX_REPLY_LEN_{32,64}.cap
    //   taille de paquets IP : MAX_REPLY_LEN == 32 => 308 octets au niveau IP
    //   taille de paquets IP : MAX_REPLY_LEN == 64 => 484 octets au niveau IP
    //   donc avec des retries, ca ferait 10 octets de plus, donc 318 et 494 respectivement
    //   donc il reste 512-494=18 octets, environ la taille d'un additional record
    //   on prend une petite marge avec MAX_REPLY_LEN = 
    //   ces paquets ont :
    //   - l'entete DNS
    //   - les flags
    //   - la query courte (pas de retransmission)
    //   - 11 answers si MAX_REPLY_LEN == 32 (car 3 * 11 == 33 >= 32) et 22 answers si MAX_REPLY_LEN == 64 (car 3 * 22 == 66 >= 64) ; 1 answer == 16 octets
    //     donc si on avait mis MAX_REPLY_LEN = 63, on aurait 21 answers car 3 * 21 == 63
    //   - un autoritative nameserver (l'IN NS pour tun.vpnoverdns.com) : 18 octets
    //   - un additional record (l'IN A pour ns2.vpnoverdns.com) : 16 octets
    //  donc on prend MAX_REPLY_LEN == 63 et on a donc :
    //  taille de paquets IP : MAX_REPLY_LEN == 63 => 468 octets au niveau IP
    //  donc reste 512-468 == 44 octets, la marge est donc plus du double qu'avec MAX_REPLY_LEN == 64
    //  au final : MAX_REPLY_LEN == 63, donc 21 rponses par datagramme, donc 63 octets par rponse

    // pb: "dig -p 54 @192.168.0.5 id-00000000.v0.mail4hotspot.fenyo.net. a +norecurse" renvoie le IN NS du domaine
    //     alors qu'avec +recurse a marche
    // dig @212.27.40.241 id-00000000.v0.mail4hotspot.fenyo.net. a +recurse
    // donc patcher BIND9 : si domaine mail4hotspot.fenyo.net, alors rajouter +recurse
    // recompil comme suit : ./configure --prefix=/usr/local/bind9 CFLAGS=-g
    // client_request(isc_task_t *task, isc_event_t *event) {
    // appelle
    // dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
    // dans lib/dns/message.c
    // solution : commenter //      allow-recursion     { 127.0.0.0/8; 192.168.0.0/24; };
    //            pour autoriser la recursion
    // et ajouter :
    // dans dns_message_parse() de lib/dns/message.c:
    //    msg->id = isc_buffer_getuint16(source);
    //    tmpflags = isc_buffer_getuint16(source);
    //{
    //char str[256];
    //sprintf(str, "-------------- 0x%lx\n", (long) tmpflags);
    //write(1, str, strlen(str));
    //tmpflags |= 0x100;
    //}
    //    msg->opcode = ((tmpflags & DNS_MESSAGE_OPCODE_MASK)
    // le mieux serait de tester le domaine pour ne faire cela que sous le domaine ad-hoc
    // rqs: on fait comme si recursion demande ET on recopie dans la rponse comme si l'metteur l'avait mis, c'est non conforme au RFC-1035

    // pour tester des critures :
    // sz-00000005.rn-00000000.id-00000001.v0.mail4hotspot.fenyo.net.
    // bf-FFFFFFFFFF.wr-00000000.id-00000256.v0.mail4hotspot.fenyo.net.
    // ck-00000000.id-00000256.v0.mail4hotspot.fenyo.net.
    // a05.rd-00000000.id-00000256.v0.mail4hotspot.fenyo.net.

    // maj 25/10/2015 : TDL :
    // la taille max d'un paquet UDP tait de 512 octets (rfc-1035) initialement, et la bibliothque perl pour le DNS utilise filtre  cette taille, donc on doit limiter  512 absolument.
    // mais mettre rfrence dans chaque RR pour limiter l'usage : rfc-10454.1.4
    // et supprimer les rponses RFC-1918 car sinon filtres par certains DNS.
    // mettre les retry-XXX dans perl.
    // Limitation de la taille max d'une rponse UDP. A mettre en relation avec MAX_REPLY_LEN dans le code flex, l'quivalent dans le code perl est l'option -Y.
    // src/net/fenyo/mail4hotspot/dns/DnsQuerier.as: indique qu'il faut mettre 48 (suite  pb dans un htel)
    // vrifier que la compression sert  quelque chose...
    // vrifier dans le code perl que les RR recus sont bien les bons

    // maj 08/11/2015 : on met 64 ce qui fait 22 RR et des paquets UDP de 440 octets au niveau UDP et 474 au niveau IP, incluant les "retry-", mais pas d'additional RRs
    // pour tester la bonne valeur : utiliser -Y avec le client perl et faire un wireshark pour regarder le nombre de RR et si a passe

    // maj 26/11/2015 : protocole "fast" : taille sans rponse :
    // RFC-1035 : Messages carried by UDP are restricted to 512 bytes (not counting the IP or UDP headers).
    // dans ces 512 : on a 12 (header DNS) + 68 (requte) => reste 432 octets
    // on met au max 2 chanes de 256 caractres chacun (dont la taille est la 1er octet), sachant que les TXT peuvent enchaner des chanes de 256.
    // on rgle la taille max demande dans le client.

    private byte[] handle(final byte query[], final Inet4Address address) throws GeneralException {
        boolean txt = false;
        boolean txtwithb64 = false;
        byte retvalues[] = null;
        if (query.length <= DNSFORMAT_DATAOFFSET)
            throw new GeneralException("datagram too short");
        if ((query[DNSFORMAT_QDCOUNT_MSB] << 8) + query[DNSFORMAT_QDCOUNT_LSB] != 1)
            throw new GeneralException("invalid query count");
        if ((query[DNSFORMAT_FLAGS_MSB] & 2) != 0)
            throw new GeneralException("truncated");

        final VersionAndDomain vad = getVersionAndDomain(query);
        final String query_string = vad.domain;
        final String version_string = vad.version;
        //      log.debug("version string: " + version_string);
        //      log.debug("query string: " + query_string);
        //      log.debug("label[0]: " + getLabel(query_string, 0));

        final String label0 = getLabel(query_string, 0);

        if (label0.toLowerCase().equals("id-00000000")) {
            final String label1 = getLabel(query_string, 1);
            if (label1 != null && label1.toLowerCase().matches("^cn-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {
                int cnt = new Integer(label1.substring(3));
                if (cnt > 192)
                    throw new GeneralException("ping request too long");
                retvalues = new byte[cnt];
                for (int i = 0; i < cnt; i++)
                    retvalues[i] = (byte) i;

            } else
                retvalues = new byte[] { 'E', 1 }; // 'E'rror 1

        } else if (label0.toLowerCase().equals("id-00000001")) {
            final String label1 = getLabel(query_string, 1);
            if (label1 != null && label1.toLowerCase().matches("^rn-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {
                final String label2 = getLabel(query_string, 2);
                if (label2 != null
                        && label2.toLowerCase().matches("^sz-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {

                    int size = new Integer(label2.substring(3));
                    final int msg_id = MsgFactory.createMsg(size);
                    retvalues = new byte[] { (byte) ((msg_id & 0x00ff0000) >> 16),
                            (byte) ((msg_id & 0x0000ff00) >> 8), (byte) (msg_id & 0x000000ff) };

                } else
                    retvalues = new byte[] { 'E', 2 }; // 'E'rror 2

            } else
                retvalues = new byte[] { 'E', 3 }; // 'E'rror 3

        } else if (label0.toLowerCase().matches("^id-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {
            final int msg_id = new Integer(label0.substring(3));
            final String label1 = getLabel(query_string, 1);

            //         requte max : 131 car : retry-001.rn-75706993.bf-00696e697469616c69c2a7436f6e6e656374536f636b6574c2a733313330.wr-00000000.id-00731058.v2.tun.vpnoverdns.com
            //         longueur max d'un nom de domaine : 250 (certains softs vont plus loin mais pas tous - cf gethostname)
            //         2 car par octet de donnes
            //         chaque bf cot 64 car (en incluant .bf-)
            //         donc on met plusieurs bf
            //         (250-131)/64 = 1,86
            //         on peut en mettre 1,8 de plus
            //         a veut dire 3 bf au total mais pas 3 pleins :
            //         3 bf  concurrence de 250 car
            //         0 bf : 67 car
            //         250 = 131-64 + 2*64 + 55
            //         55 impair => remplac par 54
            //         Donc deux de 64 (2x30 octets) et 1 de 54 (25 octets)
            //         donc 85 octets au lieu de 30
            //         donc 2,8 fois plus rapide
            if (label1 != null && label1.toLowerCase().matches("^wr-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {

                final int pos = new Integer(label1.substring(3));

                final String label2 = getLabel(query_string, 2);
                if (label2 != null && label2.toLowerCase().matches("^bf-([0-9a-f][0-9a-f])+$")) {
                    final String content = label2.substring(3);
                    if (MsgFactory.msgExists(msg_id) == false) {
                        retvalues = new byte[] { 'E', 8 }; // 'E'rror 8;
                        // log.info("Error 8-1: msg_id=" + msg_id);
                    } else
                        retvalues = MsgFactory.getMsg(msg_id).write(pos, content, advancedServices, address);
                } else
                    retvalues = new byte[] { 'E', 4 }; // 'E'rror 4

                if (retvalues != null) {
                    final String label3 = getLabel(query_string, 3);
                    if (label3 != null && label3.toLowerCase().matches("^bf-([0-9a-f][0-9a-f])+$")) {
                        final String content = label3.substring(3);
                        if (MsgFactory.msgExists(msg_id) == false) {
                            retvalues = new byte[] { 'E', 8 }; // 'E'rror 8;
                            // log.info("Error 8-2: msg_id=" + msg_id);
                        } else
                            retvalues = MsgFactory.getMsg(msg_id).write(pos + 30, content, advancedServices,
                                    address);
                    }
                }

                if (retvalues != null) {
                    final String label4 = getLabel(query_string, 4);
                    if (label4 != null && label4.toLowerCase().matches("^bf-([0-9a-f][0-9a-f])+$")) {
                        final String content = label4.substring(3);
                        if (MsgFactory.msgExists(msg_id) == false) {
                            retvalues = new byte[] { 'E', 8 }; // 'E'rror 8;
                            // log.info("Error 8-3: msg_id=" + msg_id);
                        } else
                            retvalues = MsgFactory.getMsg(msg_id).write(pos + 60, content, advancedServices,
                                    address);
                    }
                }

            } else if (label1 != null
                    && label1.toLowerCase().matches("^ck-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {
                if (MsgFactory.msgExists(msg_id) == false)
                    retvalues = new byte[] { 'E', 9 }; // 'E'rror 9;
                else {
                    final Msg msg = MsgFactory.getMsg(msg_id);
                    if (msg.isProcessed() == false) {
                        // message pas encore trait, on demande au client de refaire un ck (check)
                        //                  log.debug(msg.debugContent() + " - " + version_string + "." + query_string);
                        retvalues = new byte[] { 'E', 10 }; // 'E'rror 10
                    } else {
                        final byte len0 = (byte) (msg.outputLength() >> 16);
                        final byte len1 = (byte) ((msg.outputLength() >> 8) & 255);
                        final byte len2 = (byte) (msg.outputLength() & 255);
                        retvalues = new byte[] { 'L', len0, len1, len2 }; // 'L'ength fournie en rponse
                    }
                }

            } else if (label1 != null
                    && label1.toLowerCase().matches("^r[dxy]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")) {
                if (label1.toLowerCase().startsWith("rx") || label1.toLowerCase().startsWith("ry"))
                    txt = true;
                if (label1.toLowerCase().startsWith("ry"))
                    txtwithb64 = true;

                final String label2 = getLabel(query_string, 2);
                final int pos = new Integer(label1.substring(3));

                if (label2 != null && label2.toLowerCase().matches("^ln-[0-9][0-9][0-9]$")) {
                    final int size = new Integer(label2.substring(3));
                    if (MsgFactory.msgExists(msg_id) == false)
                        retvalues = new byte[] { 'E', 11 }; // 'E'rror 11
                    else {
                        final Msg msg = MsgFactory.getMsg(msg_id);
                        if (msg.isProcessed() == false)
                            retvalues = new byte[] { 'E', 12 }; // 'E'rror 12
                        // bug dans le protocole v0 corrig dans les versions suivantes : si on lit 2 octets de la partie compresse (typiquement les 2 derniers du buffer compress) et que le premier des deux est E, on signale une erreur involontairement
                        else {
                            if (version_string.toLowerCase().equals("v0")
                                    || version_string.toLowerCase().equals("v1"))
                                retvalues = msg.read(pos, size);
                            else {
                                byte[] content = msg.read(pos, size);
                                if (txtwithb64) {
                                    // avec Java8
                                    // content = Base64.getEncoder().encode(content);
                                    // avec apache commons-codec
                                    content = org.apache.commons.codec.binary.Base64.encodeBase64(content);

                                    retvalues = new byte[content.length + 1];
                                    // put a '0' (48) instead of 0 if base64 encoding, since base64 encoding means non-char bytes (like 0) are not handled
                                    retvalues[0] = '0';
                                    for (int i = 0; i < content.length; i++)
                                        retvalues[i + 1] = content[i];
                                } else {
                                    retvalues = new byte[size + 1];
                                    // add a 0 to the beginning of the returned byte array to be sure it can not start with 'E', so it can be distinguished from any error message on the client side
                                    retvalues[0] = 0;
                                    for (int i = 0; i < size; i++)
                                        retvalues[i + 1] = content[i];
                                }
                            }
                        }
                        // pour tester l'erreur involontaire, dcommenter :
                        // retvalues = new byte [] { 'E', 12 };
                        // Ca cre une erreur dans perl  la ligne 'die "  remote error for $names[$i]: try $tries[$i]: ".chr($msgbytes[0]).$msgbytes[1]."\n"'
                    }
                } else
                    retvalues = new byte[] { 'E', 13 }; // 'E'rror 13

            } else if (label1 != null && label1.toLowerCase().matches("^ac$")) {

                if (MsgFactory.msgExists(msg_id) == false)
                    retvalues = new byte[] { 'E', 14 }; // 'E'rror 14
                else {
                    MsgFactory.removeMsg(msg_id);
                    retvalues = new byte[] { 'E', 0 }; // 'E'rror 0 == OK
                }

            } else
                retvalues = new byte[] { 'E', 5 }; // 'E'rror 5

        } else
            retvalues = new byte[] { 'E', 6 }; // 'E'rror 6

        if (retvalues == null)
            retvalues = new byte[] { 'E', 7 }; // 'E'rror 7

        if (txt) {
            final int ntxtstrings = 1 + (retvalues.length - 1) / 255;

            final int rr_length = 2
                    /* pointeur sur le nom de domaine de la query */ + 10 /* TYPE + CLASS + TTL + RDLENGTH */
                    + ntxtstrings + retvalues.length;
            final int answer_length = ArrayUtils.indexOf(query, (byte) 0, DNSFORMAT_DATAOFFSET)
                    /* nom de domaine de la query */ + 1 /* '\0' */ + 2 /* QTYPE */ + 2 /* QCLASS */ + rr_length;

            byte answer[] = new byte[answer_length];
            answer[DNSFORMAT_ID_MSB] = query[DNSFORMAT_ID_MSB];
            answer[DNSFORMAT_ID_LSB] = query[DNSFORMAT_ID_LSB];
            answer[DNSFORMAT_FLAGS_LSB] = 0;
            answer[DNSFORMAT_FLAGS_MSB] = (byte) (((byte) (query[DNSFORMAT_FLAGS_MSB] | 128 /* QR: response */
                    | 4 /* AA */)) & (byte) ~2 /* TC */);
            answer[DNSFORMAT_QDCOUNT_MSB] = 0;
            answer[DNSFORMAT_QDCOUNT_LSB] = 1;
            answer[DNSFORMAT_ANCOUNT_MSB] = 0;
            answer[DNSFORMAT_ANCOUNT_LSB] = 1;
            answer[DNSFORMAT_NSCOUNT_MSB] = 0;
            answer[DNSFORMAT_NSCOUNT_LSB] = 0;
            answer[DNSFORMAT_ARCOUNT_MSB] = 0;
            answer[DNSFORMAT_ARCOUNT_LSB] = 0;

            for (int i = DNSFORMAT_DATAOFFSET; i < DNSFORMAT_DATAOFFSET + 1 /* octet de longueur du premier label */
                    + version_string.length() + 1 /* '.' */ + query_string.length() + 1 /* '.' */
                    + DNSDOMAIN.length() + 1 /* '\0' */ + 4 /* TYPE + CLASS */; i++)
                answer[i] = query[i];

            int current_offset = DNSFORMAT_DATAOFFSET + 1 /* octet de longueur du premier label */
                    + version_string.length() + 1 /* '.' */ + query_string.length() + 1 /* '.' */
                    + DNSDOMAIN.length() + 1 /* '\0' */ + 4 /* QTYPE + QCLASS */;

            answer[current_offset++] = (DNSFORMAT_DATAOFFSET >> 8) + (byte) 0xc0;
            answer[current_offset++] = DNSFORMAT_DATAOFFSET & 0xff;

            answer[current_offset++] = 0x00;
            answer[current_offset++] = 0x10 /* TYPE */;

            answer[current_offset++] = 0x00;
            answer[current_offset++] = 0x01 /* CLASS */;

            answer[current_offset++] = 0x00;
            answer[current_offset++] = 0x00;
            answer[current_offset++] = 0x00;
            answer[current_offset++] = 0x00 /* TTL */;

            answer[current_offset++] = (byte) ((ntxtstrings + retvalues.length) >> 8);
            answer[current_offset++] = (byte) ((ntxtstrings + retvalues.length) & 0xff);

            for (int i = 0; i < retvalues.length; i++) {
                if ((i % 255) == 0)
                    answer[current_offset++] = (byte) (Math.min(255, retvalues.length - i));
                answer[current_offset++] = retvalues[i];
            }

            return answer;

        } else {

            // calculer la taille de la section requtes
            // retvalues length: 1 2 3 4 5 6 7 8 9 10 ...
            // nombre de in a:   1 1 1 2 2 2 3 3 3  4 ...
            // retvalues.length vaut au max optmaxread (ou optmaxread + 1  partir de la v2) dans perl - il faut trouver l'optimal de optmaxread et le positionner dans perl
            // Par dfaut, c'est 48 donc ((48 + 1 /* v2 */) + 2) / 3 == 17 RR par rponse max.
            final int nanswers = (retvalues.length + 2) / 3;

            // 192 (3 * 64) bytes max per answer
            if (nanswers > 64)
                throw new GeneralException("too many answers (" + nanswers + ")");

            final int rr_length = 2
                    /* pointeur sur le nom de domaine de la query */ + 10 /* TYPE + CLASS + TTL + RDLENGTH */
                    + 4 /* taille de l'adresse IP rsultante */;
            final int answer_length = ArrayUtils.indexOf(query, (byte) 0, DNSFORMAT_DATAOFFSET)
                    /* nom de domaine de la query */ + 1 /* '\0' */ + 2 /* QTYPE */ + 2 /* QCLASS */
                    + rr_length * nanswers;

            byte answer[] = new byte[answer_length];
            answer[DNSFORMAT_ID_MSB] = query[DNSFORMAT_ID_MSB];
            answer[DNSFORMAT_ID_LSB] = query[DNSFORMAT_ID_LSB];
            answer[DNSFORMAT_FLAGS_LSB] = 0;
            answer[DNSFORMAT_FLAGS_MSB] = (byte) (((byte) (query[DNSFORMAT_FLAGS_MSB] | 128 /* QR: response */
                    | 4 /* AA */)) & (byte) ~2 /* TC */);
            answer[DNSFORMAT_QDCOUNT_MSB] = 0;
            answer[DNSFORMAT_QDCOUNT_LSB] = 1;
            answer[DNSFORMAT_ANCOUNT_MSB] = 0;
            answer[DNSFORMAT_ANCOUNT_LSB] = (byte) nanswers;
            answer[DNSFORMAT_NSCOUNT_MSB] = 0;
            answer[DNSFORMAT_NSCOUNT_LSB] = 0;
            answer[DNSFORMAT_ARCOUNT_MSB] = 0;
            answer[DNSFORMAT_ARCOUNT_LSB] = 0;

            for (int i = DNSFORMAT_DATAOFFSET; i < DNSFORMAT_DATAOFFSET + 1 /* octet de longueur du premier label */
                    + version_string.length() + 1 /* '.' */ + query_string.length() + 1 /* '.' */
                    + DNSDOMAIN.length() + 1 /* '\0' */ + 4 /* TYPE + CLASS */; i++)
                answer[i] = query[i];

            byte rr[] = { (DNSFORMAT_DATAOFFSET >> 8) + (byte) 0xc0, DNSFORMAT_DATAOFFSET & 0xff, 0x00,
                    0x01 /* TYPE */, 0x00, 0x01 /* CLASS */, 0, 0, 0, 0 /* TTL */, 0, 4 /* RDLENGTH */, (byte) 0xff,
                    (byte) 0xff, (byte) 0xff, (byte) 0xff /* IP address */ };

            int current_offset = DNSFORMAT_DATAOFFSET + 1 /* octet de longueur du premier label */
                    + version_string.length() + 1 /* '.' */ + query_string.length() + 1 /* '.' */
                    + DNSDOMAIN.length() + 1 /* '\0' */ + 4 /* QTYPE + QCLASS */;
            int retindex = 0;
            for (int ans_idx = 0; ans_idx < nanswers; ans_idx++) {
                int ncar = 0;

                if (retindex < retvalues.length) {
                    rr[rr.length - 3] = retvalues[retindex];
                    ncar++;
                }
                retindex++;
                if (retindex < retvalues.length) {
                    rr[rr.length - 2] = retvalues[retindex];
                    ncar++;
                }
                retindex++;
                if (retindex < retvalues.length) {
                    rr[rr.length - 1] = retvalues[retindex];
                    ncar++;
                }
                retindex++;

                rr[rr.length - 4] = (byte) (((byte) ans_idx) | (byte) (ncar << 6));

                for (int i = 0; i < rr.length; i++)
                    answer[current_offset++] = rr[i];
            }

            return answer;
        }
    }

    private class Handler implements Runnable {
        final DatagramPacket query;

        public Handler(final DatagramPacket query) {
            this.query = query;
        }

        public void run() {
            try {
                // 75% de pertes simules
                // if (Math.random() * 100 < 25) {
                // 85% de pertes simules
                // if (Math.random() * 100 < 15) {
                if (Math.random() * 100 < 100) {

                    final byte buf[] = handle(Arrays.copyOf(query.getData(), query.getLength()),
                            (Inet4Address) query.getAddress());
                    final DatagramPacket answer = new DatagramPacket(buf, buf.length);
                    answer.setAddress(query.getAddress());
                    answer.setPort(query.getPort());
                    socket.send(answer);

                } else {
                    log.debug("simulating loss of datagram");
                }

            } catch (final GeneralException ex) {
                ex.printStackTrace();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    public void run() {
        final ExecutorService pool = Executors.newCachedThreadPool();

        try {
            socket = new DatagramSocket(DNSPORT);
        } catch (final SocketException ex) {
            ex.printStackTrace();
            log.error("can not start DNS service");
            return;
        }

        do {
            final DatagramPacket query = new DatagramPacket(new byte[DATAGRAMMAXSIZE], DATAGRAMMAXSIZE);
            try {
                socket.receive(query);
                pool.execute(new Handler(query));
            } catch (IOException ex) {
                log.error(ex);
            }
        } while (thread.isInterrupted() == false);

        try {
            log.info("waiting for executor tasks to terminate");
            pool.awaitTermination(120, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            log.error(ex);
        }
    }

    public void destroy() throws Exception {
        try {
            thread.interrupt();
            socket.close();
            thread.join();
        } catch (final InterruptedException ex) {
            log.error(ex);
        }
    }

    public void afterPropertiesSet() throws Exception {
        if (INITIALUSERS)
            generalServices.createInitialUsers();
        if (INITIALIPS)
            generalServices.createInitialIps();

        thread = new Thread(this);
        thread.start();
    }
}