65.9K
CodeProject 正在变化。 阅读更多。
Home

DNS 服务器是工具箱中的最佳工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (20投票s)

2015年4月30日

CPOL

14分钟阅读

viewsIcon

33962

downloadIcon

3511

抵御病毒和间谍软件的第一道防线

引言

如果您和我一样,现在有十台或更多计算机连接到您的wifi网络,并希望保留对通过Internet传输的数据的一些控制,那么您可能需要考虑在一台专用机器上托管自己的DNS服务器,并在局域网(LAN)上共享该服务器。

来自Web浏览器的DNS请求使用UDP数据包在端口53上通过Internet与服务器通信,以将域名解析为Ipv4或IPv6地址,然后返回的IP地址将用于直接与网站通信,通常使用TCP数据包通过HTTP在端口80或HTTPS的端口443进行通信。

维护和运行自己的DNS服务器有很多好处,但有一个缺点,我将在下面列出。

间谍软件/病毒防护

在路由器的防火墙中阻止除托管本地DNS服务器的计算机之外的所有出站UDP端口53的流量,是杀死95%的间谍软件的绝佳方法,因为没有IP地址就无法在未经DNS服务器的许可的情况下呼叫回家。实际上应该是100%的,但由于需要呼叫回家,Microsoft和其他公司经常通过将IP地址硬编码到代码中来绕过对DNS服务器的需求。

大多数路由器将由其ISP提供一对DNS服务器地址,路由器会将这些服务器的地址传递给连接到路由器LAN的每个设备。从表面上看,这似乎很有益,因为您的ISP喜欢通过缓存eBay等网站的数据来降低上行成本,然后以最高速度向您发送缓存的数据副本。

我对这种商业安排没有异议,但您应该注意,ISP可以并且确实会将营销数据出售给第三方,如果您正在使用安全的Tor网络,则需要格外小心,因为用于缓存的某些IP地址对于您的ISP和国家来说是唯一的。

ISP的DNS劫持

我确实有问题的地方是,当ISP(如我的ISP)开始劫持DNS查找时,客户已选择退出并使用第三方提供商(如OpenDNS或Google的8.8.8.8)的服务,因为ISP就像黑客一样行事,阻止这种做法的唯一方法是切换到使用HTTPS在端口443上的DNS服务器,或使用连接到Cyber-Ghost之类的VPN。

问题是,大多数网络设备只能使用UDP端口53的DNS工作,因此您需要托管自己的DNS服务器来处理请求,并将服务器连接到端口443上的上行DNS服务器以阻止这些劫持。

了解是否有人劫持您的DNS查找的最佳方法是,将DNS服务器链接到Whois表(Whois是一个大杂烩,我花了数月时间收集、压缩和分析了42亿个IP地址),但您可以下载页面顶部Whois.zip文件中的XML数据,然后使用下面的代码开始,如果您想将“Whois”添加到您的项目中。

public static long IPtoLonger(string IP)
      {//The XML table uses longs so we need to convert Ipv4 to longs
          string[] Data = IP.ChopOffAfter(":").Split('.');
          if (Data.Length != 4 || IP.EndsWith(".")) return 0;
          long w = long.Parse(Data[0]);
          long x = long.Parse(Data[1]);
          long y = long.Parse(Data[2]);
          long z = long.Parse(Data[3]);
          return 16777216 * w + 65536 * x + 256 * y + z;
      }
public static string Long2IP(long Number)
      {//Not used here but this is how to convert longs back to an IPv4address
          long w = (Number / 16777216) % 256;
          long x = (Number / 65536) % 256;
          long y = (Number / 256) % 256;
          long z = (Number) % 256;
          return "" + w + "." + x + "." + y + "." + z;
      }

DataTable DT=new DataTable("Data");
DT.ReadXml("c:\\Whois.xml");
long IPNum = IPtoLonger("8.8.8.8");
DataRow[] Rows = DT.Select("F<=" + IPNum + " AND T>=" + IPNum);//F=From IP, T=To IP
if (Rows.Length > 0)
   return Rows[0]["W"].tostring();

反向查找

大多数人倾向于认为每个网站都有一个专用的IP地址,并且每个IP地址只连接到一个站点,但这远非事实,在DNS服务器的请求下,会返回多个IPv4地址和一个或两个IPv6地址。

反向查找是向DNS服务器发送IP地址并请求返回该IP的域名这一过程,但这些结果经常不匹配,“www.google.com”可能会返回“1.2.3.4”,但“1.2.3.4”的反向查找将返回dfw06s32-in-f16.1e100.net,如果您的ISP劫持了域名查找,它们通常会阻止对该地址的反向查找。(也许需要欺骗SSL接受伪造证书,尚不确定)

有时,域名地址会根据请求来自的国家而更改,以帮助负载均衡服务器,因此美国用户的Google.com可能会返回“1.2.3.4”,但印度的查找可能总是返回“4.3.2.1”,然而浏览器和操作系统通常会存储旧的IP地址,因此稍后使用VPN通过印度连接到Google,并将“1.2.3.4”作为缓存地址,就会暴露出来。

安全与性能

任何有价值的DNS服务器都将拥有自己的内置DNS缓存和流量保护列表(TPL),这与出色的“Ghosty”Firefox插件非常相似,该插件被全球数百万用户使用,这本身就可以将互联网流量减少一半,从而使浏览速度加倍。

DNS服务器仅在域名级别工作,从不看到URL,甚至不知道查找请求是否将使用端口443的HTTP/SSL,或者甚至在UDP请求中使用端口123,因此任何带有TPL的DNS服务器都可以简单地阻止“www.porno.com”供儿童机器使用,但这对于阻止“www.google.com/q=Big%20Girls”毫无用处,可以使用代理服务器阻止,但这只适用于HTTP而不是HTTPS。(防火墙关键字过滤也是如此,因为它们看不到SSL请求中的URL)

此DNS服务器使用的TPL与共享数据文件的代理服务器共享,但TPL已扩展为包含特定于计算机的规则,因为它在网络上共享,并且还包括将某些请求重定向到代理服务器并允许“保护”模式。

//##################### Ghostry TPL #####################
- -clicktale.          //Block if bad keyword found in the Url
- -comscore.           
-d botsvisit.com       //Block the domain name
-d brandaffinity.net
-d brat-online.ro
//############### Extended Ghostry TPL #################
-d mypornsite.com (KidsPC) //Block the kids seeing porn
+d youtube.com             //Allow even if Url has a bad keyword for everyone 
+d TimeForBed.com (KidsPC) //Allow for kids even after bed time, see time schedules later
-r .google.com             //Redirect to a local proxy server to allow or block the request
-p www.microsoft.com       //Protected so the proxy server will allow after faking parts of the request

“保护”模式用作代理服务器的标志,用于修改发送到任何受保护站点的HTTP请求,并且可能伪造用户代理或国家代码,甚至删除请求中的Cookie,然后使用Tor网络中继请求。

DNS服务器工作示意图

下面显示的是DNS服务器的工作示意图,正如您所见,服务器可以通过右键单击域名来阻止/保护/允许或重定向DNS查找,并且服务器还列出了LAN上呼叫设备的计算机名称。
绿色=允许  红色=阻止  紫色=保护  棕色=重定向到代理服务器 

dns-server

请注意,底部偏黄的区域是来自防火墙sys-logs服务器的DNS请求,该服务器使用TCP而不是UDP与DNS服务器通信,因为我们不希望阻止这些类型的请求,也不希望在结果中向sys-logs服务器返回“127.0.0.1”,并且此设置还允许服务器在定制的回复中返回国家代码和其他信息。

DNS的正式规范的一部分是TCP应该为大型回复数据包实现,但上面提到的TCP只是使用Web类型的HTTP请求与服务器通信。

服务器还可以解析本地机器上的进程ID到进程名称,这有助于追踪连接到网络的任何病毒,但在上图未开启,我将在另一篇文章中介绍如何实现,如果您想要代码,请从页面顶部下载VS2010 C#项目,然后查看“ProcEngine”类。

时间表

任何像样的企业级DNS服务器都离不开时间表,以确保它们从不阻止老板的请求,但能够阻止孩子们整夜观看YouTube,因此我们有可以配置为在夜间阻止所有内容,然后强制某些机器使用TPL的时间表。

时间表还可以用于将IP地址解析为机器名称,这很重要,因为您不想要求Google DNS服务器解析本地地址,如“192.168.1.10”,并且当DNS服务器为本地名称返回空结果时,会发生各种奇怪的事情,这些结果会通过LAN传播以进行解析。

dns-server

还需要注意不要将广播或多播地址转发到上游服务器,并且在多线程DNS服务器中,还需要注意防止浏览器泛滥,因为浏览器有时会在上游服务器有机会响应第一个请求之前每秒重复相同的请求十次。

时间表使用的代码位于名为“TimeSchedules”的类中,并且只是包装了一个XML数据表,如果您想单独使用该代码。

向服务器发出DNS请求

下面是构建请求以发送到上游服务器的简化代码版本,是的,我知道您可以发出十种类型的请求并构建一个庞大的类库来添加问题,但这对于Ipv4来说效果很好,而且不会过于技术化。

//First we need to get byte[]data to send to the upstream server
private static byte[] DnsBytesLookup(string DomainName)
       {//Builds up a request for a DNS-Lookup
           DomainName = DomainName.ToLower();
           byte[] Head = SeedTheHead(DateTime.Now.Second);
           byte[] Footer = new byte[5] { 0, 0, 1, 0, 1 };
           MemoryStream MS = new MemoryStream();
           MS.Write(Head, 0, Head.Length);
           byte[] Domain = DomainNameBytes(DomainName);
           MS.Write(Domain, 0, Domain.Length);
           MS.Write(Footer, 0, Footer.Length);
           return MS.ToArray();
       }

private static int DnsSeed = 4;//We need to send a seed number with each DNS request
private static byte[] SeedTheHead(int Seed)
       {//Returns the seeded head data for both types of DNS-Requests 
           DnsSeed++;
           if (DnsSeed > 255) DnsSeed = 1;
           byte[] Head = new byte[12] { 77, 77, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0 };
           Head[0] = (byte)DnsSeed;
           Head[1] = (byte)Seed;
           return Head;
       }

//Cut down a bit but i think this should about work
byte []Data=DnsBytesLookup("www.bing.com");//Get the data to send
IPEndPoint IPE = new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53);//Our DNS-Server
Socket Soc = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
Soc.ReceiveTimeout = 15000;
Soc.SendTimeout = 15000;
Soc.Connect(IPE);
Soc.Send(this.Request, SocketFlags.None);//Send our request
Thread.Sleep(200);//It's not going to come back much faster than this so take a break
byte[] buffer = new byte[2048];
int Size = Soc.Receive(buffer);
byte[] Reply = new byte[Size];//We will come back to our reply in a bit
Soc.Shutdown(SocketShutdown.Both);
Soc.Close();

//############### For a reverse lookup use the code below ###############
private static byte[] DnsBytesReverseLookup(string IP)
       {//Builds up a request for a Reverse DNS-Lookup
           byte[] Head = SeedTheHead(DateTime.Now.Minute);
           byte[] Footer = new byte[18] { 7, 105, 110, 45, 97, 100, 100, 114, 4, 97, 114, 112, 97, 0, 0, 12, 0, 1 };//in-addr.arpa
           string[] IPDigits = IP.Split('.');
           MemoryStream MS = new MemoryStream();
           MS.Write(Head, 0, Head.Length);
           byte[] IP0 = ByteString(IPDigits[0]); byte[] IP1 = ByteString(IPDigits[1]); byte[] IP2 = ByteString(IPDigits[2]); byte[] IP3 = ByteString(IPDigits[3]);
           MS.Write(IP3, 0, IP3.Length); MS.Write(IP2, 0, IP2.Length); MS.Write(IP1, 0, IP1.Length); MS.Write(IP0, 0, IP0.Length);
           MS.Write(Footer, 0, Footer.Length);
           return MS.ToArray();
       }

向上游服务器请求域名可能会返回多个IPv4地址,因此我们使用此函数返回的第一个地址,然后增加“StartAt”以获取可能返回的其他地址。

public static string GetIPFromReply(byte[] Data,ref int StartAt)
        {//External function calls this so i kept it public
            if (Data.Length < 15) return "";
            for (int f = StartAt; f <= Data.Length - 7; f++)
            {//We look for the first IP-Address and use that one
                if (Data[f] == 0 && Data[f + 1] == 4 && Data[f + 6] == 192)//We are looking for 0,4,IP1,IP2,IP3,IP4,192
                {
                    StartAt = f + 5;
                    return Data[f + 2] + "." + Data[f + 3] + "." + Data[f + 4] + "." + Data[f + 5];
                }
            }
            //############ Nope looks like we need to get the last-ip #############
            StartAt = 0;
            if (Data[Data.Length - 6] != 0 && Data[Data.Length - 5] != 4) return "";//The end of this packet does not contain an IP-Address
            return Data[Data.Length - 4] + "." + Data[Data.Length - 3] + "." + Data[Data.Length - 2] + "." + Data[Data.Length - 1];
        }

使用上方代码部分的Reply[]数据,我们如下使用上述函数来读取Reply[]字节数据中的IPv4地址。

int StartAt = Request.Length;//Start of the reply contains our request data, dont't read it
string IP = GetIPFromReply(this.Reply, ref StartAt);//We will return first one to the client process
string IPv4s = IP;//IPV4s will contain a full list of Ipv4 addresses that are returned
int Count = 0;
while (StartAt > 0 && Count<10)//I don't want more than ten or to get into a endless loop
   {//Keep reading until we run out of IP-Addresses
   Count++;
   string NextIP = GetIPFromReply(Reply, ref StartAt);
   if (NextIP.Length > 4 && this.IPv4s.IndexOf(NextIP) == -1)
         IPv4s += "," + NextIP;
   }

读取反向查找的回复有点复杂,而且并不总是能保证获得一个回复,如果您的ISP和我的ISP一样,但对于Ipv4来说,这就是您真正需要或看到的全部内容,请参阅DNSClient.cs获取其余代码。

但是,IPv6呢,因为我们已经用完了IPv4!

IPv4使用32位或4字节,可提供256 x 256 x 256 x 256 = 4,294,967,296或约43亿个可能的地址,其中约有5亿个由IANA保留。

IPv6使用96位或12字节,可提供65536 x 65536 x 65536 x 65536 x 65536 x 65536 = (下一行)

79,228,162,514,264,337,593,543,950,336或比IPv4提供的IP地址多**18,446,744,073,709,551,616**倍,如果我的计算是正确的。

显然,公司对未来有宏伟的计划,我想在不久的将来,冰箱里的牛奶瓶会在没电时打电话给当地商店重新订购,但在此之前,网络卡中的所有6字节MAC地址都需要升级以适应新的IPv6地址,所以我认为IPv4还会存在相当长的时间,而且IPv6与RFID跟踪的联系比互联网连接更紧密。

由于Teredo隧道(用于在仅支持IPv4的网络(实际上是大多数网络)上连接IPv4网络到IPv6网络)带来的安全风险,我已在我的网络上关闭了IPv6,因为这种VPN可以绕过防火墙规则……

抱歉,DNS服务器和我的ISP一样,不支持IPv6

其他考虑事项

此DNS服务器从一开始就设计为与代理服务器协同工作(工具箱中的第二大工具)并与服务器共享相同的TPL,该服务器能够劫持LAN的所有apiscripst.XXX.com DNS请求,然后将其指向代理服务器,代理服务器随后有机会检查Web浏览器稍后发送的完整URL,以决定是否允许或阻止该请求。

如果代理服务器允许请求,例如“apiscripst.XXX.com/JQuerry.js”,那么DNS服务器足够智能,可以向代理服务器提供正确的IP地址,而不是返回代理的劫持地址。这个小技巧对HTTP流量有效,但对HTTPS无效,除非代理服务器充当中间人并颁发伪造的CA证书。(请参阅后面的项目,我将向您展示如何做到这一点)

现在也许是时候问问自己,为什么ISP能够劫持DNS请求,而这些请求后来指向HTTPS站点而浏览器却没有弹出警告消息。(我认为这就是ISP阻止对劫持域名的反向查找的原因,仍在研究中)

另一个不错的选择是能够响应类似MyProxyIP.Home(可能是具有两个网卡的机器)或MyPublicIP.Home的请求,并过滤掉Firefox发出的伪造的十位数请求(停止堵塞我的网络),这些都已在DNS服务器中得到处理。

不太明显的是需要标记某些域名不被DNS服务器缓存,或者强制某些域名不进行负载均衡,因为IP范围在防火墙中被阻止(我工具箱中的第三个工具)并始终使用相同的单个未阻止IP地址。

多年来我发现,十次中有九次HTTPS被使用不是为了保护您的隐私或银行详细信息,因为大多数时候即使没有用户登录,SSL也会被使用,而是HTTPS被用来隐藏被下载的间谍软件脚本,然后数据从智能电视等设备流回服务器,而智能电视是世界上最差的。最差中的最差的是那些(不提名字)甚至不允许所有者选择上游代理服务器的设备!

我碰巧拥有一台这样的设备,并发现通过选择性地劫持DNS请求,然后使用代理服务器来修改它上传到全球服务器的序列号,即使在打开电视时,并且不使用任何“智能”功能,它仍然可以正常工作。

如果公司会付出如此大的努力来隐藏他们的行为,那么希望您能理解为什么我付出了如此大的努力来阻止他们。

关注点

令人惊讶的是,我在这个项目上遇到了一个关于线程的问题,当网络断开连接时,这导致程序的线程数急剧增加,最终我设法找到了下面我剥离出来的代码。

这里的代码使用异步回调来向DNS服务器发出请求,我使用的线程管理器类似于.Net 4中的System.Threading.Task,并且只是在线程运行时间过长时调用abort()

public bool Running=false;
public Socket Soc=null;
public Thread TH=null;
public void Stop()
{//Called on the main process thread
  Running=false;
  Thread.Sleep(200);//See if the thread will die on its own
  if (TH!=null && TH.State==ThreadState.Running){ShutMeUp(); TH.abort();
}

public void Abort()
{//Called by the thread manager is the threads runs too long
   if(!Running) return;//Already dead
   if (TH!=null) TH.abort();//We must never do this but what else can you do?
   ShutMeUP();
}

private void ShutMeUp()
{//Needs try; catch and should look at the socket state
  Soc.Shutdown(SocketShutdown.Both);
  Soc.Close();
}

public void Start()//Stage One
{
    Running=true;
    TH=MyMananger.NewThread(this.abort,this.run,20);//Run for 20 seconds then call this.Abort()
}                                                   //but only if the thread is still running

public void Run()//Stage Two
{
  IPEndPoint IPE = new IPEndPoint(IPAddress.Parse(ServerIP), ServerPort);//Our DNS-Server address
  this.Soc = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
  this.Soc.Timeout =12000;
  Helper.BindToLocalPort(null, Soc, Helper.MyIP4, 0);
  this.Soc.BeginConnect(IPE, new AsyncCallback(BeginConnect), null);
}

private void BeginConnect(IAsyncResult ar)//Stage Threee
{
  if (!Runing) {ShutMeUp();Return}
  Soc.EndConnect(ar);
  Soc.Send(Request, SocketFlags.None);
  Soc.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(BeginReceive), null);
}

private void BeginReceive(IAsyncResult ar)//Stage Four
{
   if (!Runing) {ShutMeUp();Return}
   int Size = Soc.EndReceive(ar, out SocError);
   //Process the data 
   ShutMeUp();
   Running=false;/JD no trouble, thread dies a normal death
}

现在我知道abort()应该只在万不得已时使用,但是连接一个挂在套接字网络调用上的线程并不会导致线程被终止,那么我们还能做什么呢?

我对此主题进行了更多研究,建议应该为此类事务使用新进程而不是线程,但我不想同时运行20个进程来处理未完成的请求,而且在这种情况下,性能成本似乎太高了。

也许我可以分解成一个运行线程来处理未完成请求的第二个进程,如果线程数过高,则终止该进程,但这在我的观点看来使程序过于复杂。

我暂时放弃了使用异步回调,而是直接使用了在线程上运行的Soc.Connect(), Soc.Send() , Soc.Read(),问题似乎已经解决了。

有人能看出代码有什么问题吗?或者有人能提出一个在3.5框架上有效的更好的建议吗?

使用代码

可执行文件“DnsApp.exe”和完整的VS2010 C#项目源代码包含在页面顶部的下载中,但由于“Whois.zip”数据库文件的大小,您需要单独下载该文件,解压缩并将其XML文件复制到项目根目录“C:\DnsServer”,然后只需运行DnsApp.exe快捷方式,然后按“开始”按钮即可开始。

需要查看的配置文件有“NoCache.txt”、“Property.txt”、“ForcedIPAddresses.txt”、“DnsAppTimeSchedules.xml”和用于跟踪保护列表的“trackinglist.txt”。

我包含了时间表的示例数据,但您可以从设置菜单中关闭它们来开始,请记住,“Common”项目中的大部分代码不被此程序使用,并且是我在许多其他项目中使用的一个共享库。更多详情请参阅ReadMe.txt。

感谢fanboyadblock@googlegroups.com提供的原始跟踪保护列表数据

尽情享用,Dr Gadgit

© . All rights reserved.