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.
 
 
 
 

281 lines
7.3 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) return -1;
  107. if(protocol>gtr.protocol) return 1;
  108. if(in<gtr.in) return -1;
  109. if(in>gtr.in) return 1;
  110. return 0;
  111. }
  112. std::ostream &operator<<(std::ostream &out, const Conn &c) {
  113. out << c.us
  114. << ( c.in ? " <- " : " -> " )
  115. << c.them
  116. << " " << c.protocol
  117. << "[" << ( c.in ? c.us_port : c.them_port ) << "] "
  118. << c.name;
  119. return out;
  120. }
  121. const Splits &operator>>(const Splits &tsv, Conn &conn) {
  122. if(tsv.count<7) throw std::runtime_error("Conn=TSV: too few columns");
  123. conn.clear();
  124. conn.us = tsv[0];
  125. conn.us_port = atoi(tsv.fields[1]);
  126. conn.them = tsv[2];
  127. conn.them_port = atoi(tsv.fields[3]);
  128. conn.name = tsv[4];
  129. conn.protocol = tsv[5];
  130. conn.in = tsv[6]=="1";
  131. return tsv;
  132. }
  133. //////////////////////////////////////////////////////////////////////
  134. // ConnList
  135. //////////////////////////////////////////////////////////////////////
  136. int ConnList::find(Conn &needle) {
  137. int r;
  138. for(r=0; r<size(); r++) if((*this)[r]==needle) return r;
  139. return -1;
  140. }
  141. //////////////////////////////////////////////////////////////////////
  142. // LogAnalyzer
  143. //////////////////////////////////////////////////////////////////////
  144. LogAnalyzer::LogAnalyzer():
  145. us(0)
  146. { // I'd rather this initialization be static...
  147. dns_ignore.push_back("v=spf1");
  148. dns_ignore.push_back("https:");
  149. dns_del.push_back("NODATA-");
  150. dns_del.push_back("NXDOMAIN-");
  151. }
  152. bool LogAnalyzer::line(const std::string &in) {
  153. int ict=0;
  154. NameVal::iterator nvp;
  155. std::string name, address, s;
  156. /// setup ///
  157. if(!us)
  158. throw std::runtime_error("LogAnalyzer::line: us list is not assigned");
  159. ln=in;
  160. /// DNS query result ///
  161. // TODO: need to get more specific on tying us + them + time to DNS
  162. if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
  163. if(ln[5]=="reply" || ln[5]=="cached") {
  164. name = ln[6];
  165. address = ln[8];
  166. /* NOTE: CNAME resolution seems to follow this order in logs:
  167. 1. A result line (reply/cached) with an address of <CNAME>
  168. 2. One or more consecutive result lines for the canonical name
  169. Looking over the logs it doesn't appear that dnsmasq will log
  170. anything between the original and CNAME resolutions. The exception
  171. is if a CNAME record is cached and it has to resolve what it
  172. points to. In this case there would be a "cached" and then a
  173. "forwarded" record eventually followed by "reply ... <CNAME>".
  174. In that case we want to operate on the reply.
  175. */
  176. /* record we're handling a CNAME cycle */
  177. if(address=="<CNAME>") {
  178. alias = name;
  179. cname = "";
  180. return 0;
  181. }
  182. /* If in cname _mode_: */
  183. if(alias!="") {
  184. if(cname=="") {
  185. cname = name; /* This is our target name */
  186. name = alias; /* substitute the alias */
  187. } else if(cname==name) {
  188. name = alias; /* substitute the alias */
  189. } else {
  190. cname = ""; /* These are different records reset */
  191. name = "";
  192. }
  193. }
  194. // Hmm... is this reply an address?
  195. if(pre_match(dns_ignore, address)) return 0; // nope
  196. if(pre_match(dns_del, address)) return 0; // does not exist reply
  197. if((nvp=rdns.find(address))!=rdns.end()) {
  198. if(nvp->second==name) return 0;
  199. //dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
  200. }
  201. rdns[address] = name;
  202. //dlog("Added "+address+" = "+name);
  203. return 0;
  204. } else if(alias!="") {
  205. alias = ""; /* we've fallen out of CNAME resolution. */
  206. cname = "";
  207. }
  208. }
  209. /// process connections ///
  210. if((ln.count>5 // old Linux style
  211. && ln[4]=="kernel:"
  212. && ln[5]=="ACCEPT"
  213. ) || (ln.count>6 // new Linux style
  214. && ln[4]=="vmunix:"
  215. && ln[6]=="ACCEPT")
  216. ) {
  217. conn = ln;
  218. conn.compact();
  219. if(!pre_match(*us, conn.us)) conn.swap();
  220. if((nvp=rdns.find(conn.them))!=rdns.end()) conn.name = nvp->second;
  221. return 1;
  222. }
  223. return 0;
  224. }