322f75c5a2
Motivation: This fixes a bug that would result in an `io.netty.channel.unix.Errors$NativeIoException: connectx(..) failed: Address family not supported by protocol family` error. This happens when the connecting socket is configured to use IPv6 but the address being connected to is IPv4. This can occur because, for instance, Netty and `InetAddress.getLoopbackAddress()` have different preferences for IPv6 vs. IPv4. Modification: Pass the correct ipv6 or ipv4 flags to connectx, depending on whether the socket was created for AF_INET or AF_INET6, rather than relying on the IP version of the destination address. Result: No more issue with TCP FastOpen on MacOS when using addresses of the "wrong" IP version.
235 lines
9.4 KiB
Java
235 lines
9.4 KiB
Java
/*
|
|
* Copyright 2016 The Netty Project
|
|
*
|
|
* The Netty Project licenses this file to you 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:
|
|
*
|
|
* https://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 io.netty.channel.kqueue;
|
|
|
|
import io.netty.channel.DefaultFileRegion;
|
|
import io.netty.channel.unix.IovArray;
|
|
import io.netty.channel.unix.PeerCredentials;
|
|
import io.netty.channel.unix.Socket;
|
|
|
|
import java.io.IOException;
|
|
import java.net.Inet6Address;
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
|
|
import static io.netty.channel.kqueue.AcceptFilter.PLATFORM_UNSUPPORTED;
|
|
import static io.netty.channel.kqueue.Native.CONNECT_TCP_FASTOPEN;
|
|
import static io.netty.channel.unix.Errors.ERRNO_EINPROGRESS_NEGATIVE;
|
|
import static io.netty.channel.unix.Errors.ioResult;
|
|
import static io.netty.channel.unix.NativeInetAddress.ipv4MappedIpv6Address;
|
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
|
|
|
/**
|
|
* A socket which provides access BSD native methods.
|
|
*/
|
|
final class BsdSocket extends Socket {
|
|
|
|
// These limits are just based on observations. I couldn't find anything in header files which formally
|
|
// define these limits.
|
|
private static final int APPLE_SND_LOW_AT_MAX = 1 << 17;
|
|
private static final int FREEBSD_SND_LOW_AT_MAX = 1 << 15;
|
|
static final int BSD_SND_LOW_AT_MAX = Math.min(APPLE_SND_LOW_AT_MAX, FREEBSD_SND_LOW_AT_MAX);
|
|
/**
|
|
* The `endpoints` structure passed to `connectx(2)` has an optional "source interface" field,
|
|
* which is the index of the network interface to use.
|
|
* According to `if_nametoindex(3)`, the value 0 is used when no interface is specified.
|
|
*/
|
|
private static final int UNSPECIFIED_SOURCE_INTERFACE = 0;
|
|
|
|
BsdSocket(int fd) {
|
|
super(fd);
|
|
}
|
|
|
|
void setAcceptFilter(AcceptFilter acceptFilter) throws IOException {
|
|
setAcceptFilter(intValue(), acceptFilter.filterName(), acceptFilter.filterArgs());
|
|
}
|
|
|
|
void setTcpNoPush(boolean tcpNoPush) throws IOException {
|
|
setTcpNoPush(intValue(), tcpNoPush ? 1 : 0);
|
|
}
|
|
|
|
void setSndLowAt(int lowAt) throws IOException {
|
|
setSndLowAt(intValue(), lowAt);
|
|
}
|
|
|
|
boolean isTcpNoPush() throws IOException {
|
|
return getTcpNoPush(intValue()) != 0;
|
|
}
|
|
|
|
int getSndLowAt() throws IOException {
|
|
return getSndLowAt(intValue());
|
|
}
|
|
|
|
AcceptFilter getAcceptFilter() throws IOException {
|
|
String[] result = getAcceptFilter(intValue());
|
|
return result == null ? PLATFORM_UNSUPPORTED : new AcceptFilter(result[0], result[1]);
|
|
}
|
|
|
|
PeerCredentials getPeerCredentials() throws IOException {
|
|
return getPeerCredentials(intValue());
|
|
}
|
|
|
|
long sendFile(DefaultFileRegion src, long baseOffset, long offset, long length) throws IOException {
|
|
// Open the file-region as it may be created via the lazy constructor. This is needed as we directly access
|
|
// the FileChannel field via JNI.
|
|
src.open();
|
|
|
|
long res = sendFile(intValue(), src, baseOffset, offset, length);
|
|
if (res >= 0) {
|
|
return res;
|
|
}
|
|
return ioResult("sendfile", (int) res);
|
|
}
|
|
|
|
/**
|
|
* Establish a connection to the given destination address, and send the given data to it.
|
|
*
|
|
* <strong>Note:</strong> This method relies on the {@code connectx(2)} system call, which is MacOS specific.
|
|
*
|
|
* @param source the source address we are connecting from.
|
|
* @param destination the destination address we are connecting to.
|
|
* @param data the data to copy to the kernel-side socket buffer.
|
|
* @param tcpFastOpen if {@code true}, set the flags needed to enable TCP FastOpen connecting.
|
|
* @return The number of bytes copied to the kernel-side socket buffer, or the number of bytes sent to the
|
|
* destination. This number is <em>negative</em> if connecting is left in an in-progress state,
|
|
* or <em>positive</em> if the connection was immediately established.
|
|
* @throws IOException if an IO error occurs, if the {@code data} is too big to send in one go,
|
|
* or if the system call is not supported on your platform.
|
|
*/
|
|
int connectx(InetSocketAddress source, InetSocketAddress destination, IovArray data, boolean tcpFastOpen)
|
|
throws IOException {
|
|
checkNotNull(destination, "Destination InetSocketAddress cannot be null.");
|
|
int flags = tcpFastOpen ? CONNECT_TCP_FASTOPEN : 0;
|
|
|
|
boolean sourceIPv6;
|
|
byte[] sourceAddress;
|
|
int sourceScopeId;
|
|
int sourcePort;
|
|
if (source == null) {
|
|
sourceIPv6 = false;
|
|
sourceAddress = null;
|
|
sourceScopeId = 0;
|
|
sourcePort = 0;
|
|
} else {
|
|
InetAddress sourceInetAddress = source.getAddress();
|
|
sourceIPv6 = useIpv6(this, sourceInetAddress);
|
|
if (sourceInetAddress instanceof Inet6Address) {
|
|
sourceAddress = sourceInetAddress.getAddress();
|
|
sourceScopeId = ((Inet6Address) sourceInetAddress).getScopeId();
|
|
} else {
|
|
// convert to ipv4 mapped ipv6 address;
|
|
sourceScopeId = 0;
|
|
sourceAddress = ipv4MappedIpv6Address(sourceInetAddress.getAddress());
|
|
}
|
|
sourcePort = source.getPort();
|
|
}
|
|
|
|
InetAddress destinationInetAddress = destination.getAddress();
|
|
boolean destinationIPv6 = useIpv6(this, destinationInetAddress);
|
|
byte[] destinationAddress;
|
|
int destinationScopeId;
|
|
if (destinationInetAddress instanceof Inet6Address) {
|
|
destinationAddress = destinationInetAddress.getAddress();
|
|
destinationScopeId = ((Inet6Address) destinationInetAddress).getScopeId();
|
|
} else {
|
|
// convert to ipv4 mapped ipv6 address;
|
|
destinationScopeId = 0;
|
|
destinationAddress = ipv4MappedIpv6Address(destinationInetAddress.getAddress());
|
|
}
|
|
int destinationPort = destination.getPort();
|
|
|
|
long iovAddress;
|
|
int iovCount;
|
|
int iovDataLength;
|
|
if (data == null || data.count() == 0) {
|
|
iovAddress = 0;
|
|
iovCount = 0;
|
|
iovDataLength = 0;
|
|
} else {
|
|
iovAddress = data.memoryAddress(0);
|
|
iovCount = data.count();
|
|
long size = data.size();
|
|
if (size > Integer.MAX_VALUE) {
|
|
throw new IOException("IovArray.size() too big: " + size + " bytes.");
|
|
}
|
|
iovDataLength = (int) size;
|
|
}
|
|
|
|
int result = connectx(intValue(),
|
|
UNSPECIFIED_SOURCE_INTERFACE, sourceIPv6, sourceAddress, sourceScopeId, sourcePort,
|
|
destinationIPv6, destinationAddress, destinationScopeId, destinationPort,
|
|
flags, iovAddress, iovCount, iovDataLength);
|
|
if (result == ERRNO_EINPROGRESS_NEGATIVE) {
|
|
// This is normal for non-blocking sockets.
|
|
// We'll know the connection has been established when the socket is selectable for writing.
|
|
// Tell the channel the data was written, so the outbound buffer can update its position.
|
|
return -iovDataLength;
|
|
}
|
|
if (result < 0) {
|
|
return ioResult("connectx", result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static BsdSocket newSocketStream() {
|
|
return new BsdSocket(newSocketStream0());
|
|
}
|
|
|
|
public static BsdSocket newSocketDgram() {
|
|
return new BsdSocket(newSocketDgram0());
|
|
}
|
|
|
|
public static BsdSocket newSocketDomain() {
|
|
return new BsdSocket(newSocketDomain0());
|
|
}
|
|
|
|
public static BsdSocket newSocketDomainDgram() {
|
|
return new BsdSocket(newSocketDomainDgram0());
|
|
}
|
|
|
|
private static native long sendFile(int socketFd, DefaultFileRegion src, long baseOffset,
|
|
long offset, long length) throws IOException;
|
|
|
|
/**
|
|
* @return If successful, zero or positive number of bytes transfered, otherwise negative errno.
|
|
*/
|
|
private static native int connectx(
|
|
int socketFd,
|
|
// sa_endpoints_t *endpoints:
|
|
int sourceInterface,
|
|
boolean sourceIPv6, byte[] sourceAddress, int sourceScopeId, int sourcePort,
|
|
boolean destinationIPv6, byte[] destinationAddress, int destinationScopeId, int destinationPort,
|
|
// sae_associd_t associd is reserved
|
|
int flags,
|
|
long iovAddress, int iovCount, int iovDataLength
|
|
// sae_connid_t *connid is reserved
|
|
);
|
|
|
|
private static native String[] getAcceptFilter(int fd) throws IOException;
|
|
|
|
private static native int getTcpNoPush(int fd) throws IOException;
|
|
|
|
private static native int getSndLowAt(int fd) throws IOException;
|
|
|
|
private static native PeerCredentials getPeerCredentials(int fd) throws IOException;
|
|
|
|
private static native void setAcceptFilter(int fd, String filterName, String filterArgs) throws IOException;
|
|
|
|
private static native void setTcpNoPush(int fd, int tcpNoPush) throws IOException;
|
|
|
|
private static native void setSndLowAt(int fd, int lowAt) throws IOException;
|
|
}
|