Merge "Introduce interface name field to SocketKey" into main
This commit is contained in:
commit
fff6a3fdcd
11 changed files with 90 additions and 42 deletions
|
|
@ -219,7 +219,7 @@ public class ConnectivityMonitorWithConnectivityManager implements ConnectivityM
|
|||
if (netIf == null) {
|
||||
return null;
|
||||
}
|
||||
return new SocketKey(match.first, netIf.getIndex());
|
||||
return new SocketKey(match.first, netIf.getIndex(), netIf.getName());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -229,7 +229,7 @@ public class ConnectivityMonitorWithConnectivityManager implements ConnectivityM
|
|||
}
|
||||
final Pair<Network, LinkProperties> match = findMatchingNetwork(
|
||||
lp -> iface.getName().equals(lp.getInterfaceName()));
|
||||
return match == null ? null : new SocketKey(match.first, iface.getIndex());
|
||||
return match == null ? null : new SocketKey(match.first, iface.getIndex(), iface.getName());
|
||||
}
|
||||
|
||||
private Pair<Network, LinkProperties> findMatchingNetwork(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import java.util.List;
|
|||
*/
|
||||
public class MdnsSocket {
|
||||
static final int INTERFACE_INDEX_UNSPECIFIED = -1;
|
||||
static final String INTERFACE_NAME_UNKNOWN = "";
|
||||
public static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
|
||||
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
|
||||
public static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
|
||||
|
|
@ -122,18 +123,35 @@ public class MdnsSocket {
|
|||
|
||||
/**
|
||||
* Returns the index of the network interface that this socket is bound to. If the interface
|
||||
* cannot be determined, returns -1.
|
||||
* cannot be determined, returns INTERFACE_INDEX_UNSPECIFIED.
|
||||
*/
|
||||
public int getInterfaceIndex() {
|
||||
if (multicastSocket.isClosed()) {
|
||||
sharedLog.e("Socket is closed");
|
||||
return -1;
|
||||
return INTERFACE_INDEX_UNSPECIFIED;
|
||||
}
|
||||
try {
|
||||
return multicastSocket.getNetworkInterface().getIndex();
|
||||
} catch (SocketException | NullPointerException e) {
|
||||
sharedLog.e("Failed to retrieve interface index for socket.", e);
|
||||
return -1;
|
||||
return INTERFACE_INDEX_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the network interface that this socket is bound to.
|
||||
*/
|
||||
@NonNull
|
||||
public String getInterfaceName() {
|
||||
if (multicastSocket.isClosed()) {
|
||||
sharedLog.e("Socket is closed");
|
||||
return INTERFACE_NAME_UNKNOWN;
|
||||
}
|
||||
try {
|
||||
return multicastSocket.getNetworkInterface().getName();
|
||||
} catch (SocketException | NullPointerException e) {
|
||||
sharedLog.e("Failed to retrieve interface name for socket.", e);
|
||||
return INTERFACE_NAME_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,8 @@ public class MdnsSocketClient implements MdnsSocketClientBase {
|
|||
throw new IllegalArgumentException("This socket client does not support requesting "
|
||||
+ "specific networks");
|
||||
}
|
||||
socketCreationCallback.onSocketCreated(new SocketKey(multicastSocket.getInterfaceIndex()));
|
||||
socketCreationCallback.onSocketCreated(new SocketKey(
|
||||
multicastSocket.getInterfaceIndex(), multicastSocket.getInterfaceName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -469,22 +470,29 @@ public class MdnsSocketClient implements MdnsSocketClientBase {
|
|||
? interfaceProvider.guessNetworkOfRemoteHost(packetAddress)
|
||||
: null;
|
||||
final int interfaceIndex;
|
||||
final String interfaceName;
|
||||
if (socket == null || !propagateInterfaceIndex) {
|
||||
interfaceIndex = MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
|
||||
interfaceName = MdnsSocket.INTERFACE_NAME_UNKNOWN;
|
||||
} else if (mdnsFeatureFlags.mIsSocketClientNetworkGuessingEnabled) {
|
||||
interfaceIndex = key == null
|
||||
? MdnsSocket.INTERFACE_INDEX_UNSPECIFIED
|
||||
: key.getInterfaceIndex();
|
||||
interfaceName = key == null
|
||||
? MdnsSocket.INTERFACE_NAME_UNKNOWN
|
||||
: key.getInterfaceName();
|
||||
} else {
|
||||
// Note this is incorrect: getInterfaceIndex is the last interface that sent
|
||||
// a packet, not the one that received the packet.
|
||||
interfaceIndex = socket.getInterfaceIndex();
|
||||
interfaceName = socket.getInterfaceName();
|
||||
}
|
||||
processResponsePacket(
|
||||
packet,
|
||||
responseType,
|
||||
interfaceIndex,
|
||||
key == null ? null : key.getNetwork());
|
||||
key == null ? null : key.getNetwork(),
|
||||
interfaceName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (!shouldStopSocketLoop) {
|
||||
|
|
@ -496,7 +504,7 @@ public class MdnsSocketClient implements MdnsSocketClientBase {
|
|||
}
|
||||
|
||||
private int processResponsePacket(@NonNull DatagramPacket packet, String responseType,
|
||||
int interfaceIndex, @Nullable Network network) {
|
||||
int interfaceIndex, @Nullable Network network, @NonNull String interfaceName) {
|
||||
int packetNumber = ++receivedPacketNumber;
|
||||
|
||||
final MdnsPacket response;
|
||||
|
|
@ -508,7 +516,7 @@ public class MdnsSocketClient implements MdnsSocketClientBase {
|
|||
responseType, packetNumber, e.code));
|
||||
if (callback != null) {
|
||||
callback.onFailedToParseMdnsResponse(packetNumber, e.code,
|
||||
new SocketKey(network, interfaceIndex));
|
||||
new SocketKey(network, interfaceIndex, interfaceName));
|
||||
}
|
||||
return e.code;
|
||||
}
|
||||
|
|
@ -519,7 +527,7 @@ public class MdnsSocketClient implements MdnsSocketClientBase {
|
|||
|
||||
if (callback != null) {
|
||||
callback.onResponseReceived(
|
||||
response, new SocketKey(network, interfaceIndex));
|
||||
response, new SocketKey(network, interfaceIndex, interfaceName));
|
||||
}
|
||||
|
||||
return MdnsResponseErrorCode.SUCCESS;
|
||||
|
|
|
|||
|
|
@ -539,7 +539,8 @@ public class MdnsSocketProvider {
|
|||
final List<LinkAddress> addresses = lp.getLinkAddresses();
|
||||
final Network network =
|
||||
networkKey == LOCAL_NET ? null : ((NetworkAsKey) networkKey).mNetwork;
|
||||
final SocketKey socketKey = new SocketKey(network, networkInterface.getIndex());
|
||||
final SocketKey socketKey = new SocketKey(
|
||||
network, networkInterface.getIndex(), networkInterface.getName());
|
||||
// TODO: technically transport types are mutable, although generally not in ways that
|
||||
// would meaningfully impact the logic using it here. Consider updating logic to
|
||||
// support transports being added/removed.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.net.Network;
|
||||
|
||||
|
|
@ -33,14 +34,21 @@ public class SocketKey {
|
|||
@Nullable
|
||||
private final Network mNetwork;
|
||||
private final int mInterfaceIndex;
|
||||
/**
|
||||
* The interface name is only for offload interface comparison and isn't used for equality or
|
||||
* hashing, which could lead to a behavior change.
|
||||
*/
|
||||
@NonNull
|
||||
private final String mInterfaceName;
|
||||
|
||||
SocketKey(int interfaceIndex) {
|
||||
this(null /* network */, interfaceIndex);
|
||||
SocketKey(int interfaceIndex, @NonNull String interfaceName) {
|
||||
this(null /* network */, interfaceIndex, interfaceName);
|
||||
}
|
||||
|
||||
SocketKey(@Nullable Network network, int interfaceIndex) {
|
||||
SocketKey(@Nullable Network network, int interfaceIndex, @NonNull String interfaceName) {
|
||||
mNetwork = network;
|
||||
mInterfaceIndex = interfaceIndex;
|
||||
mInterfaceName = interfaceName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -52,6 +60,11 @@ public class SocketKey {
|
|||
return mInterfaceIndex;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getInterfaceName() {
|
||||
return mInterfaceName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mNetwork, mInterfaceIndex);
|
||||
|
|
@ -68,6 +81,8 @@ public class SocketKey {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SocketKey{ network=" + mNetwork + " interfaceIndex=" + mInterfaceIndex + " }";
|
||||
return "SocketKey{ network=" + mNetwork
|
||||
+ " interfaceIndex=" + mInterfaceIndex
|
||||
+ " interfaceName=" + mInterfaceName + " }";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,11 +148,13 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
final Network testNetwork1 = mock(Network.class);
|
||||
final Network testNetwork2 = mock(Network.class);
|
||||
final int ifIndex1 = 1;
|
||||
final NetworkInterfaceWrapper iface1 = getTestInterface("iface1", ifIndex1);
|
||||
final NetworkInterfaceWrapper iface2 = getTestInterface("iface2", 2);
|
||||
final String ifName1 = "iface1";
|
||||
final String ifName2 = "iface2";
|
||||
final NetworkInterfaceWrapper iface1 = getTestInterface(ifName1, ifIndex1);
|
||||
final NetworkInterfaceWrapper iface2 = getTestInterface(ifName2, 2);
|
||||
|
||||
final LinkProperties lp1 = new LinkProperties();
|
||||
lp1.setInterfaceName("iface1");
|
||||
lp1.setInterfaceName(ifName1);
|
||||
lp1.addRoute(new RouteInfo(
|
||||
new IpPrefix("192.0.1.123/24"), null, lp1.getInterfaceName(), RTN_UNICAST));
|
||||
lp1.addRoute(new RouteInfo(
|
||||
|
|
@ -169,8 +171,9 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
callback.onLinkPropertiesChanged(testNetwork1, lp1);
|
||||
callback.onLinkPropertiesChanged(testNetwork2, lp2);
|
||||
|
||||
assertEquals(new SocketKey(testNetwork1, ifIndex1), monitor.guessNetworkOfRemoteHost(
|
||||
List.of(iface1, iface2), parseNumericAddress("192.0.1.124")));
|
||||
assertEquals(new SocketKey(testNetwork1, ifIndex1, ifName1),
|
||||
monitor.guessNetworkOfRemoteHost(
|
||||
List.of(iface1, iface2), parseNumericAddress("192.0.1.124")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -179,7 +182,8 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
|
||||
final Network testNetwork = mock(Network.class);
|
||||
final int ifIndex = 1;
|
||||
final NetworkInterfaceWrapper iface = getTestInterface("iface1", ifIndex);
|
||||
final String ifName = "iface1";
|
||||
final NetworkInterfaceWrapper iface = getTestInterface(ifName, ifIndex);
|
||||
|
||||
final LinkProperties lp = new LinkProperties();
|
||||
lp.setInterfaceName("iface1");
|
||||
|
|
@ -191,7 +195,7 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
|
||||
callback.onLinkPropertiesChanged(testNetwork, lp);
|
||||
|
||||
assertEquals(new SocketKey(testNetwork, ifIndex), monitor.guessNetworkOfRemoteHost(
|
||||
assertEquals(new SocketKey(testNetwork, ifIndex, ifName), monitor.guessNetworkOfRemoteHost(
|
||||
List.of(iface), parseNumericAddress("169.254.1.2")));
|
||||
}
|
||||
|
||||
|
|
@ -201,8 +205,9 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
|
||||
final Network testNetwork1 = mock(Network.class);
|
||||
final Network testNetwork2 = mock(Network.class);
|
||||
final String interfaceName = "lo";
|
||||
final NetworkInterfaceWrapper loIface = new NetworkInterfaceWrapper(
|
||||
NetworkInterface.getByName("lo"));
|
||||
NetworkInterface.getByName(interfaceName));
|
||||
final NetworkInterfaceWrapper wrongIface = getTestInterface(
|
||||
"wrongiface", loIface.getIndex() + 1);
|
||||
|
||||
|
|
@ -220,7 +225,7 @@ public class ConnectivityMonitorWithConnectivityManagerTests {
|
|||
callback.onLinkPropertiesChanged(testNetwork1, lp1);
|
||||
callback.onLinkPropertiesChanged(testNetwork2, lp2);
|
||||
|
||||
assertEquals(new SocketKey(testNetwork2, loIface.getIndex()),
|
||||
assertEquals(new SocketKey(testNetwork2, loIface.getIndex(), interfaceName),
|
||||
monitor.guessNetworkOfRemoteHost(List.of(wrongIface, loIface),
|
||||
parseNumericAddress("fe80::124%lo")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ private val TEST_ADDR2 = parseNumericAddress("2001:db8::124")
|
|||
private val TEST_LINKADDR = LinkAddress(TEST_ADDR, 64 /* prefixLength */)
|
||||
private val TEST_LINKADDR2 = LinkAddress(TEST_ADDR2, 64 /* prefixLength */)
|
||||
private val TEST_NETWORK_1 = mock(Network::class.java)
|
||||
private val TEST_SOCKETKEY_1 = SocketKey(1001 /* interfaceIndex */)
|
||||
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
|
||||
private val TEST_SOCKETKEY_1 = SocketKey(1001 /* interfaceIndex */, "testInterface1")
|
||||
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */, "testInterface2")
|
||||
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
|
||||
private const val TEST_SUBTYPE = "_subtype"
|
||||
private const val TEST_SUBTYPE2 = "_subtype2"
|
||||
|
|
|
|||
|
|
@ -69,11 +69,11 @@ public class MdnsDiscoveryManagerTests {
|
|||
private static final Network NETWORK_2 = Mockito.mock(Network.class);
|
||||
private static final int INTERFACE_INDEX_NULL_NETWORK = 123;
|
||||
private static final SocketKey SOCKET_KEY_NULL_NETWORK =
|
||||
new SocketKey(null /* network */, INTERFACE_INDEX_NULL_NETWORK);
|
||||
new SocketKey(INTERFACE_INDEX_NULL_NETWORK, "interface");
|
||||
private static final SocketKey SOCKET_KEY_NETWORK_1 =
|
||||
new SocketKey(NETWORK_1, 998 /* interfaceIndex */);
|
||||
new SocketKey(NETWORK_1, 998 /* interfaceIndex */, "interface1");
|
||||
private static final SocketKey SOCKET_KEY_NETWORK_2 =
|
||||
new SocketKey(NETWORK_2, 997 /* interfaceIndex */);
|
||||
new SocketKey(NETWORK_2, 997 /* interfaceIndex */, "interface2");
|
||||
private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK =
|
||||
Pair.create(SERVICE_TYPE_1, SOCKET_KEY_NULL_NETWORK);
|
||||
private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK =
|
||||
|
|
@ -404,9 +404,10 @@ public class MdnsDiscoveryManagerTests {
|
|||
|
||||
final SocketCreationCallback callback = expectSocketCreationCallback(
|
||||
SERVICE_TYPE_1, mockListenerOne, searchOptions);
|
||||
final SocketKey unusedIfaceKey = new SocketKey(null, INTERFACE_INDEX_NULL_NETWORK + 1);
|
||||
final SocketKey matchingIfaceWithNetworkKey =
|
||||
new SocketKey(Mockito.mock(Network.class), INTERFACE_INDEX_NULL_NETWORK);
|
||||
final SocketKey unusedIfaceKey = new SocketKey(
|
||||
INTERFACE_INDEX_NULL_NETWORK + 1, "interfaceOther");
|
||||
final SocketKey matchingIfaceWithNetworkKey = new SocketKey(
|
||||
Mockito.mock(Network.class), INTERFACE_INDEX_NULL_NETWORK, "interface");
|
||||
runOnHandler(() -> {
|
||||
callback.onSocketCreated(unusedIfaceKey);
|
||||
callback.onSocketCreated(matchingIfaceWithNetworkKey);
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
|
||||
mHandlerThread.start();
|
||||
mHandler = new Handler(mHandlerThread.getLooper());
|
||||
mSocketKey = new SocketKey(1000 /* interfaceIndex */);
|
||||
mSocketKey = new SocketKey(1000 /* interfaceIndex */, "interface");
|
||||
mSocketClient = new MdnsMultinetworkSocketClient(mHandlerThread.getLooper(), mProvider,
|
||||
mSharedLog, MdnsFeatureFlags.newBuilder().build());
|
||||
mHandler.post(() -> mSocketClient.setCallback(mCallback));
|
||||
|
|
@ -150,8 +150,8 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
doReturn(createEmptyNetworkInterface()).when(socket).getInterface();
|
||||
}
|
||||
|
||||
final SocketKey tetherSocketKey1 = new SocketKey(1001 /* interfaceIndex */);
|
||||
final SocketKey tetherSocketKey2 = new SocketKey(1002 /* interfaceIndex */);
|
||||
final SocketKey tetherSocketKey1 = new SocketKey(1001 /* interfaceIndex */, "interface1");
|
||||
final SocketKey tetherSocketKey2 = new SocketKey(1002 /* interfaceIndex */, "interface2");
|
||||
// Notify socket created
|
||||
callback.onSocketCreated(mSocketKey, mSocket, List.of());
|
||||
verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
|
||||
|
|
@ -263,8 +263,8 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
doReturn(createEmptyNetworkInterface()).when(socket2).getInterface();
|
||||
doReturn(createEmptyNetworkInterface()).when(socket3).getInterface();
|
||||
|
||||
final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
|
||||
final SocketKey socketKey3 = new SocketKey(1002 /* interfaceIndex */);
|
||||
final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */, "interface1");
|
||||
final SocketKey socketKey3 = new SocketKey(1002 /* interfaceIndex */, "interface2");
|
||||
callback.onSocketCreated(mSocketKey, mSocket, List.of());
|
||||
callback.onSocketCreated(socketKey2, socket2, List.of());
|
||||
callback.onSocketCreated(socketKey3, socket3, List.of());
|
||||
|
|
@ -338,7 +338,7 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
@Test
|
||||
public void testNotifyNetworkUnrequested_SocketsOnNullNetwork() {
|
||||
final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
|
||||
final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */);
|
||||
final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */, "interface1");
|
||||
final SocketCallback callback = expectSocketCallback(
|
||||
mListener, null /* requestedNetwork */);
|
||||
doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
|
||||
|
|
@ -364,7 +364,7 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
@Test
|
||||
public void testSocketCreatedAndDestroyed_NullNetwork() throws IOException {
|
||||
final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
|
||||
final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */);
|
||||
final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */, "interface1");
|
||||
final SocketCallback callback = expectSocketCallback(mListener, null /* network */);
|
||||
doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
|
||||
doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
|
||||
|
|
@ -386,7 +386,7 @@ public class MdnsMultinetworkSocketClientTest {
|
|||
@Test
|
||||
public void testSocketDestroyed_MultipleCallbacks() {
|
||||
final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
|
||||
final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
|
||||
final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */, "interface1");
|
||||
final SocketCreationCallback creationCallback1 = mock(SocketCreationCallback.class);
|
||||
final SocketCreationCallback creationCallback2 = mock(SocketCreationCallback.class);
|
||||
final SocketCreationCallback creationCallback3 = mock(SocketCreationCallback.class);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ private const val DEFAULT_TTL_TIME_MS = 120000L
|
|||
@RunWith(DevSdkIgnoreRunner::class)
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
|
||||
class MdnsServiceCacheTest {
|
||||
private val socketKey = SocketKey(null /* network */, INTERFACE_INDEX)
|
||||
private val socketKey = SocketKey(INTERFACE_INDEX, "interface")
|
||||
private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
|
||||
private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
|
||||
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ public class MdnsServiceTypeClientTests {
|
|||
|
||||
expectedIPv4Packets = new DatagramPacket[24];
|
||||
expectedIPv6Packets = new DatagramPacket[24];
|
||||
socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX);
|
||||
socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX, "interface");
|
||||
|
||||
for (int i = 0; i < expectedIPv4Packets.length; ++i) {
|
||||
expectedIPv4Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue