From 0e572dbe9b617287d3cd88b1b4056c32b1d4501e Mon Sep 17 00:00:00 2001 From: Costin Manolache Date: Sat, 3 Apr 2021 08:32:40 -0700 Subject: [PATCH] Updates --- Makefile | 4 + android/ugate.go | 186 ------------------------------ cmd/ugate/go.mod | 10 +- cmd/ugate/go.sum | 26 +++++ cmd/ugate/testdata/ugate.json | 12 +- cmd/ugate/testdata/ugate.yaml | 16 +-- cmd/ugate/ugate.go | 52 ++++++--- cmd/ugate/ugate_dns.go | 11 ++ cmd/ugate/ugate_iptables.go | 18 +++ cmd/ugate/ugate_local.go | 17 +++ cmd/ugate/ugate_webpush.go | 13 +++ cmd/ugatemin/ugatemin.go | 8 ++ pkg/iptables/iptables.go | 61 +++++++++- pkg/sni/sni_proxy.go | 4 + pkg/udp/udpproxy.go | 1 - pkg/ugatesvc/accept.go | 44 +++---- pkg/ugatesvc/h2r.go | 2 +- pkg/ugatesvc/http.go | 12 +- pkg/ugatesvc/httpproxy_capture.go | 152 ++++++++++++++++++++++++ pkg/ugatesvc/routing.go | 6 +- pkg/ugatesvc/ugate.go | 38 ++++-- pkg/ugatesvc/ugate_test.go | 10 +- ugate.go | 5 + webpush/mux.go | 5 +- 24 files changed, 445 insertions(+), 268 deletions(-) delete mode 100644 android/ugate.go create mode 100644 cmd/ugate/go.sum create mode 100644 cmd/ugate/ugate_dns.go create mode 100644 cmd/ugate/ugate_iptables.go create mode 100644 cmd/ugate/ugate_local.go create mode 100644 cmd/ugate/ugate_webpush.go create mode 100644 pkg/ugatesvc/httpproxy_capture.go diff --git a/Makefile b/Makefile index f5265d9..57beeab 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ push/docker.ugate: docker push/ugate push/ugate: docker push ${IMAGE}:latest +build/android: + cd android && gomobile bind -a -ldflags '-s -w' -target android -o android/ugate.aar . + + # Using Intellij plugin: missing manifest features # Build with buildpack: 30 sec to deploy # Build with docker: 26 sec diff --git a/android/ugate.go b/android/ugate.go deleted file mode 100644 index bda5e4b..0000000 --- a/android/ugate.go +++ /dev/null @@ -1,186 +0,0 @@ -package android - -import ( - "encoding/base64" - "log" - "os" - "time" - - "github.com/costinm/ugate" - "github.com/costinm/ugate/pkg/local" - "github.com/costinm/ugate/pkg/msgs" - "github.com/costinm/ugate/pkg/udp" - "github.com/costinm/ugate/pkg/ugatesvc" - "github.com/costinm/ugate/pkg/auth" -) -/* - gomobile bindings - -- called 2x, once with lang=java and once with lang=go - -- Signed integer and floating point types. - -- String and boolean types. - -- Byte slice types. Note that byte slices are passed by reference, - and support mutation. - -- Any function type all of whose parameters and results have - supported types. Functions must return either no results, - one result, or two results where the type of the second is - the built-in 'error' type. - -- Any interface type, all of whose exported methods have - supported function types. - -- Any struct type, all of whose exported methods have - supported function types and all of whose exported fields - have supported types. - - */ - - -// Adapter from func to interface -type HandlerCallbackFunc func(cmdS string, data []byte) - -type MessageHandler interface { - Handle(topic string, data []byte) -} - -var ( - ld *local.LLDiscovery - gw *ugatesvc.UGate - udpGate = udp.NewUDPGate(nil, nil) -) - -// Called to inject a message into Go impl -func Send(cmdS string, data []byte) { - switch cmdS { - case "r": - // refresh networks - log.Println("UDS: refresh network (r)") - go func() { - time.Sleep(2 * time.Second) - ld.RefreshNetworks() - }() - - // TODO: P - properties, json - // CON - STOP/START - set connected WIFI - // - } -} - -// Android and device version of DMesh. -func InitDmesh(callbackFunc MessageHandler) { - log.Print("Starting native process pwd=", os.Getenv("PWD"), os.Environ()) - - // SYSTEMSERVERCLASSPATH=/system/framework/services.jar:/system/framework/ethernet-service.jar:/system/framework/wifi-service.jar:/system/framework/com.android.location.provider.jar - // PATH=/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin - // STORAGE=/storage/emulated/0/Android/data/com.github.costinm.dmwifi/files - // ANDROID_DATA=/data - // ANDROID_SOCKET_zygote_secondary=12 - // ASEC_MOUNTPOINT=/mnt/asec - // EXTERNAL_STORAGE=/sdcard - // ANDROID_BOOTLOGO=1 - // ANDROID_ASSETS=/system/app - // BASE=/data/user/0/com.github.costinm.dmwifi/files - // ANDROID_STORAGE=/storage - // ANDROID_ROOT=/system - // DOWNLOAD_CACHE=/data/cache - // BOOTCLASSPATH=/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/bouncycastle.jar:/system/framework/apache-xml.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/android.hidl.base-V1.0-java.jar:/system/framework/android.hidl.manager-V1.0-java.jar:/system/framework/framework-oahl-backward-compatibility.jar:/system/framework/android.test.base.jar:/system/framework/com.google.vr.platform.jar] - - cfgf := os.Getenv("BASE") - if cfgf == "" { - cfgf = os.Getenv("HOME") - if cfgf == "" { - cfgf = os.Getenv("TEMPDIR") - } - if cfgf == "" { - cfgf = os.Getenv("TMP") - } - if cfgf == "" { - cfgf = "/tmp" - } - } - - cfgf += "/" - - // File-based config - config := ugatesvc.NewConf(cfgf) - - //meshH := "v.webinf.info:5222" // ugatesvc.Conf(config, "MESH", "v.webinf.info:5222") - - // Init or load certificates/keys - authz := auth.NewAuth(config, os.Getenv("HOSTNAME"), "v.webinf.info") - msgs.DefaultMux.Auth = authz - - gcfg := &ugate.GateCfg{ - BasePort: 15000, - } - - GW := ugatesvc.NewGate(nil, authz, gcfg, nil) - - // HTTPGate - common structures - //GW := mesh.New(authz, nil) - - // SSH transport + reverse streams. - //sshg := sshgate.NewSSHGate(GW, authz) - //GW.SSHGate = sshg - //sshg.InitServer() - //sshg.ListenSSH(":5222") - // - //// Connect to a mesh node - //if meshH != "" { - // GW.Vpn = meshH - // go sshgate.MaintainVPNConnection(GW) - //} - - // Local discovery interface - multicast, local network IPs - ld := local.NewLocal(GW, authz) - go ld.PeriodicThread() - local.ListenUDP(ld) - - GW.Mux.HandleFunc("/dmesh/ll/if", ld.HttpGetLLIf) - - - //h2s, err := h2.NewTransport(authz) - //if err != nil { - // log.Fatal(err) - //} - - // DNS capture, interpret the names, etc - // Off until DNS moved to smaller package. - //dnss, _ := dns.NewDmDns(5223) - //GW.DNS = dnss - //net.DefaultResolver.PreferGo = true - //net.DefaultResolver.Dial = dns.DNSDialer(5223) - - //udpNat := udp.NewUDPGate(GW) - //udpNat.DNS = dnss - - //hgw := httpproxy.NewHTTPGate(GW, h2s) - //hgw.HttpProxyCapture("localhost:5204") - - // Start a basic UI on the debug port - // u, _ := ui.NewUI(GW, h2s, hgw, ld) - - //udpNat.InitMux(h2s.LocalMux) - - //// Periodic registrations. - //m.Registry.RefreshNetworksPeriodic() - - log.Printf("Loading with VIP6: %v ID64: %s\n", - authz.VIP6, - base64.RawURLEncoding.EncodeToString(authz.VIP6[8:])) - - //dnss.Serve() - //err = http.ListenAndServe("localhost:5227", u) - //if err != nil { - // log.Println(err) - //} - - // TODO: mux adapter, so we can exchange messages. - - -} - diff --git a/cmd/ugate/go.mod b/cmd/ugate/go.mod index a1ffb83..22df2e0 100644 --- a/cmd/ugate/go.mod +++ b/cmd/ugate/go.mod @@ -3,13 +3,15 @@ module github.com/costinm/ugate/cmd/ugate go 1.16 replace github.com/costinm/ugate => ../../ + replace github.com/costinm/ugate/webrtc => ../../webrtc + replace github.com/costinm/ugate/dns => ../../dns + replace github.com/costinm/ugate/webpush => ../../webpush require ( - github.com/costinm/ugate v0.0.0-20210221155556-10edd21fadbf - - golang.org/x/net v0.0.0-20201224014010-6772e930b67b - golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 + github.com/costinm/ugate v0.0.0-20210328173325-afc113d007e8 + github.com/costinm/ugate/dns v0.0.0-00010101000000-000000000000 + github.com/costinm/ugate/webpush v0.0.0-20210329161419-fd5474ea74fe ) diff --git a/cmd/ugate/go.sum b/cmd/ugate/go.sum new file mode 100644 index 0000000..6718811 --- /dev/null +++ b/cmd/ugate/go.sum @@ -0,0 +1,26 @@ +github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= +github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/cmd/ugate/testdata/ugate.json b/cmd/ugate/testdata/ugate.json index 03652ee..b42424f 100644 --- a/cmd/ugate/testdata/ugate.json +++ b/cmd/ugate/testdata/ugate.json @@ -33,14 +33,10 @@ "hosts": { "localiperf": { "addr": "localhost:15102" - }, - "h.webinf.info": { - "addr": "h.webinf.info:15007", - "id": "B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA" - }, - "c1.webinf.info": { - "addr": "c1.webinf.info:15007" } }, - "remoteAccept": null + "remoteAccept": { + "h.webinf.info": "B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA", + "c1.webinf.info": "" + } } \ No newline at end of file diff --git a/cmd/ugate/testdata/ugate.yaml b/cmd/ugate/testdata/ugate.yaml index dd4aa3a..82fc482 100644 --- a/cmd/ugate/testdata/ugate.yaml +++ b/cmd/ugate/testdata/ugate.yaml @@ -44,19 +44,19 @@ hosts: addr: localhost:15102 # Live test server - h.webinf.info: - addr: h.webinf.info:15007 - id: B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA - - c1.webinf.info: - addr: c1.webinf.info:15007 +# h.webinf.info: +# addr: h.webinf.info:15007 +# id: B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA +# +# c1.webinf.info: +# addr: c1.webinf.info:15007 # Remote accept request (reverse forward, "-R") # Will also include relays, stun, etc. # TODO: could also be part of the hosts: definition, include a list # of ports. This way we don't dup the public key. remoteAccept: - #h.webinf.info: "B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA" + h.webinf.info: "B5B6KYYUBVKCX4PWPWSWAIHW2X2D3Q4HZPJYWZ6UECL2PAODHTFA" - #c1.webinf.info: "" + c1.webinf.info: "" diff --git a/cmd/ugate/ugate.go b/cmd/ugate/ugate.go index 190488c..cb1a1b1 100644 --- a/cmd/ugate/ugate.go +++ b/cmd/ugate/ugate.go @@ -1,14 +1,18 @@ package main import ( + "fmt" "log" "net" _ "net/http/pprof" - "github.com/costinm/ugate/pkg/local" - msgs "github.com/costinm/ugate/webpush" - ug "github.com/costinm/ugate/pkg/ugatesvc" + "github.com/costinm/ugate" + "github.com/costinm/ugate/dns" + "github.com/costinm/ugate/pkg/udp" + "github.com/costinm/ugate/pkg/ugatesvc" ) +type startFunc func(ug *ugatesvc.UGate) +var initHooks []func(gate *ugatesvc.UGate) startFunc // Minimal TCP over H2 Gateway, defaulting to Istio ports and capture behavior. // There is no envoy - all traffic is upgraded to optional mTLS + H2 and sent to @@ -21,34 +25,54 @@ import ( // - option to use mTLS - if the network is secure ( ipsec or equivalent ) no encryption // - detect TLS and pass it through // - inbound: extract metadata +// - DNS and DNS capture (if root) +// - control plane using webpush messaging +// - webRTC and H2 for mesh communication // - convert from H2/H1, based on local port config. // // Extras: // - SOCKS and PROXY // +// This does not include lwIP, which is now only used with AndroidVPN in +// JNI mode. func main() { // Load configs from the current dir and var/lib/dmesh, or env variables // Writes to current dir. - config := ug.NewConf("./", "./var/lib/dmesh") + config := ugatesvc.NewConf("./", "./var/lib/dmesh") - // // Start a Gate. Basic H2 and H2R services enabled. - ug := ug.NewGate(&net.Dialer{}, nil, nil, config) + ug := ugatesvc.NewGate(&net.Dialer{}, nil, nil, config) - // Initialize the messaging. - msgs.DefaultMux.Auth = ug.Auth - - // Discover local nodes using multicast UDP - localgw := local.NewLocal(ug, ug.Auth) - local.ListenUDP(localgw) - ug.Mux.HandleFunc("/dmesh/ll/if", localgw.HttpGetLLIf) + sf := []startFunc{} + if initHooks != nil { + for _, h := range initHooks { + s := h(ug) + if s != nil { + sf = append(sf, s) + } + } + } // Init DNS capture and server + dnss, _ := dns.NewDmDns(5223) + //GW. = dnss + net.DefaultResolver.PreferGo = true + net.DefaultResolver.Dial = dns.DNSDialer(5223) - // Init Iptables capture (off by default - android doesn't like it) + // UDP Gate is used with TProxy and lwIP. + udpNat := udp.NewUDPGate(dnss, dnss) + udpNat.InitMux(ug.Mux) + + hproxy := ugatesvc.NewHTTPProxy(ug) + hproxy.HttpProxyCapture(fmt.Sprintf("127.0.0.1:%d", ug.Config.BasePort+ugate.PORT_HTTP_PROXY)) // Init WebRTC port + go dnss.Serve() + + for _, h := range sf { + go h(ug) + } log.Println("Started: ", ug.Auth.ID) select {} diff --git a/cmd/ugate/ugate_dns.go b/cmd/ugate/ugate_dns.go new file mode 100644 index 0000000..a59ad61 --- /dev/null +++ b/cmd/ugate/ugate_dns.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/costinm/ugate/pkg/ugatesvc" +) + +func init() { + initHooks = append(initHooks, func(ug *ugatesvc.UGate) startFunc{ + return nil + }) +} diff --git a/cmd/ugate/ugate_iptables.go b/cmd/ugate/ugate_iptables.go new file mode 100644 index 0000000..2fed47a --- /dev/null +++ b/cmd/ugate/ugate_iptables.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "github.com/costinm/ugate" + "github.com/costinm/ugate/pkg/iptables" + "github.com/costinm/ugate/pkg/ugatesvc" +) + +func init() { + initHooks = append(initHooks, func(ug *ugatesvc.UGate) startFunc { + // Init Iptables capture (off by default - android doesn't like it) + iptables.IptablesCapture(ug, fmt.Sprintf("0.0.0.0:%d", ug.Config.BasePort+ugate.PORT_IPTABLES), false) + iptables.IptablesCapture(ug, fmt.Sprintf("0.0.0.0:%d", ug.Config.BasePort+ugate.PORT_IPTABLES_IN), true) + return nil + }) +} diff --git a/cmd/ugate/ugate_local.go b/cmd/ugate/ugate_local.go new file mode 100644 index 0000000..90ee9fe --- /dev/null +++ b/cmd/ugate/ugate_local.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/costinm/ugate/pkg/local" + "github.com/costinm/ugate/pkg/ugatesvc" +) + +func init() { + initHooks = append(initHooks, func(ug *ugatesvc.UGate) startFunc { + // Discover local nodes using multicast UDP + localgw := local.NewLocal(ug, ug.Auth) + local.ListenUDP(localgw) + go localgw.PeriodicThread() + ug.Mux.HandleFunc("/dmesh/ll/if", localgw.HttpGetLLIf) + return nil + }) +} diff --git a/cmd/ugate/ugate_webpush.go b/cmd/ugate/ugate_webpush.go new file mode 100644 index 0000000..7c63399 --- /dev/null +++ b/cmd/ugate/ugate_webpush.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/costinm/ugate/pkg/ugatesvc" + "github.com/costinm/ugate/webpush" +) + +func init() { + initHooks = append(initHooks, func(gate *ugatesvc.UGate) startFunc { + webpush.InitMux(webpush.DefaultMux, gate.Mux, gate.Auth) + return nil + }) +} diff --git a/cmd/ugatemin/ugatemin.go b/cmd/ugatemin/ugatemin.go index c9263e1..c4295ae 100644 --- a/cmd/ugatemin/ugatemin.go +++ b/cmd/ugatemin/ugatemin.go @@ -7,6 +7,8 @@ import ( "github.com/costinm/ugate/pkg/ugatesvc" ) +var initHooks []func(gate *ugatesvc.UGate) + // Minimal uGate - not using any optional package. // Used to determine the 'base' size and cost of various options. // @@ -26,6 +28,12 @@ func main() { // Start a Gate. Basic H2 and H2R services enabled. ug := ugatesvc.NewGate(&net.Dialer{}, nil, cfg, config) + if initHooks != nil { + for _, h := range initHooks { + h(ug) + } + } + // direct TCP connect to local iperf3 and fortio (or HTTP on default port) ug.Add(&ugate.Listener{ Address: ":12011", diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index f8d4eff..c26b550 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -2,15 +2,74 @@ package iptables import ( "errors" + "log" "net" "os" "syscall" + "time" "unsafe" "github.com/costinm/ugate" + "github.com/costinm/ugate/pkg/ugatesvc" ) // +func IptablesCapture(ug *ugatesvc.UGate, addr string, in bool) error { + // For http proxy we need a dedicated plain HTTP port + nl, err := net.Listen("tcp", addr) + if err != nil { + log.Println("Failed to listen", err) + return err + } + for { + remoteConn, err := nl.Accept() + ugate.VarzAccepted.Add(1) + if ne, ok := err.(net.Error); ok { + ugate.VarzAcceptErr.Add(1) + if ne.Temporary() { + time.Sleep(100 * time.Millisecond) + continue + } + } + if err != nil { + log.Println("Accept error, closing iptables listener ", err) + return err + } + go handleAcceptedConn(ug, remoteConn, in) + } + + return nil +} + +// Mirroring handleAcceptedConn in UGate +func handleAcceptedConn(ug *ugatesvc.UGate, acceptedCon net.Conn, in bool) { + bconn := ugate.GetConn(acceptedCon) + ug.TrackStreamIN(bconn.Meta()) + defer ug.OnAcceptDone(bconn) + str := bconn.Meta() + + //case ugate.ProtoIPTablesIn: + // // iptables is replacing the conn - process before creating the buffer + // DestAddr is also set as a sideeffect + str.Dest, str.ReadErr = SniffIptables(str) + + if str.ReadErr != nil { + return + } + + cfg := ug.FindCfgIptablesIn(bconn) + if cfg.ForwardTo != "" { + str.Dest = cfg.ForwardTo + } + str.Listener = cfg + str.Type = cfg.Protocol + + if !in { + str.Egress = true + } + + str.ReadErr = ug.HandleStream(str) +} // Status: // - TCP capture with redirect works @@ -27,7 +86,7 @@ import ( // https://github.com/ryanchapman/go-any-proxy/blob/master/any_proxy.go, // and other examples. // Based on REDIRECT. -func SniffIptables(str *ugate.Stream, proto string) (string, error) { +func SniffIptables(str *ugate.Stream) (string, error) { if _, ok := str.Out.(*net.TCPConn); !ok { return "", errors.New("invalid connection for iptbles") } diff --git a/pkg/sni/sni_proxy.go b/pkg/sni/sni_proxy.go index ab4311f..295ef6e 100644 --- a/pkg/sni/sni_proxy.go +++ b/pkg/sni/sni_proxy.go @@ -46,6 +46,7 @@ const ( extensionServerName uint16 = 0 ) +// Expecting SNI func SniffSNI(acc *ugate.RawConn) error { acc.Sniff() @@ -72,6 +73,7 @@ func SniffSNI(acc *ugate.RawConn) error { rlen := int(buf[3])<<8 | int(buf[4]) if rlen > 4096 { + log.Println("RLen ", rlen) return sniErr } @@ -94,6 +96,7 @@ func SniffSNI(acc *ugate.RawConn) error { chLen := end - 5 if chLen < 38 { + log.Println("chLen ", chLen) return sniErr } @@ -104,6 +107,7 @@ func SniffSNI(acc *ugate.RawConn) error { sessionIdLen := int(clientHello[38]) if sessionIdLen > 32 || chLen < 39+sessionIdLen { + log.Println("sLen ", sessionIdLen) return sniErr } m.sessionId = clientHello[39 : 39+sessionIdLen] diff --git a/pkg/udp/udpproxy.go b/pkg/udp/udpproxy.go index 95c4207..0201e8e 100644 --- a/pkg/udp/udpproxy.go +++ b/pkg/udp/udpproxy.go @@ -136,7 +136,6 @@ type UdpNat struct { type UDPGateConfig struct { DNS ugate.UDPHandler HostResolver ugate.HostResolver - } type UDPGate struct { diff --git a/pkg/ugatesvc/accept.go b/pkg/ugatesvc/accept.go index 186e1aa..8865765 100644 --- a/pkg/ugatesvc/accept.go +++ b/pkg/ugatesvc/accept.go @@ -20,7 +20,7 @@ import ( // Called at the end of the connection handling. After this point // nothing should use or refer to the connection, both proxy directions // should already be closed for write or fully closed. -func (ug *UGate) onAcceptDone(rc ugate.MetaConn) { +func (ug *UGate) OnAcceptDone(rc ugate.MetaConn) { str := rc.Meta() ug.m.Lock() delete(ug.ActiveTcp, str.StreamId) @@ -85,7 +85,7 @@ func (ug *UGate) onAcceptDone(rc ugate.MetaConn) { // Deferred to connection close, only for the raw accepted connection. func (ug *UGate) onAcceptDoneAndRecycle(rc *ugate.RawConn) { - ug.onAcceptDone(rc) + ug.OnAcceptDone(rc) ugate.BufferedConPool.Put(rc) } @@ -121,8 +121,8 @@ func (ug *UGate) onAcceptDoneAndRecycle(rc *ugate.RawConn) { func (ug *UGate) HandleTUN(conn net.Conn, target *net.TCPAddr) error { bconn := ugate.GetConn(conn) bconn.Meta().Egress = true - ug.trackStreamIN(bconn.Meta()) - defer ug.onAcceptDone(bconn) + ug.TrackStreamIN(bconn.Meta()) + defer ug.OnAcceptDone(bconn) ra := conn.RemoteAddr() //la := conn.LocalAddr() @@ -139,17 +139,18 @@ func (ug *UGate) HandleTUN(conn net.Conn, target *net.TCPAddr) error { } bconn.Stream.Egress = true + log.Println("TUN TCP ", bconn.Meta()) // TODO: config ? Could be shared with iptables port - return ug.handleStream(bconn.Meta()) + return ug.HandleStream(bconn.Meta()) } // Handle a virtual (multiplexed) stream, received over // another connection. func (ug *UGate) HandleVirtualIN(bconn ugate.MetaConn) error { - ug.trackStreamIN(bconn.Meta()) - defer ug.onAcceptDone(bconn) + ug.TrackStreamIN(bconn.Meta()) + defer ug.OnAcceptDone(bconn) - return ug.handleStream(bconn.Meta()) + return ug.HandleStream(bconn.Meta()) } // At this point the stream has the metadata: @@ -158,7 +159,7 @@ func (ug *UGate) HandleVirtualIN(bconn ugate.MetaConn) error { // - Headers // - TLS context // - Dest and Listener are set. -func (ug *UGate) handleStream(str *ugate.Stream) error { +func (ug *UGate) HandleStream(str *ugate.Stream) error { if str.Listener == nil { str.Listener = ug.DefaultListener } @@ -166,7 +167,7 @@ func (ug *UGate) handleStream(str *ugate.Stream) error { if cfg.Protocol == ugate.ProtoHTTPS { str.PostDial(str, nil) - return ug.h2Handler.Handle(str) + return ug.H2Handler.Handle(str) } // Config has an in-process handler - not forwarding (or the handler may @@ -182,7 +183,7 @@ func (ug *UGate) handleStream(str *ugate.Stream) error { return ug.dialOut(str) } -func (ug *UGate) trackStreamIN(s *ugate.Stream) { +func (ug *UGate) TrackStreamIN(s *ugate.Stream) { ug.m.Lock() ug.ActiveTcp[s.StreamId] = s ug.m.Unlock() @@ -211,8 +212,8 @@ func (ug *UGate) handleAcceptedConn(l *ugate.Listener, acceptedCon net.Conn) { // Get a buffered stream - this is used for sniffing. // Most common case is TLS, we want the SNI. bconn := ugate.GetConn(acceptedCon) - ug.trackStreamIN(bconn.Meta()) - defer ug.onAcceptDone(bconn) + ug.TrackStreamIN(bconn.Meta()) + defer ug.OnAcceptDone(bconn) str := bconn.Meta() // Special protocols, muxed on a single port - will extract real @@ -222,25 +223,24 @@ func (ug *UGate) handleAcceptedConn(l *ugate.Listener, acceptedCon net.Conn) { switch cfg.Protocol { // TODO: costin: does not compile on android gomobile, missing syscall. // remove dep, reverse it. - //case ugate.ProtoIPTablesIn: - // // iptables is replacing the conn - process before creating the buffer - // str.Dest, str.ReadErr = iptables.SniffIptables(str, cfg.Protocol) - // cfg = ug.findCfgIptablesIn(bconn) - //case ugate.ProtoIPTables: - // str.Dest, str.ReadErr = iptables.SniffIptables(str, cfg.Protocol) - // str.Egress = true case ugate.ProtoSocks: str.Egress = true str.ReadErr = socks.ReadSocksHeader(bconn) case ugate.ProtoHTTP: - str.ReadErr = ug.h2Handler.handleHTTPListener(l, bconn) + str.ReadErr = ug.H2Handler.handleHTTPListener(l, bconn) return case ugate.ProtoHTTPS: tlsTerm = true // Used to present the right cert str.ReadErr = sni.SniffSNI(bconn) + if str.ReadErr != nil { + log.Println("XXX Failed to snif SNI", str.ReadErr) + } case ugate.ProtoTLS: str.ReadErr = sni.SniffSNI(bconn) + if str.ReadErr != nil { + log.Println("XXX Failed to snif SNI in TLS", str.ReadErr, l.Address, l.Protocol, bconn.Meta()) + } if str.Dest == "" { // No destination - terminate here // TODO: also if dest hostname == local name or VIP or ID @@ -280,7 +280,7 @@ func (ug *UGate) handleAcceptedConn(l *ugate.Listener, acceptedCon net.Conn) { tlsOrOrigStr = tc.Meta() } - str.ReadErr = ug.handleStream(tlsOrOrigStr) + str.ReadErr = ug.HandleStream(tlsOrOrigStr) } // Auto-detect protocol on the wire, so routing info can be diff --git a/pkg/ugatesvc/h2r.go b/pkg/ugatesvc/h2r.go index 04686a7..2ffeb7b 100644 --- a/pkg/ugatesvc/h2r.go +++ b/pkg/ugatesvc/h2r.go @@ -135,7 +135,7 @@ func (t *H2Transport) GetClientConn(req *http.Request, addr string) (*http2.Clie return nil, err } - cc, err := t.ug.h2Handler.h2t.NewClientConn(tc) + cc, err := t.ug.H2Handler.h2t.NewClientConn(tc) return cc, err } diff --git a/pkg/ugatesvc/http.go b/pkg/ugatesvc/http.go index 0e9c66b..929612c 100644 --- a/pkg/ugatesvc/http.go +++ b/pkg/ugatesvc/http.go @@ -67,6 +67,7 @@ func NewH2Transport(ug *UGate) (*H2Transport, error) { } // UpdateReverseAccept updates the upstream accept connections, based on config. +// Should be called when the config changes func (t *H2Transport) UpdateReverseAccept() { for addr, key := range t.ug.Config.H2R { t.maintainRemoteAccept(addr, key) @@ -147,6 +148,12 @@ func (l *H2Transport) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // HTTP/1.1 + if r.Method == "CONNECT" { + // WS or HTTP Proxy + } + + //if r.ProtoMajor > 1 { if len(parts) > 2 { if parts[1] == "h2r" { @@ -180,11 +187,6 @@ func (l *H2Transport) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - // HTTP/1.1 - if r.Method == "CONNECT" { - // WS or HTTP Proxy - } - if strings.HasPrefix(r.RequestURI, "/ws") { } else { diff --git a/pkg/ugatesvc/httpproxy_capture.go b/pkg/ugatesvc/httpproxy_capture.go new file mode 100644 index 0000000..6787693 --- /dev/null +++ b/pkg/ugatesvc/httpproxy_capture.go @@ -0,0 +1,152 @@ +package ugatesvc + +import ( + "io" + "log" + "net" + "net/http" + "strings" + + "github.com/costinm/ugate" +) + +// Used for HTTP_PROXY=localhost:port, to intercept outbound traffic using http proxy protocol. +// CONNECT too. + +// Experimental, not the main capture mode - TUN and SOCKS should be used if possible. + +// HTTPGate handles HTTP requests +type HTTPGate struct { + //Auth *auth.Auth + gw *UGate +} + +func NewHTTPProxy(gw *UGate) *HTTPGate { + return &HTTPGate{ + gw: gw, + } +} + +// Start listening on the addr, as a HTTP_PROXY +// Handles CONNECT and PROXY requests using the gateway +// for streams. +func (gw *HTTPGate) HttpProxyCapture(addr string) error { + // For http proxy we need a dedicated plain HTTP port + nl, err := net.Listen("tcp", addr) + if err != nil { + log.Println("Failed to listen", err) + return err + } + go http.Serve(nl, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "CONNECT" { + gw.handleConnect(w, r) + return + } + // This is a real HTTP proxy + if r.URL.IsAbs() { + log.Println("HTTPPRX", r.Method, r.Host, r.RemoteAddr, r.URL) + gw.captureHttpProxyAbsURL(w, r) + return + } + })) + return nil +} + +// Http proxy to a configured HTTP host. Hostname to HTTP address explicitly +// configured. Also hostnmae to file serving. +func (gw *HTTPGate) proxy(w http.ResponseWriter, r *http.Request) bool { + // TODO: if host is XXXX.m.SUFFIX -> forward to node. + + host, found := gw.gw.Config.Hosts[r.Host] + if !found { + return false + } + if len(host.Addr) > 0 { + log.Println("FWDHTTP: ", r.Method, r.Host, r.RemoteAddr, r.URL) + gw.gw.H2Handler.ForwardHTTP(w, r, host.Addr) + } + return true +} + +// WIP: HTTP proxy with absolute address, to a QUIC server (or sidecar)` +func (gw *HTTPGate) captureHttpProxyAbsURL(w http.ResponseWriter, r *http.Request) { + // HTTP proxy mode - uses the QUIC client to connect to the node + // TODO: redirect via VPN, only root VPN can do plaintext requests + + // parse r.URL, follow the same steps as TCP - if mesh use Client/mtls, if VPN set forward to VPN, else use H2 client + + // r.Host is populated from the absolute URL. + // Typical headers (curl): + // User-Agent, Acept, Proxy-Connection:Keep-Alive + + if gw.proxy(w, r) { + return + } + + ht := &http.Transport{ + DialContext: gw.gw.DialContext, + } + hc := &http.Client{Transport: ht} + + // TODO: use VPN to Dial !!! + // + resp, err := hc.Transport.RoundTrip(r) + if err != nil { + log.Println("XXX ", err) + http.Error(w, err.Error(), 500) + return + } + origBody := resp.Body + defer origBody.Close() + CopyResponseHeaders(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + + log.Println("PHTTP: ", r.URL) + +} + +// WIP: If method is CONNECT - operate in TCP proxy mode. This can be used to proxy +// a TCP UdpNat to a mesh node, from localhost or from a net node. +// Only used to capture local traffic - should be bound to localhost only, like socks. +// It speaks HTTP/1.1, no QUIC +func (gw *HTTPGate) handleConnect(w http.ResponseWriter, r *http.Request) { + hij, ok := w.(http.Hijacker) + if !ok { + w.WriteHeader(503) + w.Write([]byte("Error - no hijack support")) + return + } + + host := r.URL.Host + if !strings.Contains(host, ":") { + host = host + ":443" + } + + // TODO: second param may contain unprocessed data. + proxyClient, _, e := hij.Hijack() + if e != nil { + w.WriteHeader(503) + w.Write([]byte("Error - no hijack support")) + return + } + + //ra := proxyClient.RemoteAddr().(*net.TCPAddr) + str := ugate.GetConn(proxyClient) + defer gw.gw.OnAcceptDone(str) + gw.gw.TrackStreamIN(str.Meta()) + + str.Stream.Dest = host + str.Stream.Egress = true + str.PostDialHandler = func(conn net.Conn, err error) { + if err != nil { + w.WriteHeader(503) + w.Write([]byte("Dial error" + err.Error())) + return + } + proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + } + // TODO: add sniffing on the outbound + + gw.gw.dialOut(str.Meta()) +} diff --git a/pkg/ugatesvc/routing.go b/pkg/ugatesvc/routing.go index f29a05a..58dc265 100644 --- a/pkg/ugatesvc/routing.go +++ b/pkg/ugatesvc/routing.go @@ -13,7 +13,7 @@ import ( "github.com/costinm/ugate/pkg/pipe" ) -// After a Stream ( TCP+meta or HTTP ) is accepted, we need to route it based on +// After a Stream ( TCP+meta or HTTP ) is accepted/captured, we need to route it based on // the config. // // Use cases: @@ -26,6 +26,8 @@ import ( var NotFound = errors.New("not found") +// Primary function for egress streams, after metadata has been parsed. +// // Dial the target and proxy to it. // - if Dest is a mesh node, use BTS // - else use TCP proxy. @@ -163,7 +165,7 @@ func (ug *UGate) CreateConn(ctx context.Context, dmn *ugate.DMNode) (error) { return err } - cc, err := ug.h2Handler.h2t.NewClientConn(lconn) + cc, err := ug.H2Handler.h2t.NewClientConn(lconn) if err != nil { nc.Close() return err diff --git a/pkg/ugatesvc/ugate.go b/pkg/ugatesvc/ugate.go index 34fac60..a97551b 100644 --- a/pkg/ugatesvc/ugate.go +++ b/pkg/ugatesvc/ugate.go @@ -14,7 +14,7 @@ import ( "github.com/costinm/ugate" "github.com/costinm/ugate/pkg/auth" - "github.com/costinm/ugate/pkg/msgs" + msgs "github.com/costinm/ugate/webpush" ) @@ -45,7 +45,7 @@ type UGate struct { // template, used for TLS connections and the host ID TLSConfig *tls.Config - h2Handler *H2Transport + H2Handler *H2Transport // Direct Nodes by interface address (which is derived from public key). This includes only // directly connected notes - either Wifi on same segment, or VPNs and @@ -85,11 +85,24 @@ func NewGate(d ugate.ContextDialer, a *auth.Auth, cfg *ugate.GateCfg, cs ugate.C } } } + if cfg.H2R == nil { + cfg.H2R = map[string]string{} + } + if cfg.Hosts == nil { + cfg.Hosts = map[string]*ugate.DMNode{} + } + if cfg.Listeners == nil { + cfg.Listeners = map[string]*ugate.Listener{} + } if cs != nil { Get(cs, "ugate", cfg) } + if d == nil { + d = &net.Dialer{} + } + if a == nil { a = auth.NewAuth(cs, cfg.Name, cfg.Domain) } @@ -137,9 +150,9 @@ func NewGate(d ugate.ContextDialer, a *auth.Auth, cfg *ugate.GateCfg, cs ugate.C //SessionTicketsDisabled: true, } - ug.h2Handler, _ = NewH2Transport(ug) + ug.H2Handler, _ = NewH2Transport(ug) - go ug.h2Handler.UpdateReverseAccept() + go ug.H2Handler.UpdateReverseAccept() ug.Mux.Handle("/debug/", http.DefaultServeMux) ug.Mux.HandleFunc("/dmesh/tcpa", ug.HttpTCP) @@ -159,6 +172,8 @@ func NewGate(d ugate.ContextDialer, a *auth.Auth, cfg *ugate.GateCfg, cs ugate.C ug.Add(t) } + + return ug } @@ -285,10 +300,7 @@ func (ug *UGate) DefaultPorts(base int) error { // Egress: iptables and SOCKS5 // Not on localhost - redirect changes the port, keeps IP - ug.Add(&ugate.Listener{ - Address: fmt.Sprintf("0.0.0.0:%d", base+ugate.PORT_IPTABLES), - Protocol: ugate.ProtoIPTables, - }) + ug.Add(&ugate.Listener{ Address: fmt.Sprintf("127.0.0.1:%d", base+ugate.PORT_SOCKS), Protocol: ugate.ProtoSocks, @@ -296,6 +308,10 @@ func (ug *UGate) DefaultPorts(base int) error { // TODO: add HTTP CONNECT for egress. // Ingress: iptables ( capture all incoming ) + ug.Add(&ugate.Listener{ + Address: fmt.Sprintf("0.0.0.0:%d", base+ugate.PORT_IPTABLES), + Protocol: ugate.ProtoIPTables, + }) ug.Add(&ugate.Listener{ Address: fmt.Sprintf("0.0.0.0:%d", base+ugate.PORT_IPTABLES_IN), Protocol: ugate.ProtoIPTablesIn, @@ -312,7 +328,7 @@ func (ug *UGate) DefaultPorts(base int) error { ug.Add(&ugate.Listener{ Address: fmt.Sprintf("0.0.0.0:%d", base+ugate.PORT_HTTPS), Protocol: ugate.ProtoHTTPS, - Handler: ug.h2Handler, + Handler: ug.H2Handler, }) go func() { err := http.ListenAndServe(fmt.Sprintf(":%d", base), ug.Mux) @@ -324,7 +340,9 @@ func (ug *UGate) DefaultPorts(base int) error { return nil } -func (ug *UGate) findCfgIptablesIn(bconn ugate.MetaConn) *ugate.Listener { +// Based on the port in the Dest, find the Listener config. +// Used when the dest IP:port is extracted from the metadata +func (ug *UGate) FindCfgIptablesIn(bconn ugate.MetaConn) *ugate.Listener { m := bconn.Meta() _, p, _ := net.SplitHostPort(m.Dest) l := ug.Config.Listeners["-:"+p] diff --git a/pkg/ugatesvc/ugate_test.go b/pkg/ugatesvc/ugate_test.go index 24c3f53..2831f4e 100644 --- a/pkg/ugatesvc/ugate_test.go +++ b/pkg/ugatesvc/ugate_test.go @@ -197,7 +197,7 @@ func TestSrv(t *testing.T) { // This is a H2 request that is forwarded to a stream. p := pipe.New() r, _ := http.NewRequest("POST", "https://127.0.0.1:6107/dm/"+"127.0.0.1:6112", p) - res, err := ag.h2Handler.RoundTrip(r) + res, err := ag.H2Handler.RoundTrip(r) if err != nil { t.Fatal(err) } @@ -214,13 +214,13 @@ func TestSrv(t *testing.T) { ag.Config.H2R = map[string]string{ "127.0.0.1:6107": "", } - ag.h2Handler.UpdateReverseAccept() + ag.H2Handler.UpdateReverseAccept() // Connecting to Bob's gateway (from c). Request should go to Alice. // p := pipe.New() r, _ := http.NewRequest("POST", "https://127.0.0.1:6107/dm/"+ag.Auth.ID, p) - res, err := cg.h2Handler.RoundTrip(r) + res, err := cg.H2Handler.RoundTrip(r) if err != nil { t.Fatal(err) @@ -235,8 +235,8 @@ func TestSrv(t *testing.T) { ag.Config.H2R = map[string]string{ } - ag.h2Handler.UpdateReverseAccept() - if len(ag.h2Handler.reverse) > 0 { + ag.H2Handler.UpdateReverseAccept() + if len(ag.H2Handler.reverse) > 0 { t.Error("Failed to disconnect") } }) diff --git a/ugate.go b/ugate.go index 8768f5b..dcf1181 100644 --- a/ugate.go +++ b/ugate.go @@ -76,7 +76,12 @@ const ( PORT_IPTABLES = 1 PORT_IPTABLES_IN = 6 PORT_SOCKS = 9 + // SNI and HTTP could share the same port - would also + // reduce missconfig risks + PORT_HTTP_PROXY = 2 PORT_SNI = 3 + + // H2, HTTPS, H2R PORT_HTTPS = 7 ) diff --git a/webpush/mux.go b/webpush/mux.go index a05d107..c5174f8 100644 --- a/webpush/mux.go +++ b/webpush/mux.go @@ -70,8 +70,11 @@ func NewMux() *Mux { var DefaultMux = NewMux() -func InitMux(mux *Mux, auth *auth.Auth) { +func InitMux(mux *Mux, hmux *http.ServeMux, auth *auth.Auth) { mux.Auth = auth + hmux.HandleFunc("/push/", DefaultMux.HTTPHandlerWebpush) + hmux.HandleFunc("/subscribe", SubscribeHandler) + hmux.HandleFunc("/s/", HTTPHandlerSend) } // Send a message to the default mux. Will serialize the event and save it for debugging.