2022-12-24 23:49:50 +00:00
// Copyright (c) 2020-2022 The Bitcoin Core developers
2020-11-27 13:59:26 +01:00
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
# include <chainparams.h>
2023-03-23 12:23:29 +01:00
# include <common/args.h>
2022-06-28 13:27:57 +01:00
# include <compat/compat.h>
2020-11-27 13:59:26 +01:00
# include <compat/endian.h>
# include <crypto/sha256.h>
# include <i2p.h>
# include <logging.h>
# include <netaddress.h>
# include <netbase.h>
# include <random.h>
2023-12-06 15:58:47 -05:00
# include <script/parsing.h>
2023-07-14 15:17:43 +02:00
# include <sync.h>
2020-11-27 13:59:26 +01:00
# include <tinyformat.h>
2023-03-15 11:18:06 +01:00
# include <util/fs.h>
2020-11-27 13:59:26 +01:00
# include <util/readwritefile.h>
# include <util/sock.h>
2022-06-08 15:27:11 +02:00
# include <util/strencodings.h>
2022-10-11 15:33:22 +08:00
# include <util/threadinterrupt.h>
2020-11-27 13:59:26 +01:00
# include <chrono>
2021-03-04 14:31:49 +01:00
# include <memory>
2020-11-27 13:59:26 +01:00
# include <stdexcept>
# include <string>
2023-12-06 15:13:39 -05:00
using util : : Split ;
2020-11-27 13:59:26 +01:00
namespace i2p {
/**
* Swap Standard Base64 < - > I2P Base64 .
* Standard Base64 uses ` + ` and ` / ` as last two characters of its alphabet .
* I2P Base64 uses ` - ` and ` ~ ` respectively .
* So it is easy to detect in which one is the input and convert to the other .
* @ param [ in ] from Input to convert .
* @ return converted ` from `
*/
static std : : string SwapBase64 ( const std : : string & from )
{
std : : string to ;
to . resize ( from . size ( ) ) ;
for ( size_t i = 0 ; i < from . size ( ) ; + + i ) {
switch ( from [ i ] ) {
case ' - ' :
to [ i ] = ' + ' ;
break ;
case ' ~ ' :
to [ i ] = ' / ' ;
break ;
case ' + ' :
to [ i ] = ' - ' ;
break ;
case ' / ' :
to [ i ] = ' ~ ' ;
break ;
default :
to [ i ] = from [ i ] ;
break ;
}
}
return to ;
}
/**
* Decode an I2P - style Base64 string .
* @ param [ in ] i2p_b64 I2P - style Base64 string .
* @ return decoded ` i2p_b64 `
* @ throw std : : runtime_error if decoding fails
*/
static Binary DecodeI2PBase64 ( const std : : string & i2p_b64 )
{
const std : : string & std_b64 = SwapBase64 ( i2p_b64 ) ;
2022-04-04 13:52:06 -04:00
auto decoded = DecodeBase64 ( std_b64 ) ;
if ( ! decoded ) {
2020-11-27 13:59:26 +01:00
throw std : : runtime_error ( strprintf ( " Cannot decode Base64: \" %s \" " , i2p_b64 ) ) ;
}
2022-04-04 13:52:06 -04:00
return std : : move ( * decoded ) ;
2020-11-27 13:59:26 +01:00
}
/**
* Derive the . b32 . i2p address of an I2P destination ( binary ) .
* @ param [ in ] dest I2P destination .
* @ return the address that corresponds to ` dest `
* @ throw std : : runtime_error if conversion fails
*/
static CNetAddr DestBinToAddr ( const Binary & dest )
{
CSHA256 hasher ;
hasher . Write ( dest . data ( ) , dest . size ( ) ) ;
unsigned char hash [ CSHA256 : : OUTPUT_SIZE ] ;
hasher . Finalize ( hash ) ;
CNetAddr addr ;
const std : : string addr_str = EncodeBase32 ( hash , false ) + " .b32.i2p " ;
if ( ! addr . SetSpecial ( addr_str ) ) {
throw std : : runtime_error ( strprintf ( " Cannot parse I2P address: \" %s \" " , addr_str ) ) ;
}
return addr ;
}
/**
* Derive the . b32 . i2p address of an I2P destination ( I2P - style Base64 ) .
* @ param [ in ] dest I2P destination .
* @ return the address that corresponds to ` dest `
* @ throw std : : runtime_error if conversion fails
*/
static CNetAddr DestB64ToAddr ( const std : : string & dest )
{
const Binary & decoded = DecodeI2PBase64 ( dest ) ;
return DestBinToAddr ( decoded ) ;
}
namespace sam {
Session : : Session ( const fs : : path & private_key_file ,
2023-07-13 12:40:42 -04:00
const Proxy & control_host ,
2020-11-27 13:59:26 +01:00
CThreadInterrupt * interrupt )
2022-06-08 15:27:11 +02:00
: m_private_key_file { private_key_file } ,
m_control_host { control_host } ,
m_interrupt { interrupt } ,
m_transient { false }
{
}
2023-07-13 12:40:42 -04:00
Session : : Session ( const Proxy & control_host , CThreadInterrupt * interrupt )
2022-06-08 15:27:11 +02:00
: m_control_host { control_host } ,
m_interrupt { interrupt } ,
m_transient { true }
2020-11-27 13:59:26 +01:00
{
}
Session : : ~ Session ( )
{
LOCK ( m_mutex ) ;
Disconnect ( ) ;
}
bool Session : : Listen ( Connection & conn )
{
try {
LOCK ( m_mutex ) ;
CreateIfNotCreatedAlready ( ) ;
conn . me = m_my_addr ;
conn . sock = StreamAccept ( ) ;
return true ;
} catch ( const std : : runtime_error & e ) {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Error , " Couldn't listen: %s \n " , e . what ( ) ) ;
2020-11-27 13:59:26 +01:00
CheckControlSock ( ) ;
}
return false ;
}
bool Session : : Accept ( Connection & conn )
{
2023-07-14 15:17:43 +02:00
AssertLockNotHeld ( m_mutex ) ;
2020-11-27 13:59:26 +01:00
2023-07-14 15:17:43 +02:00
std : : string errmsg ;
bool disconnect { false } ;
2020-11-27 13:59:26 +01:00
2023-07-14 15:17:43 +02:00
while ( ! * m_interrupt ) {
Sock : : Event occurred ;
if ( ! conn . sock - > Wait ( MAX_WAIT_FOR_IO , Sock : : RECV , & occurred ) ) {
errmsg = " wait on socket failed " ;
break ;
}
2020-11-27 13:59:26 +01:00
2023-07-14 15:17:43 +02:00
if ( occurred = = 0 ) {
// Timeout, no incoming connections or errors within MAX_WAIT_FOR_IO.
continue ;
}
2020-11-27 13:59:26 +01:00
2023-07-14 15:17:43 +02:00
std : : string peer_dest ;
try {
peer_dest = conn . sock - > RecvUntilTerminator ( ' \n ' , MAX_WAIT_FOR_IO , * m_interrupt , MAX_MSG_SIZE ) ;
} catch ( const std : : runtime_error & e ) {
errmsg = e . what ( ) ;
break ;
2020-11-27 13:59:26 +01:00
}
2023-07-14 15:17:43 +02:00
CNetAddr peer_addr ;
try {
peer_addr = DestB64ToAddr ( peer_dest ) ;
} catch ( const std : : runtime_error & e ) {
// The I2P router is expected to send the Base64 of the connecting peer,
// but it may happen that something like this is sent instead:
// STREAM STATUS RESULT=I2P_ERROR MESSAGE="Session was closed"
// In that case consider the session damaged and close it right away,
// even if the control socket is alive.
if ( peer_dest . find ( " RESULT=I2P_ERROR " ) ! = std : : string : : npos ) {
errmsg = strprintf ( " unexpected reply that hints the session is unusable: %s " , peer_dest ) ;
disconnect = true ;
} else {
errmsg = e . what ( ) ;
}
break ;
}
conn . peer = CService ( peer_addr , I2P_SAM31_PORT ) ;
return true ;
}
2024-04-08 10:50:55 -03:00
if ( * m_interrupt ) {
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Accept was interrupted \n " ) ;
} else {
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Error accepting%s: %s \n " , disconnect ? " (will close the session) " : " " , errmsg ) ;
}
2023-07-14 15:17:43 +02:00
if ( disconnect ) {
LOCK ( m_mutex ) ;
Disconnect ( ) ;
} else {
2020-11-27 13:59:26 +01:00
CheckControlSock ( ) ;
}
return false ;
}
bool Session : : Connect ( const CService & to , Connection & conn , bool & proxy_error )
{
2021-05-31 18:07:40 +02:00
// Refuse connecting to arbitrary ports. We don't specify any destination port to the SAM proxy
// when connecting (SAM 3.1 does not use ports) and it forces/defaults it to I2P_SAM31_PORT.
if ( to . GetPort ( ) ! = I2P_SAM31_PORT ) {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Error connecting to %s, connection refused due to arbitrary port %s \n " , to . ToStringAddrPort ( ) , to . GetPort ( ) ) ;
2021-05-31 18:07:40 +02:00
proxy_error = false ;
return false ;
}
2020-11-27 13:59:26 +01:00
proxy_error = true ;
std : : string session_id ;
2021-03-04 14:31:49 +01:00
std : : unique_ptr < Sock > sock ;
2020-11-27 13:59:26 +01:00
conn . peer = to ;
try {
{
LOCK ( m_mutex ) ;
CreateIfNotCreatedAlready ( ) ;
session_id = m_session_id ;
conn . me = m_my_addr ;
sock = Hello ( ) ;
}
const Reply & lookup_reply =
2022-07-18 13:28:40 +02:00
SendRequestAndGetReply ( * sock , strprintf ( " NAMING LOOKUP NAME=%s " , to . ToStringAddr ( ) ) ) ;
2020-11-27 13:59:26 +01:00
const std : : string & dest = lookup_reply . Get ( " VALUE " ) ;
const Reply & connect_reply = SendRequestAndGetReply (
2021-03-04 14:31:49 +01:00
* sock , strprintf ( " STREAM CONNECT ID=%s DESTINATION=%s SILENT=false " , session_id , dest ) ,
2020-11-27 13:59:26 +01:00
false ) ;
const std : : string & result = connect_reply . Get ( " RESULT " ) ;
if ( result = = " OK " ) {
conn . sock = std : : move ( sock ) ;
return true ;
}
if ( result = = " INVALID_ID " ) {
LOCK ( m_mutex ) ;
Disconnect ( ) ;
throw std : : runtime_error ( " Invalid session id " ) ;
}
if ( result = = " CANT_REACH_PEER " | | result = = " TIMEOUT " ) {
proxy_error = false ;
}
throw std : : runtime_error ( strprintf ( " \" %s \" " , connect_reply . full ) ) ;
} catch ( const std : : runtime_error & e ) {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Error connecting to %s: %s \n " , to . ToStringAddrPort ( ) , e . what ( ) ) ;
2020-11-27 13:59:26 +01:00
CheckControlSock ( ) ;
return false ;
}
}
// Private methods
std : : string Session : : Reply : : Get ( const std : : string & key ) const
{
const auto & pos = keys . find ( key ) ;
if ( pos = = keys . end ( ) | | ! pos - > second . has_value ( ) ) {
throw std : : runtime_error (
strprintf ( " Missing %s= in the reply to \" %s \" : \" %s \" " , key , request , full ) ) ;
}
return pos - > second . value ( ) ;
}
Session : : Reply Session : : SendRequestAndGetReply ( const Sock & sock ,
const std : : string & request ,
bool check_result_ok ) const
{
sock . SendComplete ( request + " \n " , MAX_WAIT_FOR_IO , * m_interrupt ) ;
Reply reply ;
// Don't log the full "SESSION CREATE ..." because it contains our private key.
reply . request = request . substr ( 0 , 14 ) = = " SESSION CREATE " ? " SESSION CREATE ... " : request ;
// It could take a few minutes for the I2P router to reply as it is querying the I2P network
// (when doing name lookup, for example). Notice: `RecvUntilTerminator()` is checking
// `m_interrupt` more often, so we would not be stuck here for long if `m_interrupt` is
// signaled.
static constexpr auto recv_timeout = 3 min ;
2021-03-10 12:07:08 +01:00
reply . full = sock . RecvUntilTerminator ( ' \n ' , recv_timeout , * m_interrupt , MAX_MSG_SIZE ) ;
2020-11-27 13:59:26 +01:00
2023-12-06 15:58:47 -05:00
for ( const auto & kv : Split ( reply . full , ' ' ) ) {
2020-11-27 13:59:26 +01:00
const auto & pos = std : : find ( kv . begin ( ) , kv . end ( ) , ' = ' ) ;
if ( pos ! = kv . end ( ) ) {
reply . keys . emplace ( std : : string { kv . begin ( ) , pos } , std : : string { pos + 1 , kv . end ( ) } ) ;
} else {
reply . keys . emplace ( std : : string { kv . begin ( ) , kv . end ( ) } , std : : nullopt ) ;
}
}
if ( check_result_ok & & reply . Get ( " RESULT " ) ! = " OK " ) {
throw std : : runtime_error (
strprintf ( " Unexpected reply to \" %s \" : \" %s \" " , request , reply . full ) ) ;
}
return reply ;
}
2021-03-04 14:31:49 +01:00
std : : unique_ptr < Sock > Session : : Hello ( ) const
2020-11-27 13:59:26 +01:00
{
2023-07-13 12:40:42 -04:00
auto sock = m_control_host . Connect ( ) ;
2020-11-27 13:59:26 +01:00
if ( ! sock ) {
2023-07-13 12:40:42 -04:00
throw std : : runtime_error ( strprintf ( " Cannot connect to %s " , m_control_host . ToString ( ) ) ) ;
2020-11-27 13:59:26 +01:00
}
SendRequestAndGetReply ( * sock , " HELLO VERSION MIN=3.1 MAX=3.1 " ) ;
2021-03-04 14:31:49 +01:00
return sock ;
2020-11-27 13:59:26 +01:00
}
void Session : : CheckControlSock ( )
{
LOCK ( m_mutex ) ;
std : : string errmsg ;
2021-05-25 15:01:53 +02:00
if ( m_control_sock & & ! m_control_sock - > IsConnected ( errmsg ) ) {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Control socket error: %s \n " , errmsg ) ;
2020-11-27 13:59:26 +01:00
Disconnect ( ) ;
}
}
void Session : : DestGenerate ( const Sock & sock )
{
// https://geti2p.net/spec/common-structures#key-certificates
// "7" or "EdDSA_SHA512_Ed25519" - "Recent Router Identities and Destinations".
// Use "7" because i2pd <2.24.0 does not recognize the textual form.
2022-09-12 10:57:03 +02:00
// If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1.
2020-11-27 13:59:26 +01:00
const Reply & reply = SendRequestAndGetReply ( sock , " DEST GENERATE SIGNATURE_TYPE=7 " , false ) ;
m_private_key = DecodeI2PBase64 ( reply . Get ( " PRIV " ) ) ;
}
void Session : : GenerateAndSavePrivateKey ( const Sock & sock )
{
DestGenerate ( sock ) ;
2023-05-08 11:32:13 +02:00
// umask is set to 0077 in common/system.cpp, which is ok.
2020-11-27 13:59:26 +01:00
if ( ! WriteBinaryFile ( m_private_key_file ,
std : : string ( m_private_key . begin ( ) , m_private_key . end ( ) ) ) ) {
throw std : : runtime_error (
2021-09-10 00:17:20 -04:00
strprintf ( " Cannot save I2P private key to %s " , fs : : quoted ( fs : : PathToString ( m_private_key_file ) ) ) ) ;
2020-11-27 13:59:26 +01:00
}
}
Binary Session : : MyDestination ( ) const
{
// From https://geti2p.net/spec/common-structures#destination:
// "They are 387 bytes plus the certificate length specified at bytes 385-386, which may be
// non-zero"
static constexpr size_t DEST_LEN_BASE = 387 ;
static constexpr size_t CERT_LEN_POS = 385 ;
uint16_t cert_len ;
2023-10-26 16:50:02 +01:00
if ( m_private_key . size ( ) < CERT_LEN_POS + sizeof ( cert_len ) ) {
throw std : : runtime_error ( strprintf ( " The private key is too short (%d < %d) " ,
m_private_key . size ( ) ,
CERT_LEN_POS + sizeof ( cert_len ) ) ) ;
}
2020-11-27 13:59:26 +01:00
memcpy ( & cert_len , & m_private_key . at ( CERT_LEN_POS ) , sizeof ( cert_len ) ) ;
2024-02-27 18:39:22 +00:00
cert_len = be16toh_internal ( cert_len ) ;
2020-11-27 13:59:26 +01:00
const size_t dest_len = DEST_LEN_BASE + cert_len ;
2023-10-26 16:50:02 +01:00
if ( dest_len > m_private_key . size ( ) ) {
throw std : : runtime_error ( strprintf ( " Certificate length (%d) designates that the private key should "
" be %d bytes, but it is only %d bytes " ,
cert_len ,
dest_len ,
m_private_key . size ( ) ) ) ;
}
2020-11-27 13:59:26 +01:00
return Binary { m_private_key . begin ( ) , m_private_key . begin ( ) + dest_len } ;
}
void Session : : CreateIfNotCreatedAlready ( )
{
std : : string errmsg ;
2021-05-25 15:01:53 +02:00
if ( m_control_sock & & m_control_sock - > IsConnected ( errmsg ) ) {
2020-11-27 13:59:26 +01:00
return ;
}
2022-06-08 15:27:11 +02:00
const auto session_type = m_transient ? " transient " : " persistent " ;
const auto session_id = GetRandHash ( ) . GetHex ( ) . substr ( 0 , 10 ) ; // full is overkill, too verbose in the logs
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Debug , " Creating %s SAM session %s with %s \n " , session_type , session_id , m_control_host . ToString ( ) ) ;
2020-11-27 13:59:26 +01:00
2021-03-04 14:31:49 +01:00
auto sock = Hello ( ) ;
2020-11-27 13:59:26 +01:00
2022-06-08 15:27:11 +02:00
if ( m_transient ) {
// The destination (private key) is generated upon session creation and returned
// in the reply in DESTINATION=.
const Reply & reply = SendRequestAndGetReply (
* sock ,
2023-01-06 17:11:26 +01:00
strprintf ( " SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 "
2024-01-07 15:53:08 -06:00
" i2cp.leaseSetEncType=4,0 inbound.quantity=1 outbound.quantity=1 " ,
2023-01-06 17:11:26 +01:00
session_id ) ) ;
2022-06-08 15:27:11 +02:00
m_private_key = DecodeI2PBase64 ( reply . Get ( " DESTINATION " ) ) ;
2020-11-27 13:59:26 +01:00
} else {
2022-06-08 15:27:11 +02:00
// Read our persistent destination (private key) from disk or generate
// one and save it to disk. Then use it when creating the session.
const auto & [ read_ok , data ] = ReadBinaryFile ( m_private_key_file ) ;
if ( read_ok ) {
m_private_key . assign ( data . begin ( ) , data . end ( ) ) ;
} else {
GenerateAndSavePrivateKey ( * sock ) ;
}
2020-11-27 13:59:26 +01:00
2022-06-08 15:27:11 +02:00
const std : : string & private_key_b64 = SwapBase64 ( EncodeBase64 ( m_private_key ) ) ;
2020-11-27 13:59:26 +01:00
2022-06-08 15:27:11 +02:00
SendRequestAndGetReply ( * sock ,
2023-01-06 17:41:31 +01:00
strprintf ( " SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "
2024-01-07 15:53:08 -06:00
" i2cp.leaseSetEncType=4,0 inbound.quantity=3 outbound.quantity=3 " ,
2022-06-08 15:27:11 +02:00
session_id ,
private_key_b64 ) ) ;
}
2020-11-27 13:59:26 +01:00
2021-05-31 17:18:49 +02:00
m_my_addr = CService ( DestBinToAddr ( MyDestination ( ) ) , I2P_SAM31_PORT ) ;
2020-11-27 13:59:26 +01:00
m_session_id = session_id ;
m_control_sock = std : : move ( sock ) ;
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Info , " %s SAM session %s created, my address=%s \n " ,
2022-06-08 15:27:11 +02:00
Capitalize ( session_type ) ,
m_session_id ,
2022-07-15 14:13:39 +02:00
m_my_addr . ToStringAddrPort ( ) ) ;
2020-11-27 13:59:26 +01:00
}
2021-03-04 14:31:49 +01:00
std : : unique_ptr < Sock > Session : : StreamAccept ( )
2020-11-27 13:59:26 +01:00
{
2021-03-04 14:31:49 +01:00
auto sock = Hello ( ) ;
2020-11-27 13:59:26 +01:00
const Reply & reply = SendRequestAndGetReply (
2021-03-04 14:31:49 +01:00
* sock , strprintf ( " STREAM ACCEPT ID=%s SILENT=false " , m_session_id ) , false ) ;
2020-11-27 13:59:26 +01:00
const std : : string & result = reply . Get ( " RESULT " ) ;
if ( result = = " OK " ) {
return sock ;
}
if ( result = = " INVALID_ID " ) {
// If our session id is invalid, then force session re-creation on next usage.
Disconnect ( ) ;
}
throw std : : runtime_error ( strprintf ( " \" %s \" " , reply . full ) ) ;
}
void Session : : Disconnect ( )
{
2021-05-25 15:01:53 +02:00
if ( m_control_sock ) {
2020-11-27 13:59:26 +01:00
if ( m_session_id . empty ( ) ) {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Info , " Destroying incomplete SAM session \n " ) ;
2020-11-27 13:59:26 +01:00
} else {
2024-04-08 10:48:11 -03:00
LogPrintLevel ( BCLog : : I2P , BCLog : : Level : : Info , " Destroying SAM session %s \n " , m_session_id ) ;
2020-11-27 13:59:26 +01:00
}
2021-05-25 15:01:53 +02:00
m_control_sock . reset ( ) ;
2020-11-27 13:59:26 +01:00
}
m_session_id . clear ( ) ;
}
} // namespace sam
} // namespace i2p