• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server;
18 
19 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
20 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.mockito.ArgumentMatchers.anyInt;
26 import static org.mockito.ArgumentMatchers.anyString;
27 import static org.mockito.Mockito.any;
28 import static org.mockito.Mockito.doReturn;
29 import static org.mockito.Mockito.eq;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.reset;
33 import static org.mockito.Mockito.timeout;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 
38 import android.compat.testing.PlatformCompatChangeRule;
39 import android.content.ContentResolver;
40 import android.content.Context;
41 import android.net.INetd;
42 import android.net.InetAddresses;
43 import android.net.mdns.aidl.DiscoveryInfo;
44 import android.net.mdns.aidl.GetAddressInfo;
45 import android.net.mdns.aidl.IMDnsEventListener;
46 import android.net.mdns.aidl.ResolutionInfo;
47 import android.net.nsd.INsdManagerCallback;
48 import android.net.nsd.INsdServiceConnector;
49 import android.net.nsd.MDnsManager;
50 import android.net.nsd.NsdManager;
51 import android.net.nsd.NsdServiceInfo;
52 import android.os.Binder;
53 import android.os.Build;
54 import android.os.Handler;
55 import android.os.HandlerThread;
56 import android.os.IBinder;
57 import android.os.Looper;
58 import android.os.Message;
59 
60 import androidx.annotation.NonNull;
61 import androidx.test.filters.SmallTest;
62 
63 import com.android.testutils.DevSdkIgnoreRule;
64 import com.android.testutils.DevSdkIgnoreRunner;
65 import com.android.testutils.HandlerUtils;
66 
67 import org.junit.After;
68 import org.junit.Before;
69 import org.junit.Rule;
70 import org.junit.Test;
71 import org.junit.rules.TestRule;
72 import org.junit.runner.RunWith;
73 import org.mockito.AdditionalAnswers;
74 import org.mockito.ArgumentCaptor;
75 import org.mockito.Mock;
76 import org.mockito.MockitoAnnotations;
77 
78 import java.util.LinkedList;
79 import java.util.Queue;
80 
81 // TODOs:
82 //  - test client can send requests and receive replies
83 //  - test NSD_ON ENABLE/DISABLED listening
84 @RunWith(DevSdkIgnoreRunner.class)
85 @SmallTest
86 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
87 public class NsdServiceTest {
88 
89     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
90     private static final long CLEANUP_DELAY_MS = 500;
91     private static final long TIMEOUT_MS = 500;
92 
93     // Records INsdManagerCallback created when NsdService#connect is called.
94     // Only accessed on the test thread, since NsdService#connect is called by the NsdManager
95     // constructor called on the test thread.
96     private final Queue<INsdManagerCallback> mCreatedCallbacks = new LinkedList<>();
97 
98     @Rule
99     public TestRule compatChangeRule = new PlatformCompatChangeRule();
100     @Mock Context mContext;
101     @Mock ContentResolver mResolver;
102     @Mock MDnsManager mMockMDnsM;
103     HandlerThread mThread;
104     TestHandler mHandler;
105 
106     private static class LinkToDeathRecorder extends Binder {
107         IBinder.DeathRecipient mDr;
108 
109         @Override
linkToDeath(@onNull DeathRecipient recipient, int flags)110         public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
111             super.linkToDeath(recipient, flags);
112             mDr = recipient;
113         }
114     }
115 
116     @Before
setUp()117     public void setUp() throws Exception {
118         MockitoAnnotations.initMocks(this);
119         mThread = new HandlerThread("mock-service-handler");
120         mThread.start();
121         mHandler = new TestHandler(mThread.getLooper());
122         when(mContext.getContentResolver()).thenReturn(mResolver);
123         doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
124                 .getSystemServiceName(MDnsManager.class);
125         doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
126         doReturn(true).when(mMockMDnsM).registerService(
127                 anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
128         doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
129         doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt());
130         doReturn(true).when(mMockMDnsM).resolve(
131                 anyInt(), anyString(), anyString(), anyString(), anyInt());
132     }
133 
134     @After
tearDown()135     public void tearDown() throws Exception {
136         if (mThread != null) {
137             mThread.quit();
138             mThread = null;
139         }
140     }
141 
142     @Test
143     @DisableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
testPreSClients()144     public void testPreSClients() throws Exception {
145         NsdService service = makeService();
146 
147         // Pre S client connected, the daemon should be started.
148         connectClient(service);
149         waitForIdle();
150         final INsdManagerCallback cb1 = getCallback();
151         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
152         verify(mMockMDnsM, times(1)).registerEventListener(any());
153         verify(mMockMDnsM, times(1)).startDaemon();
154 
155         connectClient(service);
156         waitForIdle();
157         final INsdManagerCallback cb2 = getCallback();
158         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
159         // Daemon has been started, it should not try to start it again.
160         verify(mMockMDnsM, times(1)).registerEventListener(any());
161         verify(mMockMDnsM, times(1)).startDaemon();
162 
163         deathRecipient1.binderDied();
164         // Still 1 client remains, daemon shouldn't be stopped.
165         waitForIdle();
166         verify(mMockMDnsM, never()).stopDaemon();
167 
168         deathRecipient2.binderDied();
169         // All clients are disconnected, the daemon should be stopped.
170         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
171     }
172 
173     @Test
174     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
testNoDaemonStartedWhenClientsConnect()175     public void testNoDaemonStartedWhenClientsConnect() throws Exception {
176         final NsdService service = makeService();
177 
178         // Creating an NsdManager will not cause daemon startup.
179         connectClient(service);
180         waitForIdle();
181         verify(mMockMDnsM, never()).registerEventListener(any());
182         verify(mMockMDnsM, never()).startDaemon();
183         final INsdManagerCallback cb1 = getCallback();
184         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
185 
186         // Creating another NsdManager will not cause daemon startup either.
187         connectClient(service);
188         waitForIdle();
189         verify(mMockMDnsM, never()).registerEventListener(any());
190         verify(mMockMDnsM, never()).startDaemon();
191         final INsdManagerCallback cb2 = getCallback();
192         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
193 
194         // If there is no active request, try to clean up the daemon but should not do it because
195         // daemon has not been started.
196         deathRecipient1.binderDied();
197         verify(mMockMDnsM, never()).unregisterEventListener(any());
198         verify(mMockMDnsM, never()).stopDaemon();
199         deathRecipient2.binderDied();
200         verify(mMockMDnsM, never()).unregisterEventListener(any());
201         verify(mMockMDnsM, never()).stopDaemon();
202     }
203 
verifyLinkToDeath(INsdManagerCallback cb)204     private IBinder.DeathRecipient verifyLinkToDeath(INsdManagerCallback cb)
205             throws Exception {
206         final IBinder.DeathRecipient dr = ((LinkToDeathRecorder) cb.asBinder()).mDr;
207         assertNotNull(dr);
208         return dr;
209     }
210 
211     @Test
212     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
testClientRequestsAreGCedAtDisconnection()213     public void testClientRequestsAreGCedAtDisconnection() throws Exception {
214         NsdService service = makeService();
215 
216         NsdManager client = connectClient(service);
217         waitForIdle();
218         final INsdManagerCallback cb1 = getCallback();
219         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
220         verify(mMockMDnsM, never()).registerEventListener(any());
221         verify(mMockMDnsM, never()).startDaemon();
222 
223         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
224         request.setPort(2201);
225 
226         // Client registration request
227         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
228         client.registerService(request, PROTOCOL, listener1);
229         waitForIdle();
230         verify(mMockMDnsM, times(1)).registerEventListener(any());
231         verify(mMockMDnsM, times(1)).startDaemon();
232         verify(mMockMDnsM, times(1)).registerService(
233                 eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
234 
235         // Client discovery request
236         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
237         client.discoverServices("a_type", PROTOCOL, listener2);
238         waitForIdle();
239         verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0));
240 
241         // Client resolve request
242         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
243         client.resolveService(request, listener3);
244         waitForIdle();
245         verify(mMockMDnsM, times(1)).resolve(
246                 eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0));
247 
248         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
249         deathRecipient.binderDied();
250         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
251         // checks that request are cleaned
252         verify(mMockMDnsM, times(1)).stopOperation(eq(2));
253         verify(mMockMDnsM, times(1)).stopOperation(eq(3));
254         verify(mMockMDnsM, times(1)).stopOperation(eq(4));
255     }
256 
257     @Test
258     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
testCleanupDelayNoRequestActive()259     public void testCleanupDelayNoRequestActive() throws Exception {
260         NsdService service = makeService();
261         NsdManager client = connectClient(service);
262 
263         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
264         request.setPort(2201);
265         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
266         client.registerService(request, PROTOCOL, listener1);
267         waitForIdle();
268         verify(mMockMDnsM, times(1)).registerEventListener(any());
269         verify(mMockMDnsM, times(1)).startDaemon();
270         final INsdManagerCallback cb1 = getCallback();
271         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
272         verify(mMockMDnsM, times(1)).registerService(
273                 eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
274 
275         client.unregisterService(listener1);
276         waitForIdle();
277         verify(mMockMDnsM, times(1)).stopOperation(eq(2));
278 
279         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
280         reset(mMockMDnsM);
281         deathRecipient.binderDied();
282         // Client disconnects, daemon should not be stopped after CLEANUP_DELAY_MS.
283         verify(mMockMDnsM, never()).unregisterEventListener(any());
284         verify(mMockMDnsM, never()).stopDaemon();
285     }
286 
287     @Test
testDiscoverOnTetheringDownstream()288     public void testDiscoverOnTetheringDownstream() throws Exception {
289         NsdService service = makeService();
290         NsdManager client = connectClient(service);
291 
292         final String serviceType = "a_type";
293         final String serviceName = "a_name";
294         final String domainName = "mytestdevice.local";
295         final int interfaceIdx = 123;
296         final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
297         client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
298         waitForIdle();
299 
300         final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
301                 ArgumentCaptor.forClass(IMDnsEventListener.class);
302         verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
303         final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
304         verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
305                 eq(0) /* interfaceIdx */);
306         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
307         // this needs to use a timeout
308         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
309 
310         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
311                 discIdCaptor.getValue(),
312                 IMDnsEventListener.SERVICE_FOUND,
313                 serviceName,
314                 serviceType,
315                 domainName,
316                 interfaceIdx,
317                 INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
318         final IMDnsEventListener eventListener = listenerCaptor.getValue();
319         eventListener.onServiceDiscoveryStatus(discoveryInfo);
320         waitForIdle();
321 
322         final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
323                 ArgumentCaptor.forClass(NsdServiceInfo.class);
324         verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
325         final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
326         assertEquals(serviceName, foundInfo.getServiceName());
327         assertEquals(serviceType, foundInfo.getServiceType());
328         assertNull(foundInfo.getHost());
329         assertNull(foundInfo.getNetwork());
330         assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
331 
332         // After discovering the service, verify resolving it
333         final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
334         client.resolveService(foundInfo, resolveListener);
335         waitForIdle();
336 
337         final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
338         verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
339                 eq("local.") /* domain */, eq(interfaceIdx));
340 
341         final int servicePort = 10123;
342         final String serviceFullName = serviceName + "." + serviceType;
343         final ResolutionInfo resolutionInfo = new ResolutionInfo(
344                 resolvIdCaptor.getValue(),
345                 IMDnsEventListener.SERVICE_RESOLVED,
346                 null /* serviceName */,
347                 null /* serviceType */,
348                 null /* domain */,
349                 serviceFullName,
350                 domainName,
351                 servicePort,
352                 new byte[0] /* txtRecord */,
353                 interfaceIdx);
354 
355         doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
356         eventListener.onServiceResolutionStatus(resolutionInfo);
357         waitForIdle();
358 
359         final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
360         verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
361                 eq(interfaceIdx));
362 
363         final String serviceAddress = "192.0.2.123";
364         final GetAddressInfo addressInfo = new GetAddressInfo(
365                 getAddrIdCaptor.getValue(),
366                 IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
367                 serviceFullName,
368                 serviceAddress,
369                 interfaceIdx,
370                 INetd.LOCAL_NET_ID);
371         eventListener.onGettingServiceAddressStatus(addressInfo);
372         waitForIdle();
373 
374         final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
375                 ArgumentCaptor.forClass(NsdServiceInfo.class);
376         verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
377         final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
378         assertEquals(serviceName, resolvedService.getServiceName());
379         assertEquals("." + serviceType, resolvedService.getServiceType());
380         assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
381         assertEquals(servicePort, resolvedService.getPort());
382         assertNull(resolvedService.getNetwork());
383         assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
384     }
385 
waitForIdle()386     private void waitForIdle() {
387         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
388     }
389 
makeService()390     NsdService makeService() {
391         final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS) {
392             @Override
393             public INsdServiceConnector connect(INsdManagerCallback baseCb) {
394                 // Wrap the callback in a transparent mock, to mock asBinder returning a
395                 // LinkToDeathRecorder. This will allow recording the binder death recipient
396                 // registered on the callback. Use a transparent mock and not a spy as the actual
397                 // implementation class is not public and cannot be spied on by Mockito.
398                 final INsdManagerCallback cb = mock(INsdManagerCallback.class,
399                         AdditionalAnswers.delegatesTo(baseCb));
400                 doReturn(new LinkToDeathRecorder()).when(cb).asBinder();
401                 mCreatedCallbacks.add(cb);
402                 return super.connect(cb);
403             }
404         };
405         return service;
406     }
407 
getCallback()408     private INsdManagerCallback getCallback() {
409         return mCreatedCallbacks.remove();
410     }
411 
connectClient(NsdService service)412     NsdManager connectClient(NsdService service) {
413         return new NsdManager(mContext, service);
414     }
415 
verifyDelayMaybeStopDaemon(long cleanupDelayMs)416     void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception {
417         waitForIdle();
418         // Stop daemon shouldn't be called immediately.
419         verify(mMockMDnsM, never()).unregisterEventListener(any());
420         verify(mMockMDnsM, never()).stopDaemon();
421 
422         // Clean up the daemon after CLEANUP_DELAY_MS.
423         verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).unregisterEventListener(any());
424         verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
425     }
426 
427     public static class TestHandler extends Handler {
428         public Message lastMessage;
429 
TestHandler(Looper looper)430         TestHandler(Looper looper) {
431             super(looper);
432         }
433 
434         @Override
handleMessage(Message msg)435         public void handleMessage(Message msg) {
436             lastMessage = obtainMessage();
437             lastMessage.copyFrom(msg);
438         }
439     }
440 }
441