Skip to content

Instantly share code, notes, and snippets.

@laanwj
Created April 22, 2025 01:19
Show Gist options
  • Save laanwj/13f88b993cc5c33826aef28dc629e37b to your computer and use it in GitHub Desktop.
Save laanwj/13f88b993cc5c33826aef28dc629e37b to your computer and use it in GitHub Desktop.
Check and report status of joinmarket directory nodes.
#!/usr/bin/python3
# Check and report status of joinmarket directory nodes.
# SPDX-License-Identifier: MIT
import socket
import subprocess
PROXY = ('127.0.0.1', 9050)
GLYPH = ['❌', '🆗']
# Nodes to check
NODES = [
'g3hv4uynnmynqqq2mchf3fcm3yd46kfzmcdogejuckgwknwyq5ya6iad.onion:5222',
'3kxw6lf5vf6y26emzwgibzhrzhmhqiw6ekrek3nqfjjmhwznb2moonad.onion:5222',
'bqlpq6ak24mwvuixixitift4yu42nxchlilrcqwk2ugn45tdclg42qid.onion:5222',
'wkd3kd73ze62sqhlj2ebwe6fxqvshw5sya6nkvrgcweegt7ljhuspaid.onion:5222',
'ylegp63psfqh3zk2huckf2xth6dxvh2z364ykjfmvsoze6tkfjceq7qd.onion:5222',
'plq5jw5hqke6werrc5duvgwbuczrg4mphlqsqbzmdfwxwkm2ncdzxeqd.onion:5222',
'odpwaf67rs5226uabcamvypg3y4bngzmfk7255flcdodesqhsvkptaid.onion:5222',
]
# Proxy handling #
### Utility functions
def recvall(s, n):
'''Receive n bytes from a socket, or fail'''
rv = bytearray()
while n > 0:
d = s.recv(n)
if not d:
raise IOError('Unexpected end of stream')
rv.extend(d)
n -= len(d)
return rv
### Protocol constants
class Command:
CONNECT = 0x01
class AddressType:
IPV4 = 0x01
DOMAINNAME = 0x03
IPV6 = 0x04
class Reply:
SUCCEEDED = 0x00 # RFC1928: Succeeded
GENFAILURE = 0x01 # RFC1928: General failure
NOTALLOWED = 0x02 # RFC1928: Connection not allowed by ruleset
NETUNREACHABLE = 0x03 # RFC1928: Network unreachable
HOSTUNREACHABLE = 0x04 # RFC1928: Network unreachable
CONNREFUSED = 0x05 # RFC1928: Connection refused
TTLEXPIRED = 0x06 # RFC1928: TTL expired
CMDUNSUPPORTED = 0x07 # RFC1928: Command not supported
ATYPEUNSUPPORTED = 0x08 # RFC1928: Address type not supported
TOR_HS_DESC_NOT_FOUND = 0xf0 # Tor: Onion service descriptor can not be found
TOR_HS_DESC_INVALID = 0xf1 # Tor: Onion service descriptor is invalid
TOR_HS_INTRO_FAILED = 0xf2 # Tor: Onion service introduction failed
TOR_HS_REND_FAILED = 0xf3 # Tor: Onion service rendezvous failed
TOR_HS_MISSING_CLIENT_AUTH = 0xf4 # Tor: Onion service missing client authorization
TOR_HS_WRONG_CLIENT_AUTH = 0xf5 # Tor: Onion service wrong client authorization
TOR_HS_BAD_ADDRESS = 0xf6 # Tor: Onion service invalid address
TOR_HS_INTRO_TIMEOUT = 0xf7 # Tor: Onion service introduction timed out
mapping = {
SUCCEEDED: 'Succeeded',
GENFAILURE: 'General failure',
NOTALLOWED: 'Connection not allowed by ruleset',
NETUNREACHABLE: 'Network unreachable',
HOSTUNREACHABLE: 'Host unreachable',
CONNREFUSED: 'Connection refused',
TTLEXPIRED: 'TTL expired',
CMDUNSUPPORTED: 'Command not supported',
ATYPEUNSUPPORTED: 'Address type not supported',
TOR_HS_DESC_NOT_FOUND: 'Onion service descriptor can not be found',
TOR_HS_DESC_INVALID: 'Onion service descriptor is invalid',
TOR_HS_INTRO_FAILED: 'Onion service introduction failed',
TOR_HS_REND_FAILED: 'Onion service rendezvous failed',
TOR_HS_MISSING_CLIENT_AUTH: 'Onion service missing client authorization',
TOR_HS_WRONG_CLIENT_AUTH: 'Onion service wrong client authorization',
TOR_HS_BAD_ADDRESS: 'Onion service invalid address',
TOR_HS_INTRO_TIMEOUT: 'Onion service introduction timed out',
}
def socks5_connect(sock, dst):
'''SOCKS5 negotiation.
sock must already be connected to proxy.
'''
(host, port) = dst
# version, nmethods, support unauthenticated
sock.sendall(bytes([0x05, 0x01, 0x00]))
# receive version, chosen method
rv = recvall(sock, 2)
if rv[0] != 0x05:
raise IOError(f"Invalid socks version {rv[0]} in auth response")
if rv[1] != 0x00:
raise IOError(f"Unsupported authentication method {rv[1]}")
# send CONNECT request
# ver,cmd,rsv,atyp,asize,addr[asize],port_hi,port_lo
assert(len(host) <= 255)
assert(port >= 0 and port < 0xffff)
sock.sendall(bytes([0x05, Command.CONNECT, 0x00, AddressType.DOMAINNAME, len(host)]) +
bytes(host.encode()) +
bytes([port >> 8, port & 0xFF]))
# receive reponse, including bind address and port (which we ignore)
# ver,status,rsv,atyp,[addr],port_hi,port_lo
rv = recvall(sock, 4)
if rv[0] != 0x05:
raise IOError(f"Invalid socks version {rv[0]} in response")
if rv[1] != 0x00:
raise IOError(f"{Reply.mapping.get(rv[1], 'Unknown')} (0x{rv[1]:02x})")
if rv[2] != 0x00:
raise IOError("SOCKS5 malformed response")
if rv[3] == AddressType.IPV4:
bindaddr = recvall(sock, 4)
elif rv[3] == AddressType.IPV6:
bindaddr = recvall(sock, 16)
elif rv[3] == AddressType.DOMAINNAME:
asize = recvall(sock, 1)[0]
bindaddr = recvall(sock, asize)
else:
raise IOError("SOCKS5 malformed response")
bindport = recvall(sock, 2)
def main():
active = []
for node in NODES:
(host, port) = node.split(':')
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(PROXY)
try:
socks5_connect(sock, (host, int(port)))
except IOError as e:
ok = False
status = str(e)
else:
ok = True
status = ''
active.append(node)
print(f'- {GLYPH[ok]} `{node}` {status}')
print('```')
print(f'directory_nodes = {",".join(active)}')
print('```')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment