@cryptotaxi247 / netdata-1 / commits / 569734dda

Add SOCKS5 and SOCKS5H proxy support to ACLK (#21831)

* Add SOCKS5 and SOCKS5H proxy support to ACLK - Introduced support for SOCKS5 and SOCKS5H proxies in ACLK communication. - Enhanced proxy negotiation logic for more robust handling of HTTP and SOCKS proxy connections. - Improved memory handling for sensitive data like proxy credentials. - Updated related documentation with detailed explanations on proxy support and configuration. * - Simplified request headers by removing support for proxy authorization. - Eliminated redundant proxy-specific conditionals and payloads. * Address review - Replace `freez` with `aclk_sensitive_free` for proxy passwords. - Enhance SOCKS5/SOCKS5H proxy negotiation logic with proper cleanup handling. - Address scenarios with and without proxy credentials for HTTP proxy connections. - Update error codes and logs to better reflect proxy negotiation failures. - Improve documentation to clarify SOCKS5/SOCKS5H proxy support and security considerations. * - Update HTTP response validation logic and improve proxy error message formatting. * Improve HTTP and SOCKS5 proxy error handling and logging. * Refactor proxy negotiation logic and memory handling - Replace `freez` with `aclk_sensitive_free` for proxy credentials. - Update HTTP response handling to avoid over-reading into TLS handshake data. - Remove unused `HTTP_REQ_CONNECT` type and related logic. - Refactor proxy timeout calculations and error message formatting. - Set default `proxy_type` for direct connections in MQTT WSS clients. - Ensure proxy credentials are securely wiped after use.

Stelios Fragkakis committed Mar 10, 2026 at 21:58 UTC 569734dda91421af3861f673ca0895f4f93354b3
11 files changed +599 -269
src/aclk/aclk.c
+5 -9
@@ -770,7 +770,8 @@ static int aclk_attempt_to_connect(mqtt_wss_client client)
770 freez((char*)mqtt_conn_params.will_msg);
771 freez((char*)proxy_conf.host);
772 freez((char*)proxy_conf.username);
773 - freez((char*)proxy_conf.password);
773 + char *proxy_password = (char *)proxy_conf.password;
774 + aclk_sensitive_free(&proxy_password);
775
776 if (!mqtt_rc) {
777 last_conn_time_mqtt = now_realtime_sec();
@@ -842,14 +843,6 @@ void aclk_main(void *ptr)
843 worker_register_job_name(WORKER_ACLK_RECLAIM_MEMORY, "reclaim");
844 worker_register_job_name(WORKER_ACLK_BUFFER_COMPACT, "compact");
845
845 - ACLK_PROXY_TYPE proxy_type;
846 - aclk_get_proxy(&proxy_type, false);
847 - if (proxy_type == PROXY_TYPE_SOCKS5) {
848 - netdata_log_error("ACLK: SOCKS5 proxy is not supported by ACLK-NG yet.");
849 - static_thread->enabled = NETDATA_MAIN_THREAD_EXITED;
850 - return;
851 - }
852 -
846 aclk_init_rx_msg_handlers();
847
848 worker_is_busy(WORKER_ACLK_WAIT_CLAIMING);
@@ -1355,6 +1348,9 @@ void add_aclk_host_labels(void) {
1348 case PROXY_TYPE_SOCKS5:
1349 proxy_str = "SOCKS5";
1350 break;
1351 + case PROXY_TYPE_SOCKS5H:
1352 + proxy_str = "SOCKS5H";
1353 + break;
1354 case PROXY_TYPE_HTTP:
1355 proxy_str = "HTTP";
1356 break;
src/aclk/aclk_otp.c
+4 -2
@@ -17,18 +17,20 @@ static https_client_resp_t aclk_https_request(https_req_t *request, https_req_re
17 (char **)&proxy_conf.proxy_destination,
18 &proxy_conf.type);
19
20 - if (proxy_conf.type == MQTT_WSS_PROXY_HTTP) {
20 + if (proxy_conf.type != MQTT_WSS_DIRECT) {
21 request->proxy_host = (char *)proxy_conf.host;
22 request->proxy_port = proxy_conf.port;
23 request->proxy_username = proxy_conf.username;
24 request->proxy_password = proxy_conf.password;
25 request->proxy = proxy_conf.proxy_destination;
26 + request->proxy_type = proxy_conf.type;
27 }
28
29 rc = https_request(request, response, fallback_ipv4);
30 freez((char*)proxy_conf.host);
31 freez((char*)proxy_conf.username);
31 - freez((char*)proxy_conf.password);
32 + char *proxy_password = (char *)proxy_conf.password;
33 + aclk_sensitive_free(&proxy_password);
34 return rc;
35 }
36
src/aclk/aclk_proxy.c
+11 -11
@@ -8,7 +8,7 @@ struct {
8 const char *url_str;
9 } supported_proxy_types[] = {
10 { .type = PROXY_TYPE_SOCKS5, .url_str = "socks5" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
11 - { .type = PROXY_TYPE_SOCKS5, .url_str = "socks5h" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
11 + { .type = PROXY_TYPE_SOCKS5H, .url_str = "socks5h" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
12 { .type = PROXY_TYPE_HTTP, .url_str = "http" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
13 { .type = PROXY_TYPE_UNKNOWN, .url_str = NULL },
14 };
@@ -89,7 +89,7 @@ void aclk_proxy_get_display(char *buf, size_t buflen, const char *proxy, ACLK_PR
89
90 static const char *proxy_source = NULL;
91
92 -static inline int check_http_environment(const char **proxy)
92 +static inline int check_environment_proxy(const char **proxy, ACLK_PROXY_TYPE *type)
93 {
94 const char *var = "http_proxy";
95 char *tmp = getenv(var);
@@ -101,16 +101,18 @@ static inline int check_http_environment(const char **proxy)
101 return 1;
102 }
103
104 - if (aclk_verify_proxy(tmp) == PROXY_TYPE_HTTP) {
104 + *type = aclk_verify_proxy(tmp);
105 + if (*type == PROXY_TYPE_HTTP || *type == PROXY_TYPE_SOCKS5 || *type == PROXY_TYPE_SOCKS5H) {
106 *proxy = tmp;
107 char display[512];
107 - aclk_proxy_get_display(display, sizeof(display), tmp, PROXY_TYPE_HTTP);
108 + aclk_proxy_get_display(display, sizeof(display), tmp, *type);
109 char source_buf[256];
110 snprintfz(source_buf, sizeof(source_buf), "environment variable '%s'", var);
111 freez((void *)proxy_source);
112 proxy_source = strdupz(source_buf);
113 nd_log(NDLS_DAEMON, NDLP_INFO,
113 - "ACLK: using HTTP proxy %s (%s, from %s)",
114 + "ACLK: using %s proxy %s (%s, from %s)",
115 + *type == PROXY_TYPE_HTTP ? "HTTP" : (*type == PROXY_TYPE_SOCKS5H ? "SOCKS5H" : "SOCKS5"),
116 display, strchr(tmp, '@') ? "with credentials" : "without credentials", proxy_source);
117 return 0;
118 }
@@ -118,7 +120,7 @@ static inline int check_http_environment(const char **proxy)
120 char buf[1024];
121 snprintfz(buf, sizeof(buf),
122 "Environment var '%s' defined but of unknown format '%s'. "
121 - "Supported syntax: 'http://[user:pass@]host:port'.",
123 + "Supported syntax: 'http://[user:pass@]host:port' or 'socks5[h]://[user:pass@]host:port'.",
124 var, tmp);
125 safe_log_proxy_error(buf, tmp);
126
@@ -141,9 +143,7 @@ const char *aclk_lws_wss_get_proxy_setting(ACLK_PROXY_TYPE *type)
143 }
144
145 if (strcmp(proxy, ACLK_PROXY_ENV) == 0) {
144 - if (check_http_environment(&proxy) == 0)
145 - *type = PROXY_TYPE_HTTP;
146 - else {
146 + if (check_environment_proxy(&proxy, type) != 0) {
147 if (cloud_config_proxy_is_explicitly_set())
148 nd_log(NDLS_DAEMON, NDLP_WARNING,
149 "ACLK: proxy is explicitly set to 'env' but neither 'http_proxy' nor 'https_proxy'"
@@ -162,7 +162,7 @@ const char *aclk_lws_wss_get_proxy_setting(ACLK_PROXY_TYPE *type)
162 *type = PROXY_DISABLED;
163 safe_log_proxy_error(
164 "Config var \"" ACLK_PROXY_CONFIG_VAR
165 - "\" defined but of unknown format. Supported syntax: \"socks5[h]://[user:pass@]host:ip\".",
165 + "\" defined but of unknown format. Supported syntax: \"http://[user:pass@]host:port\" or \"socks5[h]://[user:pass@]host:port\".",
166 proxy);
167 freez((void *)proxy_source);
168 proxy_source = NULL;
@@ -175,7 +175,7 @@ const char *aclk_lws_wss_get_proxy_setting(ACLK_PROXY_TYPE *type)
175 aclk_proxy_get_display(display, sizeof(display), proxy, *type);
176 nd_log(NDLS_DAEMON, NDLP_INFO,
177 "ACLK: using %s proxy %s (%s, from %s)",
178 - *type == PROXY_TYPE_HTTP ? "HTTP" : "SOCKS5",
178 + *type == PROXY_TYPE_HTTP ? "HTTP" : (*type == PROXY_TYPE_SOCKS5H ? "SOCKS5H" : "SOCKS5"),
179 display,
180 strchr(proxy, '@') ? "with credentials" : "without credentials",
181 proxy_source);
src/aclk/aclk_proxy.h
+2
@@ -8,6 +8,7 @@
8 typedef enum aclk_proxy_type {
9 PROXY_TYPE_UNKNOWN = 0,
10 PROXY_TYPE_SOCKS5,
11 + PROXY_TYPE_SOCKS5H,
12 PROXY_TYPE_HTTP,
13 PROXY_DISABLED,
14 PROXY_NOT_SET,
@@ -25,6 +26,7 @@ static inline const char *aclk_proxy_type_to_url(ACLK_PROXY_TYPE type) {
26 switch (type) {
27 case PROXY_TYPE_HTTP: return "http://";
28 case PROXY_TYPE_SOCKS5: return "socks5://";
29 + case PROXY_TYPE_SOCKS5H: return "socks5h://";
30 default: return "";
31 }
32 }
src/aclk/aclk_util.c
+520 -13
@@ -10,6 +10,27 @@ usec_t aclk_session_newarch = 0;
10
11 aclk_env_t *aclk_env = NULL;
12
13 +void aclk_sensitive_memzero(void *ptr, size_t len)
14 +{
15 + if (!ptr || len == 0)
16 + return;
17 +
18 + volatile unsigned char *p = (volatile unsigned char *)ptr;
19 + while (len--)
20 + *p++ = 0;
21 +}
22 +
23 +void aclk_sensitive_free(char **ptr)
24 +{
25 + if (!ptr || !*ptr)
26 + return;
27 +
28 + size_t len = strlen(*ptr);
29 + aclk_sensitive_memzero(*ptr, len);
30 + freez(*ptr);
31 + *ptr = NULL;
32 +}
33 +
34 aclk_encoding_type_t aclk_encoding_type_t_from_str(const char *str) {
35 if (!strcmp(str, "json")) {
36 return ACLK_ENC_JSON;
@@ -386,6 +407,8 @@ static inline int aclk_parse_userpass_pair(const char *src, const char c, char *
407 }
408
409 #define HTTP_PROXY_PREFIX "http://"
410 +#define SOCKS5_PROXY_PREFIX "socks5://"
411 +#define SOCKS5H_PROXY_PREFIX "socks5h://"
412 void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
413 char **log_proxy, enum mqtt_wss_proxy_type *type)
414 {
@@ -393,8 +416,10 @@ void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
416 const char *ptr = aclk_get_proxy(&pt, false);
417 *log_proxy = (char *) aclk_get_proxy(&pt, true);
418 char *tmp;
419 + const char *prefix = NULL;
420 + int default_port = 0;
421
397 - if (pt != PROXY_TYPE_HTTP)
422 + if (pt != PROXY_TYPE_HTTP && pt != PROXY_TYPE_SOCKS5 && pt != PROXY_TYPE_SOCKS5H)
423 return;
424
425 *uname = NULL;
@@ -404,8 +429,32 @@ void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
429 char *proxy = strdupz(ptr);
430 ptr = proxy;
431
407 - if (!strncmp(ptr, HTTP_PROXY_PREFIX, strlen(HTTP_PROXY_PREFIX)))
408 - ptr += strlen(HTTP_PROXY_PREFIX);
432 + switch (pt) {
433 + case PROXY_TYPE_HTTP:
434 + prefix = HTTP_PROXY_PREFIX;
435 + default_port = 8080;
436 + if (type)
437 + *type = MQTT_WSS_PROXY_HTTP;
438 + break;
439 + case PROXY_TYPE_SOCKS5:
440 + prefix = SOCKS5_PROXY_PREFIX;
441 + default_port = 1080;
442 + if (type)
443 + *type = MQTT_WSS_PROXY_SOCKS5;
444 + break;
445 + case PROXY_TYPE_SOCKS5H:
446 + prefix = SOCKS5H_PROXY_PREFIX;
447 + default_port = 1080;
448 + if (type)
449 + *type = MQTT_WSS_PROXY_SOCKS5H;
450 + break;
451 + default:
452 + aclk_sensitive_free(&proxy);
453 + return;
454 + }
455 +
456 + if (!strncmp(ptr, prefix, strlen(prefix)))
457 + ptr += strlen(prefix);
458
459 if ((tmp = strchr(ptr, '@'))) {
460 *tmp = 0;
@@ -416,9 +465,9 @@ void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
465 }
466
467 if (!*ptr) {
419 - freez(proxy);
420 - freez(*uname);
421 - freez(*pwd);
468 + aclk_sensitive_free(&proxy);
469 + aclk_sensitive_free(uname);
470 + aclk_sensitive_free(pwd);
471 return;
472 }
473
@@ -431,14 +480,472 @@ void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
480 *ohost = strdupz(ptr);
481
482 if (*port <= 0 || *port > 65535)
434 - *port = 8080;
483 + *port = default_port;
484 +
485 + if (!type) {
486 + aclk_sensitive_free(uname);
487 + aclk_sensitive_free(pwd);
488 + }
489 +
490 + aclk_sensitive_free(&proxy);
491 +}
492 +
493 +enum mqtt_wss_proxy_type aclk_proxy_type_from_scheme(const char *proxy_url)
494 +{
495 + if (!proxy_url || !*proxy_url)
496 + return MQTT_WSS_DIRECT;
497 +
498 + if (!strncmp(proxy_url, SOCKS5H_PROXY_PREFIX, strlen(SOCKS5H_PROXY_PREFIX)))
499 + return MQTT_WSS_PROXY_SOCKS5H;
500 +
501 + if (!strncmp(proxy_url, SOCKS5_PROXY_PREFIX, strlen(SOCKS5_PROXY_PREFIX)))
502 + return MQTT_WSS_PROXY_SOCKS5;
503 +
504 + if (!strncmp(proxy_url, HTTP_PROXY_PREFIX, strlen(HTTP_PROXY_PREFIX)))
505 + return MQTT_WSS_PROXY_HTTP;
506 +
507 + return MQTT_WSS_DIRECT;
508 +}
509 +
510 +const char *aclk_mqtt_proxy_type_to_scheme(enum mqtt_wss_proxy_type type)
511 +{
512 + switch (type) {
513 + case MQTT_WSS_PROXY_HTTP:
514 + return HTTP_PROXY_PREFIX;
515 + case MQTT_WSS_PROXY_SOCKS5:
516 + return SOCKS5_PROXY_PREFIX;
517 + case MQTT_WSS_PROXY_SOCKS5H:
518 + return SOCKS5H_PROXY_PREFIX;
519 + default:
520 + return "";
521 + }
522 +}
523 +
524 +static int aclk_poll_for_io(int fd, short events, int timeout_ms)
525 +{
526 + struct pollfd pfd = {
527 + .fd = fd,
528 + .events = events,
529 + .revents = 0
530 + };
531 +
532 + int rc;
533 + do {
534 + rc = poll(&pfd, 1, timeout_ms);
535 + } while (rc < 0 && errno == EINTR);
536 +
537 + if (rc <= 0)
538 + return rc;
539 +
540 + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0)
541 + return -1;
542 +
543 + return 1;
544 +}
545 +
546 +static int aclk_timeout_remaining_ms(usec_t start, int timeout_ms)
547 +{
548 + if (timeout_ms <= 0)
549 + return 0;
550 +
551 + usec_t elapsed_ms = (now_monotonic_usec() - start) / USEC_PER_MS;
552 + if (elapsed_ms >= (usec_t)timeout_ms)
553 + return 0;
554 +
555 + return timeout_ms - (int)elapsed_ms;
556 +}
557 +
558 +static int aclk_write_all_timeout(int fd, const void *buf, size_t len, int timeout_ms)
559 +{
560 + size_t written = 0;
561 + usec_t start = now_monotonic_usec();
562 +
563 + while (written < len) {
564 + int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
565 + if (remaining_ms <= 0)
566 + return 1;
567 +
568 + int rc = aclk_poll_for_io(fd, POLLOUT, remaining_ms);
569 + if (rc <= 0)
570 + return 1;
571 +
572 + ssize_t n = write(fd, ((const uint8_t *)buf) + written, len - written);
573 + if (n < 0) {
574 + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
575 + if ((now_monotonic_usec() - start) / USEC_PER_MS > (usec_t)timeout_ms)
576 + return 1;
577 + continue;
578 + }
579 + return 1;
580 + }
581 + written += (size_t)n;
582 + }
583 +
584 + return 0;
585 +}
586 +
587 +static int aclk_read_exact_timeout(int fd, void *buf, size_t len, int timeout_ms)
588 +{
589 + size_t got = 0;
590 + usec_t start = now_monotonic_usec();
591 +
592 + while (got < len) {
593 + int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
594 + if (remaining_ms <= 0)
595 + return 1;
596 +
597 + int rc = aclk_poll_for_io(fd, POLLIN, remaining_ms);
598 + if (rc <= 0)
599 + return 1;
600 +
601 + ssize_t n = read(fd, ((uint8_t *)buf) + got, len - got);
602 + if (n == 0)
603 + return 1;
604 + if (n < 0) {
605 + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
606 + if ((now_monotonic_usec() - start) / USEC_PER_MS > (usec_t)timeout_ms)
607 + return 1;
608 + continue;
609 + }
610 + return 1;
611 + }
612 + got += (size_t)n;
613 + }
614 +
615 + return 0;
616 +}
617 +
618 +static int aclk_http_proxy_negotiate(int sockfd, const char *proxy_username, const char *proxy_password,
619 + const char *target_host, int target_port, int timeout_ms)
620 +{
621 + int result = 1;
622 + bool has_creds = (proxy_username && *proxy_username);
623 + usec_t start = now_monotonic_usec();
624 + char req[4096];
625 + size_t off = 0;
626 + int rc = snprintf(req + off, sizeof(req) - off, "CONNECT %s:%d HTTP/1.1\r\nHost: %s\r\n",
627 + target_host, target_port, target_host);
628 + if (rc < 0 || (size_t)rc >= sizeof(req) - off)
629 + goto cleanup;
630 + off += (size_t)rc;
631 +
632 + if (has_creds) {
633 + size_t pass_len = proxy_password ? strlen(proxy_password) : 0;
634 + size_t creds_plain_len = strlen(proxy_username) + pass_len + 1;
635 + char *creds_plain = callocz(1, creds_plain_len + 1);
636 + snprintfz(creds_plain, creds_plain_len + 1, "%s:%s", proxy_username, proxy_password ? proxy_password : "");
637 +
638 + int creds_base64_len = (((4 * (int)creds_plain_len / 3) + 3) & ~3);
639 + creds_base64_len += (1 + (creds_base64_len / 64)) * (int)strlen("\n");
640 + char *creds_base64 = callocz(1, (size_t)creds_base64_len + 1);
641 + (void)netdata_base64_encode((unsigned char *)creds_base64, (unsigned char *)creds_plain, creds_plain_len);
642 +
643 + rc = snprintf(req + off, sizeof(req) - off, "Proxy-Authorization: Basic %s\r\n", creds_base64);
644 + aclk_sensitive_free(&creds_plain);
645 + aclk_sensitive_free(&creds_base64);
646 + if (rc < 0 || (size_t)rc >= sizeof(req) - off)
647 + goto cleanup;
648 + off += (size_t)rc;
649 + }
650 +
651 + if (off + 2 >= sizeof(req))
652 + goto cleanup;
653 + req[off++] = '\r';
654 + req[off++] = '\n';
655 +
656 + if (aclk_write_all_timeout(sockfd, req, off, aclk_timeout_remaining_ms(start, timeout_ms)))
657 + goto cleanup;
658 +
659 + // Read the HTTP response one byte at a time to avoid over-reading
660 + // beyond the header terminator into TLS handshake data.
661 + char resp[4096];
662 + size_t used = 0;
663 + while (used < sizeof(resp) - 1) {
664 + int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
665 + if (remaining_ms <= 0)
666 + goto cleanup;
667 +
668 + int prc = aclk_poll_for_io(sockfd, POLLIN, remaining_ms);
669 + if (prc <= 0)
670 + goto cleanup;
671 +
672 + ssize_t n = read(sockfd, resp + used, 1);
673 + if (n == 0)
674 + goto cleanup;
675 + if (n < 0) {
676 + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
677 + continue;
678 + goto cleanup;
679 + }
680 + used += (size_t)n;
681 + resp[used] = '\0';
682 +
683 + // Check if we have received the complete header terminator
684 + if (used >= 4 && memcmp(resp + used - 4, "\r\n\r\n", 4) == 0)
685 + break;
686 + }
687 +
688 + if (used < 4 || memcmp(resp + used - 4, "\r\n\r\n", 4) != 0)
689 + goto cleanup;
690 +
691 + if (strncmp(resp, "HTTP/1.1 ", 9) != 0 && strncmp(resp, "HTTP/1.0 ", 9) != 0)
692 + goto cleanup;
693 +
694 + if (!isdigit((unsigned char)resp[9]) || !isdigit((unsigned char)resp[10]) || !isdigit((unsigned char)resp[11])) {
695 + netdata_log_error("ACLK: HTTP proxy response missing valid status code");
696 + goto cleanup;
697 + }
698 +
699 + int status = atoi(resp + 9);
700 + if (status == 200) {
701 + result = 0;
702 + } else {
703 + // extract the status line (first line) for logging
704 + char *eol = strstr(resp, "\r\n");
705 + if (eol)
706 + *eol = '\0';
707 + netdata_log_error("ACLK: HTTP proxy CONNECT to %s:%d failed with status %d: %s",
708 + target_host, target_port, status, resp);
709 + result = 1;
710 + }
711 +
712 +cleanup:
713 + if (has_creds)
714 + aclk_sensitive_memzero(req, sizeof(req));
715 + return result;
716 +}
717 +
718 +static int aclk_socks5_resolve_local(const char *host, uint8_t *atype, uint8_t *addr, size_t *addr_len)
719 +{
720 + struct in_addr ipv4;
721 + struct in6_addr ipv6;
722 + if (inet_pton(AF_INET, host, &ipv4) == 1) {
723 + *atype = 0x01;
724 + memcpy(addr, &ipv4, sizeof(ipv4));
725 + *addr_len = sizeof(ipv4);
726 + return 0;
727 + }
728 + if (inet_pton(AF_INET6, host, &ipv6) == 1) {
729 + *atype = 0x04;
730 + memcpy(addr, &ipv6, sizeof(ipv6));
731 + *addr_len = sizeof(ipv6);
732 + return 0;
733 + }
734 +
735 + struct addrinfo hints = {
736 + .ai_family = AF_UNSPEC,
737 + .ai_socktype = SOCK_STREAM,
738 + .ai_flags = AI_ADDRCONFIG
739 + };
740 + struct addrinfo *res = NULL;
741 + if (getaddrinfo(host, NULL, &hints, &res) != 0 || !res)
742 + return 1;
743 +
744 + int rc = 1;
745 +
746 + // Prefer IPv4 for compatibility with SOCKS proxies that don't accept ATYP=0x04 (IPv6).
747 + for (struct addrinfo *it = res; it; it = it->ai_next) {
748 + if (it->ai_family == AF_INET && it->ai_addrlen >= sizeof(struct sockaddr_in)) {
749 + struct sockaddr_in *sa = (struct sockaddr_in *)it->ai_addr;
750 + *atype = 0x01;
751 + memcpy(addr, &sa->sin_addr, sizeof(sa->sin_addr));
752 + *addr_len = sizeof(sa->sin_addr);
753 + rc = 0;
754 + break;
755 + }
756 + }
757 +
758 + if (rc != 0) {
759 + for (struct addrinfo *it = res; it; it = it->ai_next) {
760 + if (it->ai_family == AF_INET6 && it->ai_addrlen >= sizeof(struct sockaddr_in6)) {
761 + struct sockaddr_in6 *sa = (struct sockaddr_in6 *)it->ai_addr;
762 + *atype = 0x04;
763 + memcpy(addr, &sa->sin6_addr, sizeof(sa->sin6_addr));
764 + *addr_len = sizeof(sa->sin6_addr);
765 + rc = 0;
766 + break;
767 + }
768 + }
769 + }
770 +
771 + freeaddrinfo(res);
772 + return rc;
773 +}
774 +
775 +static int aclk_socks5_proxy_negotiate(int sockfd, enum mqtt_wss_proxy_type proxy_type,
776 + const char *proxy_username, const char *proxy_password,
777 + const char *target_host, int target_port, int timeout_ms)
778 +{
779 + usec_t start = now_monotonic_usec();
780 +
781 + uint8_t greeting[4] = { 0x05, 0x01, 0x00, 0x00 };
782 + size_t greeting_len = 3;
783 + if (proxy_username && *proxy_username) {
784 + greeting[1] = 0x02;
785 + greeting[2] = 0x00;
786 + greeting[3] = 0x02;
787 + greeting_len = 4;
788 + }
789 +
790 + if (aclk_write_all_timeout(sockfd, greeting, greeting_len, aclk_timeout_remaining_ms(start, timeout_ms))) {
791 + return 1;
792 + }
793 +
794 + uint8_t greeting_reply[2];
795 + if (aclk_read_exact_timeout(sockfd, greeting_reply, sizeof(greeting_reply), aclk_timeout_remaining_ms(start, timeout_ms))) {
796 + return 1;
797 + }
798 + if (greeting_reply[0] != 0x05 || greeting_reply[1] == 0xFF) {
799 + netdata_log_error("ACLK: SOCKS5 proxy rejected methods (ver=0x%02x method=0x%02x)",
800 + greeting_reply[0], greeting_reply[1]);
801 + return 1;
802 + }
803 +
804 + if (greeting_reply[1] == 0x02) {
805 + if (!proxy_username || !*proxy_username) {
806 + netdata_log_error("ACLK: SOCKS5 proxy requires username/password auth but credentials are missing");
807 + return 1;
808 + }
809 + size_t user_len = strlen(proxy_username);
810 + size_t pass_len = proxy_password ? strlen(proxy_password) : 0;
811 + if (user_len > UINT8_MAX || pass_len > UINT8_MAX) {
812 + netdata_log_error("ACLK: SOCKS5 credentials exceed protocol limits");
813 + return 1;
814 + }
815 +
816 + uint8_t auth_req[513];
817 + size_t pos = 0;
818 + auth_req[pos++] = 0x01;
819 + auth_req[pos++] = (uint8_t)user_len;
820 + memcpy(auth_req + pos, proxy_username, user_len);
821 + pos += user_len;
822 + auth_req[pos++] = (uint8_t)pass_len;
823 + if (pass_len) {
824 + memcpy(auth_req + pos, proxy_password, pass_len);
825 + pos += pass_len;
826 + }
827 +
828 + int auth_rc = aclk_write_all_timeout(sockfd, auth_req, pos, aclk_timeout_remaining_ms(start, timeout_ms));
829 + aclk_sensitive_memzero(auth_req, sizeof(auth_req));
830 + if (auth_rc) {
831 + return 1;
832 + }
833 +
834 + uint8_t auth_reply[2];
835 + if (aclk_read_exact_timeout(sockfd, auth_reply, sizeof(auth_reply), aclk_timeout_remaining_ms(start, timeout_ms))) {
836 + return 1;
837 + }
838 + if (auth_reply[0] != 0x01 || auth_reply[1] != 0x00) {
839 + netdata_log_error("ACLK: SOCKS5 auth failed (ver=0x%02x status=0x%02x)",
840 + auth_reply[0], auth_reply[1]);
841 + return 1;
842 + }
843 + } else if (greeting_reply[1] != 0x00) {
844 + netdata_log_error("ACLK: SOCKS5 proxy selected unsupported method 0x%02x", greeting_reply[1]);
845 + return 1;
846 + }
847 +
848 + uint8_t connect_req[300];
849 + size_t pos = 0;
850 + connect_req[pos++] = 0x05;
851 + connect_req[pos++] = 0x01;
852 + connect_req[pos++] = 0x00;
853 +
854 + if (proxy_type == MQTT_WSS_PROXY_SOCKS5H) {
855 + size_t host_len = strlen(target_host);
856 + if (host_len == 0 || host_len > UINT8_MAX) {
857 + netdata_log_error("ACLK: SOCKS5H target hostname length invalid (%zu)", host_len);
858 + return 1;
859 + }
860 + // 1 (atype) + 1 (length) + host_len + 2 (port)
861 + if (pos + 1 + 1 + host_len + 2 > sizeof(connect_req)) {
862 + netdata_log_error("ACLK: SOCKS5H CONNECT request too large for buffer");
863 + return 1;
864 + }
865 + connect_req[pos++] = 0x03;
866 + connect_req[pos++] = (uint8_t)host_len;
867 + memcpy(connect_req + pos, target_host, host_len);
868 + pos += host_len;
869 + } else {
870 + uint8_t atyp = 0;
871 + uint8_t addr[16];
872 + size_t addr_len = 0;
873 + if (aclk_socks5_resolve_local(target_host, &atyp, addr, &addr_len)) {
874 + netdata_log_error("ACLK: SOCKS5 local DNS resolution failed for target host '%s'", target_host);
875 + return 1;
876 + }
877 + // 1 (atype) + addr_len + 2 (port)
878 + if (pos + 1 + addr_len + 2 > sizeof(connect_req)) {
879 + netdata_log_error("ACLK: SOCKS5 CONNECT request too large for buffer");
880 + return 1;
881 + }
882 + connect_req[pos++] = atyp;
883 + memcpy(connect_req + pos, addr, addr_len);
884 + pos += addr_len;
885 + }
886 +
887 + connect_req[pos++] = (uint8_t)((target_port >> 8) & 0xFF);
888 + connect_req[pos++] = (uint8_t)(target_port & 0xFF);
889 +
890 + if (aclk_write_all_timeout(sockfd, connect_req, pos, aclk_timeout_remaining_ms(start, timeout_ms))) {
891 + return 1;
892 + }
893 +
894 + uint8_t reply_hdr[4];
895 + if (aclk_read_exact_timeout(sockfd, reply_hdr, sizeof(reply_hdr), aclk_timeout_remaining_ms(start, timeout_ms))) {
896 + return 1;
897 + }
898 + if (reply_hdr[0] != 0x05 || reply_hdr[1] != 0x00) {
899 + netdata_log_error("ACLK: SOCKS5 CONNECT failed (ver=0x%02x rep=0x%02x atyp=0x%02x)",
900 + reply_hdr[0], reply_hdr[1], reply_hdr[3]);
901 + return 1;
902 + }
903
436 - if (type)
437 - *type = MQTT_WSS_PROXY_HTTP;
438 - else {
439 - freez(*uname);
440 - freez(*pwd);
904 + size_t to_read = 0;
905 + switch (reply_hdr[3]) {
906 + case 0x01:
907 + to_read = 4 + 2;
908 + break;
909 + case 0x03: {
910 + uint8_t domain_len = 0;
911 + if (aclk_read_exact_timeout(sockfd, &domain_len, 1, aclk_timeout_remaining_ms(start, timeout_ms))) {
912 + return 1;
913 + }
914 + to_read = (size_t)domain_len + 2;
915 + break;
916 + }
917 + case 0x04:
918 + to_read = 16 + 2;
919 + break;
920 + default:
921 + netdata_log_error("ACLK: SOCKS5 CONNECT reply has invalid ATYP 0x%02x", reply_hdr[3]);
922 + return 1;
923 }
924
443 - freez(proxy);
925 + uint8_t discard[260];
926 + if (to_read > sizeof(discard)) {
927 + return 1;
928 + }
929 +
930 + if (aclk_read_exact_timeout(sockfd, discard, to_read, aclk_timeout_remaining_ms(start, timeout_ms))) {
931 + return 1;
932 + }
933 +
934 + return 0;
935 +}
936 +
937 +int aclk_proxy_negotiation_connect(int sockfd, enum mqtt_wss_proxy_type proxy_type,
938 + const char *proxy_username, const char *proxy_password,
939 + const char *target_host, int target_port, int timeout_ms)
940 +{
941 + if (proxy_type == MQTT_WSS_DIRECT)
942 + return 0;
943 +
944 + if (proxy_type == MQTT_WSS_PROXY_HTTP)
945 + return aclk_http_proxy_negotiate(sockfd, proxy_username, proxy_password, target_host, target_port, timeout_ms);
946 +
947 + if (proxy_type == MQTT_WSS_PROXY_SOCKS5 || proxy_type == MQTT_WSS_PROXY_SOCKS5H)
948 + return aclk_socks5_proxy_negotiate(sockfd, proxy_type, proxy_username, proxy_password, target_host, target_port, timeout_ms);
949 +
950 + return 1;
951 }
src/aclk/aclk_util.h
+7
@@ -108,5 +108,12 @@ unsigned long int aclk_tbeb_delay(int reset, int base, unsigned long int mins_ms
108
109 void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
110 char **log_proxy, enum mqtt_wss_proxy_type *type);
111 +enum mqtt_wss_proxy_type aclk_proxy_type_from_scheme(const char *proxy_url);
112 +const char *aclk_mqtt_proxy_type_to_scheme(enum mqtt_wss_proxy_type type);
113 +int aclk_proxy_negotiation_connect(int sockfd, enum mqtt_wss_proxy_type proxy_type,
114 + const char *proxy_username, const char *proxy_password,
115 + const char *target_host, int target_port, int timeout_ms);
116 +void aclk_sensitive_memzero(void *ptr, size_t len);
117 +void aclk_sensitive_free(char **ptr);
118
119 #endif /* ACLK_UTIL_H */
src/aclk/https_client.c
+21 -58
@@ -26,8 +26,8 @@ ENUM_STR_MAP_DEFINE(https_client_resp_t) = {
26 .name = "cannot set socket to non-blocking mode",
27 },
28 {
29 - .id = HTTPS_CLIENT_RESP_PROXY_NOT_200,
30 - .name = "proxy did not return http/200",
29 + .id = HTTPS_CLIENT_RESP_PROXY_NEGOTIATION_FAILED,
30 + .name = "proxy negotiation failed",
31 },
32 {
33 .id = HTTPS_CLIENT_RESP_NO_SSL_CTX,
@@ -138,8 +138,6 @@ static const char *http_req_type_to_str(http_req_type_t req) {
138 return "GET";
139 case HTTP_REQ_POST:
140 return "POST";
141 - case HTTP_REQ_CONNECT:
142 - return "CONNECT";
141 default:
142 return "unknown";
143 }
@@ -645,7 +643,6 @@ static https_client_resp_t read_parse_response(https_req_ctx_t *ctx) {
643 static const char *http_methods[] = {
644 [HTTP_REQ_GET] = "GET ",
645 [HTTP_REQ_POST] = "POST ",
648 - [HTTP_REQ_CONNECT] = "CONNECT ",
646 };
647
648
@@ -664,15 +661,8 @@ static https_client_resp_t handle_http_request(https_req_ctx_t *ctx) {
661 }
662 buffer_strcat(hdr, http_methods[req_type]);
663
667 - if (req_type == HTTP_REQ_CONNECT) {
668 - buffer_strcat(hdr, ctx->request->host);
669 - buffer_sprintf(hdr, ":%d", ctx->request->port);
670 - http_parse_ctx_create(&ctx->parse_ctx, HTTP_PARSE_PROXY_CONNECT);
671 - }
672 - else {
673 - buffer_strcat(hdr, ctx->request->url);
674 - http_parse_ctx_create(&ctx->parse_ctx, HTTP_PARSE_INITIAL);
675 - }
664 + buffer_strcat(hdr, ctx->request->url);
665 + http_parse_ctx_create(&ctx->parse_ctx, HTTP_PARSE_INITIAL);
666
667 buffer_strcat(hdr, HTTP_1_1 HTTP_ENDL);
668
@@ -682,25 +672,6 @@ static https_client_resp_t handle_http_request(https_req_ctx_t *ctx) {
672 if (req_type == HTTP_REQ_POST && ctx->request->payload && ctx->request->payload_size) {
673 buffer_sprintf(hdr, "Content-Length: %zu\x0D\x0A", ctx->request->payload_size);
674 }
685 - if (ctx->request->proxy_username) {
686 - size_t creds_plain_len = strlen(ctx->request->proxy_username) + strlen(ctx->request->proxy_password) + 1 /* ':' */;
687 - char *creds_plain = callocz(1, creds_plain_len + 1);
688 - char *ptr = creds_plain;
689 - strcpy(ptr, ctx->request->proxy_username);
690 - ptr += strlen(ctx->request->proxy_username);
691 - *ptr++ = ':';
692 - strcpy(ptr, ctx->request->proxy_password);
693 -
694 - int creds_base64_len = (((4 * creds_plain_len / 3) + 3) & ~3);
695 - // OpenSSL encoder puts newline every 64 output bytes
696 - // we remove those but during encoding we need that space in the buffer
697 - creds_base64_len += (1+(creds_base64_len/64)) * strlen("\n");
698 - char *creds_base64 = callocz(1, creds_base64_len + 1);
699 - (void) netdata_base64_encode((unsigned char *)creds_base64, (unsigned char *)creds_plain, creds_plain_len);
700 - buffer_sprintf(hdr, "Proxy-Authorization: Basic %s\x0D\x0A", creds_base64);
701 - freez(creds_plain);
702 - }
703 -
675 buffer_strcat(hdr, "\x0D\x0A");
676
677 // Send the request
@@ -827,35 +798,27 @@ https_client_resp_t https_request(https_req_t *request, https_req_response_t *re
798
799 ctx->poll_fd.fd = ctx->sock;
800
830 - // Do the CONNECT if proxy is used
801 + // Do proxy negotiation if proxy is used.
802 if (request->proxy_host) {
832 - https_req_t req = HTTPS_REQ_T_INITIALIZER;
833 - req.request_type = HTTP_REQ_CONNECT;
834 - req.timeout_s = request->timeout_s;
835 - req.host = request->host;
836 - req.port = request->port;
837 - req.url = request->url;
838 - req.proxy_username = request->proxy_username;
839 - req.proxy_password = request->proxy_password;
840 - ctx->request = &req;
841 - rc = handle_http_request(ctx);
842 - if (rc != HTTPS_CLIENT_RESP_OK) {
843 - netdata_log_error("ACLK: failed to CONNECT via proxy %s%s:%d to %s:%d",
844 - proxy_proto, request->proxy_host, request->proxy_port,
803 + enum mqtt_wss_proxy_type proxy_type = (enum mqtt_wss_proxy_type)request->proxy_type;
804 + if (proxy_type == MQTT_WSS_DIRECT)
805 + proxy_type = aclk_proxy_type_from_scheme(request->proxy);
806 + if (proxy_type == MQTT_WSS_DIRECT)
807 + proxy_type = MQTT_WSS_PROXY_HTTP;
808 +
809 + int proxy_timeout_ms = (request->timeout_s > 0 && request->timeout_s <= 2000000)
810 + ? (int)request->timeout_s * 1000
811 + : 30000;
812 +
813 + if (aclk_proxy_negotiation_connect(ctx->sock, proxy_type, request->proxy_username, request->proxy_password,
814 + request->host, request->port, proxy_timeout_ms)) {
815 + rc = HTTPS_CLIENT_RESP_PROXY_NEGOTIATION_FAILED;
816 + netdata_log_error("ACLK: %sproxy negotiation failed via %s:%d to %s:%d",
817 + aclk_mqtt_proxy_type_to_scheme(proxy_type),
818 + request->proxy_host, request->proxy_port,
819 request->host, request->port);
846 - http_parse_ctx_destroy(&ctx->parse_ctx);
820 goto exit_sock;
821 }
849 - if (ctx->parse_ctx.http_code != 200) {
850 - rc = HTTPS_CLIENT_RESP_PROXY_NOT_200;
851 - netdata_log_error("ACLK: proxy %s%s:%d returned HTTP %d (expected 200) for CONNECT to %s:%d",
852 - proxy_proto, request->proxy_host, request->proxy_port,
853 - ctx->parse_ctx.http_code,
854 - request->host, request->port);
855 - http_parse_ctx_destroy(&ctx->parse_ctx);
856 - goto exit_sock;
857 - }
858 - http_parse_ctx_destroy(&ctx->parse_ctx);
822 }
823 ctx->request = request;
824
src/aclk/https_client.h
+4 -3
@@ -13,7 +13,7 @@ typedef enum https_client_resp {
13 HTTPS_CLIENT_RESP_UNKNOWN_ERROR = ND_SOCK_ERR_MAX,
14 HTTPS_CLIENT_RESP_NO_MEM,
15 HTTPS_CLIENT_RESP_NONBLOCK_FAILED,
16 - HTTPS_CLIENT_RESP_PROXY_NOT_200,
16 + HTTPS_CLIENT_RESP_PROXY_NEGOTIATION_FAILED,
17 HTTPS_CLIENT_RESP_NO_SSL_CTX,
18 HTTPS_CLIENT_RESP_NO_SSL_VERIFY_PATHS,
19 HTTPS_CLIENT_RESP_NO_SSL_NEW,
@@ -48,7 +48,6 @@ ENUM_STR_DEFINE_FUNCTIONS_EXTERN(https_client_resp_t);
48 typedef enum http_req_type {
49 HTTP_REQ_GET = 0,
50 HTTP_REQ_POST,
51 - HTTP_REQ_CONNECT,
51 HTTP_REQ_INVALID
52 } http_req_type_t;
53
@@ -69,6 +68,7 @@ typedef struct {
68 const char *proxy_username;
69 const char *proxy_password;
70 const char *proxy;
71 + int proxy_type; // enum mqtt_wss_proxy_type (int avoids extra header coupling)
72 } https_req_t;
73
74 typedef struct {
@@ -113,7 +113,8 @@ void https_req_response_free(https_req_response_t *res);
113 .payload = NULL, \
114 .payload_size = 0, \
115 .proxy_host = NULL, \
116 - .proxy_port = 8080 \
116 + .proxy_port = 8080, \
117 + .proxy_type = 0 \
118 }
119
120 https_client_resp_t https_request(https_req_t *request, https_req_response_t *response, bool *fallback_ipv4);
src/aclk/mqtt_websockets/mqtt_wss_client.c
+12 -166
@@ -11,6 +11,7 @@
11 #include "ws_client.h"
12 #include "common_internal.h"
13 #include "../aclk.h"
14 +#include "../aclk_util.h"
15
16 #define PIPE_READ_END 0
17 #define PIPE_WRITE_END 1
@@ -217,7 +218,7 @@ void mqtt_wss_destroy(mqtt_wss_client client)
218 if (client->host)
219 freez(client->host);
220
220 - freez(client->proxy_passwd);
221 + aclk_sensitive_free(&client->proxy_passwd);
222 freez(client->proxy_uname);
223
224 if (client->ssl)
@@ -255,166 +256,6 @@ static int cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
256 return preverify_ok;
257 }
258
258 -#define PROXY_CONNECT "CONNECT"
259 -#define PROXY_HTTP "HTTP/1.1"
260 -#define PROXY_HTTP10 "HTTP/1.0"
261 -#define HTTP_ENDLINE "\x0D\x0A"
262 -#define HTTP_HDR_TERMINATOR "\x0D\x0A\x0D\x0A"
263 -#define HTTP_CODE_LEN 4
264 -#define HTTP_REASON_MAX_LEN 512
265 -static int http_parse_reply(rbuf_t buf)
266 -{
267 - char http_code_s[4];
268 - int idx;
269 -
270 - if (rbuf_memcmp_n(buf, PROXY_HTTP, strlen(PROXY_HTTP))) {
271 - if (rbuf_memcmp_n(buf, PROXY_HTTP10, strlen(PROXY_HTTP10))) {
272 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy expected reply with \"" PROXY_HTTP "\" or \"" PROXY_HTTP10 "\"");
273 - return 1;
274 - }
275 - }
276 -
277 - rbuf_bump_tail(buf, strlen(PROXY_HTTP));
278 -
279 - if (!rbuf_pop(buf, http_code_s, 1) || http_code_s[0] != 0x20) {
280 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy missing space after \"" PROXY_HTTP "\" or \"" PROXY_HTTP10 "\"");
281 - return 2;
282 - }
283 -
284 - if (!rbuf_pop(buf, http_code_s, HTTP_CODE_LEN)) {
285 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy missing HTTP code");
286 - return 3;
287 - }
288 -
289 - for (int i = 0; i < HTTP_CODE_LEN - 1; i++)
290 - if (http_code_s[i] > 0x39 || http_code_s[i] < 0x30) {
291 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy HTTP code non numeric");
292 - return 4;
293 - }
294 -
295 - http_code_s[HTTP_CODE_LEN - 1] = 0;
296 - int http_code = str2i(http_code_s);
297 -
298 - // TODO check if we ever have more headers here
299 - rbuf_find_bytes(buf, HTTP_ENDLINE, strlen(HTTP_ENDLINE), &idx);
300 - if (idx >= HTTP_REASON_MAX_LEN) {
301 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy returned reason that is too long");
302 - return 5;
303 - }
304 -
305 - if (http_code != 200) {
306 - char *ptr = mallocz(idx + 1);
307 - rbuf_pop(buf, ptr, idx);
308 - ptr[idx] = 0;
309 -
310 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy returned error code %d \"%s\"", http_code, ptr);
311 - freez(ptr);
312 - return 7;
313 - }/* else
314 - rbuf_bump_tail(buf, idx);*/
315 -
316 - rbuf_find_bytes(buf, HTTP_HDR_TERMINATOR, strlen(HTTP_HDR_TERMINATOR), &idx);
317 - if (idx)
318 - rbuf_bump_tail(buf, idx);
319 -
320 - rbuf_bump_tail(buf, strlen(HTTP_HDR_TERMINATOR));
321 -
322 - if (rbuf_bytes_available(buf)) {
323 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy unexpected trailing bytes after end of HTTP hdr");
324 - return 8;
325 - }
326 -
327 - nd_log(NDLS_DAEMON, NDLP_DEBUG, "http_proxy CONNECT succeeded");
328 - return 0;
329 -}
330 -
331 -static int http_proxy_connect(mqtt_wss_client client)
332 -{
333 - int rc;
334 - struct pollfd poll_fd;
335 - rbuf_t r_buf = rbuf_create(4096);
336 - if (!r_buf)
337 - return 1;
338 - size_t r_buf_linear_insert_capacity;
339 -
340 - poll_fd.fd = client->sockfd;
341 - poll_fd.events = POLLIN;
342 -
343 - char *r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
344 - snprintf(r_buf_ptr, r_buf_linear_insert_capacity,"%s %s:%d %s" HTTP_ENDLINE "Host: %s" HTTP_ENDLINE, PROXY_CONNECT,
345 - client->target_host, client->target_port, PROXY_HTTP, client->target_host);
346 -
347 - if(write(client->sockfd, r_buf_ptr, strlen(r_buf_ptr)) <= 0) { ; }
348 -
349 - if (client->proxy_uname) {
350 - size_t pass_len = client->proxy_passwd ? strlen(client->proxy_passwd) : 0;
351 - size_t creds_plain_len = strlen(client->proxy_uname) + pass_len + 2;
352 -
353 - char *creds_plain = mallocz(creds_plain_len);
354 - size_t creds_base64_len = (((4 * creds_plain_len / 3) + 3) & ~3);
355 - // OpenSSL encoder puts newline every 64 output bytes
356 - // we remove those but during encoding we need that space in the buffer
357 - creds_base64_len += (1 + (creds_base64_len / 64)) * strlen("\n");
358 -
359 - char *creds_base64 = mallocz(creds_base64_len + 1);
360 - char *ptr = creds_plain;
361 - strcpy(ptr, client->proxy_uname);
362 - ptr += strlen(client->proxy_uname);
363 - *ptr++ = ':';
364 - if (pass_len)
365 - strcpy(ptr, client->proxy_passwd);
366 -
367 - (void) netdata_base64_encode((unsigned char*)creds_base64, (unsigned char*)creds_plain, strlen(creds_plain));
368 - freez(creds_plain);
369 -
370 - r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
371 - snprintf(r_buf_ptr, r_buf_linear_insert_capacity,"Proxy-Authorization: Basic %s" HTTP_ENDLINE, creds_base64);
372 -
373 - if(write(client->sockfd, r_buf_ptr, strlen(r_buf_ptr)) <= 0) { ; }
374 -
375 - freez(creds_base64);
376 - }
377 - if(write(client->sockfd, HTTP_ENDLINE, strlen(HTTP_ENDLINE)) <= 0) { ; }
378 -
379 - // read until you find CRLF, CRLF (HTTP HDR end)
380 - // or ring buffer is full
381 - // or timeout
382 - while ((rc = poll(&poll_fd, 1, 1000)) >= 0) {
383 - if (!rc) {
384 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy timeout waiting reply from proxy server");
385 - rc = 2;
386 - goto cleanup;
387 - }
388 - r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
389 - if (!r_buf_ptr) {
390 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy read ring buffer full");
391 - rc = 3;
392 - goto cleanup;
393 - }
394 - if ((rc = read(client->sockfd, r_buf_ptr, r_buf_linear_insert_capacity)) < 0) {
395 - if (errno == EWOULDBLOCK || errno == EAGAIN) {
396 - continue;
397 - }
398 - nd_log(NDLS_DAEMON, NDLP_ERR, "http_proxy error reading from socket \"%s\"", strerror(errno));
399 - rc = 4;
400 - goto cleanup;
401 - }
402 - rbuf_bump_head(r_buf, rc);
403 - if (rbuf_find_bytes(r_buf, HTTP_HDR_TERMINATOR, strlen(HTTP_HDR_TERMINATOR), &rc)) {
404 - rc = 0;
405 - if (http_parse_reply(r_buf))
406 - rc = 5;
407 -
408 - goto cleanup;
409 - }
410 - }
411 - nd_log(NDLS_DAEMON, NDLP_ERR, "proxy negotiation poll error \"%s\"", strerror(errno));
412 - rc = 5;
413 -cleanup:
414 - rbuf_free(r_buf);
415 - return rc;
416 -}
417 -
259 int mqtt_wss_connect(
260 mqtt_wss_client client,
261 char *host,
@@ -450,8 +291,7 @@ int mqtt_wss_connect(
291 }
292
293 if (client->proxy_passwd) {
453 - freez(client->proxy_passwd);
454 - client->proxy_passwd = NULL;
294 + aclk_sensitive_free(&client->proxy_passwd);
295 }
296
297 if (proxy && proxy->type != MQTT_WSS_DIRECT) {
@@ -469,6 +309,7 @@ int mqtt_wss_connect(
309 client->port = port;
310 client->target_host = client->host;
311 client->target_port = port;
312 + client->proxy_type = MQTT_WSS_DIRECT;
313 }
314
315 client->ssl_flags = ssl_flags;
@@ -480,7 +321,7 @@ int mqtt_wss_connect(
321 snprintf(port_str, sizeof(port_str) -1, "%d", client->port);
322
323 if (proxy && proxy->type != MQTT_WSS_DIRECT) {
483 - const char *proxy_proto = (proxy->type == MQTT_WSS_PROXY_HTTP) ? "http://" : "socks5://";
324 + const char *proxy_proto = aclk_mqtt_proxy_type_to_scheme(proxy->type);
325 nd_log_daemon(NDLP_INFO, "ACLK: connecting to %s:%d via proxy %s%s:%d%s",
326 client->target_host, client->target_port,
327 proxy_proto, client->host, client->port,
@@ -517,10 +358,15 @@ int mqtt_wss_connect(
358 return -8;
359 }
360
520 - if (client->proxy_type != MQTT_WSS_DIRECT)
521 - if (http_proxy_connect(client))
361 + if (client->proxy_type != MQTT_WSS_DIRECT) {
362 + if (aclk_proxy_negotiation_connect(client->sockfd, client->proxy_type, client->proxy_uname, client->proxy_passwd,
363 + client->target_host, client->target_port, 10000))
364 return -4;
365
366 + // Credentials are only needed for proxy negotiation; wipe them now.
367 + aclk_sensitive_free(&client->proxy_passwd);
368 + }
369 +
370 #if OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110
371 #if (SSLEAY_VERSION_NUMBER >= OPENSSL_VERSION_097)
372 OPENSSL_config(NULL);
src/aclk/mqtt_websockets/mqtt_wss_client.h
+3 -1
@@ -86,7 +86,9 @@ struct mqtt_connect_params {
86
87 enum mqtt_wss_proxy_type {
88 MQTT_WSS_DIRECT = 0,
89 - MQTT_WSS_PROXY_HTTP
89 + MQTT_WSS_PROXY_HTTP,
90 + MQTT_WSS_PROXY_SOCKS5,
91 + MQTT_WSS_PROXY_SOCKS5H
92 };
93
94 struct mqtt_wss_proxy {
src/claim/README.md
+10 -6
@@ -93,26 +93,26 @@ You can set the `proxy` option at the `[global]` section in `claim.conf` to:
93 Netdata uses the `http_proxy` environment variable only when you set the `proxy` option to `env` (which is the default). You can set the `http_proxy` environment variable to:
94
95 - `http://[user:pass@]host:port`, to connect via an HTTP proxy
96 -- `socks5[h]://[user:pass@]host:port`, to connect via a SOCKS5 or SOCKS5h proxy
96 +- `socks5[h]://[user:pass@]host:port`, to connect via a SOCKS5 or SOCKS5H proxy
97
98 #### Proxy Security Considerations
99
100 :::note
101
102 -Netdata does not support secure connections to proxies. **Data between Netdata Agents and Netdata Cloud remains end-to-end encrypted** since the Agent requests a TCP tunnel (HTTP `CONNECT`) from the proxy and handles all encryption directly, however initial Agent-to-proxy communication is not encrypted.
102 +Netdata does not support secure connections to proxies. **Data between Netdata Agents and Netdata Cloud remains end-to-end encrypted** since the Agent establishes a TCP tunnel through the proxy (HTTP `CONNECT` for HTTP proxies, SOCKS5 `CONNECT` for SOCKS proxies) and handles all encryption directly, however initial Agent-to-proxy communication is not encrypted.
103
104 :::
105
106 **How End-to-End Encryption Works with Proxies:**
107
108 -1. **Proxy Connection**: The Agent connects to the HTTP proxy using a plain HTTP connection.
109 -2. **TCP Tunneling Request**: The Agent sends an HTTP CONNECT request to the proxy, asking it to establish a TCP tunnel to the Netdata Cloud server.
110 -3. **Proxy Tunneling**: Once the proxy accepts the CONNECT request (responds with HTTP 200), it creates a TCP tunnel between the Agent and the Netdata Cloud server. At this point, the proxy simply forwards raw TCP data in both directions without interpreting it.
108 +1. **Proxy Connection**: The Agent connects to the configured proxy using a plain TCP connection.
109 +2. **TCP Tunneling Request**: The Agent asks the proxy to establish a TCP tunnel to the Netdata Cloud server (HTTP `CONNECT` for HTTP proxy, SOCKS5 `CONNECT` for SOCKS5/SOCKS5H).
110 +3. **Proxy Tunneling**: Once accepted, the proxy forwards raw TCP data in both directions without interpreting application-layer traffic.
111 4. **Encrypted Communication**: The Agent then establishes a TLS/SSL connection through this tunnel directly with the Netdata Cloud server. All subsequent data (including the WebSocket handshake and MQTT protocol data) is encrypted end-to-end.
112
113 :::note
114
115 -The proxy only sees encrypted TLS traffic flowing through the tunnel it established, never the decrypted content. This standard method is called "TCP tunneling" or "HTTP CONNECT tunneling."
115 +The proxy only sees encrypted TLS traffic flowing through the tunnel it established, never the decrypted content.
116
117 :::
118
@@ -120,6 +120,10 @@ The proxy only sees encrypted TLS traffic flowing through the tunnel it establis
120
121 Netdata uses **two connection libraries**: **libcurl for claiming and MQTToWSoHTTPS for the actual Cloud connection**. While libcurl supports encrypted proxy connections, MQTToWSoHTTPS does not - so encrypted proxy connections will fail during the Cloud connection phase. The proxy configuration patterns above work for both libraries and provide end-to-end encryption for Netdata Cloud communication.
122
123 +For SOCKS proxies:
124 +- `socks5://` resolves target hostnames locally on the Agent and sends IP to the proxy.
125 +- `socks5h://` sends hostname to the proxy and resolves DNS remotely on the proxy side.
126 +
127 :::
128
129 ## Manage Connections