;;; socks4.el --- socks4(a) proxy connection method ;; Author: Noah Friedman ;; Created: 2015-10-20 ;; Public domain ;; $Id: socks4.el,v 1.2 2015/10/21 02:54:12 friedman Exp $ ;;; Commentary: ;; This is a quick and dirty proxy method suitable for use with ssh -D. ;; Since no auth mechanisms are needed for that, socks4 is good enough for ;; ipv4 connections. n.b. ipv6 requires socks5. ;; To use, call open-socks4-stream instead of open-network-stream. ;;; Code: ;;;###autoload (defvar socks4-server '("localhost" 1080) "Default socks proxy server for use with `open-socks4-stream'. This may be a list of the form (HOST PORT) or just a string, HOST.") (defconst socks4-error-responses '((#x5b . "request rejected or failed") (#x5c . "request failed: client identd unreachable from server") (#x5d . "request failed: client identd could not verify user id"))) ;; Methods we have to call to modify process once it's been made. ;; 2nd arg `force' means must set to nil, even if prop not provided. (defconst socks4-process-prop-set-alist '((:sentinel set-process-sentinel force) (:filter set-process-filter force) (:filter-multibyte set-process-filter-multibyte))) ;; These are just properties to pass along to make-network-process. ;; :plist is not here because it's handled specially. (defconst socks4-misc-process-props '(:coding :noquery :nowait :keepalive :linger :dontroute :oobinline :priority)) (defmacro socks4-plist-push (plist prop val) (list 'setq plist (list 'plist-put plist prop val))) ;;;###autoload (defun open-socks4-stream (name buffer host port &rest extra) "Open a proxy TCP connection to HOST. Input and output work as for subprocesses; ‘delete-process’ closes it. NAME is the name for the process. It is modified if necessary to make it unique. BUFFER is a buffer or buffer name to associate with the process. Process output goes at end of that buffer. BUFFER may be nil, meaning that the process is not associated with any buffer. HOST is the name or IP address of the host to connect to. PORT is the name of the service desired, or an integer specifying a port number to connect to. The remaining EXTRA parameters should be a sequence of keywords and values: :socks4-host specifies the proxy server to use for the connection to HOST. If omitted or nil, use the default value in `socks4-server'. :socks4-port specifies the port on the proxy server to connect to. If omitted or nil, use the default value in `socks4-server' or, if none is specified there either, use port 1080 which is the canonical socks proxy port. :socks4a is a boolean. If non-nil, resolve the destination host name on the socks server instead of the client. Any other parameters specified are passed to `make-network-process'." (let* ((socks4-host (or (plist-get extra :socks4-host) (if (consp socks4-server) (car socks4-server) socks4-server))) (socks4-port (or (plist-get extra :socks4-port) (if (consp socks4-server) (cadr socks4-server) 1080))) ;; Resolve this now if we should, before opening connection. (rresolve (plist-get extra :socks4a)) (raddr (if rresolve (vector 0 0 0 1 port) (socks4-name-to-addr host port))) (proc-plist (plist-get extra :plist)) (nowait (plist-get extra :nowait)) (params (list :name (format "socks4:%s" name) :host socks4-host :service socks4-port :filter 'socks4-verify-connection :sentinel (if nowait 'socks4-setup-sentinel) ))) ;; buffer wasn't passed in as a prop originally. (if buffer (socks4-plist-push params :buffer buffer)) (mapc (lambda (prop) (let ((elt (plist-member extra prop))) (if elt (socks4-plist-push params prop (cadr elt))))) socks4-misc-process-props) (if rresolve (socks4-plist-push proc-plist :socks4a rresolve)) (socks4-plist-push proc-plist :socks4-remote-addr raddr) (socks4-plist-push proc-plist :socks4-remote-name host) (let ((proc (apply 'make-network-process params))) ;; First we set the process plist to extra. ;; After initialization we set the process plist to proc-plist. ;; In the meantime, store the real proc-plist on the extra plist. ;; We set this here rather than passing it to make-network-process ;; because doing the former returns stale plist data in ;; `process-contact' later. (socks4-plist-push extra :plist proc-plist) (set-process-plist proc extra) (if nowait proc (socks4-request-connection proc))))) ;; For async (nowait processes, we have to wait until the connection is ;; open before we can send the initial request. (defun socks4-setup-sentinel (proc &optional data) (set-process-sentinel proc nil) (if (memq (process-status proc) '(open run)) (socks4-request-connection proc) (let ((conn (process-contact proc t))) (delete-process proc) (error (format "socks4: %s: Connection failed" (plist-get conn :host)))))) ;; Ask the socks server for a proxy connection (defun socks4-request-connection (proc) (let* ((contact (process-contact proc t)) (extra (process-plist proc)) (nowait (plist-get contact :nowait)) (proc-plist (plist-get extra :plist)) (rresolve (plist-get proc-plist :socks4a)) (rhost (plist-get proc-plist :socks4-remote-name)) (raddr (plist-get proc-plist :socks4-remote-addr)) (rport (aref raddr (1- (length raddr)))) (req (format "%c%c%c%c%c%c%c%c%s\0" 4 ;; version 1 ;; make stream connection (lsh rport -8) ;; port, high byte (logand rport #xff) ;; port, low byte (aref raddr 0) ;; address, high byte (aref raddr 1) ;; (aref raddr 2) ;; (aref raddr 3) ;; address, low byte (user-login-name)))) (if rresolve ;; append dns name for remote resolution (setq req (format "%s%s\0" req rhost))) (process-send-string proc (string-make-unibyte req)) ;; can't accept process output from inside sentinel. ;; the output never seems to arrive. (unless nowait (let ((inhibit-quit nil)) ;; Otherwise allow a 30-second timeout (accept-process-output proc 30))) proc)) ;; Read back reply from socks server. If this succeeds, it finishes ;; setting up the connection parameters (originally-specified process ;; filter, etc.) (defun socks4-verify-connection (proc &optional response) (let* ((extra (process-plist proc)) (proc-plist (plist-get extra :plist)) (rresolve (plist-get proc-plist :socks4a)) (rhost (plist-get proc-plist :socks4-remote-name)) (raddr (plist-get proc-plist :socks4-remote-addr)) (rport (aref raddr (1- (length raddr))))) (let ((code (aref response 1))) (unless (= code #x5a) (delete-process proc) (let ((msg (or (cdr (assq code socks4-error-responses)) "no recognizable acknowledgement from socks server"))) (error (format "socks4: %s:%s: (0x%x) %s" rhost rport code msg))))) (when rresolve (setq raddr (vector (aref response 4) ;; address, high byte (aref response 5) (aref response 6) (aref response 7) ;; address, low byte (logior (lsh (aref response 2) 8) ;; port (lsh (aref response 3) 0)))) (socks4-plist-push proc-plist :socks4a-reported-addr raddr)) (set-process-plist proc proc-plist) (mapc (lambda (elt) (let* ((prop (car elt)) (def (plist-member extra prop)) (force (eq 'force (nth 2 elt)))) (if (or def force) (funcall (nth 1 elt) proc (cadr def))))) socks4-process-prop-set-alist)) proc) ;; poor man's nslookup ;; Create a temporary datagram socket and then get the peer address. (defun socks4-name-to-addr (host &optional port) (let* ((proc (make-network-process :name "socks4-name-to-addr" :family 'ipv4 ;; socks4 only supports ip4 :host host :service (or port 0) :type 'datagram :noquery t)) (addr (plist-get (process-contact proc t) :remote))) (delete-process proc) addr)) (provide 'socks4) ;; eof