
com.hazelcast.client.impl.spi.impl.TranslateToPublicAddressProvider Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.client.impl.spi.impl;
import com.hazelcast.client.config.ClientNetworkConfig;
import com.hazelcast.client.properties.ClientProperty;
import com.hazelcast.cluster.Address;
import com.hazelcast.cluster.InitialMembershipEvent;
import com.hazelcast.cluster.InitialMembershipListener;
import com.hazelcast.cluster.Member;
import com.hazelcast.cluster.MembershipEvent;
import com.hazelcast.config.SSLConfig;
import com.hazelcast.instance.EndpointQualifier;
import com.hazelcast.instance.ProtocolType;
import com.hazelcast.internal.util.AddressUtil;
import com.hazelcast.logging.ILogger;
import com.hazelcast.spi.properties.HazelcastProperties;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.BooleanSupplier;
public class TranslateToPublicAddressProvider implements InitialMembershipListener, BooleanSupplier {
private static final int REACHABLE_ADDRESS_TIMEOUT_MILLIS = 1000;
private static final int NON_REACHABLE_ADDRESS_TIMEOUT_MILLIS = 3000;
private static final int REACHABLE_CHECK_NUMBER = 3;
private static final EndpointQualifier CLIENT_PUBLIC_ENDPOINT_QUALIFIER =
EndpointQualifier.resolve(ProtocolType.CLIENT, "public");
private final ClientNetworkConfig config;
private final HazelcastProperties properties;
private final ILogger logger;
private volatile boolean translateToPublicAddress;
public TranslateToPublicAddressProvider(ClientNetworkConfig config, HazelcastProperties properties, ILogger logger) {
this.config = config;
this.properties = properties;
this.logger = logger;
}
private boolean resolve(Collection members) {
// Default value of DISCOVERY_SPI_PUBLIC_IP_ENABLED is `null` intentionally.
// If DISCOVERY_SPI_PUBLIC_IP_ENABLED is not set to true/false, we don't know the intention of the user,
// we will try to decide if we should use private/public address automatically in that case.
String publicIpEnabledProperty = properties.getString(ClientProperty.DISCOVERY_SPI_PUBLIC_IP_ENABLED);
if (publicIpEnabledProperty == null) {
SSLConfig sslConfig = config.getSSLConfig();
// When ssl is enabled, we don't want to check if addresses are accessible and return false with a log.
// Because when client tries to check if addresses are reachable, because of SSL handshakes the members prints
// too many warnings which will alarm the users even if the behaviour is expected.
if (sslConfig != null && sslConfig.isEnabled()) {
if (logger.isFineEnabled()) {
logger.fine("SSL is configured. The client will use internal addresses to communicate with the cluster. If "
+ "members are not reachable via private addresses, "
+ "please set \"hazelcast.discovery.public.ip.enabled\" to true ");
}
return false;
}
if (memberInternalAddressAsDefinedInClientConfig(members)) {
if (logger.isFineEnabled()) {
logger.fine("There are internal addresses of members used in the config."
+ " The client will use internal addresses");
}
return false;
}
return membersReachableOnlyViaPublicAddress(members);
}
return properties.getBoolean(ClientProperty.DISCOVERY_SPI_PUBLIC_IP_ENABLED);
}
/**
* Checks if any member has its internal address as configured in ClientConfig.
*
* If any member has its internal/private address the same as configured in ClientConfig, then it means that the client is
* able to connect to members via configured address. No need to use make any address translation.
*/
boolean memberInternalAddressAsDefinedInClientConfig(Collection members) {
List addresses = config.getAddresses();
List resolvedHosts = addresses.stream().map(s -> {
try {
return InetAddress.getByName(AddressUtil.getAddressHolder(s, -1).getAddress()).getHostAddress();
} catch (UnknownHostException e) {
return null;
}
}).filter(Objects::nonNull).toList();
return members.stream()
.map(memberInfo -> {
try {
return memberInfo.getAddress().getInetAddress().getHostAddress();
} catch (UnknownHostException e) {
return null;
}
}).anyMatch(resolvedHosts::contains);
}
/**
* Checks if members are reachable via public addresses, but not reachable via internal addresses.
*
* We check only limited number of random members to reduce the slowdown of the startup.
*/
private boolean membersReachableOnlyViaPublicAddress(Collection members) {
List shuffledList = new ArrayList<>(members);
Collections.shuffle(shuffledList);
Iterator iter = shuffledList.iterator();
for (int i = 0; i < REACHABLE_CHECK_NUMBER; i++) {
if (!iter.hasNext()) {
iter = shuffledList.iterator();
}
Member member = iter.next();
Address publicAddress = member.getAddressMap().get(CLIENT_PUBLIC_ENDPOINT_QUALIFIER);
Address internalAddress = member.getAddress();
if (publicAddress == null) {
if (logger.isFineEnabled()) {
logger.fine("The public address is not available on the member. The client will use internal addresses");
}
return false;
}
if (isReachable(internalAddress, REACHABLE_ADDRESS_TIMEOUT_MILLIS)) {
if (logger.isFineEnabled()) {
logger.fine("The internal address is reachable. The client will use the internal addresses");
}
return false;
}
if (!isReachable(publicAddress, NON_REACHABLE_ADDRESS_TIMEOUT_MILLIS)) {
if (logger.isFineEnabled()) {
logger.fine("Public address + " + publicAddress
+ " is not reachable. The client will use internal addresses");
}
return false;
}
}
if (logger.isFineEnabled()) {
logger.fine("Members are accessible via only public addresses. The client will use public addresses");
}
return true;
}
private boolean isReachable(Address address, int timeoutMs) {
try (Socket s = new Socket()) {
s.connect(new InetSocketAddress(address.getHost(), address.getPort()), timeoutMs);
} catch (Exception e) {
if (logger.isFineEnabled()) {
logger.fine("TranslateToPublicAddressProvider can not reach to address " + address
+ " in " + timeoutMs + " millis", e);
}
return false;
}
return true;
}
@Override
public void init(InitialMembershipEvent event) {
translateToPublicAddress = resolve(event.getMembers());
}
@Override
public void memberAdded(MembershipEvent membershipEvent) {
}
@Override
public void memberRemoved(MembershipEvent membershipEvent) {
}
@Override
public boolean getAsBoolean() {
return translateToPublicAddress;
}
}