refactor: move more commands to internal/cmd (#207)
* refactor: move more commands to internal/cmd Part of https://github.com/ooni/probe/issues/1335. We would like all commands to be at the same level of engine rather than inside engine (now that we can do it). * fix: update .gitignore * refactor: also move jafar outside engine * We should be good now?
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Package resolver contains a censoring DNS resolver. Most queries are
|
||||
// answered without censorship, but selected queries could either be
|
||||
// discarded or replied to with a bogon or NXDOMAIN answer.
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
// CensoringResolver is a censoring resolver.
|
||||
type CensoringResolver struct {
|
||||
blocked []string
|
||||
hijacked []string
|
||||
ignored []string
|
||||
lookupHost func(ctx context.Context, host string) ([]string, error)
|
||||
}
|
||||
|
||||
// NewCensoringResolver creates a new CensoringResolver instance using
|
||||
// the specified list of keywords to censor. blocked is the list of
|
||||
// keywords that trigger NXDOMAIN if they appear in a query. hijacked
|
||||
// is similar but redirects to 127.0.0.1, where the transparent HTTP
|
||||
// and TLS proxies will pick them up. dnsNetwork and dnsAddress are the
|
||||
// settings to configure the upstream, non censored DNS.
|
||||
func NewCensoringResolver(
|
||||
blocked, hijacked, ignored []string, uncensored netx.Resolver,
|
||||
) *CensoringResolver {
|
||||
return &CensoringResolver{
|
||||
blocked: blocked,
|
||||
hijacked: hijacked,
|
||||
ignored: ignored,
|
||||
lookupHost: uncensored.LookupHost,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CensoringResolver) roundtrip(rw dns.ResponseWriter, req *dns.Msg) {
|
||||
name := req.Question[0].Name
|
||||
addrs, err := r.lookupHost(context.Background(), name)
|
||||
var ips []net.IP
|
||||
if err == nil {
|
||||
for _, addr := range addrs {
|
||||
if ip := net.ParseIP(addr); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
r.reply(rw, req, ips)
|
||||
}
|
||||
|
||||
func (r *CensoringResolver) reply(
|
||||
rw dns.ResponseWriter, req *dns.Msg, ips []net.IP,
|
||||
) {
|
||||
m := new(dns.Msg)
|
||||
m.Compress = true
|
||||
m.MsgHdr.RecursionAvailable = true
|
||||
m.SetReply(req)
|
||||
for _, ip := range ips {
|
||||
ipv6 := strings.Contains(ip.String(), ":")
|
||||
if !ipv6 && req.Question[0].Qtype == dns.TypeA {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
A: ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
if m.Answer == nil {
|
||||
m.SetRcode(req, dns.RcodeNameError)
|
||||
}
|
||||
rw.WriteMsg(m)
|
||||
}
|
||||
|
||||
func (r *CensoringResolver) failure(rw dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.Compress = true
|
||||
m.MsgHdr.RecursionAvailable = true
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
rw.WriteMsg(m)
|
||||
}
|
||||
|
||||
// ServeDNS serves a DNS request
|
||||
func (r *CensoringResolver) ServeDNS(rw dns.ResponseWriter, req *dns.Msg) {
|
||||
if len(req.Question) < 1 {
|
||||
r.failure(rw, req)
|
||||
return
|
||||
}
|
||||
name := req.Question[0].Name
|
||||
for _, pattern := range r.blocked {
|
||||
if strings.Contains(name, pattern) {
|
||||
r.reply(rw, req, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, pattern := range r.hijacked {
|
||||
if strings.Contains(name, pattern) {
|
||||
r.reply(rw, req, []net.IP{net.IPv4(127, 0, 0, 1)})
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, pattern := range r.ignored {
|
||||
if strings.Contains(name, pattern) {
|
||||
return
|
||||
}
|
||||
}
|
||||
r.roundtrip(rw, req)
|
||||
}
|
||||
|
||||
// Start starts the DNS resolver
|
||||
func (r *CensoringResolver) Start(address string) (*dns.Server, error) {
|
||||
packetconn, err := net.ListenPacket("udp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server := &dns.Server{
|
||||
Addr: address,
|
||||
Handler: r,
|
||||
Net: "udp",
|
||||
PacketConn: packetconn,
|
||||
}
|
||||
go server.ActivateAndServe()
|
||||
return server, nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/uncensored"
|
||||
)
|
||||
|
||||
func TestPass(t *testing.T) {
|
||||
server := newresolver(t, []string{"ooni.io"}, []string{"ooni.nu"}, nil)
|
||||
checkrequest(t, server, "example.com", "success", nil)
|
||||
killserver(t, server)
|
||||
}
|
||||
|
||||
func TestBlock(t *testing.T) {
|
||||
server := newresolver(t, []string{"ooni.io"}, []string{"ooni.nu"}, nil)
|
||||
checkrequest(t, server, "mia-ps.ooni.io", "blocked", nil)
|
||||
killserver(t, server)
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
server := newresolver(t, []string{"ooni.io"}, []string{"ooni.nu"}, nil)
|
||||
checkrequest(t, server, "hkgmetadb.ooni.nu", "hijacked", nil)
|
||||
killserver(t, server)
|
||||
}
|
||||
|
||||
func TestIgnore(t *testing.T) {
|
||||
server := newresolver(t, nil, nil, []string{"ooni.nu"})
|
||||
iotimeout := "i/o timeout"
|
||||
checkrequest(t, server, "hkgmetadb.ooni.nu", "hijacked", &iotimeout)
|
||||
killserver(t, server)
|
||||
}
|
||||
|
||||
func TestLookupFailure(t *testing.T) {
|
||||
server := newresolver(t, nil, nil, nil)
|
||||
// we should receive same response as when we're blocked
|
||||
checkrequest(t, server, "example.antani", "blocked", nil)
|
||||
killserver(t, server)
|
||||
}
|
||||
|
||||
func TestFailureNoQuestion(t *testing.T) {
|
||||
resolver := NewCensoringResolver(
|
||||
nil, nil, nil, uncensored.DefaultClient,
|
||||
)
|
||||
resolver.ServeDNS(&fakeResponseWriter{t: t}, new(dns.Msg))
|
||||
}
|
||||
|
||||
func TestListenFailure(t *testing.T) {
|
||||
resolver := NewCensoringResolver(
|
||||
nil, nil, nil, uncensored.DefaultClient,
|
||||
)
|
||||
server, err := resolver.Start("8.8.8.8:53")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if server != nil {
|
||||
t.Fatal("expected nil server here")
|
||||
}
|
||||
}
|
||||
|
||||
func newresolver(t *testing.T, blocked, hijacked, ignored []string) *dns.Server {
|
||||
resolver := NewCensoringResolver(
|
||||
blocked, hijacked, ignored,
|
||||
// using faster dns because dot here causes miekg/dns's
|
||||
// dns.Exchange to timeout and I don't want more complexity
|
||||
uncensored.Must(uncensored.NewClient("system:///")),
|
||||
)
|
||||
server, err := resolver.Start("127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func killserver(t *testing.T, server *dns.Server) {
|
||||
err := server.Shutdown()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkrequest(
|
||||
t *testing.T, server *dns.Server, host string, expectStatus string,
|
||||
expectErrorSuffix *string,
|
||||
) {
|
||||
address := server.PacketConn.LocalAddr().String()
|
||||
query := newquery(host)
|
||||
reply, err := dns.Exchange(query, address)
|
||||
if err != nil {
|
||||
if expectErrorSuffix != nil &&
|
||||
strings.HasSuffix(err.Error(), *expectErrorSuffix) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch expectStatus {
|
||||
case "success":
|
||||
checksuccess(t, reply)
|
||||
case "hijacked":
|
||||
checkhijacked(t, reply)
|
||||
case "blocked":
|
||||
checkblocked(t, reply)
|
||||
default:
|
||||
panic("unexpected value")
|
||||
}
|
||||
}
|
||||
|
||||
func checksuccess(t *testing.T, reply *dns.Msg) {
|
||||
if reply.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("unexpected rcode")
|
||||
}
|
||||
if len(reply.Answer) < 1 {
|
||||
t.Fatal("too few answers")
|
||||
}
|
||||
for _, answer := range reply.Answer {
|
||||
if rr, ok := answer.(*dns.A); ok {
|
||||
if rr.A.String() == "127.0.0.1" {
|
||||
t.Fatal("unexpected hijacked response here")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkhijacked(t *testing.T, reply *dns.Msg) {
|
||||
if reply.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("unexpected rcode")
|
||||
}
|
||||
if len(reply.Answer) < 1 {
|
||||
t.Fatal("too few answers")
|
||||
}
|
||||
for _, answer := range reply.Answer {
|
||||
if rr, ok := answer.(*dns.A); ok {
|
||||
if rr.A.String() != "127.0.0.1" {
|
||||
t.Fatal("unexpected non-hijacked response here")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkblocked(t *testing.T, reply *dns.Msg) {
|
||||
if reply.Rcode != dns.RcodeNameError {
|
||||
t.Fatal("unexpected rcode")
|
||||
}
|
||||
if len(reply.Answer) >= 1 {
|
||||
t.Fatal("too many answers")
|
||||
}
|
||||
}
|
||||
|
||||
func newquery(name string) *dns.Msg {
|
||||
query := new(dns.Msg)
|
||||
query.Id = dns.Id()
|
||||
query.RecursionDesired = true
|
||||
query.Question = append(query.Question, dns.Question{
|
||||
Name: dns.Fqdn(name),
|
||||
Qclass: dns.ClassINET,
|
||||
Qtype: dns.TypeA,
|
||||
})
|
||||
return query
|
||||
}
|
||||
|
||||
type fakeResponseWriter struct {
|
||||
dns.ResponseWriter
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (rw *fakeResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||
if m.Rcode != dns.RcodeServerFailure {
|
||||
rw.t.Fatal("unexpected rcode")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user