503 lines
17 KiB
Go
503 lines
17 KiB
Go
|
|
// peerClient provides functions to create and manage outgoing connections to hosts.
|
|
// Each connections has an associated encryption key that is established through a key handshake
|
|
// The encryption key encrypts all requests and responses between the host and requestor
|
|
// These connections are useful because they negate the need to reconnect and perform a new key handshake with each request to the same host
|
|
|
|
package peerClient
|
|
|
|
import "seekia/internal/cryptography/kyber"
|
|
import "seekia/internal/cryptography/nacl"
|
|
import "seekia/internal/helpers"
|
|
import "seekia/internal/mySettings"
|
|
import "seekia/internal/network/maliciousHosts"
|
|
import "seekia/internal/network/peerConnection"
|
|
import "seekia/internal/network/serverRequest"
|
|
import "seekia/internal/network/serverResponse"
|
|
import "seekia/internal/network/unreachableHosts"
|
|
import "seekia/internal/profiles/profileStorage"
|
|
import "seekia/internal/profiles/readProfiles"
|
|
import "seekia/internal/profiles/viewableProfiles"
|
|
|
|
import messagepack "github.com/vmihailenco/msgpack/v5"
|
|
|
|
import "net"
|
|
import "net/url"
|
|
import "net/netip"
|
|
import "sync"
|
|
import "time"
|
|
import "errors"
|
|
|
|
type connectionMetadata struct{
|
|
|
|
// The host that the connection is made with
|
|
HostIdentityHash [16]byte
|
|
|
|
// The key to use to encrypt all communications
|
|
ConnectionKey [32]byte
|
|
|
|
// The NetworkType that the connection exists on
|
|
NetworkType byte
|
|
}
|
|
|
|
var hostConnectionObjectsMapMutex sync.RWMutex
|
|
|
|
// Map structure: Connection Identifier -> Connection object
|
|
var hostConnectionObjectsMap map[[21]byte]net.Conn = make(map[[21]byte]net.Conn)
|
|
|
|
var hostConnectionMetadatasMapMutex sync.RWMutex
|
|
|
|
// Map structure: Connection Identifier -> Connection Metadata
|
|
var hostConnectionMetadatasMap map[[21]byte]connectionMetadata = make(map[[21]byte]connectionMetadata)
|
|
|
|
func addConnectionToConnectionObjectsMap(connectionIdentifier [21]byte, connectionObject net.Conn){
|
|
|
|
hostConnectionObjectsMapMutex.Lock()
|
|
hostConnectionObjectsMap[connectionIdentifier] = connectionObject
|
|
hostConnectionObjectsMapMutex.Unlock()
|
|
}
|
|
|
|
func getConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte)(bool, net.Conn){
|
|
|
|
hostConnectionObjectsMapMutex.RLock()
|
|
connectionObject, exists := hostConnectionObjectsMap[connectionIdentifier]
|
|
hostConnectionObjectsMapMutex.RUnlock()
|
|
|
|
if (exists == false){
|
|
return false, nil
|
|
}
|
|
|
|
return true, connectionObject
|
|
}
|
|
|
|
func deleteConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte){
|
|
|
|
hostConnectionObjectsMapMutex.Lock()
|
|
delete(hostConnectionObjectsMap, connectionIdentifier)
|
|
hostConnectionObjectsMapMutex.Unlock()
|
|
}
|
|
|
|
func addMetadataToConnectionMetadatasMap(connectionIdentifier [21]byte, hostIdentityHash [16]byte, connectionKey [32]byte, networkType byte)error{
|
|
|
|
connectionMetadataObject := connectionMetadata{
|
|
HostIdentityHash: hostIdentityHash,
|
|
ConnectionKey: connectionKey,
|
|
NetworkType: networkType,
|
|
}
|
|
|
|
hostConnectionMetadatasMapMutex.Lock()
|
|
hostConnectionMetadatasMap[connectionIdentifier] = connectionMetadataObject
|
|
hostConnectionMetadatasMapMutex.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteMetadataFromConnectionMetadatasMap(connectionIdentifier [21]byte){
|
|
|
|
hostConnectionMetadatasMapMutex.Lock()
|
|
delete(hostConnectionMetadatasMap, connectionIdentifier)
|
|
hostConnectionMetadatasMapMutex.Unlock()
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Connection found
|
|
// -[16]byte: Host identity hash
|
|
// -[32]byte: Connection key
|
|
// -byte: Network type
|
|
// -error
|
|
func GetConnectionMetadata(connectionIdentifier [21]byte)(bool, [16]byte, [32]byte, byte, error){
|
|
|
|
hostConnectionMetadatasMapMutex.RLock()
|
|
connectionMetadataObject, exists := hostConnectionMetadatasMap[connectionIdentifier]
|
|
hostConnectionMetadatasMapMutex.RUnlock()
|
|
if (exists == false){
|
|
return false, [16]byte{}, [32]byte{}, 0, nil
|
|
}
|
|
|
|
hostIdentityHash := connectionMetadataObject.HostIdentityHash
|
|
connectionKey := connectionMetadataObject.ConnectionKey
|
|
networkType := connectionMetadataObject.NetworkType
|
|
|
|
return true, hostIdentityHash, connectionKey, networkType, nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Connection successful
|
|
// -[]byte: Response received
|
|
// -error
|
|
func SendRequestThroughConnection(connectionIdentifier [21]byte, contentToSend []byte)(bool, []byte, error){
|
|
|
|
connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier)
|
|
if (connectionExists == false){
|
|
return false, nil, nil
|
|
}
|
|
|
|
//TODO: Fix max response size
|
|
requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, contentToSend, 100000)
|
|
if (err != nil) { return false, nil, err }
|
|
if (requestSuccessful == false){
|
|
return false, nil, nil
|
|
}
|
|
|
|
return true, responseBytes, nil
|
|
}
|
|
|
|
func CloseConnection(connectionIdentifier [21]byte)error{
|
|
|
|
connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier)
|
|
if (connectionExists == false){
|
|
deleteMetadataFromConnectionMetadatasMap(connectionIdentifier)
|
|
return nil
|
|
}
|
|
|
|
_ = connectionObject.Close()
|
|
//TODO: Deal with error
|
|
|
|
deleteConnectionFromConnectionObjectsMap(connectionIdentifier)
|
|
deleteMetadataFromConnectionMetadatasMap(connectionIdentifier)
|
|
|
|
return nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Host profile found
|
|
// -bool: Connection established
|
|
// -[21]byte: The connection identifier
|
|
// -error
|
|
func EstablishNewConnectionToHost(allowClearnet bool, hostIdentityHash [16]byte, networkType byte)(bool, bool, [21]byte, error){
|
|
|
|
isValid := helpers.VerifyNetworkType(networkType)
|
|
if (isValid == false){
|
|
networkTypeString := helpers.ConvertByteToString(networkType)
|
|
return false, false, [21]byte{}, errors.New("EstablishNewConnectionToHost called with invalid networkType: " + networkTypeString)
|
|
}
|
|
|
|
getHostRawProfileMap := func()(bool, map[int]messagepack.RawMessage, error){
|
|
|
|
profileExists, _, _, _, _, hostRawProfileMap, err := viewableProfiles.GetNewestViewableUserProfile(hostIdentityHash, networkType, true, true, true)
|
|
if (err != nil) { return false, nil, err }
|
|
if (profileExists == true){
|
|
return true, hostRawProfileMap, nil
|
|
}
|
|
|
|
// Now we check to see if any host profile exists.
|
|
// This would be true if host's profile is banned.
|
|
// We will query banned hosts if there are not enough viewable hosts available
|
|
|
|
profileExists, _, _, _, _, hostRawProfileMap, err = profileStorage.GetNewestUserProfile(hostIdentityHash, networkType)
|
|
if (err != nil) { return false, nil, err }
|
|
if (profileExists == true){
|
|
return true, hostRawProfileMap, nil
|
|
}
|
|
|
|
return false, nil, nil
|
|
}
|
|
|
|
hostProfileExists, hostRawProfileMap, err := getHostRawProfileMap()
|
|
if (err != nil) { return false, false, [21]byte{}, err }
|
|
if (hostProfileExists == false){
|
|
// This should only happen if host profile was deleted after eligible hosts list was created
|
|
return false, false, [21]byte{}, nil
|
|
}
|
|
|
|
|
|
profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Disabled")
|
|
if (err != nil) { return false, false, [21]byte{}, err }
|
|
if (profileIsDisabled == true){
|
|
// Profile is disabled.
|
|
return false, false, [21]byte{}, nil
|
|
}
|
|
|
|
// This function will return the address and method we will use to contact the host
|
|
//
|
|
// If allowClearnet is false, function will only send request over Tor
|
|
// If allowClearnet is true, function will determine if we are in HostOverClearnet mode
|
|
// If we are, function will see if Host has a clearnet address and send request to that
|
|
// Otherwise, function will send request to host's .onion address over tor
|
|
//
|
|
// Sending over Tor prefers .onion address, but will connect to host's clearnet address over Tor exit node if onion address is offline or not provided
|
|
// Onion address could be offline due to DDOS attack, too many requests, or host network is blocking Tor access
|
|
// A host's client should automatically disable the .onion address if it is not reachable
|
|
// We will probably want to prefer connecting to clearnet URLs over Tor because .onion domains have become much slower due to recent spam attacks
|
|
//
|
|
//Outputs:
|
|
// -string: Method to use (Tor/Clearnet)
|
|
// -string: Address type (Tor/Clearnet)
|
|
// -string: Address to contact
|
|
// -int: Address port (if addressType is clearnet)
|
|
// -error
|
|
getHostAddressAndMethod := func()(string, string, string, int, error){
|
|
|
|
//Outputs:
|
|
// -bool: Host has tor address
|
|
// -string: Host tor address
|
|
// -error: Host
|
|
getHostTorAddress := func()(bool, string, error){
|
|
|
|
exists, hostTorAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "TorAddress")
|
|
if (err != nil) { return false, "", err }
|
|
if (exists == false) {
|
|
return false, "", nil
|
|
}
|
|
return true, hostTorAddress, nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Host clearnet address exists
|
|
// -string: Host clearnet address
|
|
// -int: Host clearnet port
|
|
// -error
|
|
getHostClearnetAddress := func()(bool, string, int, error){
|
|
|
|
exists, clearnetAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetAddress")
|
|
if (err != nil) { return false, "", 0, err }
|
|
if (exists == false) {
|
|
return false, "", 0, nil
|
|
}
|
|
|
|
exists, clearnetPort, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetPort")
|
|
if (err != nil) { return false, "", 0, err }
|
|
if (exists == false) {
|
|
return false, "", 0, errors.New("Host profile with ClearnetAddress missing ClearnetPort")
|
|
}
|
|
|
|
clearnetPortInt, err := helpers.ConvertStringToInt(clearnetPort)
|
|
if (err != nil) {
|
|
return false, "", 0, errors.New("Database corrupt: Contains host profile with invalid ClearnetPort: " + clearnetPort)
|
|
}
|
|
|
|
return true, clearnetAddress, clearnetPortInt, nil
|
|
}
|
|
|
|
hostHasTorAddress, hostTorAddress, err := getHostTorAddress()
|
|
if (err != nil) { return "", "", "", 0, err }
|
|
|
|
hostHasClearnetAddress, hostClearnetAddress, hostClearnetPort, err := getHostClearnetAddress()
|
|
if (err != nil) { return "", "", "", 0, err }
|
|
|
|
if (hostHasTorAddress == false && hostHasClearnetAddress == false){
|
|
return "", "", "", 0, errors.New("Database corrupt: Contains host profile missing Tor and Clearnet addresses.")
|
|
}
|
|
|
|
hostIsUnreachableClearnet, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Clearnet")
|
|
if (err != nil) { return "", "", "", 0, err }
|
|
|
|
hostIsUnreachableTor, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Tor")
|
|
if (err != nil) { return "", "", "", 0, err }
|
|
|
|
// If host is unreachable from both addresses, we will assume that eligibleHosts provided the host because there were not enough reachable hosts available
|
|
|
|
getHostIsFullyUnreachableStatus := func()bool{
|
|
|
|
if (hostIsUnreachableClearnet == true && hostIsUnreachableTor == true){
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
hostIsFullyUnreachable := getHostIsFullyUnreachableStatus()
|
|
|
|
if (allowClearnet == true && hostHasClearnetAddress == true){
|
|
|
|
checkIfHostOverClearnetModeIsEnabled := func()(bool, error){
|
|
|
|
exists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus")
|
|
if (err != nil) { return false, err }
|
|
if (exists == false){
|
|
return false, nil
|
|
}
|
|
|
|
if (hostModeOnOffStatus != "On"){
|
|
return false, nil
|
|
}
|
|
|
|
exists, hostOverClearnetStatus, err := mySettings.GetSetting("HostOverClearnetOnOffStatus")
|
|
if (err != nil) { return false, err }
|
|
if (exists == false){
|
|
return false, nil
|
|
}
|
|
if (hostOverClearnetStatus != "On"){
|
|
return false, nil
|
|
}
|
|
|
|
//TODO: Check for ModerateOverClearnet mode
|
|
|
|
return true, nil
|
|
}
|
|
|
|
hostOverClearnetModeEnabledStatus, err := checkIfHostOverClearnetModeIsEnabled()
|
|
if (err != nil) { return "", "", "", 0, err }
|
|
|
|
if (hostOverClearnetModeEnabledStatus == true){
|
|
|
|
if (hostIsUnreachableClearnet == false || hostIsFullyUnreachable == true){
|
|
|
|
return "Clearnet", "Clearnet", hostClearnetAddress, hostClearnetPort, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Either hostOverClearnet is disabled or recipient does not have reachable clearnet address
|
|
// We will send request over tor
|
|
// We prefer .onion address but will also try to access their clearnet address over tor if .onion is unreachable/nonexistent
|
|
|
|
if (hostHasTorAddress == true){
|
|
|
|
if (hostIsUnreachableTor == false || hostIsFullyUnreachable == true){
|
|
|
|
return "Tor", "Tor", hostTorAddress, 0, nil
|
|
}
|
|
}
|
|
|
|
return "Tor", "Clearnet", hostClearnetAddress, hostClearnetPort, nil
|
|
}
|
|
|
|
methodToUse, hostAddressType, hostAddress, addressPort, err := getHostAddressAndMethod()
|
|
if (err != nil) { return false, false, [21]byte{}, err }
|
|
|
|
getFormattedAddressToContact := func()(string, error){
|
|
|
|
if (hostAddressType == "Tor"){
|
|
return hostAddress, nil
|
|
}
|
|
hostIPAddressObject, err := netip.ParseAddr(hostAddress)
|
|
if (err == nil){
|
|
|
|
portString := helpers.ConvertIntToString(addressPort)
|
|
|
|
addressIsIpv6 := hostIPAddressObject.Is6()
|
|
if (addressIsIpv6 == false){
|
|
formattedAddress := hostAddress + ":" + portString
|
|
return formattedAddress, nil
|
|
}
|
|
formattedAddress := "[" + hostAddress + "]" + ":" + portString
|
|
return formattedAddress, nil
|
|
}
|
|
|
|
// Address must be a clearnet URL, rather than an IP address
|
|
addressObject, err := url.Parse(hostAddress)
|
|
if (err != nil){
|
|
// Address is not IP or clearnet. The profile is invalid.
|
|
return "", errors.New("Database corrupt: Contains host profile with invalid clearnetAddress: " + hostAddress)
|
|
}
|
|
|
|
addressHost := addressObject.Host
|
|
formattedAddress := addressHost + ":http"
|
|
|
|
return formattedAddress, nil
|
|
}
|
|
|
|
formattedAddressToContact, err := getFormattedAddressToContact()
|
|
if (err != nil){ return false, false, [21]byte{}, err }
|
|
|
|
//Outputs:
|
|
// -bool: Connection successful
|
|
// -net.Conn: Connection object
|
|
// -error
|
|
getConnectionObject := func()(bool, net.Conn, error){
|
|
|
|
if (methodToUse == "Tor"){
|
|
//TODO
|
|
return false, nil, nil
|
|
}
|
|
// methodToUse == "Clearnet"
|
|
|
|
newDialer := &net.Dialer{
|
|
Timeout: time.Minute,
|
|
}
|
|
connectionObject, err := newDialer.Dial("tcp", formattedAddressToContact)
|
|
if (err != nil) {
|
|
//TODO: Distinguish between network error and other types of errors
|
|
return false, nil, nil
|
|
}
|
|
return true, connectionObject, nil
|
|
}
|
|
|
|
connectionSuccessful, connectionObject, err := getConnectionObject()
|
|
if (err != nil) { return false, false, [21]byte{}, err }
|
|
if (connectionSuccessful == false){
|
|
return true, false, [21]byte{}, nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Handshake successful
|
|
// -[32]byte: Connection key
|
|
// -error
|
|
performKeyHandshake := func()(bool, [32]byte, error){
|
|
|
|
myNaclPublicKey, myNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys()
|
|
if (err != nil) { return false, [32]byte{}, err }
|
|
|
|
myKyberPublicKey, myKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys()
|
|
if (err != nil) { return false, [32]byte{}, err }
|
|
|
|
requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_EstablishConnectionKey(hostIdentityHash, networkType, myNaclPublicKey, myKyberPublicKey)
|
|
if (err != nil) { return false, [32]byte{}, err }
|
|
|
|
//TODO: Fix max response size
|
|
requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, requestBytes, 100000)
|
|
if (err != nil) { return false, [32]byte{}, err }
|
|
if (requestSuccessful == false){
|
|
return false, [32]byte{}, nil
|
|
}
|
|
|
|
//TODO: Add ability to handle busy response/different network type response
|
|
// If host is busy, we will keep track of that in a new package and not try to recontact them for a while
|
|
|
|
ableToRead, requestIdentifier_Received, hostIdentityHash_Received, connectionKey, err := serverResponse.ReadServerResponse_EstablishConnectionKey(responseBytes, myNaclPublicKey, myNaclPrivateKey, myKyberPrivateKey)
|
|
if (err != nil) { return false, [32]byte{}, err }
|
|
if (ableToRead == false){
|
|
// Peer sent invalid response, must be malicious
|
|
|
|
err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash)
|
|
if (err != nil){ return false, [32]byte{}, err }
|
|
|
|
return false, [32]byte{}, nil
|
|
}
|
|
|
|
checkIfResponseIsValid := func()bool{
|
|
|
|
if (requestIdentifier_Received != requestIdentifier){
|
|
return false
|
|
}
|
|
if (hostIdentityHash_Received != hostIdentityHash){
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
responseIsValid := checkIfResponseIsValid()
|
|
if (responseIsValid == false){
|
|
|
|
err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash)
|
|
if (err != nil){ return false, [32]byte{}, err }
|
|
}
|
|
|
|
return true, connectionKey, nil
|
|
}
|
|
|
|
handshakeSuccessful, connectionKey, err := performKeyHandshake()
|
|
if (err != nil){
|
|
_ = connectionObject.Close()
|
|
//TODO: Deal with possible errors
|
|
return false, false, [21]byte{}, err
|
|
}
|
|
if (handshakeSuccessful == false){
|
|
_ = connectionObject.Close()
|
|
//TODO: Deal with possible errors
|
|
return true, false, [21]byte{}, nil
|
|
}
|
|
|
|
newConnectionIdentifier, err := helpers.GetNewRandomBytes(21)
|
|
if (err != nil) { return false, false, [21]byte{}, err }
|
|
|
|
newConnectionIdentifierArray := [21]byte(newConnectionIdentifier)
|
|
|
|
addConnectionToConnectionObjectsMap(newConnectionIdentifierArray, connectionObject)
|
|
addMetadataToConnectionMetadatasMap(newConnectionIdentifierArray, hostIdentityHash, connectionKey, networkType)
|
|
|
|
return true, true, newConnectionIdentifierArray, nil
|
|
}
|
|
|
|
|
|
|