Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dns over http2 5773 v1 #9965

Closed
wants to merge 11 commits into from

Conversation

catenacyber
Copy link
Contributor

Link to redmine ticket:
https://redmine.openinfosecfoundation.org/issues/5773

Describe changes:

  • analyze DNS over HTTP2
SV_BRANCH=pr/1509

OISF/suricata-verify#1509

Draft to get feedback about approach...
Leaving comments on the code for specific questions

TODO :

  • Commit history to clean
  • free dns state in HTTP2 state

Generic approach :

  • recognize DNS over HTTP2
  • Enqueue a pseudo-packet with DNS payload on same flow
  • Dequeue and analyze packet as DNS
  • restore HTTP2 state of flow

@@ -477,6 +477,7 @@ pub const APP_LAYER_PARSER_EOF_TS : u16 = BIT_U16!(5);
pub const APP_LAYER_PARSER_EOF_TC : u16 = BIT_U16!(6);
pub const APP_LAYER_PARSER_TRUNC_TS : u16 = BIT_U16!(7);
pub const APP_LAYER_PARSER_TRUNC_TC : u16 = BIT_U16!(8);
pub const APP_LAYER_PARSER_LAYERED_PACKET : u16 = BIT_U16!(11);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please suggest me a better name :-p

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stacked? Encapped? Encapsulated? They'll kinda mean the same thing.

@@ -25,6 +25,9 @@ use crate::conf::conf_get;
use crate::core::*;
use crate::filecontainer::*;
use crate::filetracker::*;

use crate::dns::dns::rs_dns_state_new;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO use rs_dns_state_free

@@ -144,6 +147,8 @@ pub struct HTTP2Transaction {
pub escaped: Vec<Vec<u8>>,
pub req_line: Vec<u8>,
pub resp_line: Vec<u8>,

pub doh: Vec<u8>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: add comment to explain this field

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like it needs a longer name, I suppose a comment would help.

let mut host = None;
for block in blocks {
if block.name == b"content-encoding" {
self.decoder.http2_encoding_fromvec(&block.value, dir);
} else if block.name == b"accept" {
//TODO? faster pattern matching
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Victor, is this ok to match on the different header names ?

}
} else if block.name == b"content-type" {
if block.value == b"application/dns-message" {
// push original 2-bytes DNS/TCP header
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: have a longer comment

@@ -299,13 +327,17 @@ impl HTTP2Transaction {
&xid,
);
};
if !self.doh.is_empty() {
self.doh.extend_from_slice(decompressed);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO comment

@@ -441,6 +474,9 @@ pub struct HTTP2State {
dynamic_headers_tc: HTTP2DynTable,
transactions: VecDeque<HTTP2Transaction>,
progress: HTTP2ConnectionState,
// layered packets contents for DNS over HTTP2
layered: Vec<Vec<u8>>,
dns_state: Option<*mut std::os::raw::c_void>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO comment

I am not sure if this should be generic or DNS-specific...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is DoH a little special in that it doesn't follow the normal PROTO-over-HTTP formats like something like GRPC might do? Where they just use the bodies for their payload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where they just use the bodies for their payload?

Indeed DoH is not exactly like that : it has DNS message in the URI for requests (a base64-encoded parameter), and in bodies for responses.

Why does it matter ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where they just use the bodies for their payload?

Indeed DoH is not exactly like that : it has DNS message in the URI for requests (a base64-encoded parameter), and in bodies for responses.

Why does it matter ?

Say we were to support gRPC over HTTP, and Avro over HTTP, and Thrift over HTTP. Those would also make sense to have something generic. As DoH is different, could it be put within the same generic container or does it always need some special handling due to be in the URI. I guess not. But that's the path of thought I was on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed I was hesitating for naming dns_state or something more generic...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK without over thinking making it more generic until we have the need to make it more generic.

flow).is_err() {
self.set_event(HTTP2Event::FailedDecompression);
flow) {
Ok(_) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO helper function for less indent

unsafe {
self.dns_state = Some(rs_dns_state_new(std::ptr::null_mut(), ALPROTO_HTTP2));
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This is a key first step :
we

  • set a flag in pstate (so that AppLayerParserParse knows that it should deque packets)
  • push the to-be pseudo-packet payload in HTTP2 state
  • create a DNS state if there is not one yet

@@ -1438,6 +1441,32 @@ int AppLayerParserParse(ThreadVars *tv, AppLayerParserThreadCtx *alp_tctx, Flow
}
}

if (pstate->flags & APP_LAYER_PARSER_LAYERED_PACKET) {
// hardcoded logic
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: instead of hardcoded logic, protocols (such as HTTP2) should have a generic function that returns the pseudo-packets payloads

continue;
}
PKT_SET_SRC(np, PKT_SRC_APP_LAYER_LAYERED);
// TODO np->flags |= PKT_PSEUDO_DETECTLOG_FLUSH;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: remove until we need special flagging

np->payload = SCMalloc(b_len);
memcpy(np->payload, b, b_len);
np->payload_len = b_len;
PacketEnqueueNoLock(&tv->decode_pq, np);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ this is the second key step :

@victorjulien is &tv->decode_pq the right place to ensue such pseudo packets ?
I fear this will not work if I encapsulate my DNS over HTTP2 on GRE or other tunnels...

(&fw->pq is not already accessible here)

// switch flow to new protocol
void *alstate_orig = p->flow->alstate;
AppProto alproto_orig = p->flow->alproto;
// hardcoded logic
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: have a not hardcoded logic for this

SCFree(x->payload);
FlowDeReference(&x->flow);
TmqhOutputPacketpool(tv, x);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ this is the third key step

We deque and process the packets, switching the protocol and state...

FramesPrune(x->flow, x);

// switch back to real protocol
p->flow->alstate = alstate_orig;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is flow locked ? Can a race condition happen between changing alstate and alproto ?

// TODO np->flags |= PKT_PSEUDO_DETECTLOG_FLUSH;
np->payload = SCMalloc(b_len);
memcpy(np->payload, b, b_len);
np->payload_len = b_len;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I put a timestamp for this packet ? Where can I get it ?

@@ -205,5 +205,8 @@ uint64_t StreamDataRightEdge(const TcpStream *stream, const bool eof);
void StreamTcpThreadCacheEnable(void);
void StreamTcpThreadCacheCleanup(void);

// move elsewhere
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideas where this function should belong ?

return np;
error:
FlowDeReference(&np->flow);
PacketPoolReturnPacket(np);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PacketPoolReturnPacket was missing originally, correct ?

FLOWWORKER_PROFILING_END(x, PROFILE_FLOWWORKER_DETECT);
}
OutputLoggerLog(tv, x, fw->output_thread);
FramesPrune(x->flow, x);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should test frames I guess

@victorjulien
Copy link
Member

Before diving into the details I would like to read what the high level design is around the "layered" logic and pseudo packet(s), and how it would hook into detect and logging.

@catenacyber
Copy link
Contributor Author

Before diving into the details I would like to read what the high level design is around the "layered" logic and pseudo packet(s), and how it would hook into detect and logging.

I wrote this :

  • recognize DNS over HTTP2
  • Enqueue a pseudo-packet with DNS payload on same flow
  • Dequeue and analyze packet as DNS
  • restore HTTP2 state of flow

What do you expect different for a high-level design ?

@catenacyber
Copy link
Contributor Author

There is no change to the json schema.

  • flow events remain HTTP2
  • for this flow, we have both http and dns events (and alerts for both as well)

@victorjulien
Copy link
Member

victorjulien commented Dec 5, 2023

Before diving into the details I would like to read what the high level design is around the "layered" logic and pseudo packet(s), and how it would hook into detect and logging.

I wrote this :

* recognize DNS over HTTP2

* Enqueue a pseudo-packet with DNS payload on same flow

* Dequeue and analyze packet as DNS

* restore HTTP2 state of flow

What do you expect different for a high-level design ?

An actual explanation of the flow of packets and logic? Just a few half sentences isn't an explanation.

@catenacyber
Copy link
Contributor Author

This PR is a POC for DNS over HTTP2.
This is HTTP2 traffic where the HTTP2 payload can be interpreted as DNS.

As such, it is an example of a flow having multiple app-layer protocols. (DCERPC over SMB is the example in the current code).

high level explanation of the design

Functionnaly, in terms of output :

The goal, in terms of output, is to keep the same flow event (and so the same Flow structure in Suricata) which will have http2 as its app_proto, but have both http and dns events related to this flow (thanks to the flow_id). Signatures and alerts that work for dns, will work out of the bow for DNS over HTTP2. DNS over HTTP2 logs will be the same as normal DNS logs.
Another approach could be to make flow having a list of app_protos. (Looks too much a breaking change)
Or another approach could be to create a new app_proto dns_over_http2 (kind of like http1 and http2 share keywords under the http app_proto). But nothing prevents the flow to have both DoH and plain HTTP2 requests...

How does layering work

  • Memory management :
    Into the code, memory ownership and structures, the DNS stuff must go somewhere related to the Flow. As Flow structure does not change, we use one of its fields, the alstate which is a pointer to one of the app-protocols state. So, the HTTP2State may own a DNSState. (Actually, I am not sure that we need a DNSState, maybe we can do only with DNSTransactions, but the current code uses state)

  • Data processing steps :
    To have logging and detection for DNS, the hack is to switch the Flow's app_proto + alstate back and forth between HTTP2 and DNS. The normal state is to have HTTP2, log it and detect it.
    If there is a DNS message in the HTTP2 payload,

  • we parse, log and detect on the HTTP2 layer,

  • we somehow extract the bytes of the DNS message out of the HTTP2 payload

  • we create a pseudo-packet with the DNS payload. Pseudo-packets already exist in Suricata to be able to run logging/detection on events such as flow timeouts for instance (or protocol change, like HTTP1->HTTP2 upgrade where one packet has the first bytes being HTTP1 and the last one being HTTP2). (Actually I am not sure we need a full packet instead of just a payload + an app_proto but Detect and OutputLoggerLog take a packet as an argument). Note that one HTTP2 packet can have multiple frames/streams and thus multiple DNS messages.

  • we switch app_proto + alstate to DNS, parse log and detect the DNS layer (watching out for type confusion)

  • then switch back app_proto + alstate to HTTP2 to be ready to process next frames

  • Code API
    Generically for layered protocols, and for the app-proto API, this means a callback function that will give the payload (and the top app_proto like dns in this case) for pseudo-packets, and a function that will return the top app_proto state (the DNSState here) out of the base one (the HTTP2State here)

The current implementation also adds one bit flag for the u16 app-layer-parser flags (like APP_LAYER_PARSER_TRUNC_TC)

How is the implementation dealing with the current constraints ?

Which constraints ?

What are (expected) performance impacts ?

For time, I would roughly expect that a DoH packet takes twice as much time as a HTTP2 or DNS packet as we parse/log/detect both twice.

For space, we use a bit more memory for HTTP2 flows (not for other flows) and transactions, adding one or two fields that are pointers, unused if it is not DoH, and that are roughly twice the size of a HTTP2 flow when it is DoH (as it is both a DNS and a HTTP2 flow)

This is for regular processing.
For worst-case scenarios, the current POC has certainly its specific issues (like we do not enforce a U16_MAX limit for DNS messages as HTTP2 can transport more than 65k bytes)

@suricata-qa
Copy link

Information: QA ran without warnings.

Pipeline 16892

@victorjulien
Copy link
Member

Thanks Philippe, this is a very helpful explanation. Lets discuss the overall design before doing more code work on the current PR, as I'm not happy with the double packet logic. I would like the engine itself to understand layered protocols, instead of working around the current lack of support for this use case.

Some questions on what the design should support:

Conceptually, would we want to offer the user a way to match on both http and dns at the same time? e.g.

alert doh ... http.header; content:":authority: dns.google"; dns.query; content:"malware.com"; ..

Would we want EVE to log both in a single record? e.g.

{
  "event_type":"doh"
  "dns": {
  }
  "http2": {
  }
}

@victorjulien
Copy link
Member

victorjulien commented Dec 7, 2023

In my thinking we could do 2 models:

  • flow->alstate(http2)->alstate(dns) - where the engine is updated to inspect/manage/log both states more or less independently
  • flow->alstate(http2)->tx(http2)->tx(dns) - here there would be a 1:1 mapping between http2 and DNS tx's (kind of like files are in a tx) and the engine could inspect and log them in a single pass, making it possible to support the examples above

@catenacyber
Copy link
Contributor Author

Conceptually, would we want to offer the user a way to match on both http and dns at the same time?

Looks nice as it brings more features.

This would be possible with the ALPROTO_DOH solution

This looks kind of a specific solution for DoH, does not seem so generic for layered protocols. I mean will there be a combinatorial explosion for ALPROTO with such a solution ? Or not because there are not so many existing combinations

flow->alstate(http2)->tx(http2)->tx(dns) - here there would be a 1:1 mapping between http2 and DNS tx's (kind of like files are in a tx) and the engine could inspect and log them in a single pass, making it possible to support the examples above

Are not DNS transactions unidirectional (and HTTP2 are bidirectional) ?

@victorjulien
Copy link
Member

Conceptually, would we want to offer the user a way to match on both http and dns at the same time?

Looks nice as it brings more features.

This would be possible with the ALPROTO_DOH solution

This looks kind of a specific solution for DoH, does not seem so generic for layered protocols. I mean will there be a combinatorial explosion for ALPROTO with such a solution ? Or not because there are not so many existing combinations

SMB/DCERPC seem quite similar. I think there the (hardcoded) solution is closer to having 2 states and have the detect/log etc code iterate them where needed.

flow->alstate(http2)->tx(http2)->tx(dns) - here there would be a 1:1 mapping between http2 and DNS tx's (kind of like files are in a tx) and the engine could inspect and log them in a single pass, making it possible to support the examples above

Are not DNS transactions unidirectional (and HTTP2 are bidirectional) ?

Yeah, so perhaps in that case you'd need tx(http2)->tx(dns)[2] or something, where the direction determines which dns tx to use

@catenacyber
Copy link
Contributor Author

So, I will try the ALPROTO_DOH2 next

@zoomequipd
Copy link

Conceptually, would we want to offer the user a way to match on both http and dns at the same time? e.g.

alert doh ... http.header; content:":authority: dns.google"; dns.query; content:"malware.com"; ..

The more I thought about this, the more I liked this idea. As we start seeing some DOH Providers offering "specialized" dns services and and being used to facilitate Alternated DNS Roots, the ability to match the :authority/host header with the DNS query/domain could be used to provide high fidelity detection.

So, given a single query for DOH, the following signatures would fire.
Assuming that all these other

alert dns ... dns.query; content:"malware.com"; ... sid:1; ...
alert http ... http.uri; content:"<base64 encoded dns query for malware.com>"; ... sid:2; ...
 ^^^^ (would likely need 3 rules for the various base64 offsets)
alert doh ... http.header; content:":authority: dns.google"; dns.query; content:"malware.com"; ... sid:3;...

sid:1 allows us to write typical DNS formatted rules, where I don't care what protocol is used to make the query
sid:2 allows us to write signature to detect protocol layer issues and other detections on dns traffic
sid:3 allows us to write signatures base specifically on DOH traffic and can combined elements of the HTTP layer and DNS buffers

Question:

  1. As I don't understand the full context of this PR, how does this impact the additional DNS buffers efforts that @jasonish is working on? Will alert doh and alert dns both benefit from the additional buffers?

  2. with alert doh can the to_server/to_client be used to enforce matching on request vs response?

@catenacyber
Copy link
Contributor Author

Note : The ALPROTO_DOH2 implementation will not be able to use DNS frames. (because frames are meant to reference a UDP payload or TCP stream, not a base64-decoded temporary buffer, right ?)

@catenacyber
Copy link
Contributor Author

with alert doh can the to_server/to_client be used to enforce matching on request vs response?

I do not see any difficulties for this

Will alert doh and alert dns both benefit from the additional buffers?

doh means to use all dns buffers

@victorjulien
Copy link
Member

Note : The ALPROTO_DOH2 implementation will not be able to use DNS frames. (because frames are meant to reference a UDP payload or TCP stream, not a base64-decoded temporary buffer, right ?)

Correct.

@catenacyber
Copy link
Contributor Author

New version in #10040

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
5 participants