diff --git a/src/acme.go b/src/acme.go new file mode 100644 index 0000000..690a505 --- /dev/null +++ b/src/acme.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "net/http" + + "imuslab.com/zoraxy/mod/dynamicproxy" +) + +/* + acme.go + + This script handle special routing required for acme auto cert renew functions +*/ + +func acmeRegisterSpecialRoutingRule() { + err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{ + ID: "acme-autorenew", + MatchRule: func(r *http.Request) bool { + if r.RequestURI == "/.well-known/" { + return true + } + + return false + }, + RoutingHandler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("HELLO WORLD, THIS IS ACME REQUEST HANDLER")) + }, + Enabled: true, + }) + + if err != nil { + log.Println("[Err] " + err.Error()) + } +} diff --git a/src/api.go b/src/api.go index b8fd360..ed3c6be 100644 --- a/src/api.go +++ b/src/api.go @@ -6,6 +6,7 @@ import ( "imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/utils" ) @@ -55,6 +56,7 @@ func initAPIs() { //TLS / SSL config authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) + authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) authRouter.HandleFunc("/api/cert/upload", handleCertUpload) authRouter.HandleFunc("/api/cert/list", handleListCertificate) authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) @@ -81,6 +83,11 @@ func initAPIs() { authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) + //Path Blocker APIs + authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath) + authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath) + authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath) + //Statistic & uptime monitoring API authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) @@ -126,6 +133,8 @@ func initAPIs() { //Network utilities authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan) + authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute) + authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing) authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession) authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck) authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan) diff --git a/src/cert.go b/src/cert.go index 85873d3..e3fcb74 100644 --- a/src/cert.go +++ b/src/cert.go @@ -130,6 +130,33 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { } } +// Handle the GET and SET of reverse proxy TLS versions +func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) { + newState, err := utils.PostPara(r, "set") + if err != nil { + //GET + var reqLatestTLS bool = false + if sysdb.KeyExists("settings", "forceLatestTLS") { + sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS) + } + + js, _ := json.Marshal(reqLatestTLS) + utils.SendJSONResponse(w, string(js)) + } else { + if newState == "true" { + sysdb.Write("settings", "forceLatestTLS", true) + log.Println("Updating minimum TLS version to v1.2 or above") + dynamicProxyRouter.UpdateTLSVersion(true) + } else if newState == "false" { + sysdb.Write("settings", "forceLatestTLS", false) + log.Println("Updating minimum TLS version to v1.0 or above") + dynamicProxyRouter.UpdateTLSVersion(false) + } else { + utils.SendErrorResponse(w, "invalid state given") + } + } +} + // Handle upload of the certificate func handleCertUpload(w http.ResponseWriter, r *http.Request) { // check if request method is POST diff --git a/src/go.mod b/src/go.mod index 5d33b58..58eefc7 100644 --- a/src/go.mod +++ b/src/go.mod @@ -12,5 +12,6 @@ require ( github.com/microcosm-cc/bluemonday v1.0.24 github.com/oschwald/geoip2-golang v1.8.0 github.com/satori/go.uuid v1.2.0 + golang.org/x/net v0.10.0 golang.org/x/sys v0.8.0 ) diff --git a/src/main.go b/src/main.go index 29bddc8..16bf3bb 100644 --- a/src/main.go +++ b/src/main.go @@ -21,6 +21,7 @@ import ( "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/pathrule" "imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic/analytic" @@ -38,7 +39,7 @@ var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local no var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port") var ( name = "Zoraxy" - version = "2.6.3" + version = "2.6.4" nodeUUID = "generic" development = false //Set this to false to use embedded web fs bootTime = time.Now().Unix() @@ -57,6 +58,7 @@ var ( authAgent *auth.AuthAgent //Authentication agent tlsCertManager *tlscert.Manager //TLS / SSL management redirectTable *redirection.RuleTable //Handle special redirection rule sets + pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers statisticCollector *statistic.Collector //Collecting statistic from visitors @@ -149,6 +151,9 @@ func main() { time.Sleep(500 * time.Millisecond) + //Start the finalize sequences + finalSequence() + log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port) err = http.ListenAndServe(handler.Port, nil) diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index be86a0e..d6d3a63 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -115,7 +115,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.proxyRequest(w, r, targetProxyEndpoint) } else if !strings.HasSuffix(proxyingPath, "/") { potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/") - if potentialProxtEndpoint != nil { //Missing tailing slash. Redirect to target proxy endpoint http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect) diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 3fcae74..8ffe096 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -45,6 +45,13 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) { router.Restart() } +// Update TLS Version in runtime. Will restart proxy server if running. +// Set this to true to force TLS 1.2 or above +func (router *Router) UpdateTLSVersion(requireLatest bool) { + router.Option.ForceTLSLatest = requireLatest + router.Restart() +} + // Update https redirect, which will require updates func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) { router.Option.ForceHttpsRedirect = useRedirect @@ -62,8 +69,13 @@ func (router *Router) StartProxyService() error { return errors.New("Reverse proxy router root not set") } + minVersion := tls.VersionTLS10 + if router.Option.ForceTLSLatest { + minVersion = tls.VersionTLS12 + } config := &tls.Config{ GetCertificate: router.Option.TlsManager.GetCert, + MinVersion: uint16(minVersion), } if router.Option.UseTls { @@ -171,18 +183,22 @@ func (router *Router) StopProxyService() error { } // Restart the current router if it is running. -// Startup the server if it is not running initially func (router *Router) Restart() error { //Stop the router if it is already running + var err error = nil if router.Running { err := router.StopProxyService() if err != nil { return err } + + // Start the server + err = router.StartProxyService() + if err != nil { + return err + } } - //Start the server - err := router.StartProxyService() return err } diff --git a/src/mod/dynamicproxy/special.go b/src/mod/dynamicproxy/special.go index c7d4707..37fe80b 100644 --- a/src/mod/dynamicproxy/special.go +++ b/src/mod/dynamicproxy/special.go @@ -15,12 +15,12 @@ import ( type RoutingRule struct { ID string MatchRule func(r *http.Request) bool - RoutingHandler http.Handler + RoutingHandler func(http.ResponseWriter, *http.Request) Enabled bool } -//Router functions -//Check if a routing rule exists given its id +// Router functions +// Check if a routing rule exists given its id func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) { for _, rr := range router.routingRules { if rr.ID == rrid { @@ -31,19 +31,19 @@ func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) { return nil, errors.New("routing rule with given id not found") } -//Add a routing rule to the router +// Add a routing rule to the router func (router *Router) AddRoutingRules(rr *RoutingRule) error { _, err := router.GetRoutingRuleById(rr.ID) - if err != nil { + if err == nil { //routing rule with given id already exists - return err + return errors.New("routing rule with same id already exists") } router.routingRules = append(router.routingRules, rr) return nil } -//Remove a routing rule from the router +// Remove a routing rule from the router func (router *Router) RemoveRoutingRule(rrid string) { newRoutingRules := []*RoutingRule{} for _, rr := range router.routingRules { @@ -55,13 +55,13 @@ func (router *Router) RemoveRoutingRule(rrid string) { router.routingRules = newRoutingRules } -//Get all routing rules +// Get all routing rules func (router *Router) GetAllRoutingRules() []*RoutingRule { return router.routingRules } -//Get the matching routing rule that describe this request. -//Return nil if no routing rule is match +// Get the matching routing rule that describe this request. +// Return nil if no routing rule is match func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule { for _, thisRr := range router.routingRules { if thisRr.IsMatch(r) { @@ -71,8 +71,8 @@ func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule { return nil } -//Routing Rule functions -//Check if a request object match the +// Routing Rule functions +// Check if a request object match the func (e *RoutingRule) IsMatch(r *http.Request) bool { if !e.Enabled { return false @@ -81,5 +81,5 @@ func (e *RoutingRule) IsMatch(r *http.Request) bool { } func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) { - e.RoutingHandler.ServeHTTP(w, r) + e.RoutingHandler(w, r) } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 3afb13a..a865b66 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -22,13 +22,14 @@ type ProxyHandler struct { } type RouterOption struct { - HostUUID string - Port int - UseTls bool - ForceHttpsRedirect bool + HostUUID string //The UUID of Zoraxy, use for heading mod + Port int //Incoming port + UseTls bool //Use TLS to serve incoming requsts + ForceTLSLatest bool //Force TLS1.2 or above + ForceHttpsRedirect bool //Force redirection of http to https endpoint TlsManager *tlscert.Manager RedirectRuleTable *redirection.RuleTable - GeodbStore *geodb.Store + GeodbStore *geodb.Store //GeoIP blacklist and whitelist StatisticCollector *statistic.Collector } diff --git a/src/mod/expose/expose.go b/src/mod/expose/expose.go new file mode 100644 index 0000000..49dc4bf --- /dev/null +++ b/src/mod/expose/expose.go @@ -0,0 +1,16 @@ +package expose + +/* + Service Expose Proxy + + A tunnel for getting your local server online in one line + (No, this is not ngrok) +*/ + +type Router struct { +} + +//Create a new service expose router +func NewServiceExposeRouter() { + +} diff --git a/src/mod/expose/security.go b/src/mod/expose/security.go new file mode 100644 index 0000000..43525b2 --- /dev/null +++ b/src/mod/expose/security.go @@ -0,0 +1,111 @@ +package expose + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/pem" + "errors" + "log" +) + +// GenerateKeyPair generates a new key pair +func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) { + privkey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return privkey, &privkey.PublicKey, nil +} + +// PrivateKeyToBytes private key to bytes +func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte { + privBytes := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + }, + ) + + return privBytes +} + +// PublicKeyToBytes public key to bytes +func PublicKeyToBytes(pub *rsa.PublicKey) ([]byte, error) { + pubASN1, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return []byte(""), err + } + + pubBytes := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: pubASN1, + }) + + return pubBytes, nil +} + +// BytesToPrivateKey bytes to private key +func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + log.Println("is encrypted pem block") + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} + +// BytesToPublicKey bytes to public key +func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(pub) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + log.Println("is encrypted pem block") + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + ifc, err := x509.ParsePKIXPublicKey(b) + if err != nil { + return nil, err + } + key, ok := ifc.(*rsa.PublicKey) + if !ok { + return nil, errors.New("key not valid") + } + return key, nil +} + +// EncryptWithPublicKey encrypts data with public key +func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) ([]byte, error) { + hash := sha512.New() + ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) + if err != nil { + return []byte(""), err + } + return ciphertext, nil +} + +// DecryptWithPrivateKey decrypts data with private key +func DecryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) { + hash := sha512.New() + plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) + if err != nil { + return []byte(""), err + } + return plaintext, nil +} diff --git a/src/mod/netutils/netutils.go b/src/mod/netutils/netutils.go new file mode 100644 index 0000000..7988fc9 --- /dev/null +++ b/src/mod/netutils/netutils.go @@ -0,0 +1,69 @@ +package netutils + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + This script handles basic network utilities like + - traceroute + - ping +*/ + +func HandleTraceRoute(w http.ResponseWriter, r *http.Request) { + targetIpOrDomain, err := utils.GetPara(r, "target") + if err != nil { + utils.SendErrorResponse(w, "invalid target (domain or ip) address given") + return + } + + maxhopsString, err := utils.GetPara(r, "maxhops") + if err != nil { + maxhopsString = "64" + } + + maxHops, err := strconv.Atoi(maxhopsString) + if err != nil { + maxHops = 64 + } + + results, err := TraceRoute(targetIpOrDomain, maxHops) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(results) + utils.SendJSONResponse(w, string(js)) +} + +func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) { + return traceroute(targetIpOrDomain, maxHops) +} + +func HandlePing(w http.ResponseWriter, r *http.Request) { + targetIpOrDomain, err := utils.GetPara(r, "target") + if err != nil { + utils.SendErrorResponse(w, "invalid target (domain or ip) address given") + return + } + + results := []string{} + for i := 0; i < 4; i++ { + realIP, pingTime, ttl, err := PingIP(targetIpOrDomain) + if err != nil { + results = append(results, "Reply from "+realIP+": "+err.Error()) + } else { + results = append(results, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl)) + } + } + + js, _ := json.Marshal(results) + utils.SendJSONResponse(w, string(js)) + +} diff --git a/src/mod/netutils/netutils_test.go b/src/mod/netutils/netutils_test.go new file mode 100644 index 0000000..118249d --- /dev/null +++ b/src/mod/netutils/netutils_test.go @@ -0,0 +1,28 @@ +package netutils_test + +import ( + "testing" + + "imuslab.com/zoraxy/mod/netutils" +) + +func TestHandleTraceRoute(t *testing.T) { + results, err := netutils.TraceRoute("imuslab.com", 64) + if err != nil { + t.Fatal(err) + } + + t.Log(results) +} + +func TestHandlePing(t *testing.T) { + ipOrDomain := "example.com" + + realIP, pingTime, ttl, err := netutils.PingIP(ipOrDomain) + if err != nil { + t.Fatal("Error:", err) + return + } + + t.Log(realIP, pingTime, ttl) +} diff --git a/src/mod/netutils/pingip.go b/src/mod/netutils/pingip.go new file mode 100644 index 0000000..f8126d4 --- /dev/null +++ b/src/mod/netutils/pingip.go @@ -0,0 +1,48 @@ +package netutils + +import ( + "fmt" + "net" + "time" +) + +func PingIP(ipOrDomain string) (string, time.Duration, int, error) { + ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain) + if err != nil { + return "", 0, 0, fmt.Errorf("failed to resolve IP address: %v", err) + } + + ip := ipAddr.IP.String() + + start := time.Now() + + conn, err := net.Dial("ip:icmp", ip) + if err != nil { + return ip, 0, 0, fmt.Errorf("failed to establish ICMP connection: %v", err) + } + defer conn.Close() + + icmpMsg := []byte{8, 0, 0, 0, 0, 1, 0, 0} + _, err = conn.Write(icmpMsg) + if err != nil { + return ip, 0, 0, fmt.Errorf("failed to send ICMP message: %v", err) + } + + reply := make([]byte, 1500) + err = conn.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return ip, 0, 0, fmt.Errorf("failed to set read deadline: %v", err) + } + + _, err = conn.Read(reply) + if err != nil { + return ip, 0, 0, fmt.Errorf("failed to read ICMP reply: %v", err) + } + + elapsed := time.Since(start) + pingTime := elapsed.Round(time.Millisecond) + + ttl := int(reply[8]) + + return ip, pingTime, ttl, nil +} diff --git a/src/mod/netutils/traceroute.go b/src/mod/netutils/traceroute.go new file mode 100644 index 0000000..c26f9be --- /dev/null +++ b/src/mod/netutils/traceroute.go @@ -0,0 +1,212 @@ +package netutils + +import ( + "fmt" + "net" + "os" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" +) + +const ( + protocolICMP = 1 +) + +// liveTraceRoute return realtime tracing information to live response handler +func liveTraceRoute(dst string, maxHops int, liveRespHandler func(string)) error { + timeout := time.Second * 3 + // resolve the host name to an IP address + ipAddr, err := net.ResolveIPAddr("ip4", dst) + if err != nil { + return fmt.Errorf("failed to resolve IP address for %s: %v", dst, err) + } + // create a socket to listen for incoming ICMP packets + conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return fmt.Errorf("failed to create ICMP listener: %v", err) + } + defer conn.Close() + id := os.Getpid() & 0xffff + seq := 0 +loop_ttl: + for ttl := 1; ttl <= maxHops; ttl++ { + // set the TTL on the socket + if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil { + return fmt.Errorf("failed to set TTL: %v", err) + } + seq++ + // create an ICMP message + msg := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Code: 0, + Body: &icmp.Echo{ + ID: id, + Seq: seq, + Data: []byte("zoraxy_trace"), + }, + } + // serialize the ICMP message + msgBytes, err := msg.Marshal(nil) + if err != nil { + return fmt.Errorf("failed to serialize ICMP message: %v", err) + } + // send the ICMP message + start := time.Now() + if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil { + //log.Printf("%d: %v", ttl, err) + liveRespHandler(fmt.Sprintf("%d: %v", ttl, err)) + continue loop_ttl + } + // listen for the reply + replyBytes := make([]byte, 1500) + if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("failed to set read deadline: %v", err) + } + for i := 0; i < 3; i++ { + n, peer, err := conn.ReadFrom(replyBytes) + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + //fmt.Printf("%d: *\n", ttl) + liveRespHandler(fmt.Sprintf("%d: *\n", ttl)) + continue loop_ttl + } else { + liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err)) + } + continue + } + // parse the ICMP message + replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n]) + if err != nil { + liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err)) + continue + } + // check if the reply is an echo reply + if replyMsg.Type == ipv4.ICMPTypeEchoReply { + echoReply, ok := msg.Body.(*icmp.Echo) + if !ok || echoReply.ID != id || echoReply.Seq != seq { + continue + } + liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, peer, time.Since(start))) + break loop_ttl + } + if replyMsg.Type == ipv4.ICMPTypeTimeExceeded { + echoReply, ok := msg.Body.(*icmp.Echo) + if !ok || echoReply.ID != id || echoReply.Seq != seq { + continue + } + var raddr = peer.String() + names, _ := net.LookupAddr(raddr) + if len(names) > 0 { + raddr = names[0] + " (" + raddr + ")" + } else { + raddr = raddr + " (" + raddr + ")" + } + liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, raddr, time.Since(start))) + continue loop_ttl + } + } + + } + return nil +} + +// Standard traceroute, return results after complete +func traceroute(dst string, maxHops int) ([]string, error) { + results := []string{} + timeout := time.Second * 3 + // resolve the host name to an IP address + ipAddr, err := net.ResolveIPAddr("ip4", dst) + if err != nil { + return results, fmt.Errorf("failed to resolve IP address for %s: %v", dst, err) + } + // create a socket to listen for incoming ICMP packets + conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return results, fmt.Errorf("failed to create ICMP listener: %v", err) + } + defer conn.Close() + id := os.Getpid() & 0xffff + seq := 0 +loop_ttl: + for ttl := 1; ttl <= maxHops; ttl++ { + // set the TTL on the socket + if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil { + return results, fmt.Errorf("failed to set TTL: %v", err) + } + seq++ + // create an ICMP message + msg := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Code: 0, + Body: &icmp.Echo{ + ID: id, + Seq: seq, + Data: []byte("zoraxy_trace"), + }, + } + // serialize the ICMP message + msgBytes, err := msg.Marshal(nil) + if err != nil { + return results, fmt.Errorf("failed to serialize ICMP message: %v", err) + } + // send the ICMP message + start := time.Now() + if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil { + //log.Printf("%d: %v", ttl, err) + results = append(results, fmt.Sprintf("%d: %v", ttl, err)) + continue loop_ttl + } + // listen for the reply + replyBytes := make([]byte, 1500) + if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return results, fmt.Errorf("failed to set read deadline: %v", err) + } + for i := 0; i < 3; i++ { + n, peer, err := conn.ReadFrom(replyBytes) + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + //fmt.Printf("%d: *\n", ttl) + results = append(results, fmt.Sprintf("%d: *", ttl)) + continue loop_ttl + } else { + results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err)) + } + continue + } + // parse the ICMP message + replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n]) + if err != nil { + results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err)) + continue + } + // check if the reply is an echo reply + if replyMsg.Type == ipv4.ICMPTypeEchoReply { + echoReply, ok := msg.Body.(*icmp.Echo) + if !ok || echoReply.ID != id || echoReply.Seq != seq { + continue + } + results = append(results, fmt.Sprintf("%d: %v %v", ttl, peer, time.Since(start))) + break loop_ttl + } + if replyMsg.Type == ipv4.ICMPTypeTimeExceeded { + echoReply, ok := msg.Body.(*icmp.Echo) + if !ok || echoReply.ID != id || echoReply.Seq != seq { + continue + } + var raddr = peer.String() + names, _ := net.LookupAddr(raddr) + if len(names) > 0 { + raddr = names[0] + " (" + raddr + ")" + } else { + raddr = raddr + " (" + raddr + ")" + } + results = append(results, fmt.Sprintf("%d: %v %v", ttl, raddr, time.Since(start))) + continue loop_ttl + } + } + + } + return results, nil +} diff --git a/src/mod/pathrule/handler.go b/src/mod/pathrule/handler.go new file mode 100644 index 0000000..1253554 --- /dev/null +++ b/src/mod/pathrule/handler.go @@ -0,0 +1,100 @@ +package pathrule + +import ( + "encoding/json" + "net/http" + "strconv" + + uuid "github.com/satori/go.uuid" + "imuslab.com/zoraxy/mod/utils" +) + +/* + handler.go + + This script handles pathblock api +*/ + +func (h *Handler) HandleListBlockingPath(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(h.BlockingPaths) + utils.SendJSONResponse(w, string(js)) +} + +func (h *Handler) HandleAddBlockingPath(w http.ResponseWriter, r *http.Request) { + matchingPath, err := utils.PostPara(r, "matchingPath") + if err != nil { + utils.SendErrorResponse(w, "invalid matching path given") + return + } + + exactMatch, err := utils.PostPara(r, "exactMatch") + if err != nil { + utils.SendErrorResponse(w, "invalid exact match value given") + return + } + + statusCodeString, err := utils.PostPara(r, "statusCode") + if err != nil { + utils.SendErrorResponse(w, "invalid status code given") + return + } + + statusCode, err := strconv.Atoi(statusCodeString) + if err != nil { + utils.SendErrorResponse(w, "invalid status code given") + return + } + + enabled, err := utils.PostPara(r, "enabled") + if err != nil { + utils.SendErrorResponse(w, "invalid enabled value given") + return + } + + caseSensitive, err := utils.PostPara(r, "caseSensitive") + if err != nil { + utils.SendErrorResponse(w, "invalid case sensitive value given") + return + } + + targetBlockingPath := BlockingPath{ + UUID: uuid.NewV4().String(), + MatchingPath: matchingPath, + ExactMatch: exactMatch == "true", + StatusCode: statusCode, + CustomHeaders: http.Header{}, + CustomHTML: []byte(""), + Enabled: enabled == "true", + CaseSenitive: caseSensitive == "true", + } + + err = h.AddBlockingPath(&targetBlockingPath) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +func (h *Handler) HandleRemoveBlockingPath(w http.ResponseWriter, r *http.Request) { + blockerUUID, err := utils.PostPara(r, "uuid") + if err != nil { + utils.SendErrorResponse(w, "invalid uuid given") + return + } + + targetRule := h.GetPathBlockerFromUUID(blockerUUID) + if targetRule == nil { + //Not found + utils.SendErrorResponse(w, "target path blocker not found") + return + } + + err = h.RemoveBlockingPathByUUID(blockerUUID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + utils.SendOK(w) +} diff --git a/src/mod/pathrule/pathrule.go b/src/mod/pathrule/pathrule.go new file mode 100644 index 0000000..8846ffa --- /dev/null +++ b/src/mod/pathrule/pathrule.go @@ -0,0 +1,175 @@ +package pathrule + +import ( + "encoding/json" + "errors" + "net/http" + "os" + "path/filepath" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Pathblock.go + + This script block off some of the specific pathname in access + For example, this module can help you block request for a particular + apache directory or functional endpoints like /.well-known/ when you + are not using it +*/ + +type Options struct { + ConfigFolder string //The folder to store the path blocking config files +} + +type BlockingPath struct { + UUID string + MatchingPath string + ExactMatch bool + StatusCode int + CustomHeaders http.Header + CustomHTML []byte + Enabled bool + CaseSenitive bool +} + +type Handler struct { + Options *Options + BlockingPaths []*BlockingPath +} + +// Create a new path blocker handler +func NewPathBlocker(options *Options) *Handler { + //Create folder if not exists + if !utils.FileExists(options.ConfigFolder) { + os.Mkdir(options.ConfigFolder, 0775) + } + + //Load the configs from file + //TODO + + return &Handler{ + Options: options, + BlockingPaths: []*BlockingPath{}, + } +} + +func (h *Handler) ListBlockingPath() []*BlockingPath { + return h.BlockingPaths +} + +// Get the blocker from matching path (path match, ignore tailing slash) +func (h *Handler) GetPathBlockerFromMatchingPath(matchingPath string) *BlockingPath { + for _, blocker := range h.BlockingPaths { + if blocker.MatchingPath == matchingPath { + return blocker + } else if strings.TrimSuffix(blocker.MatchingPath, "/") == strings.TrimSuffix(matchingPath, "/") { + return blocker + } + } + + return nil +} + +func (h *Handler) GetPathBlockerFromUUID(UUID string) *BlockingPath { + for _, blocker := range h.BlockingPaths { + if blocker.UUID == UUID { + return blocker + } + } + + return nil +} + +func (h *Handler) AddBlockingPath(pathBlocker *BlockingPath) error { + //Check if the blocker exists + blockerPath := pathBlocker.MatchingPath + targetBlocker := h.GetPathBlockerFromMatchingPath(blockerPath) + if targetBlocker != nil { + //Blocker with the same matching path already exists + return errors.New("path blocker with the same path already exists") + } + + h.BlockingPaths = append(h.BlockingPaths, pathBlocker) + + //Write the new config to file + return h.SaveBlockerToFile(pathBlocker) +} + +func (h *Handler) RemoveBlockingPathByUUID(uuid string) error { + newBlockingList := []*BlockingPath{} + for _, thisBlocker := range h.BlockingPaths { + if thisBlocker.UUID != uuid { + newBlockingList = append(newBlockingList, thisBlocker) + } + } + + if len(h.BlockingPaths) == len(newBlockingList) { + //Nothing is removed + return errors.New("given matching path blocker not exists") + } + + h.BlockingPaths = newBlockingList + + return h.RemoveBlockerFromFile(uuid) +} + +func (h *Handler) SaveBlockerToFile(pathBlocker *BlockingPath) error { + saveFilename := filepath.Join(h.Options.ConfigFolder, pathBlocker.UUID) + js, _ := json.MarshalIndent(pathBlocker, "", " ") + return os.WriteFile(saveFilename, js, 0775) +} + +func (h *Handler) RemoveBlockerFromFile(uuid string) error { + expectedConfigFile := filepath.Join(h.Options.ConfigFolder, uuid) + if !utils.FileExists(expectedConfigFile) { + return errors.New("config file not found on disk") + } + + return os.Remove(expectedConfigFile) +} + +// Get all the matching blockers for the given URL path +// return all the path blockers and the max length matching rule +func (h *Handler) GetMatchingBlockers(urlPath string) ([]*BlockingPath, *BlockingPath) { + urlPath = strings.TrimSuffix(urlPath, "/") + matchingBlockers := []*BlockingPath{} + var longestMatchingPrefix *BlockingPath = nil + for _, thisBlocker := range h.BlockingPaths { + if thisBlocker.Enabled == false { + //This blocker is not enabled. Ignore this + continue + } + + incomingURLPath := urlPath + matchingPath := strings.TrimSuffix(thisBlocker.MatchingPath, "/") + + if !thisBlocker.CaseSenitive { + //This is not case sensitive + incomingURLPath = strings.ToLower(incomingURLPath) + matchingPath = strings.ToLower(matchingPath) + } + + if matchingPath == incomingURLPath { + //This blocker have exact url path match + matchingBlockers = append(matchingBlockers, thisBlocker) + if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) { + longestMatchingPrefix = thisBlocker + } + continue + } + + if !thisBlocker.ExactMatch && strings.HasPrefix(incomingURLPath, matchingPath) { + //This blocker have prefix url match + matchingBlockers = append(matchingBlockers, thisBlocker) + if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) { + longestMatchingPrefix = thisBlocker + } + continue + } + } + + return matchingBlockers, longestMatchingPrefix +} diff --git a/src/reverseproxy.go b/src/reverseproxy.go index da71ba2..754fd6e 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -38,6 +38,14 @@ func ReverseProxtInit() { log.Println("TLS mode disabled. Serving proxy request with plain http") } + forceLatestTLSVersion := false + sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion) + if forceLatestTLSVersion { + log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2") + } else { + log.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0") + } + forceHttpsRedirect := false sysdb.Read("settings", "redirect", &forceHttpsRedirect) if forceHttpsRedirect { @@ -50,6 +58,7 @@ func ReverseProxtInit() { HostUUID: nodeUUID, Port: inboundPort, UseTls: useTls, + ForceTLSLatest: forceLatestTLSVersion, ForceHttpsRedirect: forceHttpsRedirect, TlsManager: tlsCertManager, RedirectRuleTable: redirectTable, diff --git a/src/start.go b/src/start.go index c1f618f..9517dc3 100644 --- a/src/start.go +++ b/src/start.go @@ -15,6 +15,7 @@ import ( "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/pathrule" "imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic/analytic" @@ -67,7 +68,7 @@ func startupSequence() { } //Create a redirection rule table - redirectTable, err = redirection.NewRuleTable("./rules") + redirectTable, err = redirection.NewRuleTable("./rules/redirect") if err != nil { panic(err) } @@ -93,6 +94,17 @@ func startupSequence() { panic(err) } + /* + Path Blocker + + This section of starutp script start the pathblocker + from file. + */ + + pathRuleHandler = pathrule.NewPathBlocker(&pathrule.Options{ + ConfigFolder: "./rules/pathblock", + }) + /* MDNS Discovery Service @@ -177,3 +189,9 @@ func startupSequence() { //Create an analytic loader AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector) } + +// This sequence start after everything is initialized +func finalSequence() { + //Start ACME renew agent + acmeRegisterSpecialRoutingRule() +} diff --git a/src/web/components/access.html b/src/web/components/access.html index ee5a6ee..c7324e8 100644 --- a/src/web/components/access.html +++ b/src/web/components/access.html @@ -1082,24 +1082,51 @@

Quick Ban List

//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string function isValidIpFilter(input) { - // Check if input is a valid IP address - const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input); + // Check if input is a valid IPv4 address + const isValidIPv4 = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input); - if (isValidIp) { + if (isValidIPv4) { return true; } - // Check if input is a wildcard IP address - const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input); + // Check if input is a valid IPv4 wildcard address + const isValidIPv4Wildcard = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input); - if (isValidWildcardIp) { + if (isValidIPv4Wildcard) { return true; } - // Check if input is a valid CIDR address string - const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input); + // Check if input is a valid IPv4 CIDR address + const isValidIPv4CIDR = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input); - if (isValidCidr) { + if (isValidIPv4CIDR) { + return true; + } + + + // Check if input is loopback ipv6 + if (input == "::1"){ + return true; + } + + // Check if input is a valid IPv6 address + const isValidIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(input); + + if (isValidIPv6) { + return true; + } + + //Pure magic, I have no idea how this works + //src: https://stackoverflow.com/questions/70348674/alternate-solution-validate-ipv4-and-ipv6-with-wildcard-characters-using-r + function evalIp6(t){var e=t.split(":"),n=t.split("::").length-1;if(8Network Tools
+

Multicast DNS (mDNS) Scanner

Discover mDNS enabled service in this gateway forwarded network

+

IP Scanner

Discover local area network devices by pinging them one by one

+
+ +

Traceroute / Ping

+

Trace the network nodes that your packets hops through

+
+
+
+ + +
+
+ + +
+
+ + +

+
+ + +
+ +
+
+
@@ -435,7 +463,32 @@

Network Interfaces

} updateMDNSListForWoL(); +function traceroute(){ + let domain = $("#traceroute_domain").val().trim(); + let maxhops = $("#traceroute_maxhops").val().trim(); + $("#traceroute_results").val("Loading..."); + $.get("/api/tools/traceroute?target=" + domain + "&maxhops=" + maxhops, function(data){ + if (data.error != undefined){ + $("#traceroute_results").val(""); + msgbox(data.error, false, 6000); + }else{ + $("#traceroute_results").val(data.join("\n")); + } + }); +} +function ping(){ + let domain = $("#traceroute_domain").val().trim(); + $("#traceroute_results").val("Loading..."); + $.get("/api/tools/ping?target=" + domain, function(data){ + if (data.error != undefined){ + $("#traceroute_results").val(""); + msgbox(data.error, false, 6000); + }else{ + $("#traceroute_results").val(data.join("\n")); + } + }); +} diff --git a/src/web/components/redirection.html b/src/web/components/redirection.html index fd7ae7d..1087f2a 100644 --- a/src/web/components/redirection.html +++ b/src/web/components/redirection.html @@ -1,78 +1,100 @@ +
-
-

Redirection Rules

-

Add exception case for redirecting any matching URLs

-
-
- - - - - - - - - - - - - - - - - - -
Redirection URLDestination URLCopy PathnameStatus CodeRemove
-
- -
-

Add Redirection Rule

-
-
- - - Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com -
-
- - - The target URL request being redirected to, e.g. dest.example.com/mysite -
-
-
- - +
+

Redirection Rules

+

Add exception case for redirecting any matching URLs

+
+
+ + + + + + + + + + + + + + + + + + +
Redirection URLDestination URLCopy PathnameStatus CodeRemove
+
+ +
+

Add Redirection Rule

+
+
+ + + Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com
-
-

Append the current pathname after the redirect destination

- old.example.com/blog?post=13 new.example.com/blog?post=13
- old.example.com/blog?post=13 new.example.com +
+ + + The target URL request being redirected to, e.g. dest.example.com/mysite
-
-
- -
-
- - -
+
+
+ + +
+
+

Append the current pathname after the redirect destination

+ old.example.com/blog?post=13 new.example.com/blog?post=13
+ old.example.com/blog?post=13 new.example.com
-
-
- - +
+
+ +
+
+ + +
+
+
+
+ + +
+
+
+ + +

+ +
+ + + +
+
+ +
+
+ Special Path Rules +
Advanced customization for response on particular matching path or URL
+
+
+

Current list of special path rules.

+
+ + + + + + + + + + + + + + + + + + + + + +
Matching PathStatus CodeExact MatchCase SensitiveEnabledActions
+
+
+

Add Special Path Rule

+
+
+ + + Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret +
+
+
+ + +
+
+

Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of example.com/secret/ and set it to return 401

+ example.com/secret/image.png (content of image.png)
+ example.com/secret/image.png HTTP 401 +
+
+
+ + + HTTP Status Code to be served by this rule +
+
+

+ +
+ + + \ No newline at end of file