The Poor Man's (or Woman's) Intrusion Detection System
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

293 lines
7.7 KiB

  1. //////////////////////////////////////////////////////////////////////
  2. // IP traffic analyzer - data objects
  3. // Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net>
  4. // Started April 23rd, 2021
  5. // Copyright JF Possibilities, Inc. All rights reserved.
  6. //////////////////////////////////////////////////////////////////////
  7. #include <arpa/inet.h>
  8. #include <string.h>
  9. #include <stdlib.h>
  10. #include <stdexcept>
  11. #include <iostream>
  12. #include "data.h"
  13. //////////////////////////////////////////////////////////////////////
  14. // Utils
  15. //////////////////////////////////////////////////////////////////////
  16. std::string ipv6opt(const std::string &addr) {
  17. in6_addr buf;
  18. char s[256];
  19. if(inet_pton(AF_INET6, addr.c_str(), &buf)<1) throw
  20. std::runtime_error("ipv6opt: inet_pton() says '"+addr+"' is not a valid IPv6 address");
  21. if(!inet_ntop(AF_INET6, &buf, s, 255)) throw // should never happen
  22. std::runtime_error("ipv6opt: inet_ntop() refused to convert the address back");
  23. return std::string(s);
  24. }
  25. int addr_wild_comp(const std::string &str1, const std::string &str2) {
  26. int spre1=0, spre2=0;
  27. if(str1=="*" || str2=="*") return 0;
  28. if(str1!="" && (str1.end()[-1]=='.' || str1.end()[-1]==':')) spre1=str1.size();
  29. if(str2!="" && (str2.end()[-1]=='.' || str2.end()[-1]==':')) spre2=str2.size();
  30. if(spre2>spre1) spre1=spre2;
  31. if(spre1) return strncmp(str1.c_str(), str2.c_str(), spre1);
  32. else if(str1<str2) return -1;
  33. else if(str1>str2) return 1;
  34. return 0;
  35. }
  36. //////////////////////////////////////////////////////////////////////
  37. // Conn
  38. //////////////////////////////////////////////////////////////////////
  39. void Conn::clear() {
  40. us = them = name = protocol = "";
  41. in=false;
  42. us_port = them_port = 0;
  43. }
  44. void Conn::compact() {
  45. if(us.find(':')!=us.npos) us=ipv6opt(us);
  46. if(them.find(':')!=us.npos) them=ipv6opt(them);
  47. }
  48. void Conn::swap() {
  49. std::string s;
  50. int x;
  51. s = us;
  52. us = them;
  53. them =s;
  54. x = us_port;
  55. us_port = them_port;
  56. them_port = x;
  57. in=!in;
  58. }
  59. Conn &Conn::operator=(const Splits &sp) {
  60. int x;
  61. clear();
  62. for(x=0; x<sp.count; x++) {
  63. if(!strncmp(sp.fields[x], "SRC=", 4)) {
  64. us = sp.fields[x]+4;
  65. continue;
  66. }
  67. if(!strncmp(sp.fields[x], "DST=", 4)) {
  68. them = sp.fields[x]+4;
  69. continue;
  70. }
  71. if(!strncmp(sp.fields[x], "SPT=", 4)) {
  72. us_port = atoi(sp.fields[x]+4);
  73. continue;
  74. }
  75. if(!strncmp(sp.fields[x], "DPT=", 4)) {
  76. them_port = atoi(sp.fields[x]+4);
  77. continue;
  78. }
  79. if(!strncmp(sp.fields[x], "TYPE=", 5) && protocol=="ICMP") {
  80. us_port = them_port = atoi(sp.fields[x]+5);
  81. continue;
  82. }
  83. if(!strncmp(sp.fields[x], "PROTO=", 6))
  84. protocol = sp.fields[x]+6;
  85. }
  86. }
  87. // TODO: does < > have any actual meaning in this context?
  88. int Conn::cmp(const Conn &gtr) const {
  89. int r;
  90. if(r = addr_wild_comp(us, gtr.us)) return r;
  91. // TODO: auto-wildcard port based on in?
  92. if(us_port && gtr.us_port) { // 0 = no comparison wildcard
  93. if(us_port<gtr.us_port) return -1;
  94. if(us_port>gtr.us_port) return 1;
  95. }
  96. if(r = addr_wild_comp(them, gtr.them)) return r;
  97. if(them_port && gtr.them_port) { // 0 = no comparison wildcard
  98. if(them_port<gtr.them_port) return -1;
  99. if(them_port>gtr.them_port) return 1;
  100. }
  101. // TODO: do we want to consider the name?
  102. if(name!="*" && gtr.name!="*") {
  103. if(name<gtr.name) return -1;
  104. if(name>gtr.name) return 1;
  105. }
  106. if(protocol!="*" && gtr.protocol!="*") {
  107. if(protocol<gtr.protocol) return -1;
  108. if(protocol>gtr.protocol) return 1;
  109. }
  110. if(in<gtr.in) return -1;
  111. if(in>gtr.in) return 1;
  112. return 0;
  113. }
  114. std::ostream &operator<<(std::ostream &out, const Conn &c) {
  115. out << c.us
  116. << ( c.in ? " <- " : " -> " )
  117. << c.them
  118. << " " << c.protocol
  119. << "[" << ( c.in ? c.us_port : c.them_port ) << "] "
  120. << c.name;
  121. return out;
  122. }
  123. const Splits &operator>>(const Splits &tsv, Conn &conn) {
  124. if(tsv.count<7) throw std::runtime_error("Conn=TSV: too few columns");
  125. conn.clear();
  126. conn.us = tsv[0];
  127. conn.us_port = atoi(tsv.fields[1]);
  128. conn.them = tsv[2];
  129. conn.them_port = atoi(tsv.fields[3]);
  130. conn.name = tsv[4];
  131. conn.protocol = tsv[5];
  132. conn.in = tsv[6]=="1";
  133. return tsv;
  134. }
  135. //////////////////////////////////////////////////////////////////////
  136. // ConnList
  137. //////////////////////////////////////////////////////////////////////
  138. int ConnList::find(Conn &needle) {
  139. int r;
  140. for(r=0; r<size(); r++) if((*this)[r]==needle) return r;
  141. return -1;
  142. }
  143. //////////////////////////////////////////////////////////////////////
  144. // LogAnalyzer
  145. //////////////////////////////////////////////////////////////////////
  146. LogAnalyzer::LogAnalyzer():
  147. us(0)
  148. { // I'd rather this initialization be static...
  149. dns_ignore.push_back("v=spf1");
  150. dns_ignore.push_back("https:");
  151. dns_del.push_back("NODATA-");
  152. dns_del.push_back("NXDOMAIN-");
  153. }
  154. bool LogAnalyzer::line(const std::string &in) {
  155. int ict=0;
  156. NameVal::iterator nvp;
  157. std::string name, address, s;
  158. /// setup ///
  159. if(!us)
  160. throw std::runtime_error("LogAnalyzer::line: us list is not assigned");
  161. ln=in;
  162. /// DNS query result ///
  163. // TODO: need to get more specific on tying us + them + time to DNS
  164. if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
  165. if(ln[5]=="reply" || ln[5]=="cached") {
  166. name = ln[6];
  167. address = ln[8];
  168. /* NOTE: CNAME resolution seems to follow this order in logs:
  169. 1. A result line (reply/cached) with an address of <CNAME>
  170. 2. One or more consecutive result lines for the canonical name
  171. Looking over the logs it doesn't appear that dnsmasq will log
  172. anything between the original and CNAME resolutions. The exception
  173. is if a CNAME record is cached and it has to resolve what it
  174. points to. In this case there would be a "cached" and then a
  175. "forwarded" record eventually followed by "reply ... <CNAME>".
  176. In that case we want to operate on the reply.
  177. I just saw that CNAME log entries can be chained. It looks like
  178. they are an "is <CNAME>" entry followed by another. We want to
  179. keep the original name (alias).
  180. */
  181. // we're handling a CNAME entry
  182. if(address=="<CNAME>") {
  183. // If we don't have a cname yet then this is a CNAME to a CNAME.
  184. if(alias=="" || cname!="") {
  185. alias = name;
  186. cname = "";
  187. }
  188. return 0;
  189. }
  190. // If in cname _mode_:
  191. if(alias!="") {
  192. if(cname=="") {
  193. cname = name; // This is our target name
  194. name = alias; // substitute the alias
  195. } else if(cname==name) {
  196. name = alias; // substitute the alias
  197. } else {
  198. cname = ""; // These are different records reset
  199. name = "";
  200. }
  201. }
  202. // Hmm... is this reply an address?
  203. if(pre_match(dns_ignore, address)) return 0; // nope
  204. if(pre_match(dns_del, address)) return 0; // does not exist reply
  205. if((nvp=rdns.find(address))!=rdns.end()) {
  206. if(nvp->second==name) return 0;
  207. //dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
  208. }
  209. rdns[address] = name;
  210. //dlog("Added "+address+" = "+name);
  211. return 0;
  212. } else if(alias!="") {
  213. // we've fallen out of CNAME resolution.
  214. alias = "";
  215. cname = "";
  216. }
  217. }
  218. /// process connections ///
  219. if((ln.count>5 // old Linux style
  220. && ln[4]=="kernel:"
  221. && ln[5]=="ACCEPT"
  222. ) || (ln.count>6 // new Linux style
  223. && ln[4]=="vmunix:"
  224. && ln[6]=="ACCEPT")
  225. ) {
  226. conn = ln;
  227. conn.compact();
  228. if(!pre_match(*us, conn.us)) conn.swap();
  229. if((nvp=rdns.find(conn.them))!=rdns.end()) conn.name = nvp->second;
  230. return 1;
  231. }
  232. return 0;
  233. }