From 8b51cb2ede10a1779cce18b3aede7092538104b1 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 14 Feb 2022 02:36:57 -0800 Subject: [PATCH 2/4] [POC] Support SOCKS resolve extension This change provides an alternate means of resolving host names for users running a local Tor daemon. Users can avoid appealing to the glibc getaddrinfo suite and, by extension, the system itself, e.g., systemd-resolve, nss-dns, etc., for name resolution. * lisp/net/socks.el (socks-resolve-command): Add new constant for the RESOLVE command, a nonstandard SOCKS extension from the Tor project. It mirrors CONNECT in most respects but asks the server to RESOLVE a host name and return its IP. For details, see doc/socks/socks-extensions.txt in the source tree for torsocks. This shouldn't be confused with 5h/5-hostname, which is used by clients like cURL to allow users to bypass attempts to resolve a name locally. (socks--convert-address, socks-tor-resolve): Add utility functions to query a SOCKS service supporting the RESOLVE extension. * test/lisp/net/socks-tests.el (tor-resolve-4a, tor-resolve-4a-fail) (tor-resolve-5-fail, tor-resolve-5, tor-resolve-4a-fail/ipv6) (tor-resolve-5/ipv6): New tests. (Bug#53941) --- lisp/net/socks.el | 83 ++++++++++++++++++++++++++ test/lisp/net/socks-tests.el | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/lisp/net/socks.el b/lisp/net/socks.el index ecbac7e2345..dad47f9c660 100644 --- a/lisp/net/socks.el +++ b/lisp/net/socks.el @@ -181,6 +181,9 @@ socks-udp-associate-command (defconst socks-authentication-null 0) (defconst socks-authentication-failure 255) +;; Extensions +(defconst socks-resolve-command #xf0) + ;; Response codes (defconst socks-response-success 0) (defconst socks-response-general-failure 1) @@ -655,6 +658,86 @@ socks-nslookup-host res) host)) +(defun socks--extract-resolve-response (proc) + "Parse response for PROC and maybe return destination IP address." + (when-let ((response (process-get proc 'socks-response))) + (pcase (process-get proc 'socks-server-protocol) + (4 ; https://www.openssh.com/txt/socks4a.protocol + ;; In the latest Tor version (0.4.8.12 in Sept 2024), a query + ;; that normally returns an IPv6 address gives a 91 error. + (and-let* (((zerop (process-get proc 'socks-reply))) + ((eq (aref response 1) 90)) ; #x5a request granted + (a (substring response 4)) ; ignore port for now + ((not (string-empty-p a))) + ((not (string= a "\0\0\0\0")))) + a)) + (5 ; https://tools.ietf.org/html/rfc1928 + (cl-assert (eq 5 (aref response 0)) t) + (pcase (aref response 3) ; ATYP + (1 (and-let* ((a (substring response 4 8)) + ((not (string= a "\0\0\0\0"))) + a))) + ;; No reason to support RESOLVE_PTR [F1] extension, right? + (3 (let ((len (1- (aref response 4)))) + (substring response 5 (+ 5 len)))) + (4 (socks--convert-address (substring response 4 20)))))))) + +(defun socks--convert-address (input) + "Unpack pairs of bytes into 2-byte words." + ;; This is a poor man's version of: + ;; + ;; (let ((spec (bindat-type (words vec 8 uint 16)))) + ;; (bindat-get-field (bindat-unpack spec input) 'words)) + ;; + (cl-assert (arrayp input)) + (cl-assert (= (length input) 16)) + (let ((result (make-vector 8 0)) + (i -1)) + (while-let (((< (cl-incf i) 8)) + (j (* i 2)) + (hi (aref input j)) + (lo (aref input (1+ j)))) + (aset result i (logior (ash hi 8) lo))) + result)) + +(declare-function puny-encode-domain "puny" (domain)) + +;; This is a hash table containing the most recent lookups. It's +;; cleared every 5 minutes, which is obviously dumb. +;; FIXME use some kind of LRU instead. +(defvar socks--tor-resolve-cache nil) + +(defun socks-tor-resolve (name &optional _family _flags) + "Return list with a single IPv4 address for domain NAME. + +See `make-network-process' for the format of the return value. Note +that this function exists for novelty purposes only." + (unless (string-match (rx bot (+ ascii) eot) name) + (require 'puny) + (setq name (puny-encode-domain name))) + (if socks--tor-resolve-cache + (when (time-less-p (car socks--tor-resolve-cache) (current-time)) + (clrhash (cdr socks--tor-resolve-cache))) + (setq socks--tor-resolve-cache (cons (time-add (* 60 5) (current-time)) + (make-hash-table :test #'equal)))) + (with-memoization (gethash name (cdr socks--tor-resolve-cache)) + (condition-case err + (if-let ((socks-password (or socks-password "")) + (route (socks-find-route name nil)) + (proc (socks-send-command (socks-open-connection route) + socks-resolve-command + socks-address-type-name + name + 0)) + (ip (prog1 (socks--extract-resolve-response proc) + (delete-process proc)))) + (list (vconcat ip [0])) + (error "Failed to resolve %s" name)) + (error + (unless (member (cadr err) '("SOCKS: Host unreachable" + "SOCKS: Rejected or failed")) + (signal (car err) (cdr err))))))) + (provide 'socks) ;;; socks.el ends here diff --git a/test/lisp/net/socks-tests.el b/test/lisp/net/socks-tests.el index b9515876d6c..7d65188872e 100644 --- a/test/lisp/net/socks-tests.el +++ b/test/lisp/net/socks-tests.el @@ -327,4 +327,114 @@ socks-override-functions (should-not (advice-member-p #'socks--open-network-stream 'open-network-stream))) +(ert-deftest tor-resolve-4a () + "Make request to TOR resolve service over SOCKS4a" + (let* ((socks-server '("server" "127.0.0.1" t 4a)) + (socks-username "foo") ; defaults to (user-login-name) + (socks-tests-canned-server-patterns + '(([4 #xf0 0 0 0 0 0 1 ?f ?o ?o 0 ?e ?x ?a ?m ?p ?l ?e ?. ?c ?o ?m 0] + . [0 90 0 0 93 184 216 34]))) + (inhibit-message noninteractive) + (server (socks-tests-canned-server-create)) + socks--tor-resolve-cache) + (ert-info ("Query TOR RESOLVE service over SOCKS4") + (cl-letf (((symbol-function 'user-full-name) + (lambda (&optional _) "foo"))) + (should (equal '([93 184 216 34 0]) + (socks-tor-resolve "example.com"))))) + (kill-buffer (process-buffer server)) + (delete-process server))) + +(ert-deftest tor-resolve-4a-fail () + (let* ((socks-server '("server" "127.0.0.1" t 4a)) + (socks-username "foo") ; defaults to (user-login-name) + (socks-tests-canned-server-patterns + '(([4 #xf0 0 0 0 0 0 1 ?f ?o ?o 0 ?e ?x ?a ?m ?p ?l ?e ?. ?c ?o ?m 0] + . [0 91 0 0 0 0 0 0]))) + (inhibit-message noninteractive) + (server (socks-tests-canned-server-create)) + socks--tor-resolve-cache) + (ert-info ("Query TOR RESOLVE service over SOCKS4") + (cl-letf (((symbol-function 'user-full-name) + (lambda (&optional _) "foo"))) + (should-not (socks-tor-resolve "example.com")))) + (kill-buffer (process-buffer server)) + (delete-process server))) + +(ert-deftest tor-resolve-5-fail () + (let* ((socks-server '("server" "127.0.0.1" t 5)) + (socks-username "") + (socks-authentication-methods (copy-sequence + socks-authentication-methods)) + (inhibit-message noninteractive) + (socks-tests-canned-server-patterns + '(([5 2 0 2] . [5 2]) + ([1 0 0] . [1 0]) + ([5 #xf0 0 3 11 ?e ?x ?a ?m ?p ?l ?e ?. ?c ?o ?m 0 0] + . [5 4 0 0 0 0 0 0 0 0]))) + (server (socks-tests-canned-server-create))) + (ert-info ("Query TOR RESOLVE service over SOCKS5") + (should-not (socks-tor-resolve "example.com"))) + (kill-buffer (process-buffer server)) + (delete-process server))) + +(ert-deftest tor-resolve-5 () + "Make request to TOR resolve service over SOCKS5" + (let* ((socks-server '("server" "127.0.0.1" t 5)) + (socks-username "foo") + (socks-authentication-methods (append socks-authentication-methods + nil)) + (inhibit-message noninteractive) + (socks-tests-canned-server-patterns + '(([5 2 0 2] . [5 2]) + ([1 3 ?f ?o ?o 0] . [1 0]) + ([5 #xf0 0 3 11 ?e ?x ?a ?m ?p ?l ?e ?. ?c ?o ?m 0 0] + . [5 0 0 1 93 184 216 34 0 0]))) + (server (socks-tests-canned-server-create)) + socks--tor-resolve-cache) + (ert-info ("Query TOR RESOLVE service over SOCKS5") + (should (equal '([93 184 216 34 0]) (socks-tor-resolve "example.com")))) + (kill-buffer (process-buffer server)) + (delete-process server))) + +;; On the latest Tor (0.4.8.12 as of Sept 2024), a query via tor-resolve +;; that normally returns an IPv6 address instead returns a 91 code. +(ert-deftest tor-resolve-4a-fail/ipv6 () + (let* ((socks-server '("server" "127.0.0.1" t 4a)) + (socks-username "") ; defaults to (user-login-name) + (socks-tests-canned-server-patterns + '(([4 #xf0 0 0 0 0 0 1 0 + ?i ?p ?v ?6 ?. ?g ?o ?o ?g ?l ?e ?. ?c ?o ?m 0] + . [0 91 0 0 0 0 0 0]))) + (inhibit-message noninteractive) + (server (socks-tests-canned-server-create)) + socks--tor-resolve-cache) + (ert-info ("Query TOR RESOLVE service over SOCKS4") + (should-not (socks-tor-resolve "ipv6.google.com"))) + (kill-buffer (process-buffer server)) + (delete-process server))) + +(ert-deftest tor-resolve-5/ipv6 () + "Make request to TOR resolve service over SOCKS5" + (let* ((socks-server '("server" "127.0.0.1" t 5)) + (socks-username "foo") + (socks-authentication-methods + (copy-sequence socks-authentication-methods)) + (inhibit-message noninteractive) + (socks-tests-canned-server-patterns + '(([5 2 0 2] . [5 2]) + ([1 3 ?f ?o ?o 0] . [1 0]) + ([5 #xf0 0 3 15 ?i ?p ?v ?6 ?. ?g ?o ?o ?g ?l ?e ?. ?c ?o ?m 0 0] + . [5 0 0 4 + #x2a #x00 #x14 #x50 #x40 #x0e #x80 #xe0 + #x00 #x00 #x00 #x00 #x00 #x00 #x20 #x0e + 0 0]))) + (server (socks-tests-canned-server-create)) + socks--tor-resolve-cache) + (ert-info ("Query TOR RESOLVE service over SOCKS5") + (should (equal '([#x2a00 #x1450 #x400e #x80e0 0 0 0 #x200e 0]) + (socks-tor-resolve "ipv6.google.com")))) + (kill-buffer (process-buffer server)) + (delete-process server))) + ;;; socks-tests.el ends here -- 2.46.0