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

View File

@@ -56,7 +56,7 @@ class AccessLogWriter(
@PostConstruct @PostConstruct
fun start() { fun start() {
if (!config.enabled) { if (!config.enabled) {
log.info("Access Log is diabled") log.info("Access Log is disabled")
return return
} }
log.info("Writing Access Log to ${filename.absolutePath}") 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 com.fasterxml.jackson.annotation.JsonInclude
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.grpc.Chain 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 org.slf4j.LoggerFactory
import java.net.InetAddress
import java.net.InetSocketAddress
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class Events { class Events {
@@ -65,11 +70,13 @@ class Events {
data class StreamRequestDetails( data class StreamRequestDetails(
val id: UUID, val id: UUID,
val start: Instant val start: Instant,
val remote: Remote
) )
data class Remote( data class Remote(
val ips: List<String>, val ips: List<String>,
val ip: String,
val userAgent: String val userAgent: String
) )
@@ -88,15 +95,82 @@ class Events {
class NativeCallBuilder() { 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(), UUID.randomUUID(),
Instant.now() Instant.now(),
Remote(emptyList(), "", "")
) )
var chain: Int = Chain.UNSPECIFIED.id var chain: Int = Chain.UNSPECIFIED.id
val items = ArrayList<NativeCallItemDetails>() val items = ArrayList<NativeCallItemDetails>()
val replies = HashMap<Int, NativeCallReplyDetails>() 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 { fun withChain(chain: Int): NativeCallBuilder {
this.chain = chain this.chain = chain
return this 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_"
}
}
}