seekia/internal/network/peerClient/peerClient.go

504 lines
17 KiB
Go
Raw Normal View History

// 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
}