-
Notifications
You must be signed in to change notification settings - Fork 13
feat: Add username/password authentication for forward proxies #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import logging | ||
| import string | ||
| import sys | ||
| import os | ||
|
|
||
| import argparse | ||
|
|
||
|
|
@@ -107,6 +108,19 @@ def record_header_version(arg): | |
| help='''Whether to resolve domains before including them in the HTTP CONNECT request to the | ||
| second proxy''') | ||
|
|
||
| forward_proxy.add_argument('--forward_proxy_username', type=str, | ||
| default=None, | ||
| help='Username for the forward proxy authentication (HTTP Basic / SOCKS5)') | ||
|
|
||
| forward_proxy.add_argument('--forward_proxy_password', type=str, | ||
| default=None, | ||
| help='Password for the forward proxy authentication (HTTP Basic / SOCKS5)') | ||
|
|
||
| forward_proxy.add_argument('--forward_proxy_socks5_auth', type=str, | ||
| choices=['auto', 'no_auth', 'userpass'], | ||
| default='auto', | ||
| help="SOCKS5 auth policy when using forward proxy: 'auto', 'no_auth', or 'userpass'") | ||
|
|
||
| return parser.parse_args() | ||
|
|
||
|
|
||
|
|
@@ -122,6 +136,19 @@ def main(): | |
| else: | ||
| logging.basicConfig(stream=sys.stderr, level=logging.INFO) | ||
|
|
||
| username = args.forward_proxy_username or os.getenv("FORWARD_PROXY_USERNAME") | ||
| password = args.forward_proxy_password or os.getenv("FORWARD_PROXY_PASSWORD") | ||
|
|
||
| if args.forward_proxy_socks5_auth == 'userpass' and (username is None or password is None): | ||
| print("--forward_proxy_socks5_auth=userpass requires --forward_proxy_username and --forward_proxy_password", file=sys.stderr) | ||
| sys.exit(2) | ||
|
|
||
| if args.forward_proxy_mode.name == "SOCKSv4" and (username or password): | ||
| print("[warn] Credentials are ignored for SOCKS4 forward proxy", file=sys.stderr) | ||
|
|
||
| if args.forward_proxy_socks5_auth == 'no_auth' and (username or password): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a warning for incomplete credentials for HTTPS proxy mode and a warning for a configured SOCKSv5 proxy auth when the mode is HTTPS
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed I should add sanity checks to the rest of the parameters as well... thanks for the implicit heads up!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JonSnowWhite I think cli is starting to bloat significantly due to the large number of command flags. Maybe migrate to some sort of config? Probably as part of a separate issue.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely an option for the future, yes |
||
| print("[warn] Credentials provided but --forward_proxy_socks5_auth=no_auth; creds will be ignored", file=sys.stderr) | ||
|
|
||
| server_address = NetworkAddress(args.host, args.port) | ||
| forward_proxy = None | ||
| if args.forward_proxy_port is not None: | ||
|
|
@@ -133,7 +160,8 @@ def main(): | |
|
|
||
| proxy = Proxy(server_address, args.timeout, args.record_header_version, args.record_frag, args.tcp_frag, | ||
| args.frag_size, args.dot_resolver, args.disabled_modes, forward_proxy, args.forward_proxy_mode, | ||
| args.forward_proxy_resolve_address) | ||
| args.forward_proxy_resolve_address, username, password, | ||
| args.forward_proxy_socks5_auth) | ||
| proxy.start() | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import logging | ||
| import socket | ||
| from typing import Optional | ||
|
|
||
| from enumerators.ProxyMode import ProxyMode | ||
| from exception.ParserException import ParserException | ||
|
|
@@ -29,11 +30,14 @@ def __init__(self, | |
| record_frag: bool, | ||
| tcp_frag: bool, | ||
| frag_size: int, | ||
| dot_ip: str, | ||
| dot_ip: Optional[str], | ||
| disabled_modes: list[ProxyMode], | ||
| forward_proxy: NetworkAddress, | ||
| forward_proxy: Optional[NetworkAddress], | ||
| forward_proxy_mode: ProxyMode, | ||
| forward_proxy_resolve_address: bool): | ||
| forward_proxy_resolve_address: bool, | ||
| forward_proxy_username: Optional[str] = None, | ||
| forward_proxy_password: Optional[str] = None, | ||
| forward_proxy_socks5_auth: str = 'auto'): | ||
| self.connection_socket = connection_socket | ||
| self.address = address | ||
| self.proxy_mode = None | ||
|
|
@@ -47,6 +51,9 @@ def __init__(self, | |
| self.forward_proxy = forward_proxy | ||
| self.forward_proxy_mode = forward_proxy_mode | ||
| self.forward_proxy_resolve_address = forward_proxy_resolve_address | ||
| self.forward_proxy_username = forward_proxy_username | ||
| self.forward_proxy_password = forward_proxy_password | ||
| self.forward_proxy_socks5_auth = forward_proxy_socks5_auth | ||
|
|
||
| def handle(self): | ||
| """ | ||
|
|
@@ -183,35 +190,56 @@ def connect_forward_proxy(self, server_socket: WrappedSocket, | |
| try: | ||
| # send proxy messages if necessary | ||
| if self.forward_proxy_mode == ProxyMode.HTTPS: | ||
| server_socket.send(Http.connect_message(final_server_address, http_version)) | ||
| proxy_auth_header = None | ||
| if self.forward_proxy_username is not None and self.forward_proxy_password is not None: | ||
| proxy_auth_header = Http.proxy_authorization_basic(self.forward_proxy_username, self.forward_proxy_password) | ||
| server_socket.send(Http.connect_message_with_auth(final_server_address, http_version, proxy_auth_header)) | ||
| self.debug(f"Sent HTTP CONNECT to forward proxy") | ||
| # receive HTTP 200 OK | ||
| answer = server_socket.recv(STANDARD_SOCKET_RECEIVE_SIZE) | ||
| if not answer.upper().startswith(Http.http_200_ok(http_version)): | ||
| raise ParserException(f"Forward proxy rejected the connection with {answer}") | ||
| # receive HTTP response and check status code | ||
| version, status_code, reason = Http.read_http_response_status(server_socket) | ||
| if status_code != 200: | ||
| if status_code == 407: | ||
| raise ParserException("Forward proxy authentication failed (HTTP 407). Check credentials or proxy auth scheme.") | ||
| raise ParserException(f"Forward proxy CONNECT failed: {version} {status_code} {reason}") | ||
|
|
||
| elif self.forward_proxy == ProxyMode.SOCKSv4: | ||
| elif self.forward_proxy_mode == ProxyMode.SOCKSv4: | ||
| server_socket.send(Socksv4.socks4_request(final_server_address)) | ||
| self.debug(f"Sent SOCKSv4 to forward proxy") | ||
| # receive SOCKSv4 OK | ||
| answer = server_socket.recv(STANDARD_SOCKET_RECEIVE_SIZE) | ||
| if not answer.upper().startswith(Socksv4.socks4_ok()) and len(answer) != 8: | ||
| if not answer.startswith(Socksv4.socks4_ok()) or len(answer) != 8: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add parantheses here |
||
| raise ParserException(f"Forward proxy rejected the connection with {answer}") | ||
|
|
||
| elif self.forward_proxy == ProxyMode.SOCKSv5: | ||
| server_socket.send(Socksv5.socks5_auth_methods()) | ||
| elif self.forward_proxy_mode == ProxyMode.SOCKSv5: | ||
| server_socket.send(Socksv5.socks5_auth_methods( | ||
| self.forward_proxy_username, | ||
| self.forward_proxy_password, | ||
| self.forward_proxy_socks5_auth | ||
| )) | ||
| self.debug("Sent SOCKSv5 auth methods") | ||
| answer = server_socket.recv(2) | ||
| if answer == b'\x05\xFF': | ||
| raise ParserException("Forward proxy does not support no auth") | ||
| if answer != b'\x05\x00': | ||
| raise ParserException(f"Forward proxy rejected the connection with {answer}") | ||
| if len(answer) != 2 or answer[0] != 0x05: | ||
| raise ParserException(f"Invalid SOCKSv5 method selection response: {answer}") | ||
| method = answer[1:2] | ||
| if method == b'\xFF': | ||
| raise ParserException("Forward proxy did not accept any auth method") | ||
| if method == Socksv5.USERPASS: | ||
| if self.forward_proxy_username is None or self.forward_proxy_password is None: | ||
| raise ParserException("Forward proxy requires username/password but none provided") | ||
| server_socket.send(Socksv5.socks5_auth_username_password(self.forward_proxy_username, self.forward_proxy_password)) | ||
| auth_answer = server_socket.recv(2) | ||
| if len(auth_answer) != 2 or auth_answer[0] != 0x01 or auth_answer[1] != 0x00: | ||
| raise ParserException(f"SOCKSv5 username/password authentication failed: {auth_answer}") | ||
| elif method == Socksv5.NO_AUTH: | ||
| pass | ||
| else: | ||
| raise ParserException(f"Unsupported SOCKSv5 method selected by server: {method}") | ||
| server_socket.send(Socksv5.socks5_request(final_server_address)) | ||
| self.debug(f"Sent SOCKSv5 to forward proxy") | ||
| # receive SOCKSv5 OK | ||
| answer = server_socket.recv(STANDARD_SOCKET_RECEIVE_SIZE) | ||
| if not answer.upper().startswith(Socksv5.socks5_ok(server_socket)): | ||
| self.debug(f"Forward proxy rejected the connection with {answer}") | ||
| if len(answer) < 5 or answer[0] != 0x05 or answer[1] != 0x00: | ||
| raise ParserException(f"Forward proxy rejected the SOCKSv5 CONNECT with {answer}") | ||
| except: | ||
| self.debug("Could not send proxy message") | ||
| self.connection_socket.try_close() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add warnings to the Readme, help command, and execution when any kind of authentication is used that passwords are transmitted in cleartext. Users should be aware, but I would still like to provide an explicit hint.