Coverage Report

Created: 2026-10-13 06:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/haproxy/src/proto_rhttp.c
Line
Count
Source
1
#include <stdio.h>
2
#include <string.h>
3
4
#include <haproxy/api.h>
5
#include <haproxy/connection.h>
6
#include <haproxy/errors.h>
7
#include <haproxy/intops.h>
8
#include <haproxy/list.h>
9
#include <haproxy/listener.h>
10
#include <haproxy/log.h>
11
#include <haproxy/proto_tcp.h>
12
#include <haproxy/protocol.h>
13
#include <haproxy/proxy.h>
14
#include <haproxy/sample.h>
15
#include <haproxy/server.h>
16
#include <haproxy/session.h>
17
#include <haproxy/sock.h>
18
#include <haproxy/ssl_sock.h>
19
#include <haproxy/task.h>
20
21
#include <haproxy/proto_rhttp.h>
22
23
struct proto_fam proto_fam_rhttp = {
24
  .name = "rhttp",
25
  .sock_domain = AF_INET,
26
  .sock_family = AF_CUST_RHTTP_SRV,
27
  .real_family = AF_CUST_RHTTP_SRV,
28
  .bind = rhttp_bind_receiver,
29
};
30
31
struct protocol proto_rhttp = {
32
  .name = "rev",
33
34
  /* connection layer (no outgoing connection) */
35
  .listen      = rhttp_bind_listener,
36
  .enable      = rhttp_enable_listener,
37
  .disable     = rhttp_disable_listener,
38
  .suspend     = rhttp_suspend_listener,
39
  .add         = default_add_listener,
40
  .unbind      = rhttp_unbind_receiver,
41
  .resume      = default_resume_listener,
42
  .accept_conn = rhttp_accept_conn,
43
  .bind_tid_prep = rhttp_bind_tid_prep,
44
45
  /* address family */
46
  .fam  = &proto_fam_rhttp,
47
48
  /* socket layer */
49
  .proto_type     = PROTO_TYPE_STREAM,
50
  .sock_type      = SOCK_STREAM,
51
  .sock_prot      = IPPROTO_TCP,
52
  .rx_listening   = rhttp_accepting_conn,
53
  .receivers      = LIST_HEAD_INIT(proto_rhttp.receivers),
54
};
55
56
static struct connection *new_reverse_conn(struct listener *l, struct server *srv)
57
0
{
58
0
  struct connection *conn = conn_new(srv);
59
0
  struct sockaddr_storage *bind_addr = NULL;
60
0
  struct session *sess = NULL;
61
0
  if (!conn)
62
0
    goto err;
63
64
0
  HA_ATOMIC_INC(&th_ctx->nb_rhttp_conns);
65
66
  /* session origin is only set after reversal. This ensures fetches
67
   * will be functional only after reversal, in particular src/dst.
68
   */
69
0
  sess = session_new(l->bind_conf->frontend, l, NULL);
70
0
  if (!sess)
71
0
    goto err;
72
73
0
  conn_set_owner(conn, sess, conn_session_free);
74
0
  conn_set_reverse(conn, &l->obj_type);
75
76
0
  if (alloc_bind_address(&bind_addr, srv, srv->proxy, NULL) != SRV_STATUS_OK)
77
0
    goto err;
78
0
  conn->src = bind_addr;
79
80
0
  sockaddr_alloc(&conn->dst, 0, 0);
81
0
  if (!conn->dst)
82
0
    goto err;
83
0
  *conn->dst = srv->addr;
84
0
  set_host_port(conn->dst, srv->svc_port);
85
86
0
  conn->send_proxy_ofs = 0;
87
0
  if (srv->pp_opts & SRV_PP_ENABLED) {
88
0
    conn->flags |= CO_FL_SEND_PROXY;
89
0
    conn->send_proxy_ofs = 1; /* must compute size */
90
0
  }
91
92
  /* TODO support SOCKS4 */
93
94
0
  if (conn_prepare(conn, protocol_lookup(conn->dst->ss_family, PROTO_TYPE_STREAM, 0), srv->xprt))
95
0
    goto err;
96
97
0
  if (conn->ctrl->connect(conn, 0) != SF_ERR_NONE)
98
0
    goto err;
99
100
#ifdef USE_OPENSSL
101
  if (conn_is_ssl(conn) && srv->ssl_ctx.sni) {
102
    struct sample *sni_smp = NULL;
103
    /* TODO remove NULL session which can cause crash depending on the SNI sample expr used. */
104
    sni_smp = sample_fetch_as_type(srv->proxy, sess, NULL,
105
                                   SMP_OPT_DIR_REQ | SMP_OPT_FINAL,
106
                                   srv->ssl_ctx.sni, SMP_T_STR);
107
    if (smp_make_safe(sni_smp))
108
      ssl_sock_set_servername(conn, sni_smp->data.u.str.area);
109
  }
110
#endif /* USE_OPENSSL */
111
112
  /* The CO_FL_SEND_PROXY flag may have been set by the connect method,
113
   * if so, add our handshake pseudo-XPRT now.
114
   */
115
0
  if (conn->flags & CO_FL_HANDSHAKE) {
116
0
    if (xprt_add_hs(conn) < 0)
117
0
      goto err;
118
0
  }
119
120
0
  if (conn_xprt_start(conn) < 0)
121
0
    goto err;
122
123
0
  if (!srv->use_ssl ||
124
0
      (!srv->ssl_ctx.alpn_str && !srv->ssl_ctx.npn_str) ||
125
0
      srv->mux_proto) {
126
0
    if (conn_install_mux_be(conn, NULL, sess, NULL) < 0)
127
0
      goto err;
128
0
  }
129
130
0
  return conn;
131
132
0
 err:
133
0
  if (l->rx.rhttp.state != LI_PRECONN_ST_ERR) {
134
0
    send_log(l->bind_conf->frontend, LOG_ERR,
135
0
             "preconnect %s::%s: Error on conn allocation.\n",
136
0
             l->bind_conf->frontend->id, l->bind_conf->rhttp_srvname);
137
0
    l->rx.rhttp.state = LI_PRECONN_ST_ERR;
138
0
  }
139
140
  /* No need to free session as conn.destroy_cb will take care of it. */
141
0
  if (conn) {
142
0
    conn_stop_tracking(conn);
143
0
    conn_xprt_shutw(conn);
144
0
    conn_xprt_close(conn);
145
0
    conn_sock_shutw(conn, 0);
146
0
    conn_ctrl_close(conn);
147
148
0
    if (conn->destroy_cb)
149
0
      conn->destroy_cb(conn);
150
151
    /* Mark connection as non-reversable. This prevents conn_free()
152
     * to reschedule rhttp task on freeing a preconnect connection.
153
     */
154
0
    conn->reverse.target = NULL;
155
0
    conn_free(conn);
156
0
  }
157
158
0
  return NULL;
159
0
}
160
161
/* Report that a connection used for preconnect on listener <l> is freed before
162
 * reversal is completed. This is used to cleanup any reference to the
163
 * connection and rearm a new preconnect attempt.
164
 */
165
void rhttp_notify_preconn_err(struct listener *l)
166
0
{
167
  /* Receiver must reference a reverse connection as pending. */
168
0
  BUG_ON(!l->rx.rhttp.pend_conn);
169
170
  /* Remove reference to the freed connection. */
171
0
  l->rx.rhttp.pend_conn = NULL;
172
173
0
  if (l->rx.rhttp.state != LI_PRECONN_ST_ERR) {
174
0
    send_log(l->bind_conf->frontend, LOG_ERR,
175
0
            "preconnect %s::%s: Error encountered.\n",
176
0
             l->bind_conf->frontend->id, l->bind_conf->rhttp_srvname);
177
0
    l->rx.rhttp.state = LI_PRECONN_ST_ERR;
178
0
  }
179
180
  /* Rearm a new preconnect attempt. */
181
0
  l->rx.rhttp.task->expire = MS_TO_TICKS(now_ms + 1000);
182
0
  task_queue(l->rx.rhttp.task);
183
0
}
184
185
/* Lookup over listener <l> threads for their current count of active reverse
186
 * HTTP connections. Returns the less loaded thread ID.
187
 */
188
static unsigned int select_thread(struct listener *l)
189
0
{
190
0
  unsigned long mask = l->rx.bind_thread & _HA_ATOMIC_LOAD(&tg->threads_enabled);
191
0
  unsigned int load_min = HA_ATOMIC_LOAD(&th_ctx->nb_rhttp_conns);
192
0
  unsigned int load_thr;
193
0
  unsigned int ret = tid;
194
0
  int i;
195
196
  /* Returns current tid if listener runs on one thread only. */
197
0
  if (!atleast2(mask))
198
0
    goto end;
199
200
  /* Loop over all threads and return the less loaded one. This needs to
201
   * be just an approximation so it's not important if the selected
202
   * thread load has varied since its selection.
203
   */
204
205
0
  for (i = tg->base; mask; mask >>= 1, i++) {
206
0
    if (!(mask & 0x1))
207
0
      continue;
208
209
0
    load_thr = HA_ATOMIC_LOAD(&ha_thread_ctx[i].nb_rhttp_conns);
210
0
    if (load_min > load_thr) {
211
0
      ret = i;
212
0
      load_min = load_thr;
213
0
    }
214
0
  }
215
216
0
 end:
217
0
  return ret;
218
0
}
219
220
/* Detach <task> from its thread and assign it to <new_tid> thread. The task is
221
 * queued to be woken up on the new thread.
222
 */
223
static void task_migrate(struct task *task, uint new_tid)
224
0
{
225
0
  task_unlink_wq(task);
226
0
  task->expire = TICK_ETERNITY;
227
0
  task_set_thread(task, new_tid);
228
0
  task_wakeup(task, TASK_WOKEN_MSG);
229
0
}
230
231
struct task *rhttp_process(struct task *task, void *ctx, unsigned int state)
232
0
{
233
0
  struct listener *l = ctx;
234
0
  struct connection *conn = l->rx.rhttp.pend_conn;
235
236
0
  if (conn) {
237
    /* Either connection is on error ot the connect timeout fired. */
238
0
    if (conn->flags & CO_FL_ERROR || tick_is_expired(task->expire, now_ms)) {
239
      /* If mux already instantiated, let it release the
240
       * connection along with its context. Else do cleanup
241
       * directly.
242
       */
243
0
      if (conn->mux && conn->mux->destroy) {
244
0
        CALL_MUX_NO_RET(conn->mux, destroy(conn->ctx));
245
0
      }
246
0
      else {
247
0
        conn_stop_tracking(conn);
248
0
        conn_xprt_shutw(conn);
249
0
        conn_xprt_close(conn);
250
0
        conn_sock_shutw(conn, 0);
251
0
        conn_ctrl_close(conn);
252
253
0
        if (conn->destroy_cb)
254
0
          conn->destroy_cb(conn);
255
0
        conn_free(conn);
256
0
      }
257
258
      /* conn_free() must report preconnect failure using rhttp_notify_preconn_err(). */
259
0
      BUG_ON(l->rx.rhttp.pend_conn);
260
261
0
      l->rx.rhttp.task->expire = TICKS_TO_MS(now_ms);
262
0
    }
263
0
    else {
264
      /* Spurious receiver task woken up despite pend_conn not ready/on error. */
265
0
      BUG_ON(!(conn->flags & CO_FL_ACT_REVERSING));
266
267
      /* A connection is ready to be accepted. */
268
0
      listener_accept(l);
269
0
      l->rx.rhttp.task->expire = TICK_ETERNITY;
270
0
    }
271
0
  }
272
0
  else {
273
0
    struct server *srv = l->rx.rhttp.srv;
274
275
0
    if ((state & TASK_WOKEN_ANY) != TASK_WOKEN_MSG) {
276
0
      unsigned int new_tid = select_thread(l);
277
0
      if (new_tid != tid) {
278
0
        task_migrate(l->rx.rhttp.task, new_tid);
279
0
        return task;
280
0
      }
281
0
    }
282
283
    /* No pending reverse connection, prepare a new one. Store it in the
284
     * listener and return NULL. Connection will be returned later after
285
     * reversal is completed.
286
     */
287
0
    conn = new_reverse_conn(l, srv);
288
0
    l->rx.rhttp.pend_conn = conn;
289
290
    /* On success task will be woken up by H2 mux after reversal. */
291
0
    l->rx.rhttp.task->expire = conn && tick_isset(srv->proxy->timeout.connect) ?
292
0
      tick_add_ifset(now_ms, srv->proxy->timeout.connect) :
293
0
      MS_TO_TICKS(now_ms + 1000);
294
0
  }
295
296
0
  return task;
297
0
}
298
299
int rhttp_bind_receiver(struct receiver *rx, char **errmsg)
300
0
{
301
0
  rx->flags |= RX_F_BOUND;
302
0
  return ERR_NONE;
303
0
}
304
305
int rhttp_bind_listener(struct listener *listener, char *errmsg, int errlen)
306
0
{
307
0
  struct task *task;
308
0
  struct proxy *be;
309
0
  struct server *srv;
310
0
  struct ist be_name, sv_name;
311
0
  char *name = NULL;
312
313
0
  unsigned long mask;
314
0
  uint task_tid;
315
316
0
  if (listener->state != LI_ASSIGNED)
317
0
    return ERR_NONE; /* already bound */
318
319
  /* Retrieve the first thread usable for this listener. */
320
0
  mask = listener->rx.bind_thread & _HA_ATOMIC_LOAD(&tg->threads_enabled);
321
0
  task_tid = my_ffsl(mask) - 1 + ha_tgroup_info[listener->rx.bind_tgroup].base;
322
0
  if (!(task = task_new_on(task_tid))) {
323
0
    snprintf(errmsg, errlen, "Out of memory.");
324
0
    goto err;
325
0
  }
326
327
0
  task->process = rhttp_process;
328
0
  task->context = listener;
329
0
  listener->rx.rhttp.task = task;
330
0
  listener->rx.rhttp.state = LI_PRECONN_ST_STOP;
331
332
  /* Set maxconn which is defined via the special kw nbconn for reverse
333
   * connect. Use a default value of 1 if not set. This guarantees that
334
   * listener will be automatically re-enable each time it fell back below
335
   * it due to a connection error.
336
   */
337
0
  listener->bind_conf->maxconn = listener->bind_conf->rhttp_nbconn;
338
0
  if (!listener->bind_conf->maxconn)
339
0
    listener->bind_conf->maxconn = 1;
340
341
0
  name = strdup(listener->bind_conf->rhttp_srvname);
342
0
  if (!name) {
343
0
    snprintf(errmsg, errlen, "Out of memory.");
344
0
    goto err;
345
0
  }
346
347
0
  sv_name = ist(name);
348
0
  be_name = istsplit(&sv_name, '/');
349
0
  if (!istlen(sv_name)) {
350
0
    snprintf(errmsg, errlen, "Invalid server name: '%s'.", name);
351
0
    goto err;
352
0
  }
353
354
0
  if (!(be = proxy_be_by_name(ist0(be_name)))) {
355
0
    snprintf(errmsg, errlen, "No such backend: '%s'.", name);
356
0
    goto err;
357
0
  }
358
0
  if (!(srv = server_find(be, ist0(sv_name)))) {
359
0
    snprintf(errmsg, errlen, "No such server: '%s/%s'.", ist0(be_name), ist0(sv_name));
360
0
    goto err;
361
0
  }
362
363
0
  if (srv->flags & SRV_F_RHTTP) {
364
0
    snprintf(errmsg, errlen, "Cannot use reverse HTTP server '%s/%s' as target to a reverse bind.", ist0(be_name), ist0(sv_name));
365
0
    goto err;
366
0
  }
367
368
0
  if (srv_is_transparent(srv)) {
369
0
    snprintf(errmsg, errlen, "Cannot use transparent server '%s/%s' as target to a reverse bind.", ist0(be_name), ist0(sv_name));
370
0
    goto err;
371
0
  }
372
373
  /* Check that server uses HTTP/2 either with proto or ALPN. */
374
0
  if ((!srv->mux_proto || !isteqi(srv->mux_proto->mux_proto, ist("h2"))) &&
375
0
      (!srv->use_ssl || !isteqi(ist(srv->ssl_ctx.alpn_str), ist("\x02h2")))) {
376
0
    snprintf(errmsg, errlen, "Cannot reverse connect with server '%s/%s' unless HTTP/2 is activated on it with either proto or alpn keyword.", name, ist0(sv_name));
377
0
    goto err;
378
0
  }
379
380
  /* Prevent dynamic source address settings. */
381
0
  if (((srv->conn_src.opts & CO_SRC_TPROXY_MASK) &&
382
0
       (srv->conn_src.opts & CO_SRC_TPROXY_MASK) != CO_SRC_TPROXY_ADDR) ||
383
0
      ((srv->proxy->conn_src.opts & CO_SRC_TPROXY_MASK) &&
384
0
       (srv->proxy->conn_src.opts & CO_SRC_TPROXY_MASK) != CO_SRC_TPROXY_ADDR)) {
385
0
    snprintf(errmsg, errlen, "Cannot reverse connect with server '%s/%s' which uses dynamic source address setting.", name, ist0(sv_name));
386
0
    goto err;
387
0
  }
388
389
0
  ha_free(&name);
390
391
0
  listener->rx.rhttp.srv = srv;
392
0
  listener_set_state(listener, LI_LISTEN);
393
394
0
  return ERR_NONE;
395
396
0
 err:
397
0
  ha_free(&name);
398
0
  return ERR_ALERT | ERR_FATAL;
399
0
}
400
401
/* Do not support "disable frontend" for rhttp protocol. */
402
int rhttp_suspend_listener(struct listener *l)
403
0
{
404
0
  send_log(l->bind_conf->frontend, LOG_ERR, "cannot disable a reverse-HTTP listener.\n");
405
0
  return -1;
406
0
}
407
408
void rhttp_enable_listener(struct listener *l)
409
0
{
410
0
  if (l->rx.rhttp.state < LI_PRECONN_ST_INIT) {
411
0
    send_log(l->bind_conf->frontend, LOG_INFO,
412
0
             "preconnect %s::%s: Initiating.\n",
413
0
             l->bind_conf->frontend->id, l->bind_conf->rhttp_srvname);
414
0
    l->rx.rhttp.state = LI_PRECONN_ST_INIT;
415
0
  }
416
417
0
  task_wakeup(l->rx.rhttp.task, TASK_WOKEN_INIT);
418
0
}
419
420
void rhttp_disable_listener(struct listener *l)
421
0
{
422
0
  if (l->rx.rhttp.state < LI_PRECONN_ST_FULL) {
423
0
    send_log(l->bind_conf->frontend, LOG_INFO,
424
0
             "preconnect %s::%s: Running with nbconn %d reached.\n",
425
0
             l->bind_conf->frontend->id, l->bind_conf->rhttp_srvname,
426
0
             l->bind_conf->maxconn);
427
0
    l->rx.rhttp.state = LI_PRECONN_ST_FULL;
428
0
  }
429
0
}
430
431
struct connection *rhttp_accept_conn(struct listener *l, int *status)
432
0
{
433
0
  struct connection *conn = l->rx.rhttp.pend_conn;
434
435
0
  if (!conn) {
436
    /* Reverse connect listener must have an explicit maxconn set
437
     * to ensure it is re-enabled on connection error.
438
     */
439
0
    BUG_ON(!l->bind_conf->maxconn);
440
441
    /* Instantiate a new conn if maxconn not yet exceeded. */
442
0
    if (l->nbconn <= l->bind_conf->maxconn) {
443
      /* Try first if a new thread should be used for the new connection. */
444
0
      unsigned int new_tid = select_thread(l);
445
0
      if (new_tid != tid) {
446
0
        task_migrate(l->rx.rhttp.task, new_tid);
447
0
        *status = CO_AC_DONE;
448
0
        return NULL;
449
0
      }
450
451
      /* No need to use a new thread, use the opportunity to alloc the connection right now. */
452
0
      l->rx.rhttp.pend_conn = new_reverse_conn(l, l->rx.rhttp.srv);
453
0
      if (!l->rx.rhttp.pend_conn) {
454
0
        *status = CO_AC_PAUSE;
455
0
        return NULL;
456
0
      }
457
0
    }
458
459
0
    *status = CO_AC_DONE;
460
0
    return NULL;
461
0
  }
462
463
  /* listener_accept() must not be called if no pending connection is not yet reversed. */
464
0
  BUG_ON(!(conn->flags & CO_FL_ACT_REVERSING));
465
0
  conn->flags &= ~CO_FL_ACT_REVERSING;
466
0
  conn->flags |= CO_FL_REVERSED;
467
0
  CALL_MUX_NO_RET(conn->mux, ctl(conn, MUX_CTL_REVERSE_CONN, NULL));
468
469
0
  l->rx.rhttp.pend_conn = NULL;
470
0
  *status = CO_AC_NONE;
471
472
0
  return conn;
473
0
}
474
475
void rhttp_unbind_receiver(struct listener *l)
476
0
{
477
0
  l->rx.flags &= ~RX_F_BOUND;
478
0
}
479
480
int rhttp_bind_tid_prep(struct connection *conn, int new_tid)
481
0
{
482
  /* Explicitly disable connection thread migration on accept. Indeed,
483
   * it's unsafe to move a connection with its FD to another thread. Note
484
   * that active reverse task thread migration should be sufficient to
485
   * ensure repartition of reversed connections across listener threads.
486
   */
487
0
  return -1;
488
0
}
489
490
int rhttp_accepting_conn(const struct receiver *rx)
491
0
{
492
0
  return 1;
493
0
}
494
495
INITCALL1(STG_REGISTER, protocol_register, &proto_rhttp);
496
497
/* perform minimal initializations */
498
static void init_rhttp()
499
0
{
500
0
  int i;
501
502
0
  for (i = 0; i < MAX_THREADS; i++)
503
0
    ha_thread_ctx[i].nb_rhttp_conns = 0;
504
0
}
505
506
INITCALL0(STG_PREPARE, init_rhttp);