Merge "Sip Message Validators (2/2)"
diff --git a/src/com/android/services/telephony/rcs/MessageTransportWrapper.java b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
index 159e3e7..c4edf49 100644
--- a/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
+++ b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
@@ -34,6 +34,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.SipMessageParsingUtils;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.RcsProvisioningMonitor;
 import com.android.services.telephony.rcs.validator.ValidationResult;
 
 import java.io.PrintWriter;
@@ -49,7 +50,7 @@
  * <p>
  * Queues incoming and outgoing SIP messages on an Executor and deliver to IMS application and
  * SipDelegate in order. If there is an error delivering the message, the caller is notified.
- * Uses {@link TransportSipSessionTracker} to track ongoing SIP dialogs and verify outgoing
+ * Uses {@link TransportSipMessageValidator} to track ongoing SIP dialogs and verify outgoing
  * messages.
  * <p>
  * Note: This handles incoming binder calls, so all calls from other processes should be handled on
@@ -127,6 +128,7 @@
                 mExecutor.execute(() -> {
                     ValidationResult result =
                             mSipSessionTracker.verifyOutgoingMessage(sipMessage, configVersion);
+                    result = maybeOverrideValidationForTesting(result);
                     if (!result.isValidated) {
                         notifyDelegateSendError("Outgoing messages restricted", sipMessage,
                                 result.restrictedReason);
@@ -253,10 +255,24 @@
         }
     };
 
+    /**
+     * Interface for injecting validator override dependencies for testing.
+     */
+    @VisibleForTesting
+    public interface ValidatorOverride {
+        /**
+         * @return {@code null} if the validation result should not be overridden, {@code true} if
+         * the validation result should always pass, {@code false} if the validation result should
+         * always fail.
+         */
+        Boolean getValidatorOverrideState();
+    }
+
+    private final ValidatorOverride mValidatorOverride;
     private final ISipDelegateMessageCallback mAppCallback;
     private final Executor mExecutor;
     private final int mSubId;
-    private final TransportSipSessionTracker mSipSessionTracker;
+    private final TransportSipMessageValidator mSipSessionTracker;
     private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
 
     private ISipDelegate mSipDelegate;
@@ -266,7 +282,9 @@
         mSubId = subId;
         mAppCallback = appMessageCallback;
         mExecutor = executor;
-        mSipSessionTracker = new TransportSipSessionTracker(subId, executor);
+        mSipSessionTracker = new TransportSipMessageValidator(subId, executor);
+        mValidatorOverride = () -> RcsProvisioningMonitor.getInstance()
+                .getImsFeatureValidationOverride(mSubId);
     }
 
     /**
@@ -275,11 +293,13 @@
     @VisibleForTesting
     public MessageTransportWrapper(int subId, ScheduledExecutorService executor,
             ISipDelegateMessageCallback appMessageCallback,
-            TransportSipSessionTracker sipSessionTracker) {
+            TransportSipMessageValidator sipSessionTracker) {
         mSubId = subId;
         mAppCallback = appMessageCallback;
         mExecutor = executor;
         mSipSessionTracker = sipSessionTracker;
+        // Remove links to static methods calls querying overrides for testing.
+        mValidatorOverride = () -> null;
     }
 
     @Override
@@ -424,6 +444,21 @@
         }
     }
 
+    private ValidationResult maybeOverrideValidationForTesting(ValidationResult result) {
+        Boolean isValidatedOverride = mValidatorOverride.getValidatorOverrideState();
+        if (isValidatedOverride == null) {
+            return result;
+        }
+        if (isValidatedOverride) {
+            return ValidationResult.SUCCESS;
+        } else if (result.isValidated) {
+            // if override is set to false and the original result was validated, return a new
+            // restricted result with UNKNOWN reason.
+            return new ValidationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN);
+        }
+        return result;
+    }
+
     private void notifyDelegateSendError(String logReason, SipMessage message, int reasonCode) {
         String transactionId = SipMessageParsingUtils.getTransactionId(message.getHeaderSection());
         logi("Error sending SipMessage[id: " + transactionId + ", code: " + reasonCode
diff --git a/src/com/android/services/telephony/rcs/TransportSipSessionTracker.java b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
similarity index 90%
rename from src/com/android/services/telephony/rcs/TransportSipSessionTracker.java
rename to src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
index b90f625..66dea85 100644
--- a/src/com/android/services/telephony/rcs/TransportSipSessionTracker.java
+++ b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
@@ -25,8 +25,12 @@
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.services.telephony.rcs.validator.IncomingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.MalformedSipMessageValidator;
 import com.android.services.telephony.rcs.validator.OutgoingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.RestrictedOutgoingSipRequestValidator;
+import com.android.services.telephony.rcs.validator.RestrictedOutgoingSubscribeValidator;
 import com.android.services.telephony.rcs.validator.SipMessageValidator;
 import com.android.services.telephony.rcs.validator.ValidationResult;
 
@@ -47,20 +51,20 @@
  *    associated with a stale IMS configuration version.</li>
  *    <li>Track the SipDelegate open/close state to allow/deny outgoing messages based on the
  *    session's state.</li>
+ *    <li>Validate outgoing SIP messages for both restricted request methods as well as restricted/
+ *    malformed headers.</li>
  * </ul>
  */
-public class TransportSipSessionTracker {
+public class TransportSipMessageValidator {
 
-    private static final String LOG_TAG = "SipSessionT";
+    private static final String LOG_TAG = "SipMessageV";
 
     private final int mSubId;
     private final ScheduledExecutorService mExecutor;
     private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
     // Validators
-    private final IncomingTransportStateValidator mIncomingTransportStateValidator =
-            new IncomingTransportStateValidator();
-    private final OutgoingTransportStateValidator mOutgoingTransportStateValidator =
-            new OutgoingTransportStateValidator();
+    private final IncomingTransportStateValidator mIncomingTransportStateValidator;
+    private final OutgoingTransportStateValidator mOutgoingTransportStateValidator;
     private final SipMessageValidator mOutgoingMessageValidator;
     private final SipMessageValidator mIncomingMessageValidator;
 
@@ -71,11 +75,25 @@
     private Consumer<List<String>> mClosingCompleteConsumer;
     private Consumer<List<String>> mRegistrationAppliedConsumer;
 
+    public TransportSipMessageValidator(int subId, ScheduledExecutorService executor) {
+        this(subId, executor, new OutgoingTransportStateValidator(),
+                new IncomingTransportStateValidator(), new MalformedSipMessageValidator().andThen(
+                new RestrictedOutgoingSipRequestValidator()).andThen(
+                new RestrictedOutgoingSubscribeValidator()));
+    }
 
-    public TransportSipSessionTracker(int subId, ScheduledExecutorService executor) {
+
+    @VisibleForTesting
+    public TransportSipMessageValidator(int subId, ScheduledExecutorService executor,
+            OutgoingTransportStateValidator outgoingStateValidator,
+            IncomingTransportStateValidator incomingStateValidator,
+            SipMessageValidator statelessMessageValidator) {
         mSubId = subId;
         mExecutor = executor;
-        mOutgoingMessageValidator = mOutgoingTransportStateValidator;
+        mOutgoingTransportStateValidator = outgoingStateValidator;
+        mIncomingTransportStateValidator = incomingStateValidator;
+        mOutgoingMessageValidator = mOutgoingTransportStateValidator.andThen(
+                statelessMessageValidator);
         mIncomingMessageValidator = mIncomingTransportStateValidator;
     }
 
diff --git a/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java b/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java
index dce7841..2ab4bbe 100644
--- a/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java
+++ b/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java
@@ -24,7 +24,7 @@
  * Tracks the incoming SIP message transport state from the ImsService to the remote IMS
  * application. Validates incoming SIP messages based on this state.
  */
-public final class IncomingTransportStateValidator implements SipMessageValidator {
+public class IncomingTransportStateValidator implements SipMessageValidator {
 
     /**
      * The message transport is closed, meaning there can be no more incoming messages
diff --git a/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidator.java b/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidator.java
new file mode 100644
index 0000000..3ac461d
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import com.android.internal.telephony.SipMessageParsingUtils;
+
+/**
+ * Validates that the SipMessage is not malformed before sending the message to the vendor
+ * ImsService by ensuring:
+ * <ul>The SipMessage is a valid SIP request or SIP response.</ul>
+ */
+public class MalformedSipMessageValidator implements SipMessageValidator {
+
+    @Override
+    public ValidationResult validate(SipMessage message) {
+        // Verify the request and response start lines are valid.
+        if (!SipMessageParsingUtils.isSipRequest(message.getStartLine())
+                && !SipMessageParsingUtils.isSipResponse(message.getStartLine())) {
+            return new ValidationResult(
+                    SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE);
+        }
+        return ValidationResult.SUCCESS;
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java b/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java
index 5055e36..348c213 100644
--- a/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java
+++ b/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java
@@ -24,7 +24,7 @@
  * Tracks the state of the outgoing SIP message transport from the remote IMS application to the
  * ImsService. Used to validate outgoing SIP messages based off of this state.
  */
-public final class OutgoingTransportStateValidator implements SipMessageValidator {
+public class OutgoingTransportStateValidator implements SipMessageValidator {
 
     /**
      * The message transport is closed, meaning there can be no more outgoing messages
diff --git a/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidator.java b/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidator.java
new file mode 100644
index 0000000..e3aba25
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import com.android.internal.telephony.SipMessageParsingUtils;
+
+import java.util.Arrays;
+
+/**
+ * Validate that any outgoing SIP request message does not contain methods that are only generated
+ * internally by the ImsService implementation.
+ */
+public class RestrictedOutgoingSipRequestValidator implements SipMessageValidator {
+
+    /**
+     * These SIP requests are always handled by the ImsService and are restricted to being
+     * generated internally. Messages with these request methods should fail validation.
+     */
+    private static final String[] IMS_SERVICE_HANDLED_REQUEST_METHODS = new String[]{
+            "register", "options", "publish"};
+
+    @Override
+    public ValidationResult validate(SipMessage message) {
+        String startLine = message.getStartLine();
+        if (SipMessageParsingUtils.isSipRequest(startLine)) {
+            String[] segments = SipMessageParsingUtils.splitStartLineAndVerify(startLine);
+            if (segments == null) {
+                return new ValidationResult(
+                        SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE);
+            }
+            if (Arrays.stream(IMS_SERVICE_HANDLED_REQUEST_METHODS).anyMatch(
+                    s -> segments[0].toLowerCase().contains(s))) {
+                return new ValidationResult(
+                        SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE);
+            }
+        }
+        return ValidationResult.SUCCESS;
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidator.java b/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidator.java
new file mode 100644
index 0000000..0db3381
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.util.Pair;
+
+import com.android.internal.telephony.SipMessageParsingUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Ensure that if there is an outgoing SUBSCRIBE request, that it does not contain the "Event"
+ * header "presence"
+ */
+public class RestrictedOutgoingSubscribeValidator implements SipMessageValidator {
+
+    private static final String SUBSCRIBE_REQUEST = "subscribe";
+    private static final String SUBSCRIBE_EVENT_HEADER = "event";
+    private static final String[] RESTRICTED_EVENTS = new String[]{ "presence" };
+
+
+    @Override
+    public ValidationResult validate(SipMessage message) {
+        if (!SipMessageParsingUtils.isSipRequest(message.getStartLine())) {
+            return ValidationResult.SUCCESS;
+        }
+        String[] requestSegments = SipMessageParsingUtils.splitStartLineAndVerify(
+                message.getStartLine());
+        if (requestSegments == null) {
+            return new ValidationResult(
+                    SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE);
+        }
+        // Request-Line  =  Method SP Request-URI SP SIP-Version CRLF, verify Method
+        if (!requestSegments[0].equalsIgnoreCase(SUBSCRIBE_REQUEST)) {
+            return ValidationResult.SUCCESS;
+        }
+
+        List<Pair<String, String>> eventHeaders = SipMessageParsingUtils.parseHeaders(
+                message.getHeaderSection(), true /*stopAtFirstMatch*/, SUBSCRIBE_EVENT_HEADER);
+        if (eventHeaders.size() == 0) {
+            return ValidationResult.SUCCESS;
+        }
+        boolean isRestricted = eventHeaders.stream().map(e -> e.second)
+                .anyMatch(e -> Arrays.asList(RESTRICTED_EVENTS).contains(e.trim().toLowerCase()));
+
+        return isRestricted ? new ValidationResult(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_HEADER_FIELDS) :
+                ValidationResult.SUCCESS;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java b/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java
index f273854..3542cc1 100644
--- a/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java
+++ b/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java
@@ -74,7 +74,7 @@
     private static final String TEST_TRANSACTION_ID = "z9hG4bK776asdhds";
 
     @Mock private ISipDelegateMessageCallback mDelegateMessageCallback;
-    @Mock private TransportSipSessionTracker mTransportSipSessionTracker;
+    @Mock private TransportSipMessageValidator mTransportSipSessionValidator;
     @Mock private ISipDelegate mISipDelegate;
     @Mock private Consumer<Boolean> mMockCloseConsumer;
 
@@ -103,7 +103,7 @@
                 SipDelegateConfiguration.SIP_TRANSPORT_TCP, localAddr, serverAddr).build();
         // Ensure IMS config changes are propagated to the message tracker.
         tracker.onConfigurationChanged(c);
-        verify(mTransportSipSessionTracker).onConfigurationChanged(c);
+        verify(mTransportSipSessionValidator).onConfigurationChanged(c);
     }
 
     @SmallTest
@@ -117,7 +117,7 @@
         MessageTransportWrapper tracker = createTestMessageTransportWrapper();
         // Ensure openTransport passes denied tags to the session tracker
         tracker.openTransport(mISipDelegate, allowedTags, deniedTags);
-        verify(mTransportSipSessionTracker).onTransportOpened(allowedTags, deniedTags);
+        verify(mTransportSipSessionValidator).onTransportOpened(allowedTags, deniedTags);
     }
 
     @SmallTest
@@ -135,7 +135,7 @@
         callIdConsumer.accept(callIds);
         // Verify that the pending call IDs are closed properly.
         for (String callId : callIds) {
-            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mTransportSipSessionValidator).onSipSessionCleanup(callId);
             verify(mISipDelegate).cleanupSession(callId);
         }
     }
@@ -152,7 +152,7 @@
                 closedReason, (r) -> result[0] = r);
         callIdConsumer.accept(Collections.emptyList());
         // Verify that the pending call IDs are closed properly.
-        verify(mTransportSipSessionTracker, never()).onSipSessionCleanup(anyString());
+        verify(mTransportSipSessionValidator, never()).onSipSessionCleanup(anyString());
         verify(mISipDelegate, never()).cleanupSession(anyString());
         // Result is true in the case that all call IDs were successfully closed.
         assertTrue(result[0]);
@@ -175,7 +175,7 @@
         callIdConsumer.accept(callIds);
         // Verify that the pending call IDs are closed properly.
         for (String callId : callIds) {
-            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mTransportSipSessionValidator).onSipSessionCleanup(callId);
             verify(mISipDelegate).cleanupSession(callId);
         }
         // Result is false in this case because there were still callIds left that were not
@@ -190,14 +190,14 @@
         callIds.add("callId1");
         callIds.add("callId2");
         int closedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
-        doReturn(callIds).when(mTransportSipSessionTracker).closeSessionsForcefully(closedReason);
+        doReturn(callIds).when(mTransportSipSessionValidator).closeSessionsForcefully(closedReason);
         MessageTransportWrapper tracker = createTestMessageTransportWrapper();
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
 
         tracker.close(closedReason);
         // Verify that the pending call IDs are closed properly.
         for (String callId : callIds) {
-            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mTransportSipSessionValidator).onSipSessionCleanup(callId);
             verify(mISipDelegate).cleanupSession(callId);
         }
     }
@@ -209,7 +209,7 @@
 
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
         doReturn(ValidationResult.SUCCESS)
-                .when(mTransportSipSessionTracker)
+                .when(mTransportSipSessionValidator)
                 .verifyOutgoingMessage(TEST_MESSAGE, 1 /*version*/);
         tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
         verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
@@ -221,7 +221,7 @@
 
         doReturn(new ValidationResult(
                 SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED))
-                .when(mTransportSipSessionTracker)
+                .when(mTransportSipSessionValidator)
                 .verifyOutgoingMessage(TEST_MESSAGE, 1 /*version*/);
         tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
         verify(mDelegateMessageCallback).onMessageSendFailure(TEST_TRANSACTION_ID,
@@ -235,7 +235,7 @@
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
         tracker.getDelegateConnection().notifyMessageReceived(TEST_TRANSACTION_ID);
         verify(mISipDelegate).notifyMessageReceived(TEST_TRANSACTION_ID);
-        verify(mTransportSipSessionTracker).acknowledgePendingMessage(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionValidator).acknowledgePendingMessage(TEST_TRANSACTION_ID);
     }
 
     @SmallTest
@@ -247,7 +247,7 @@
                 SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
         verify(mISipDelegate).notifyMessageReceiveError(TEST_TRANSACTION_ID,
                 SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
-        verify(mTransportSipSessionTracker).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionValidator).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
     }
 
     @SmallTest
@@ -257,7 +257,7 @@
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
         tracker.getDelegateConnection().cleanupSession("testCallId");
         verify(mISipDelegate).cleanupSession("testCallId");
-        verify(mTransportSipSessionTracker).onSipSessionCleanup("testCallId");
+        verify(mTransportSipSessionValidator).onSipSessionCleanup("testCallId");
     }
 
     @SmallTest
@@ -267,7 +267,7 @@
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
 
         doReturn(ValidationResult.SUCCESS)
-                .when(mTransportSipSessionTracker).verifyIncomingMessage(TEST_MESSAGE);
+                .when(mTransportSipSessionValidator).verifyIncomingMessage(TEST_MESSAGE);
         tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
         verify(mDelegateMessageCallback).onMessageReceived(TEST_MESSAGE);
 
@@ -278,7 +278,7 @@
 
         doReturn(new ValidationResult(
                 SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD))
-                .when(mTransportSipSessionTracker).verifyIncomingMessage(TEST_MESSAGE);
+                .when(mTransportSipSessionValidator).verifyIncomingMessage(TEST_MESSAGE);
         tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
         verify(mISipDelegate, times(2)).notifyMessageReceiveError(TEST_TRANSACTION_ID,
                 SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
@@ -290,7 +290,7 @@
         MessageTransportWrapper tracker = createTestMessageTransportWrapper();
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
         tracker.getMessageCallback().onMessageSent(TEST_TRANSACTION_ID);
-        verify(mTransportSipSessionTracker).acknowledgePendingMessage(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionValidator).acknowledgePendingMessage(TEST_TRANSACTION_ID);
         verify(mDelegateMessageCallback).onMessageSent(TEST_TRANSACTION_ID);
     }
 
@@ -301,14 +301,14 @@
         tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
         tracker.getMessageCallback().onMessageSendFailure(TEST_TRANSACTION_ID,
                 SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
-        verify(mTransportSipSessionTracker).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionValidator).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
         verify(mDelegateMessageCallback).onMessageSendFailure(TEST_TRANSACTION_ID,
                 SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
     }
 
     private MessageTransportWrapper createTestMessageTransportWrapper() {
         return new MessageTransportWrapper(TEST_SUB_ID,
-                mExecutor, mDelegateMessageCallback, mTransportSipSessionTracker);
+                mExecutor, mDelegateMessageCallback, mTransportSipSessionValidator);
     }
 
     private Consumer<List<String>> trackerRegStateChanged(MessageTransportWrapper tracker,
@@ -318,9 +318,9 @@
             // Capture the consumer here.
             consumerCaptor.add(it.getArgument(0));
             return null;
-        }).when(mTransportSipSessionTracker).onRegistrationStateChanged(any(), eq(state));
+        }).when(mTransportSipSessionValidator).onRegistrationStateChanged(any(), eq(state));
         tracker.onRegistrationStateChanged(state);
-        verify(mTransportSipSessionTracker).onRegistrationStateChanged(any(), eq(state));
+        verify(mTransportSipSessionValidator).onRegistrationStateChanged(any(), eq(state));
         assertFalse(consumerCaptor.isEmpty());
         return consumerCaptor.get(0);
     }
@@ -332,10 +332,10 @@
             // Capture the consumer here.
             consumerCaptor.add(it.getArgument(0));
             return null;
-        }).when(mTransportSipSessionTracker).closeSessionsGracefully(any(), eq(closingReason),
+        }).when(mTransportSipSessionValidator).closeSessionsGracefully(any(), eq(closingReason),
                 eq(closedReason));
         tracker.closeGracefully(closingReason, closedReason, resultConsumer);
-        verify(mTransportSipSessionTracker).closeSessionsGracefully(any(), eq(closingReason),
+        verify(mTransportSipSessionValidator).closeSessionsGracefully(any(), eq(closingReason),
                 eq(closedReason));
         assertFalse(consumerCaptor.isEmpty());
         return consumerCaptor.get(0);
diff --git a/tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java b/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
similarity index 73%
rename from tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java
rename to tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
index e076605..c991a8c 100644
--- a/tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
@@ -20,6 +20,11 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
 import android.net.InetAddresses;
 import android.telephony.ims.DelegateRegistrationState;
 import android.telephony.ims.SipDelegateConfiguration;
@@ -30,20 +35,23 @@
 
 import com.android.TelephonyTestBase;
 import com.android.TestExecutorService;
+import com.android.services.telephony.rcs.validator.IncomingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.OutgoingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.SipMessageValidator;
 import com.android.services.telephony.rcs.validator.ValidationResult;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 
 import java.net.InetSocketAddress;
 import java.util.Collections;
 import java.util.concurrent.ScheduledExecutorService;
 
 @RunWith(AndroidJUnit4.class)
-public class TransportSipSessionTrackerTest extends TelephonyTestBase {
-
+public class TransportSipMessageValidatorTest extends TelephonyTestBase {
     private static final int TEST_SUB_ID = 1;
     private static final int TEST_CONFIG_VERSION = 1;
     private static final SipMessage TEST_MESSAGE = new SipMessage(
@@ -60,6 +68,13 @@
                     + "Content-Length: 142",
             new byte[0]);
 
+    @Mock
+    private IncomingTransportStateValidator mIncomingStateValidator;
+    @Mock
+    private OutgoingTransportStateValidator mOutgoingStateValidator;
+    @Mock
+    private SipMessageValidator mStatelessValidator;
+
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -73,11 +88,10 @@
     @Test
     public void testTransportOpening() {
         TestExecutorService executor = new TestExecutorService();
-        TransportSipSessionTracker tracker = getTestTracker(executor);
-        // Before the transport is opened, incoming/outgoing messages must fail.
-        assertFalse(isIncomingTransportOpen(tracker));
-        assertFalse(isOutgoingTransportOpen(tracker));
+        TransportSipMessageValidator tracker = getTestTracker(executor);
         tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
+        verify(mOutgoingStateValidator).open();
+        verify(mIncomingStateValidator).open();
         // Incoming messages are already verified
         assertTrue(isIncomingTransportOpen(tracker));
         // IMS config and registration state needs to be sent before outgoing messages can be
@@ -96,7 +110,7 @@
     @Test
     public void testTransportOpenConfigChange() {
         TestExecutorService executor = new TestExecutorService();
-        TransportSipSessionTracker tracker = getTestTracker(executor);
+        TransportSipMessageValidator tracker = getTestTracker(executor);
         tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
         tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
         tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
@@ -113,7 +127,7 @@
     @Test
     public void testTransportClosingGracefully() {
         TestExecutorService executor = new TestExecutorService(true /*wait*/);
-        TransportSipSessionTracker tracker = getTestTracker(executor);
+        TransportSipMessageValidator tracker = getTestTracker(executor);
         tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
         tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
         tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
@@ -127,20 +141,17 @@
 
         // Before executor executes, outgoing messages will be restricted.
         assertTrue(isIncomingTransportOpen(tracker));
-        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
-                verifyOutgoingTransportClosed(tracker));
+        verify(mOutgoingStateValidator).restrict(anyInt());
         executor.executePending();
         // After Executor executes, all messages will be rejected.
-        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
-                verifyOutgoingTransportClosed(tracker));
-        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
-                verifyIncomingTransportClosed(tracker));
+        verify(mOutgoingStateValidator).close(anyInt());
+        verify(mIncomingStateValidator).close(anyInt());
     }
 
     @Test
     public void testTransportClosingForcefully() {
         TestExecutorService executor = new TestExecutorService();
-        TransportSipSessionTracker tracker = getTestTracker(executor);
+        TransportSipMessageValidator tracker = getTestTracker(executor);
         tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
         tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
         tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
@@ -152,10 +163,8 @@
                 SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
 
         // All messages will be rejected.
-        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
-                verifyOutgoingTransportClosed(tracker));
-        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
-                verifyIncomingTransportClosed(tracker));
+        verify(mOutgoingStateValidator).close(anyInt());
+        verify(mIncomingStateValidator).close(anyInt());
     }
 
     private SipDelegateConfiguration.Builder getConfigBuilder(int version) {
@@ -167,21 +176,16 @@
                 SipDelegateConfiguration.SIP_TRANSPORT_TCP, localAddr, serverAddr);
     }
 
-    private boolean isIncomingTransportOpen(TransportSipSessionTracker tracker) {
+
+    private boolean isIncomingTransportOpen(TransportSipMessageValidator tracker) {
         return tracker.verifyIncomingMessage(TEST_MESSAGE).isValidated;
     }
 
-    private boolean isOutgoingTransportOpen(TransportSipSessionTracker tracker) {
+    private boolean isOutgoingTransportOpen(TransportSipMessageValidator tracker) {
         return tracker.verifyOutgoingMessage(TEST_MESSAGE, TEST_CONFIG_VERSION).isValidated;
     }
 
-    private int verifyIncomingTransportClosed(TransportSipSessionTracker tracker) {
-        ValidationResult result = tracker.verifyIncomingMessage(TEST_MESSAGE);
-        assertFalse(result.isValidated);
-        return result.restrictedReason;
-    }
-
-    private int verifyOutgoingTransportClosed(TransportSipSessionTracker tracker) {
+    private int verifyOutgoingTransportClosed(TransportSipMessageValidator tracker) {
         ValidationResult result = tracker.verifyOutgoingMessage(TEST_MESSAGE, TEST_CONFIG_VERSION);
         assertFalse(result.isValidated);
         return result.restrictedReason;
@@ -191,7 +195,15 @@
         return new DelegateRegistrationState.Builder().build();
     }
 
-    private TransportSipSessionTracker getTestTracker(ScheduledExecutorService executor) {
-        return new TransportSipSessionTracker(TEST_SUB_ID, executor);
+    private TransportSipMessageValidator getTestTracker(ScheduledExecutorService executor) {
+        doReturn(ValidationResult.SUCCESS).when(mStatelessValidator).validate(any());
+        doReturn(mStatelessValidator).when(mStatelessValidator).andThen(any());
+        doReturn(ValidationResult.SUCCESS).when(mOutgoingStateValidator).validate(any());
+        // recreate chain for mocked instances.
+        doReturn(mStatelessValidator).when(mOutgoingStateValidator).andThen(any());
+        doReturn(ValidationResult.SUCCESS).when(mIncomingStateValidator).validate(any());
+        doReturn(mIncomingStateValidator).when(mIncomingStateValidator).andThen(any());
+        return new TransportSipMessageValidator(TEST_SUB_ID, executor, mOutgoingStateValidator,
+                mIncomingStateValidator, mStatelessValidator);
     }
 }
diff --git a/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java
index 37cd462..0e7e1be 100644
--- a/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java
+++ b/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java
@@ -23,7 +23,7 @@
 import android.telephony.ims.SipDelegateManager;
 import android.telephony.ims.SipMessage;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tests/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidatorTest.java
new file mode 100644
index 0000000..18b37fc
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/validator/MalformedSipMessageValidatorTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MalformedSipMessageValidatorTest {
+
+    @Test
+    public void testValidRequest() {
+        SipMessage msg = new SipMessage(
+                "INVITE sip:bob@biloxi.com SIP/2.0",
+                // Typical Via
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK.TeSt\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                        + "CSeq: 314159 INVITE\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                        + "Content-Type: application/sdp\n"
+                        + "Content-Length: 142",
+                new byte[0]);
+        ValidationResult result = new MalformedSipMessageValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+
+    @Test
+    public void testInvalidRequest() {
+        SipMessage msg = new SipMessage(
+                "INVITE sip:bob@biloxi.comSIP/2.0",
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                        + "CSeq: 314159 INVITE\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                        + "Content-Type: application/sdp\n"
+                        + "Content-Length: 142",
+                new byte[0]);
+        ValidationResult result = new MalformedSipMessageValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE,
+                result.restrictedReason);
+    }
+
+    @Test
+    public void testInvalidResponse() {
+        SipMessage msg = new SipMessage(
+                "SIP/2.0 200OK",
+                "Via: SIP/2.0/TCP terminal.vancouver.example.com;"
+                        + "branch=z9hG4bKwYb6QREiCL\n"
+                        + "To: <sip:adam-buddies@pres.vancouver.example.com>;tag=zpNctbZq\n"
+                        + "From: <sip:adam@vancouver.example.com>;tag=ie4hbb8t\n"
+                        + "Call-ID: cdB34qLToC@terminal.vancouver.example.com\n"
+                        + "CSeq: 322723822 SUBSCRIBE\n"
+                        + "Contact: <sip:pres.vancouver.example.com>\n"
+                        + "Expires: 7200\n"
+                        + "Require: eventlist\n"
+                        + "Content-Length: 0",
+                new byte[0]);
+        ValidationResult result = new MalformedSipMessageValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE,
+                result.restrictedReason);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java
index ae20688..e54e5ff 100644
--- a/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java
+++ b/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java
@@ -23,7 +23,7 @@
 import android.telephony.ims.SipDelegateManager;
 import android.telephony.ims.SipMessage;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidatorTest.java
new file mode 100644
index 0000000..a90aaeb
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSipRequestValidatorTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RestrictedOutgoingSipRequestValidatorTest {
+
+    @Test
+    public void testRegisterNotAllowed() {
+        SipMessage msg = new SipMessage(
+                "REGISTER sip:bob@biloxi.com SIP/2.0",
+                // Not representative of real REGISTER message, but close enough for validation.
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "CSeq: 314159 REGISTER\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSipRequestValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE,
+                result.restrictedReason);
+    }
+
+    @Test
+    public void testPublishNotAllowed() {
+        SipMessage msg = new SipMessage(
+                "PUBLISH sip:bob@biloxi.com SIP/2.0",
+                // Not representative of real REGISTER message, but close enough for validation.
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "CSeq: 314159 PUBLISH\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSipRequestValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE,
+                result.restrictedReason);
+
+    }
+
+    @Test
+    public void testOptionsNotAllowed() {
+        SipMessage msg = new SipMessage(
+                "OPTIONS sip:bob@biloxi.com SIP/2.0",
+                // Not representative of real REGISTER message, but close enough for validation.
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "CSeq: 314159 OPTIONS\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSipRequestValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_START_LINE,
+                result.restrictedReason);
+
+    }
+
+    @Test
+    public void testInviteAllowed() {
+        SipMessage msg = new SipMessage(
+                "INVITE sip:bob@biloxi.com SIP/2.0",
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                        + "CSeq: 314159 INVITE\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                        + "Content-Type: application/sdp\n"
+                        + "Content-Length: 142",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSipRequestValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+
+    @Test
+    public void testResponseAllowed() {
+        SipMessage msg = new SipMessage(
+                "SIP/2.0 200 OK",
+                "Via: SIP/2.0/TCP terminal.vancouver.example.com;"
+                        + "branch=z9hG4bKwYb6QREiCL\n"
+                        + "To: <sip:adam-buddies@pres.vancouver.example.com>;tag=zpNctbZq\n"
+                        + "From: <sip:adam@vancouver.example.com>;tag=ie4hbb8t\n"
+                        + "Call-ID: cdB34qLToC@terminal.vancouver.example.com\n"
+                        + "CSeq: 322723822 SUBSCRIBE\n"
+                        + "Contact: <sip:pres.vancouver.example.com>\n"
+                        + "Expires: 7200\n"
+                        + "Require: eventlist\n"
+                        + "Content-Length: 0",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSipRequestValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidatorTest.java
new file mode 100644
index 0000000..9864872
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/validator/RestrictedOutgoingSubscribeValidatorTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.services.telephony.rcs.validator;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RestrictedOutgoingSubscribeValidatorTest {
+
+    @Test
+    public void testValidUnrelatedRequest() {
+        SipMessage msg = new SipMessage(
+                "INVITE sip:bob@biloxi.com SIP/2.0",
+                // Typical Via
+                "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK.TeSt\n"
+                        + "Max-Forwards: 70\n"
+                        + "To: Bob <sip:bob@biloxi.com>\n"
+                        + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                        + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                        + "CSeq: 314159 INVITE\n"
+                        + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                        + "Content-Type: application/sdp\n"
+                        + "Content-Length: 142",
+                new byte[0]);
+        ValidationResult result = new MalformedSipMessageValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+
+    @Test
+    public void testValidUnrelatedResponse() {
+        SipMessage msg = new SipMessage(
+                "SIP/2.0 200 OK",
+                "Via: SIP/2.0/TCP terminal.vancouver.example.com;"
+                        + "branch=z9hG4bKwYb6QREiCL\n"
+                        + "To: <sip:adam-buddies@pres.vancouver.example.com>;tag=zpNctbZq\n"
+                        + "From: <sip:adam@vancouver.example.com>;tag=ie4hbb8t\n"
+                        + "Call-ID: cdB34qLToC@terminal.vancouver.example.com\n"
+                        + "CSeq: 322723822 SUBSCRIBE\n"
+                        + "Contact: <sip:pres.vancouver.example.com>\n"
+                        + "Expires: 7200\n"
+                        + "Require: eventlist\n"
+                        + "Content-Length: 0",
+                new byte[0]);
+        ValidationResult result = new MalformedSipMessageValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+
+    @Test
+    public void testValidSubscribeRequest() {
+        SipMessage msg = new SipMessage(
+                "SUBSCRIBE sip:joe@example.com SIP/2.0",
+                "Via: SIP/2.0/UDP app.example.com;branch=z9hG4bKnashds7\n"
+                        + "From: sip:app.example.com;tag=123aa9\n"
+                        + "To: sip:joe@example.com\n"
+                        + "Call-ID: 9987@app.example.com\n"
+                        + "CSeq: 9887 SUBSCRIBE\n"
+                        + "Contact: sip:app.example.com\n"
+                        + "Event:    conference \n"
+                        + "Max-Forwards: 70\n",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSubscribeValidator().validate(msg);
+        assertTrue(result.isValidated);
+    }
+
+    @Test
+    public void testInvalidSubscribeRequest() {
+        SipMessage msg = new SipMessage(
+                "SUBSCRIBE sip:joe@example.com SIP/2.0",
+                "Via: SIP/2.0/UDP app.example.com;branch=z9hG4bKnashds7\n"
+                        + "From: sip:app.example.com;tag=123aa9\n"
+                        + "To: sip:joe@example.com\n"
+                        + "Call-ID: 9987@app.example.com\n"
+                        + "CSeq: 9887 SUBSCRIBE\n"
+                        + "Contact: sip:app.example.com\n"
+                        + "Event:  presence  \n"
+                        + "Max-Forwards: 70\n",
+                new byte[0]);
+        ValidationResult result = new RestrictedOutgoingSubscribeValidator().validate(msg);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INVALID_HEADER_FIELDS,
+                result.restrictedReason);
+
+    }
+}