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