diff --git a/go.mod b/go.mod index 64cbb7fdd1..135bee79b9 100644 --- a/go.mod +++ b/go.mod @@ -35,18 +35,23 @@ require ( github.com/pkg/errors v0.9.1 github.com/rogpeppe/go-internal v1.9.0 github.com/rubenv/sql-migrate v1.2.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/upper/db/v4 v4.6.0 gitlab.com/yawning/obfs4.git v0.0.0-20220904064028-336a71d6e4cf gitlab.com/yawning/utls.git v0.0.12-1 - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 + golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be golang.org/x/net v0.0.0-20220906165146-f3363e06e74c - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec ) require ( + github.com/google/btree v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect + gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 // indirect ) require ( @@ -130,6 +135,8 @@ require ( golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.12 // indirect + golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c google.golang.org/protobuf v1.28.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index aea655d2ce..f30ca7c585 100644 --- a/go.sum +++ b/go.sum @@ -297,6 +297,8 @@ github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -833,6 +835,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= @@ -995,8 +999,8 @@ golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1214,8 +1218,8 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= @@ -1235,6 +1239,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1309,6 +1314,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c h1:Okh6a1xpnJslG9Mn84pId1Mn+Q8cvpo4HCeeFWHo0cA= +golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1389,8 +1398,8 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1418,8 +1427,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84 h1:hZAzgyItS2MPyqvdC8wQZI99ZLGP9Vwijyfr0dmYWc4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1469,7 +1478,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 h1:cv/zaNV0nr1mJzaeo4S5mHIm5va1W0/9J3/5prlsuRM= +gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/cmd/miniooni/main.go b/internal/cmd/miniooni/main.go index a12ae4803d..4fe3184420 100644 --- a/internal/cmd/miniooni/main.go +++ b/internal/cmd/miniooni/main.go @@ -37,6 +37,7 @@ type Options struct { ProbeServicesURL string Proxy string Random bool + RemoteName string RepeatEvery int64 ReportFile string SnowflakeRendezvous string @@ -111,6 +112,13 @@ func main() { "set proxy URL to communicate with the OONI backend (mutually exclusive with --tunnel)", ) + flags.StringVar( + &globalOptions.RemoteName, + "remote", + "", + "name of the remote to use to hijack all network traffic", + ) + flags.Int64Var( &globalOptions.RepeatEvery, "repeat-every", @@ -174,6 +182,8 @@ func main() { registerAllExperiments(rootCmd, &globalOptions) registerOONIRun(rootCmd, &globalOptions) + registerRemoteTCP(rootCmd) + registerRemoteSSH(rootCmd) if err := rootCmd.Execute(); err != nil { os.Exit(1) @@ -300,6 +310,7 @@ func MainWithConfiguration(experimentName string, currentOptions *Options) { currentOptions.ReportFile = "report.jsonl" } log.Log = logger + remoteMaybeHijack(currentOptions) for { mainSingleIteration(logger, experimentName, currentOptions) if currentOptions.RepeatEvery <= 0 { @@ -333,11 +344,7 @@ func mainSingleIteration(logger model.Logger, experimentName string, currentOpti //Mon Jan 2 15:04:05 -0700 MST 2006 log.Infof("Current time: %s", time.Now().Format("2006-01-02 15:04:05 MST")) - homeDir := gethomedir(currentOptions.HomeDir) - runtimex.Assert(homeDir != "", "home directory is empty") - miniooniDir := path.Join(homeDir, ".miniooni") - err := os.MkdirAll(miniooniDir, 0700) - runtimex.PanicOnError(err, "cannot create $HOME/.miniooni directory") + miniooniDir := createAndReturnMiniooniDir(currentOptions) // We cleanup the assets files used by versions of ooniprobe // older than v3.9.0, where we started embedding the assets diff --git a/internal/cmd/miniooni/remotecore.go b/internal/cmd/miniooni/remotecore.go new file mode 100644 index 0000000000..60a2341348 --- /dev/null +++ b/internal/cmd/miniooni/remotecore.go @@ -0,0 +1,436 @@ +package main + +// +// Core remote implementation +// + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/netip" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/apex/log" + "github.com/google/shlex" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/songgao/water" + "golang.org/x/sys/execabs" + "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" +) + +const ( + // remoteTUNDeviceName is the name assigned to the TUN device by the server. + remoteTUNDeviceName = "miniooni0" + + // remoteServerAddr is the address assigned to the server. + remoteServerAddr = "10.14.17.1" +) + +var ( + // remoteClientAddr is the address assigned to the client. + remoteClientAddr = netip.MustParseAddr("10.14.17.4") + + // remoteResolvers are the IP addresses used to implement getaddrinfo on the remote. + remoteResolvers = []netip.Addr{ + netip.MustParseAddr("8.8.8.8"), + netip.MustParseAddr("8.8.4.4"), + } +) + +// remoteServerConfig contains server configuration for remote operations. +type remoteServerConfig struct { + // iface is the output interface to use. + iface string +} + +// remoteServerListenerFactory creates a remoteServerListener. +type remoteServerListenerFactory interface { + // Listen returns a new listener instance or an error. + Listen() (remoteServerListener, error) +} + +// remoteServerListener creates remoteConns. +type remoteServerListener interface { + // Accept should return a new remoteConn or an error. This function + // MUST return net.ErrClosed after Close has been called. + Accept() (remoteConn, error) + + // Close closes the listener. + Close() error +} + +// remoteConn is a connection between a server and a remote miniooni client. +type remoteConn interface { + io.Reader + io.Writer + io.Closer +} + +// remoteServerMain is the main of a remote subcommand. +func remoteServerMain(config *remoteServerConfig, factory remoteServerListenerFactory) error { + // create the listener + listener, err := factory.Listen() + if err != nil { + return err + } + defer listener.Close() + + // create the TUN device + tunConfig := water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: remoteTUNDeviceName, + }, + } + tun, err := water.New(tunConfig) + if err != nil { + log.Errorf("remote: water.New failed: %s", err.Error()) + return err + } + defer tun.Close() + + // assign the correct IP address to the TUN device + if err := remoteServerAssignAddress(config); err != nil { + log.Errorf("remote: cannot assign address to TUN device: %s", err.Error()) + return err + } + defer remoteServerCleanupIPTables(config) + + // listen for signals and cleanup when we receive them + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigch + log.Infof("remote: interrupted by signal") + listener.Close() + }() + + // accept incoming connections + for { + conn, err := listener.Accept() + if err != nil && errors.Is(err, net.ErrClosed) { + return nil // this is how we terminate successfully + } + if err != nil { + log.Warnf("remote: listener.Accept failed: %s", err.Error()) + continue + } + + // route traffic + go remoteServerRoute(conn, tun) + } +} + +// remoteServerRoute routes traffic between the remote conn and the TUN device. +func remoteServerRoute(conn remoteConn, tun *water.Interface) { + // route from the remote conn to the TUN device + go func() { + for { + pkt, err := remoteReadPacket(conn) + if err != nil { + log.Warnf("remote: cannot read from conn: %s", err.Error()) + return + } + if _, err := tun.Write(pkt); err != nil { + log.Warnf("remote: cannot write to TUN device: %s", err.Error()) + return + } + } + }() + + // route from the TUN device to the remote conn + go func() { + buffer := make([]byte, remoteMaxPacketSize) + for { + count, err := tun.Read(buffer) + if err != nil { + log.Warnf("remote: cannot read from TUN device: %s", err.Error()) + return + } + pkt := buffer[:count] + if err := remoteWritePacket(conn, pkt); err != nil { + log.Warnf("remote: cannot write to conn: %s", err.Error()) + return + } + } + }() +} + +// remoteReadPacket reads a packet from conn. +func remoteReadPacket(conn io.Reader) ([]byte, error) { + header := make([]byte, 3) + if _, err := io.ReadFull(conn, header); err != nil { + return nil, err + } + var length int + length |= int(header[0]) << 16 + length |= int(header[1]) << 8 + length |= int(header[2]) << 0 + pkt := make([]byte, length) + if _, err := io.ReadFull(conn, pkt); err != nil { + return nil, err + } + return pkt, nil +} + +// remoteMaxPacketSize is the maximum packet size. +const remoteMaxPacketSize = (1 << 24) - 1 + +// errRemotePacketTooBig indicates that a packet is too big +var errRemotePacketTooBig = errors.New("packet too big") + +// remoteWritePacket writes a packet to the conn. +func remoteWritePacket(conn io.Writer, pkt []byte) error { + length := len(pkt) + if length > remoteMaxPacketSize { + return errRemotePacketTooBig + } + data := make([]byte, 3) + data[0] = byte((length >> 16) & 0xff) + data[1] = byte((length >> 8) & 0xff) + data[2] = byte((length >> 0) & 0xff) + data = append(data, pkt...) + _, err := conn.Write(data) + return err +} + +// remoteServerAssignAddress assigns an address to the TUN device. +func remoteServerAssignAddress(config *remoteServerConfig) error { + script := []string{ + fmt.Sprintf("ip addr add %s/24 dev %s", remoteServerAddr, remoteTUNDeviceName), + fmt.Sprintf("ip link set dev %s up", remoteTUNDeviceName), + fmt.Sprintf("iptables -t nat -I POSTROUTING -o %s -j MASQUERADE", config.iface), + "sysctl net.ipv4.ip_forward=1", + } + for _, cmd := range script { + if err := remoteServerExec(cmd); err != nil { + return err + } + } + return nil +} + +// remoteServerExec executes a command. +func remoteServerExec(cmdline string) error { + argv, err := shlex.Split(cmdline) + runtimex.PanicOnError(err, "shlex.Split failed") + runtimex.Assert(len(argv) >= 1, "expected at least one argv entry") + cmd := execabs.Command(argv[0], argv[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Infof("remote: exec: %s", cmd.String()) + return cmd.Run() +} + +// remoteServerCleanupIPTables removes iptables rules we have added. +func remoteServerCleanupIPTables(config *remoteServerConfig) { + remoteServerExec(fmt.Sprintf( + "iptables -t nat -D POSTROUTING -o %s -j MASQUERADE", + config.iface, + )) +} + +// remoteClientDialer creates connections. +type remoteClientDialer interface { + Dial() (remoteConn, error) +} + +// remoteClient is a client for the remote protocol +type remoteClient struct { + // closeOnce allows to call Close just once + closeOnce *sync.Once + + // conn is the transport connection. + conn remoteConn + + // net is the underlying userspace network stack + net *netstack.Net + + // tun is the TUN device in userspace. + tun tun.Device +} + +// newRemoteClient creates a new remote client. +func newRemoteClient(dialer remoteClientDialer) (*remoteClient, error) { + // establish a connection with the remote host + conn, err := dialer.Dial() + if err != nil { + return nil, err + } + + const mtu = 1300 // must be >= 1252, which is used by quic-go + + // create the TUN device in userspace + tun, net, err := netstack.CreateNetTUN( + []netip.Addr{remoteClientAddr}, + remoteResolvers, + mtu, + ) + if err != nil { + conn.Close() + return nil, err + } + + client := &remoteClient{ + closeOnce: &sync.Once{}, + net: net, + tun: tun, + conn: conn, + } + return client, nil +} + +// Close closes the connections used by a client. +func (c *remoteClient) Close() error { + var err error + c.closeOnce.Do(func() { + if e := c.tun.Close(); e != nil { + err = e + } + if e := c.conn.Close(); e != nil && err == nil { + err = e + } + }) + return err +} + +// route routes the traffic +func (c *remoteClient) route() { + // the following code has been adapted from ooni/minivpn + const zeroOffset = 0 + + go func() { + for { + pkt, err := remoteReadPacket(c.conn) + if err != nil { + log.Errorf("remote: cannot read from conn: %s", err.Error()) + return + } + if _, err = c.tun.Write(pkt, zeroOffset); err != nil { + log.Errorf("remote: cannot write to TUN device: %v", err) + break + } + } + }() + + go func() { + buf := make([]byte, remoteMaxPacketSize) + for { + count, err := c.tun.Read(buf, zeroOffset) + if err != nil { + log.Errorf("remote: cannot read from TUN device: %v", err) + break + } + pkt := buf[:count] + if err := remoteWritePacket(c.conn, pkt); err != nil { + log.Errorf("remote: cannot write to conn: %s", err.Error()) + return + } + } + }() +} + +// DialContext implements UnderlyingNetwork.DialContext. +func (c *remoteClient) DialContext(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + if remoteIsIPv6(address) { + // TODO(bassosimone): extend this implementation to support IPv6 + return nil, syscall.EHOSTUNREACH + } + return c.net.DialContext(ctx, network, address) +} + +// remoteIsIPv6 returns whether the given endpoint contains an IPv6 address +func remoteIsIPv6(endpoint string) bool { + addr, _, err := net.SplitHostPort(endpoint) + if err != nil { + return false + } + v6, err := netxlite.IsIPv6(addr) + if err != nil { + return false + } + return v6 +} + +// ListenUDP implements UnderlyingNetwork. +func (c *remoteClient) ListenUDP(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { + pconn, err := c.net.ListenUDP(addr) + if err != nil { + return nil, err + } + pwrap := &remoteClientUDPConn{pconn} + return pwrap, nil +} + +// remoteClientUDPConn adapts to model.UDPLikeConn. +type remoteClientUDPConn struct { + net.PacketConn +} + +// WriteTo implements net.PacketConn. +func (c *remoteClientUDPConn) WriteTo(pkt []byte, dest net.Addr) (int, error) { + if remoteIsIPv6(dest.String()) { + // TODO(bassosimone): extend this implementation to support IPv6 + return 0, syscall.EHOSTUNREACH + } + return c.PacketConn.WriteTo(pkt, dest) +} + +// SetReadBuffer allows setting the read buffer. +func (c *remoteClientUDPConn) SetReadBuffer(bytes int) error { + return nil +} + +// SyscallConn returns a conn suitable for calling syscalls, +// which is also instrumental to setting the read buffer. +// +// We need to mock SyscallConn and return a fake syscall.RawConn +// because otherwise lucas-clemente/quic-go would not work as intended. +func (c *remoteClientUDPConn) SyscallConn() (syscall.RawConn, error) { + return &remoteClientRawConnUDP{}, nil +} + +// remoteClientRawConnUDP implements syscall.RawConn +type remoteClientRawConnUDP struct{} + +// Control implements syscall.RawConn +func (*remoteClientRawConnUDP) Control(f func(fd uintptr)) error { + return nil +} + +// Read implements syscall.RawConn +func (*remoteClientRawConnUDP) Read(f func(fd uintptr) (done bool)) error { + return nil +} + +// Write implements syscall.RawConn +func (*remoteClientRawConnUDP) Write(f func(fd uintptr) (done bool)) error { + return nil +} + +// GetaddrinfoLookupANY implements UnderlyingNetwork. +func (c *remoteClient) GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { + addrs, err := c.net.LookupContextHost(ctx, domain) + return addrs, "", err +} + +// GetaddrinfoResolverNetwork implements UnderlyingNetwork. +func (c *remoteClient) GetaddrinfoResolverNetwork() string { + return netxlite.StdlibResolverGetaddrinfo +} diff --git a/internal/cmd/miniooni/remotedialer.go b/internal/cmd/miniooni/remotedialer.go new file mode 100644 index 0000000000..5973d9eebe --- /dev/null +++ b/internal/cmd/miniooni/remotedialer.go @@ -0,0 +1,31 @@ +package main + +// +// Common code for dialing TCP connections +// + +import "net" + +// remoteDialer implements remoteClientDialer. +type remoteDialer struct { + // remoteAddr is the remote address to use. + remoteAddr string + + // wrapConn wraps the established conn. + wrapConn remoteConnWrapper +} + +var _ remoteClientDialer = &remoteDialer{} + +// Dial implements remoteClientDialer. +func (rcd *remoteDialer) Dial() (remoteConn, error) { + conn, err := net.Dial("tcp", rcd.remoteAddr) + if err != nil { + return nil, err + } + cw, err := rcd.wrapConn(conn) + if err != nil { + return nil, err + } + return cw, nil +} diff --git a/internal/cmd/miniooni/remotehijack.go b/internal/cmd/miniooni/remotehijack.go new file mode 100644 index 0000000000..d4cef95069 --- /dev/null +++ b/internal/cmd/miniooni/remotehijack.go @@ -0,0 +1,96 @@ +package main + +// +// Client-side connection hijacking implementation +// + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "gopkg.in/yaml.v3" +) + +// remoteMaybeHijack hijacks miniooni connections using the selected remote +// name unless the remote name is empty. +func remoteMaybeHijack(options *Options) error { + remoteName := options.RemoteName + if remoteName == "" { + return nil + } + + // obtain the remote configuration from the config file + cfg, err := remoteReadConfigFile(options) + if err != nil { + return fmt.Errorf("remote: cannot read config file: %w", err) + } + remote := cfg.Remotes[remoteName] + if remote == nil { + return fmt.Errorf("remote: %s: no such remote", remoteName) + } + + // establish the specified remote connection + var client *remoteClient + switch txp := remote.Transport; txp { + case "tcp": + client, err = newRemoteTCPClient(remote) + case "ssh": + client, err = newRemoteSSHClient(remote) + default: + return fmt.Errorf("remote: %s: no such transport", txp) + } + if err != nil { + return err + } + + // start routing traffic + go client.route() + + // hijack netxlite's fundamental network operations + netxlite.TProxy = client + log.Infof("remote: %s: hijacked netxlite network primitives", remoteName) + + return nil +} + +// remoteConfigFile contains the configuration file content. +type remoteConfigFile struct { + // Remotes maps a remote name to its settings. + Remotes map[string]*remoteConfig `yaml:"remotes"` +} + +// remoteConfig is the configuration of a specific remote. +type remoteConfig struct { + // Address is the remote endpoint to use. + Address string `yaml:"address"` + + // Transport is the transport to use. + Transport string `yaml:"transport"` + + // SSH contains optional SSH configuration. + SSH *remoteConfigSSH `yaml:"ssh"` +} + +// remoteConfigSSH contains SSH specific configuration. +type remoteConfigSSH struct { + // User is the user name to use + User string `yaml:"user"` +} + +// remoteReadConfigFile reads the remote config file. +func remoteReadConfigFile(options *Options) (*remoteConfigFile, error) { + miniooniDir := createAndReturnMiniooniDir(options) + filename := filepath.Join(miniooniDir, "remote", "config.yaml") + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var cfg remoteConfigFile + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/internal/cmd/miniooni/remotelistener.go b/internal/cmd/miniooni/remotelistener.go new file mode 100644 index 0000000000..64b8b1ae9f --- /dev/null +++ b/internal/cmd/miniooni/remotelistener.go @@ -0,0 +1,143 @@ +package main + +// +// Common code for listening for TCP conns +// + +import ( + "errors" + "net" + "sync" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/netxlite" +) + +// remoteConnWrapper wraps a net.Conn to implement the specific +// protocol used by this remote transport. +type remoteConnWrapper func(conn net.Conn) (remoteConn, error) + +// remoteListenerFactory implements remoteServerListenerFactory. +type remoteListenerFactory struct { + iface string + port string + wrapconn remoteConnWrapper +} + +var _ remoteServerListenerFactory = &remoteListenerFactory{} + +// Listen implements remoteServerListenerFactory. +func (slf *remoteListenerFactory) Listen() (remoteServerListener, error) { + dev, err := net.InterfaceByName(slf.iface) + if err != nil { + return nil, err + } + cidrs, err := dev.Addrs() + if err != nil { + return nil, err + } + lst := []net.Listener{} + for _, cidr := range cidrs { + addr, _, err := net.ParseCIDR(cidr.String()) + if err != nil { + return nil, err + } + if netxlite.IsBogon(addr.String()) { + // We don't care about listening on link local IPv6 addresses + // and listening will fail anyway, so... + continue + } + endpoint := net.JoinHostPort(addr.String(), slf.port) + listener, err := net.Listen("tcp", endpoint) + if err != nil { + return nil, err + } + log.Infof("remotelistener: listening at %s", listener.Addr().String()) + lst = append(lst, listener) + } + wl := &remoteListener{ + closeOnce: &sync.Once{}, + wrapconn: slf.wrapconn, + isclosed: make(chan any), + listeners: lst, + newconnch: make(chan remoteConn), + startOnce: &sync.Once{}, + } + return wl, nil +} + +// remoteListener implements remoteServerListener. +type remoteListener struct { + closeOnce *sync.Once + isclosed chan any + listeners []net.Listener + newconnch chan remoteConn + startOnce *sync.Once + wrapconn remoteConnWrapper +} + +var _ remoteServerListener = &remoteListener{} + +// Accept implements remoteServerListener. +func (rsl *remoteListener) Accept() (remoteConn, error) { + rsl.startOnce.Do(rsl.startAccepting) + select { + case conn := <-rsl.newconnch: + return conn, nil + case <-rsl.isclosed: + return nil, net.ErrClosed + } +} + +// startAccepting starts accepting incoming connections. +func (rsl *remoteListener) startAccepting() { + for _, lst := range rsl.listeners { + go rsl.acceptloop(lst) + } +} + +// acceptloop is the accept loop. +func (rsl *remoteListener) acceptloop(listener net.Listener) { + for { + conn, err := listener.Accept() + if err != nil && errors.Is(err, net.ErrClosed) { + return + } + if err != nil { + log.Warnf("remotelistener: listener.Accept failed: %s", err.Error()) + continue + } + go rsl.wrapAndDispatchConn(conn) + } +} + +// wrapAndDispatchConn wraps the connection and then dispatches it to +// the code that will route incoming and outgoing packets. +func (rsl *remoteListener) wrapAndDispatchConn(conn net.Conn) { + wrapped, err := rsl.wrapconn(conn) + if err != nil { + log.Warnf("remotelistener: rsl.wrap failed: %s", err.Error()) + conn.Close() + return + } + select { + case rsl.newconnch <- wrapped: + case <-rsl.isclosed: + conn.Close() + return + } +} + +// Close implements remoteServerListener. +func (rsl *remoteListener) Close() error { + var err error + rsl.closeOnce.Do(func() { + for _, lst := range rsl.listeners { + if e := lst.Close(); e != nil && err == nil { + err = e + } + } + close(rsl.isclosed) + }) + return err +} diff --git a/internal/cmd/miniooni/remotessh.go b/internal/cmd/miniooni/remotessh.go new file mode 100644 index 0000000000..a43e7e985c --- /dev/null +++ b/internal/cmd/miniooni/remotessh.go @@ -0,0 +1,299 @@ +package main + +// +// SSH remote implementation +// + +import ( + "errors" + "fmt" + "net" + "os" + "path/filepath" + "sync" + + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +var ( + // remoteSSHPort is the port used by default by this remote. + remoteSSHPort string + + // remoteSSHInterface is the interface used by default by this remote. + remoteSSHInterface string +) + +// registerRemoteSSH registers the remotessh command. +func registerRemoteSSH(rootCmd *cobra.Command) { + subCmd := &cobra.Command{ + Use: "remotessh", + Short: "RemoteSSH protocol server", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + remoteSSHServerMain(remoteSSHPort, remoteSSHInterface) + }, + } + + flags := subCmd.Flags() + + flags.StringVar( + &remoteSSHPort, + "port", + "2222", + "selects the port to use", + ) + + flags.StringVar( + &remoteSSHInterface, + "interface", + "eth0", + "selects the interface to use", + ) + + rootCmd.AddCommand(subCmd) +} + +// remoteSSHServerMain is the main of the remotessh subcommand. +func remoteSSHServerMain(port, iface string) { + config := &remoteServerConfig{ + iface: iface, + } + sh, err := newRemoteSSHServerHandler() + runtimex.PanicOnError(err, "newRemoteSSHServerHandler failed") + factory := &remoteListenerFactory{ + iface: iface, + port: port, + wrapconn: sh.wrapConn, + } + err = remoteServerMain(config, factory) + runtimex.PanicOnError(err, "remoteServerMain failed") +} + +// remoteSSHServerHandler handles incoming SSH conns. +type remoteSSHServerHandler struct { + config *ssh.ServerConfig +} + +// remoteSSHReadAuthorizedKeys reads and parses the authorized_keys file. +func remoteSSHReadAuthorizedKeys() (map[string]bool, error) { + homeDir := gethomedir("") + filename := filepath.Join(homeDir, ".ssh", "authorized_keys") + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + akmap := map[string]bool{} + for len(data) > 0 { + pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(data) + if err != nil { + return nil, err + } + akmap[string(pubKey.Marshal())] = true + data = rest + } + return akmap, nil +} + +// remoteSSHReadSSHHostRSAKey reads the host's private key. +func remoteSSHReadSSHHostRSAKey() (ssh.Signer, error) { + data, err := os.ReadFile("/etc/ssh/ssh_host_rsa_key") + if err != nil { + return nil, err + } + return ssh.ParsePrivateKey(data) +} + +// newRemoteSSHServerHandler creates a new remoteSSHServerHandler instance. +func newRemoteSSHServerHandler() (*remoteSSHServerHandler, error) { + akmap, err := remoteSSHReadAuthorizedKeys() + if err != nil { + return nil, err + } + signer, err := remoteSSHReadSSHHostRSAKey() + if err != nil { + return nil, err + } + config := &ssh.ServerConfig{ + PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + if akmap[string(pubKey.Marshal())] { + return &ssh.Permissions{ + // Record the public key used for authentication. + Extensions: map[string]string{ + "pubkey-fp": ssh.FingerprintSHA256(pubKey), + }, + }, nil + } + return nil, fmt.Errorf("unknown public key for %q", c.User()) + }, + } + config.AddHostKey(signer) + handler := &remoteSSHServerHandler{ + config: config, + } + return handler, nil +} + +// errRemoteSSHInvalidChannelType indicates that the channel type is invalid +var errRemoteSSHInvalidChannelType = errors.New("invalid SSH channel type") + +// wrapConn wraps a server-side net.Conn to be an SSH conn. +func (h *remoteSSHServerHandler) wrapConn(conn net.Conn) (remoteConn, error) { + sshConn, chans, sshReqs, err := ssh.NewServerConn(conn, h.config) + if err != nil { + return nil, err + } + go ssh.DiscardRequests(sshReqs) + candidate := <-chans + if candidate.ChannelType() != "miniooni-remote" { + return nil, errRemoteSSHInvalidChannelType + } + channel, chanReqs, err := candidate.Accept() + if err != nil { + return nil, err + } + go ssh.DiscardRequests(chanReqs) + rc := &remoteSSHServerRemoteConn{ + channel: channel, + closeOnce: &sync.Once{}, + conn: sshConn, + } + return rc, nil +} + +// remoteSSHServerRemoteConn implements remoteConn +type remoteSSHServerRemoteConn struct { + channel ssh.Channel + closeOnce *sync.Once + conn *ssh.ServerConn +} + +var _ remoteConn = &remoteSSHServerRemoteConn{} + +func (c *remoteSSHServerRemoteConn) Read(data []byte) (int, error) { + return c.channel.Read(data) +} + +func (c *remoteSSHServerRemoteConn) Write(data []byte) (int, error) { + return c.channel.Write(data) +} + +func (c *remoteSSHServerRemoteConn) Close() error { + var err error + c.closeOnce.Do(func() { + if e := c.conn.Close(); e != nil { + err = e + } + }) + return err +} + +// newRemoteSSHClient creates a new remoteClient using SSH. +func newRemoteSSHClient(remote *remoteConfig) (*remoteClient, error) { + hx, err := newRemoteSSHClientHandshaker(remote) + if err != nil { + return nil, err + } + dialer := &remoteDialer{ + remoteAddr: remote.Address, + wrapConn: hx.wrapConn, + } + return newRemoteClient(dialer) +} + +// remoteSSHClientHandshaker performs the SSH handshake and returns +// a suitable connection for forwarding traffic. +type remoteSSHClientHandshaker struct { + config *ssh.ClientConfig +} + +// errRemoteSSHMissingConfig indicates SSH specific config is missing. +var errRemoteSSHMissingConfig = errors.New("SSH specific config is missing") + +// newRemoteSSHClientHandshaker creates a new remoteSSHClientHandshaker. +func newRemoteSSHClientHandshaker(remote *remoteConfig) (*remoteSSHClientHandshaker, error) { + if remote.SSH == nil { + return nil, errRemoteSSHMissingConfig + } + agentClient, err := remoteSSHClientCreateSSHAgent() + if err != nil { + return nil, err + } + config := &ssh.ClientConfig{ + User: remote.SSH.User, + Auth: []ssh.AuthMethod{ + ssh.PublicKeysCallback(agentClient.Signers), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + hx := &remoteSSHClientHandshaker{ + config: config, + } + return hx, nil +} + +// wrapConn wraps a server-side net.Conn to be an SSH conn. +func (hx *remoteSSHClientHandshaker) wrapConn(conn net.Conn) (remoteConn, error) { + sshConn, _, sshReqs, err := ssh.NewClientConn(conn, conn.RemoteAddr().String(), hx.config) + if err != nil { + return nil, err + } + go ssh.DiscardRequests(sshReqs) + channel, chanReqs, err := sshConn.OpenChannel("miniooni-remote", nil) + if err != nil { + return nil, err + } + go ssh.DiscardRequests(chanReqs) + rc := &remoteSSHClientRemoteConn{ + channel: channel, + closeOnce: &sync.Once{}, + conn: sshConn, + } + return rc, nil +} + +// remoteSSHClientRemoteConn implements remoteConn +type remoteSSHClientRemoteConn struct { + channel ssh.Channel + closeOnce *sync.Once + conn ssh.Conn +} + +var _ remoteConn = &remoteSSHClientRemoteConn{} + +func (c *remoteSSHClientRemoteConn) Read(data []byte) (int, error) { + return c.channel.Read(data) +} + +func (c *remoteSSHClientRemoteConn) Write(data []byte) (int, error) { + return c.channel.Write(data) +} + +func (c *remoteSSHClientRemoteConn) Close() error { + var err error + c.closeOnce.Do(func() { + if e := c.conn.Close(); e != nil { + err = e + } + }) + return err +} + +// errRemoteSSHNoAuthSock indicates that there is no SSH_AUTH_SOCK variable +var errRemoteSSHNoAuthSock = errors.New("no SSH_AUTH_SOCK environment variable") + +// remoteSSHClientCreateSSHAgent creates a SSH agent instance. +func remoteSSHClientCreateSSHAgent() (agent.ExtendedAgent, error) { + socket, found := os.LookupEnv("SSH_AUTH_SOCK") + if !found { + return nil, errRemoteSSHNoAuthSock + } + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, err + } + agentClient := agent.NewClient(conn) + return agentClient, nil +} diff --git a/internal/cmd/miniooni/remotetcp.go b/internal/cmd/miniooni/remotetcp.go new file mode 100644 index 0000000000..10ceebb713 --- /dev/null +++ b/internal/cmd/miniooni/remotetcp.go @@ -0,0 +1,77 @@ +package main + +// +// TCP remote implementation +// + +import ( + "net" + + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/spf13/cobra" +) + +var ( + // remoteTCPPort is the port used by default by this remote. + remoteTCPPort string + + // remoteTCPInterface is the interface used by default by this remote. + remoteTCPInterface string +) + +// registerRemoteTCP registers the remotetcp command. +func registerRemoteTCP(rootCmd *cobra.Command) { + subCmd := &cobra.Command{ + Use: "remotetcp", + Short: "RemoteTCP protocol server", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + remoteTCPServerMain(remoteTCPPort, remoteTCPInterface) + }, + } + + flags := subCmd.Flags() + + flags.StringVar( + &remoteTCPPort, + "port", + "5555", + "selects the port to use", + ) + + flags.StringVar( + &remoteTCPInterface, + "interface", + "eth0", + "selects the interface to use", + ) + + rootCmd.AddCommand(subCmd) +} + +// remoteTCPServerMain is the main of the remotetcp subcommand. +func remoteTCPServerMain(port, iface string) { + config := &remoteServerConfig{ + iface: iface, + } + factory := &remoteListenerFactory{ + iface: iface, + port: port, + wrapconn: func(conn net.Conn) (remoteConn, error) { + return conn, nil + }, + } + err := remoteServerMain(config, factory) + runtimex.PanicOnError(err, "remoteServerMain failed") +} + +// newRemoteTCPClient creates a new remoteClient using TCP. +func newRemoteTCPClient(remote *remoteConfig) (*remoteClient, error) { + dialer := &remoteDialer{ + remoteAddr: remote.Address, + wrapConn: func(conn net.Conn) (remoteConn, error) { + return conn, nil + }, + } + return newRemoteClient(dialer) +} diff --git a/internal/cmd/miniooni/utils.go b/internal/cmd/miniooni/utils.go index cb51e13398..84e265146c 100644 --- a/internal/cmd/miniooni/utils.go +++ b/internal/cmd/miniooni/utils.go @@ -8,6 +8,7 @@ import ( "errors" "net/url" "os" + "path" "runtime" "strings" @@ -86,3 +87,14 @@ func gethomedir(optionsHome string) string { } return os.Getenv("HOME") } + +// createAndReturnMiniooniDir creates the $HOME/.miniooni directory +// and returns its full path to the caller. +func createAndReturnMiniooniDir(options *Options) string { + homeDir := gethomedir(options.HomeDir) + runtimex.Assert(homeDir != "", "home directory is empty") + miniooniDir := path.Join(homeDir, ".miniooni") + err := os.MkdirAll(miniooniDir, 0700) + runtimex.PanicOnError(err, "cannot create $HOME/.miniooni directory") + return miniooniDir +}