Путаница в отношении очереди синхронизации и очереди приема - PullRequest
1 голос
/ 03 августа 2020

При чтении исходного кода TCP я обнаружил путаницу:

Я знаю, что TCP имеет две очереди с трехсторонним рукопожатием:

  • Первая очередь хранит соединения, полученные сервером SYN и отправляем обратно ACK + SYN, который мы называем syn queue .
  • Во второй очереди хранятся соединения, которые 3WHS были успешными и соединение установлено, которое мы называем accept queue .

Но при чтении кодов я обнаружил, что listen() вызовет inet_csk_listen_start(), который вызовет reqsk_queue_alloc() для создания icsk_accept_queue. И эта очередь используется в accept(), когда мы обнаруживаем, что очередь не пуста, мы получим от нее соединение и вернемся.

Более того, после отслеживания процесса приема стек вызовов похож на

tcp_v4_rcv()->tcp_v4_do_rcv()->tcp_rcv_state_process()

Состояние сервера - СЛУШАТЬ при получении первого подтверждения. Таким образом, он будет вызывать

`tcp_v4_conn_request()->tcp_conn_request()`

In tcp_conn_request()

if (!want_cookie)
    // Add the req into the queue
    inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));

Но здесь очередь - это именно icsk_accept_queue, а не очередь синхронизации.

void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
                   unsigned long timeout)
{
    reqsk_queue_hash_req(req, timeout);
    inet_csk_reqsk_queue_added(sk);
}

static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
    reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}

accept() вернет установленное соединение, что означает, что icsk_accept_queue - вторая очередь, но где находится первая очередь?

Где соединение меняется с первой очереди на вторую?

Почему Linux добавляет новый запрос в icsk_accept_queue?

Ответы [ 2 ]

1 голос
/ 10 августа 2020

В дальнейшем мы будем следовать наиболее типичному пути кода и игнорировать проблемы, возникающие из-за потери пакетов, повторной передачи и использования нетипичных функций, таких как быстрое открытие TCP (TFO в комментариях к коду).

Вызов accept обрабатывается intet_csk_accept, который вызывает reqsk_queue_remove, чтобы получить сокет из очереди приема &icsk->icsk_accept_queue из прослушивающего сокета:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct request_sock *req;
    struct sock *newsk;
    int error;

    lock_sock(sk);

    [...]

    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;

    [...]

    return newsk;

    [...]
}

В reqsk_queue_remove он использует rskq_accept_head и rskq_accept_tail, чтобы вытащить сокет из очереди и вызвать sk_acceptq_removed:

static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue,
                              struct sock *parent)
{
    struct request_sock *req;

    spin_lock_bh(&queue->rskq_lock);
    req = queue->rskq_accept_head;
    if (req) {
        sk_acceptq_removed(parent);
        WRITE_ONCE(queue->rskq_accept_head, req->dl_next);
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_tail = NULL;
    }
    spin_unlock_bh(&queue->rskq_lock);
    return req;
}

И sk_acceptq_removed сокращает длину очереди сокетов, ожидающих приема в sk_ack_backlog :

static inline void sk_acceptq_removed(struct sock *sk)
{
    WRITE_ONCE(sk->sk_ack_backlog, sk->sk_ack_backlog - 1);
}

Это, я думаю, полностью понимает спрашивающий. Теперь давайте посмотрим, что происходит, когда получено SYN, и когда приходит последний ACK 3WH.

Сначала получение SYN. Опять же, давайте предположим, что файлы cookie TFO и SYN не используются, и посмотрим на наиболее распространенный путь (по крайней мере, не при наличии SYN-потока).

SYN обрабатывается в tcp_conn_request, где запрос на соединение (не полноценный сокет) сохраняется (мы скоро увидим), вызывая inet_csk_reqsk_queue_hash_add, а затем вызывая send_synack для ответа на SYN:

int tcp_conn_request(struct request_sock_ops *rsk_ops,
             const struct tcp_request_sock_ops *af_ops,
             struct sock *sk, struct sk_buff *skb)
{

   [...] 

   if (!want_cookie)
            inet_csk_reqsk_queue_hash_add(sk, req,
                tcp_timeout_init((struct sock *)req));
   af_ops->send_synack(sk, dst, &fl, req, &foc,
                    !want_cookie ? TCP_SYNACK_NORMAL :
                           TCP_SYNACK_COOKIE);

   [...]

   return 0;

   [...]
}

inet_csk_reqsk_queue_hash_add звонки reqsk_queue_hash_req и inet_csk_reqsk_queue_added для сохранения запроса.

void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
                   unsigned long timeout)
{
    reqsk_queue_hash_req(req, timeout);
    inet_csk_reqsk_queue_added(sk);
}

reqsk_queue_hash_req помещает запрос в eha sh.

static void reqsk_queue_hash_req(struct request_sock *req,
                 unsigned long timeout)
{
    [...]

    inet_ehash_insert(req_to_sk(req), NULL);

    [...]
}

И затем inet_csk_reqsk_queue_added вызывает reqsk_queue_added с icsk_accept_queue:

static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
    reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}

Что увеличивает qlen (не sk_ack_backlog):

static inline void reqsk_queue_added(struct request_sock_queue *queue)
{
    atomic_inc(&queue->young);
    atomic_inc(&queue->qlen);
}

eha sh - это то место, где все Сокеты ESTABLISHED и TIMEWAIT были сохранены, а в последнее время в них хранится «очередь» SYN. ​​

Обратите внимание, что на самом деле нет никакой цели хранить поступившие запросы на соединение в надлежащей очереди. Их порядок не имеет значения (последние ACK могут поступать в любом порядке), и, перемещая их из прослушивающего сокета, нет необходимости блокировать прослушивающий сокет для обработки последнего ACK.

См. этот коммит для кода, который произвел это изменение.

Наконец, мы можем наблюдать, как запрос удаляется из eha sh и добавляется как полный сокет в очередь приема.

Последний ACK 3WH обрабатывается tcp_check_req, который создает полный дочерний сокет, а затем вызывает inet_csk_complete_hashdance:

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               bool fastopen, bool *req_stolen)
{

    [...]

    /* OK, ACK is valid, create big socket and
     * feed this segment to it. It will repeat all
     * the tests. THIS SEGMENT MUST MOVE SOCKET TO
     * ESTABLISHED STATE. If it will be dropped after
     * socket is created, wait for troubles.
     */
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
                             req, &own_req);

    [...]

    return inet_csk_complete_hashdance(sk, child, req, own_req);

    [...]

}

Затем inet_csk_complete_hashdance вызывает inet_csk_reqsk_queue_drop и reqsk_queue_removed на request и inet_csk_reqsk_queue_add для дочернего элемента:

struct sock *inet_csk_complete_hashdance(struct sock *sk, struct sock *child,
                     struct request_sock *req, bool own_req)
{
    if (own_req) {
        inet_csk_reqsk_queue_drop(sk, req);
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        if (inet_csk_reqsk_queue_add(sk, req, child))
            return child;
    }
    [...]
}

inet_csk_reqsk_queue_drop вызывает reqsk_queue_unlink, который удаляет запрос из eha sh, и reqsk_queue_removed, который уменьшает qlen:

void inet_csk_reqsk_queue_drop(struct sock *sk, struct request_sock *req)
{
    if (reqsk_queue_unlink(req)) {
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        reqsk_put(req);
    }
}

Наконец, inet_csk_reqsk_queue_add добавляет полный сокет в очередь приема.

struct sock *inet_csk_reqsk_queue_add(struct sock *sk,
                      struct request_sock *req,
                      struct sock *child)
{
    struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;

    spin_lock(&queue->rskq_lock);
    if (unlikely(sk->sk_state != TCP_LISTEN)) {
        inet_child_forget(sk, req, child);
        child = NULL;
    } else {
        req->sk = child;
        req->dl_next = NULL;
        if (queue->rskq_accept_head == NULL)
            WRITE_ONCE(queue->rskq_accept_head, req);
        else
            queue->rskq_accept_tail->dl_next = req;
        queue->rskq_accept_tail = req;
        sk_acceptq_added(sk);
    }
    spin_unlock(&queue->rskq_lock);
    return child;
}

TL; DR он находится в eha sh, а количество таких SYN составляет qlen (а не sk_ack_backlog, который содержит количество сокетов в очереди приема).

0 голосов
/ 09 августа 2020

Короткий ответ: очереди SYN опасны. Причина, по которой они опасны, заключается в том, что отправляя один пакет (SYN), отправитель может заставить получателя зафиксировать ресурсы (память для записи очереди SYN). если вы отправите достаточно таких пакетов достаточно быстро, возможно, с поддельным адресом отправителя, вы либо заставите получателя исчерпать ресурсы своей памяти, либо начнете отказываться принимать законные соединения.

По этим причинам современные операционные системы не поддерживают есть очередь SYN. Вместо этого они будут использовать различные методы (наиболее распространенные из них - файлы cookie SYN), которые позволят им иметь очередь только для тех соединений, которые уже ответили на начальный пакет SYN ACK и, таким образом, доказали, что они сами выделили ресурсы для этого соединения.

Итак, вы правы - нет очереди SYN. ​​

...