15da0f5344
There's no point in doing that. Also, once this change is merged, it becomes easier to cleanup/simplify netx. See https://github.com/ooni/probe/issues/2121
285 lines
7.8 KiB
Go
285 lines
7.8 KiB
Go
// Jafar is a censorship simulation tool used for testing OONI.
|
|
package main
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/execabs"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/apex/log/handlers/cli"
|
|
"github.com/miekg/dns"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/badproxy"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/flagx"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/httpproxy"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/iptables"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/resolver"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/tlsproxy"
|
|
"github.com/ooni/probe-cli/v3/internal/cmd/jafar/uncensored"
|
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
"github.com/ooni/probe-cli/v3/internal/shellx"
|
|
)
|
|
|
|
var (
|
|
badProxyAddress *string
|
|
badProxyAddressTLS *string
|
|
badProxyTLSOutputCA *string
|
|
|
|
dnsProxyAddress *string
|
|
dnsProxyBlock flagx.StringArray
|
|
dnsProxyHijack flagx.StringArray
|
|
dnsProxyIgnore flagx.StringArray
|
|
|
|
httpProxyAddress *string
|
|
httpProxyBlock flagx.StringArray
|
|
|
|
iptablesDropIP flagx.StringArray
|
|
iptablesDropKeywordHex flagx.StringArray
|
|
iptablesDropKeyword flagx.StringArray
|
|
iptablesHijackDNSTo *string
|
|
iptablesHijackHTTPSTo *string
|
|
iptablesHijackHTTPTo *string
|
|
iptablesResetIP flagx.StringArray
|
|
iptablesResetKeywordHex flagx.StringArray
|
|
iptablesResetKeyword flagx.StringArray
|
|
|
|
mainCh chan os.Signal
|
|
mainCommand *string
|
|
mainUser *string
|
|
|
|
tag *string
|
|
|
|
tlsProxyAddress *string
|
|
tlsProxyBlock flagx.StringArray
|
|
|
|
uncensoredResolverDoH *string
|
|
)
|
|
|
|
func init() {
|
|
// badProxy
|
|
badProxyAddress = flag.String(
|
|
"bad-proxy-address", "127.0.0.1:7117",
|
|
"Address where to listen for TCP connections",
|
|
)
|
|
badProxyAddressTLS = flag.String(
|
|
"bad-proxy-address-tls", "127.0.0.1:4114",
|
|
"Address where to listen for TLS connections",
|
|
)
|
|
badProxyTLSOutputCA = flag.String(
|
|
"bad-proxy-tls-output-ca", "badproxy.pem",
|
|
"File where to write the CA used by the bad proxy",
|
|
)
|
|
|
|
// dnsProxy
|
|
dnsProxyAddress = flag.String(
|
|
"dns-proxy-address", "127.0.0.1:53",
|
|
"Address where the DNS proxy should listen",
|
|
)
|
|
flag.Var(
|
|
&dnsProxyBlock, "dns-proxy-block",
|
|
"Register keyword triggering NXDOMAIN censorship",
|
|
)
|
|
flag.Var(
|
|
&dnsProxyHijack, "dns-proxy-hijack",
|
|
"Register keyword triggering redirection to 127.0.0.1",
|
|
)
|
|
flag.Var(
|
|
&dnsProxyIgnore, "dns-proxy-ignore",
|
|
"Register keyword causing the proxy to ignore the query",
|
|
)
|
|
|
|
// httpProxy
|
|
httpProxyAddress = flag.String(
|
|
"http-proxy-address", "127.0.0.1:80",
|
|
"Address where the HTTP proxy should listen",
|
|
)
|
|
flag.Var(
|
|
&httpProxyBlock, "http-proxy-block",
|
|
"Register keyword triggering HTTP 451 censorship",
|
|
)
|
|
|
|
// iptables
|
|
flag.Var(
|
|
&iptablesDropIP, "iptables-drop-ip",
|
|
"Drop traffic to the specified IP address",
|
|
)
|
|
flag.Var(
|
|
&iptablesDropKeywordHex, "iptables-drop-keyword-hex",
|
|
"Drop traffic containing the specified keyword in hex",
|
|
)
|
|
flag.Var(
|
|
&iptablesDropKeyword, "iptables-drop-keyword",
|
|
"Drop traffic containing the specified keyword",
|
|
)
|
|
iptablesHijackDNSTo = flag.String(
|
|
"iptables-hijack-dns-to", "",
|
|
"Hijack all DNS UDP traffic to the specified endpoint",
|
|
)
|
|
iptablesHijackHTTPSTo = flag.String(
|
|
"iptables-hijack-https-to", "",
|
|
"Hijack all HTTPS traffic to the specified endpoint",
|
|
)
|
|
iptablesHijackHTTPTo = flag.String(
|
|
"iptables-hijack-http-to", "",
|
|
"Hijack all HTTP traffic to the specified endpoint",
|
|
)
|
|
flag.Var(
|
|
&iptablesResetIP, "iptables-reset-ip",
|
|
"Reset TCP/IP traffic to the specified IP address",
|
|
)
|
|
flag.Var(
|
|
&iptablesResetKeywordHex, "iptables-reset-keyword-hex",
|
|
"Reset TCP/IP traffic containing the specified keyword in hex",
|
|
)
|
|
flag.Var(
|
|
&iptablesResetKeyword, "iptables-reset-keyword",
|
|
"Reset TCP/IP traffic containing the specified keyword",
|
|
)
|
|
|
|
// main
|
|
mainCh = make(chan os.Signal, 1)
|
|
signal.Notify(
|
|
mainCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT,
|
|
)
|
|
mainCommand = flag.String("main-command", "", "Optional command to execute")
|
|
mainUser = flag.String("main-user", "nobody", "Run command as user")
|
|
|
|
// tag
|
|
tag = flag.String("tag", "", "Add tag to a specific run")
|
|
|
|
// tlsProxy
|
|
tlsProxyAddress = flag.String(
|
|
"tls-proxy-address", "127.0.0.1:443",
|
|
"Address where the HTTP proxy should listen",
|
|
)
|
|
flag.Var(
|
|
&tlsProxyBlock, "tls-proxy-block",
|
|
"Register keyword triggering TLS censorship",
|
|
)
|
|
|
|
// uncensored
|
|
uncensoredResolverDoH = flag.String(
|
|
"uncensored-resolver-doh", "https://1.1.1.1/dns-query",
|
|
"URL of an hopefully uncensored DoH resolver",
|
|
)
|
|
}
|
|
|
|
func badProxyStart() net.Listener {
|
|
proxy := badproxy.NewCensoringProxy()
|
|
listener, err := proxy.Start(*badProxyAddress)
|
|
runtimex.PanicOnError(err, "proxy.Start failed")
|
|
return listener
|
|
}
|
|
|
|
func badProxyStartTLS() net.Listener {
|
|
proxy := badproxy.NewCensoringProxy()
|
|
listener, cert, err := proxy.StartTLS(*badProxyAddressTLS)
|
|
runtimex.PanicOnError(err, "proxy.StartTLS failed")
|
|
err = os.WriteFile(*badProxyTLSOutputCA, pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
}), 0644)
|
|
runtimex.PanicOnError(err, "os.WriteFile failed")
|
|
return listener
|
|
}
|
|
|
|
func dnsProxyStart(uncensored *uncensored.Client) *dns.Server {
|
|
proxy := resolver.NewCensoringResolver(
|
|
dnsProxyBlock, dnsProxyHijack, dnsProxyIgnore, uncensored,
|
|
)
|
|
server, err := proxy.Start(*dnsProxyAddress)
|
|
runtimex.PanicOnError(err, "proxy.Start failed")
|
|
return server
|
|
}
|
|
|
|
func httpProxyStart(uncensored *uncensored.Client) *http.Server {
|
|
proxy := httpproxy.NewCensoringProxy(httpProxyBlock, uncensored)
|
|
server, _, err := proxy.Start(*httpProxyAddress)
|
|
runtimex.PanicOnError(err, "proxy.Start failed")
|
|
return server
|
|
}
|
|
|
|
func iptablesStart() *iptables.CensoringPolicy {
|
|
policy := iptables.NewCensoringPolicy()
|
|
// For robustness waive the policy so we start afresh
|
|
policy.Waive()
|
|
policy.DropIPs = iptablesDropIP
|
|
policy.DropKeywordsHex = iptablesDropKeywordHex
|
|
policy.DropKeywords = iptablesDropKeyword
|
|
policy.HijackDNSAddress = *iptablesHijackDNSTo
|
|
policy.HijackHTTPSAddress = *iptablesHijackHTTPSTo
|
|
policy.HijackHTTPAddress = *iptablesHijackHTTPTo
|
|
policy.ResetIPs = iptablesResetIP
|
|
policy.ResetKeywordsHex = iptablesResetKeywordHex
|
|
policy.ResetKeywords = iptablesResetKeyword
|
|
err := policy.Apply()
|
|
runtimex.PanicOnError(err, "policy.Apply failed")
|
|
return policy
|
|
}
|
|
|
|
func tlsProxyStart(uncensored *uncensored.Client) net.Listener {
|
|
proxy := tlsproxy.NewCensoringProxy(tlsProxyBlock, uncensored)
|
|
listener, err := proxy.Start(*tlsProxyAddress)
|
|
runtimex.PanicOnError(err, "proxy.Start failed")
|
|
return listener
|
|
}
|
|
|
|
func newUncensoredClient() *uncensored.Client {
|
|
return uncensored.NewClient(*uncensoredResolverDoH)
|
|
}
|
|
|
|
func mustx(err error, message string, osExit func(int)) {
|
|
if err != nil {
|
|
var (
|
|
exitcode = 1
|
|
exiterr *execabs.ExitError
|
|
)
|
|
if errors.As(err, &exiterr) {
|
|
exitcode = exiterr.ExitCode()
|
|
}
|
|
log.Errorf("%s", message)
|
|
osExit(exitcode)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
// TODO(bassosimone): we may want a verbose flag
|
|
log.SetLevel(log.InfoLevel)
|
|
log.SetHandler(cli.Default)
|
|
log.Infof("jafar command line: [%s]", strings.Join(os.Args, ", "))
|
|
log.Infof("jafar tag: %s", *tag)
|
|
uncensoredClient := newUncensoredClient()
|
|
defer uncensoredClient.CloseIdleConnections()
|
|
badlistener := badProxyStart()
|
|
defer badlistener.Close()
|
|
badtlslistener := badProxyStartTLS()
|
|
defer badtlslistener.Close()
|
|
dnsproxy := dnsProxyStart(uncensoredClient)
|
|
defer dnsproxy.Shutdown()
|
|
httpproxy := httpProxyStart(uncensoredClient)
|
|
defer httpproxy.Close()
|
|
tlslistener := tlsProxyStart(uncensoredClient)
|
|
defer tlslistener.Close()
|
|
policy := iptablesStart()
|
|
var err error
|
|
if *mainCommand != "" {
|
|
err = shellx.RunCommandline(log.Log, fmt.Sprintf(
|
|
"sudo -u '%s' -- %s", *mainUser, *mainCommand,
|
|
))
|
|
} else {
|
|
<-mainCh
|
|
}
|
|
policy.Waive()
|
|
mustx(err, "subcommand failed", os.Exit)
|
|
}
|