diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilter.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilter.java
new file mode 100644
index 000000000..b2570f797
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilter.java
@@ -0,0 +1,26 @@
+package org.whispersystems.textsecuregcm.util.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.filter.Filter;
+import ch.qos.logback.core.spi.FilterReply;
+
+/**
+ * Filters spurious warnings about setting a very specific channel option on local channels.
+ *
+ * gRPC unconditionally tries to set the {@code SO_KEEPALIVE} option on all of its channels, but local channels, which
+ * are used by the Noise-over-WebSocket tunnel, do not support {@code SO_KEEPALIVE} and log a warning on each new
+ * channel. We don't want to filter all warnings from the relevant logger, and so this custom filter denies
+ * attempts to log the specific spurious message.
+ */
+public class UnknownKeepaliveOptionFilter extends Filter {
+
+ private static final String MESSAGE_PREFIX = "Unknown channel option 'SO_KEEPALIVE'";
+
+ @Override
+ public FilterReply decide(final ILoggingEvent event) {
+ final boolean loggerNameMatches = "io.netty.bootstrap.Bootstrap".equals(event.getLoggerName()) ||
+ "io.netty.bootstrap.ServerBootstrap".equals(event.getLoggerName());
+
+ return loggerNameMatches && event.getMessage().startsWith(MESSAGE_PREFIX) ? FilterReply.DENY : FilterReply.NEUTRAL;
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilterFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilterFactory.java
new file mode 100644
index 000000000..31d09bb06
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UnknownKeepaliveOptionFilterFactory.java
@@ -0,0 +1,15 @@
+package org.whispersystems.textsecuregcm.util.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.filter.Filter;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.logging.common.filter.FilterFactory;
+
+@JsonTypeName("unknownKeepaliveOption")
+public class UnknownKeepaliveOptionFilterFactory implements FilterFactory {
+
+ @Override
+ public Filter build() {
+ return new UnknownKeepaliveOptionFilter();
+ }
+}
diff --git a/service/src/main/resources/META-INF/services/io.dropwizard.logging.common.filter.FilterFactory b/service/src/main/resources/META-INF/services/io.dropwizard.logging.common.filter.FilterFactory
index 5e33b192a..1de0bbae5 100644
--- a/service/src/main/resources/META-INF/services/io.dropwizard.logging.common.filter.FilterFactory
+++ b/service/src/main/resources/META-INF/services/io.dropwizard.logging.common.filter.FilterFactory
@@ -1 +1,2 @@
org.whispersystems.textsecuregcm.util.logging.RequestLogEnabledFilterFactory
+org.whispersystems.textsecuregcm.util.logging.UnknownKeepaliveOptionFilterFactory
diff --git a/service/src/test/resources/logback-test.xml b/service/src/test/resources/logback-test.xml
index b01f95f92..f149694a8 100644
--- a/service/src/test/resources/logback-test.xml
+++ b/service/src/test/resources/logback-test.xml
@@ -1,5 +1,9 @@
+
+
+
+
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n