solution: save remote client details into access log

This commit is contained in:
Igor Artamonov
2021-06-26 22:15:46 -04:00
parent 4875ade0e9
commit fd26d79f6e
4 changed files with 287 additions and 6 deletions

View File

@@ -16,7 +16,6 @@
package io.emeraldpay.dshackle.monitoring.accesslog
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global
import io.grpc.*
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
@@ -39,6 +38,7 @@ class AccessHandler(
when (val method = call.methodDescriptor.bareMethodName) {
"NativeCall" -> {
val builder = Events.NativeCallBuilder()
.start(headers, call.attributes)
return OnNativeCall<ReqT, RespT>(
next.startCall(OnNativeCallResponse(call, builder), headers),
builder) { logs ->

View File

@@ -56,7 +56,7 @@ class AccessLogWriter(
@PostConstruct
fun start() {
if (!config.enabled) {
log.info("Access Log is diabled")
log.info("Access Log is disabled")
return
}
log.info("Writing Access Log to ${filename.absolutePath}")

View File

@@ -18,10 +18,15 @@ package io.emeraldpay.dshackle.monitoring.accesslog
import com.fasterxml.jackson.annotation.JsonInclude
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.grpc.Chain
import io.grpc.Attributes
import io.grpc.Grpc
import io.grpc.Metadata
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.net.InetSocketAddress
import java.time.Instant
import java.util.*
import kotlin.collections.ArrayList
class Events {
@@ -65,11 +70,13 @@ class Events {
data class StreamRequestDetails(
val id: UUID,
val start: Instant
val start: Instant,
val remote: Remote
)
data class Remote(
val ips: List<String>,
val ip: String,
val userAgent: String
)
@@ -88,15 +95,82 @@ class Events {
class NativeCallBuilder() {
private val requestDetails = StreamRequestDetails(
companion object {
private val remoteIpKeys = listOf(
Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER),
Metadata.Key.of("x-forwarded-for", Metadata.ASCII_STRING_MARSHALLER)
)
private val invalidCharacters = Regex("[\n\t]+")
}
private var requestDetails = StreamRequestDetails(
UUID.randomUUID(),
Instant.now()
Instant.now(),
Remote(emptyList(), "", "")
)
var chain: Int = Chain.UNSPECIFIED.id
val items = ArrayList<NativeCallItemDetails>()
val replies = HashMap<Int, NativeCallReplyDetails>()
private fun toInetAddress(ip: String): InetAddress? {
val isIp = Character.digit(ip[0], 16) != -1
if (!isIp) {
return null
}
return try {
InetAddress.getByName(ip)
} catch (t: Throwable) {
null
}
}
private fun findBestIp(ips: List<InetAddress>): InetAddress? {
// check if a real remote address is provided, otherwise use any local address
return ips.sortedWith(kotlin.Comparator { a, b ->
val aLocal = a.isLoopbackAddress || a.isSiteLocalAddress
val bLocal = b.isLoopbackAddress || b.isSiteLocalAddress
when {
aLocal && bLocal -> 0
aLocal -> 1
else -> -1
}
}).firstOrNull()
}
private fun clean(s: String): String {
return StringUtils.truncate(s, 128)
.replace(invalidCharacters, " ")
.trim()
}
fun start(metadata: Metadata, attributes: Attributes): NativeCallBuilder {
val userAgent = metadata.get(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER))
?.let(this@NativeCallBuilder::clean)
?: ""
val ips = ArrayList<InetAddress>()
remoteIpKeys.forEach { key ->
metadata.get(key)?.let {
it.trim().ifEmpty { null }
?.let(this@NativeCallBuilder::toInetAddress)
?.let(ips::add)
}
}
attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)?.let { addr ->
if (addr is InetSocketAddress) {
ips.add(addr.address)
}
}
val ip = findBestIp(ips)?.hostAddress ?: ""
this.requestDetails = this.requestDetails
.copy(remote = Remote(
ips = ips.map { it.hostAddress },
ip = ip,
userAgent = userAgent
))
return this
}
fun withChain(chain: Int): NativeCallBuilder {
this.chain = chain
return this

View File

@@ -0,0 +1,207 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* 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 io.emeraldpay.dshackle.monitoring.accesslog
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.grpc.Chain
import io.grpc.Attributes
import io.grpc.Grpc
import io.grpc.Metadata
import spock.lang.Specification
class EventsNativeCallBuilderSpec extends Specification {
def "Parse headers from direct local access"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("127.0.0.1"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0]) {
it.request != null
it.request.remote != null
with(it.request.remote) {
ips == ["127.0.0.1"]
userAgent == "grpc-go/1.30.0"
ip == "127.0.0.1"
}
}
}
def "Extracts real remote ip"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "30.56.100.15")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("127.0.0.1"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
ips == ["30.56.100.15", "127.0.0.1"]
ip == "30.56.100.15"
}
}
def "Ignores remote ip header if already connected from remote"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "192.168.1.1")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
ips == ["192.168.1.1", "30.56.100.15"]
userAgent == "grpc-go/1.30.0"
ip == "30.56.100.15"
}
}
def "Ignores invalid ip header"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "271.194.19.1")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
ips == ["30.56.100.15"]
userAgent == "grpc-go/1.30.0"
ip == "30.56.100.15"
}
}
def "Ignores host addr in ip header"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "google.com")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
ips == ["30.56.100.15"]
userAgent == "grpc-go/1.30.0"
ip == "30.56.100.15"
}
}
def "Extracts ipv6 addresses"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "2001:0db8:0000:0000:0000:ff00:0042:8329")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet6Address.getByName("::1"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
ips == ["2001:db8:0:0:0:ff00:42:8329", "0:0:0:0:0:0:0:1"]
ip == "2001:db8:0:0:0:ff00:42:8329"
}
}
def "Cleans up user agent"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0\nxss\n\r")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
userAgent == "grpc-go/1.30.0 xss"
}
}
def "Truncates up user agent to 128 characters max"() {
setup:
def metadata = new Metadata()
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER),
"0123456_1_0123456_2_0123456_3_0123456_4_0123456_5_0123456_6_0123456_7_0123456_8_0123456_9_0123456_0_0123456_1_0123456_2_0123456_3_0123456_4_0123456_5")
def attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
.build()
when:
def act = new Events.NativeCallBuilder()
.start(metadata, attributes)
.withChain(Chain.ETHEREUM.id)
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
.build()
then:
act.size() == 1
with(act[0].request.remote) {
userAgent.length() == 128
userAgent == "0123456_1_0123456_2_0123456_3_0123456_4_0123456_5_0123456_6_0123456_7_0123456_8_0123456_9_0123456_0_0123456_1_0123456_2_0123456_"
}
}
}