diff --git a/example/plugins/debugger/main.go b/example/plugins/debugger/main.go index fc9e5b7..a5c3b2d 100644 --- a/example/plugins/debugger/main.go +++ b/example/plugins/debugger/main.go @@ -78,8 +78,8 @@ func main() { Dynamic Captures */ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult { - fmt.Println("Dynamic Capture Sniffed Request:") - fmt.Println("Request URI: " + dsfr.RequestURI) + //fmt.Println("Dynamic Capture Sniffed Request:") + //fmt.Println("Request URI: " + dsfr.RequestURI) //In this example, we want to capture all URI //that start with /test_ and forward it to the dynamic capture handler diff --git a/src/accesslist.go b/src/accesslist.go index 6cdb34e..3c18321 100644 --- a/src/accesslist.go +++ b/src/accesslist.go @@ -547,6 +547,38 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) { } } +func handleWhitelistAllowLoopback(w http.ResponseWriter, r *http.Request) { + enable, _ := utils.PostPara(r, "enable") + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if enable == "" { + //Return the current enabled state + currentEnabled := rule.WhitelistAllowLocalAndLoopback + js, _ := json.Marshal(currentEnabled) + utils.SendJSONResponse(w, string(js)) + } else { + if enable == "true" { + rule.ToggleAllowLoopback(true) + } else if enable == "false" { + rule.ToggleAllowLoopback(false) + } else { + utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") + return + } + + utils.SendOK(w) + } +} + // List all quick ban ip address func handleListQuickBan(w http.ResponseWriter, r *http.Request) { currentSummary := statisticCollector.GetCurrentDailySummary() diff --git a/src/api.go b/src/api.go index 88401b2..576ef14 100644 --- a/src/api.go +++ b/src/api.go @@ -114,7 +114,7 @@ func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) - + authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback) /* Quick Ban List */ authRouter.HandleFunc("/api/quickban/list", handleListQuickBan) } diff --git a/src/def.go b/src/def.go index 7bbff5d..a17c919 100644 --- a/src/def.go +++ b/src/def.go @@ -62,12 +62,13 @@ const ( STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */ /* Configuration Folder Storage Path Constants */ - CONF_HTTP_PROXY = "./conf/proxy" - CONF_STREAM_PROXY = "./conf/streamproxy" - CONF_CERT_STORE = "./conf/certs" - CONF_REDIRECTION = "./conf/redirect" - CONF_ACCESS_RULE = "./conf/access" - CONF_PATH_RULE = "./conf/rules/pathrules" + CONF_HTTP_PROXY = "./conf/proxy" + CONF_STREAM_PROXY = "./conf/streamproxy" + CONF_CERT_STORE = "./conf/certs" + CONF_REDIRECTION = "./conf/redirect" + CONF_ACCESS_RULE = "./conf/access" + CONF_PATH_RULE = "./conf/rules/pathrules" + CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json" ) /* System Startup Flags */ diff --git a/src/mod/access/access.go b/src/mod/access/access.go index ae5cd9b..b4c8fe1 100644 --- a/src/mod/access/access.go +++ b/src/mod/access/access.go @@ -94,7 +94,7 @@ func NewAccessController(options *Options) (*Controller, error) { thisAccessRule := AccessRule{} err = json.Unmarshal(configContent, &thisAccessRule) if err != nil { - options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err) + options.Logger.PrintAndLog("access", "Unable to parse config "+filepath.Base(configFile), err) continue } thisAccessRule.parent = &thisController @@ -102,6 +102,19 @@ func NewAccessController(options *Options) (*Controller, error) { } thisController.ProxyAccessRule = &ProxyAccessRules + //Start the public ip ticker + if options.PublicIpCheckInterval <= 0 { + options.PublicIpCheckInterval = 12 * 60 * 60 //12 hours + } + thisController.ServerPublicIP = "127.0.0.1" + go func() { + err = thisController.UpdatePublicIP() + if err != nil { + options.Logger.PrintAndLog("access", "Unable to update public IP address", err) + } + + thisController.StartPublicIPUpdater() + }() return &thisController, nil } @@ -147,11 +160,7 @@ func (c *Controller) ListAllAccessRules() []*AccessRule { // Check if an access rule exists given the rule id func (c *Controller) AccessRuleExists(ruleID string) bool { r, _ := c.GetAccessRuleByID(ruleID) - if r != nil { - //An access rule with identical ID exists - return true - } - return false + return r != nil } // Add a new access rule to runtime and save it to file @@ -219,3 +228,7 @@ func (c *Controller) RemoveAccessRuleByID(ruleID string) error { //Remove it return c.DeleteAccessRuleByID(ruleID) } + +func (c *Controller) Close() { + c.StopPublicIPUpdater() +} diff --git a/src/mod/access/accessRule.go b/src/mod/access/accessRule.go index c272911..c4d2a21 100644 --- a/src/mod/access/accessRule.go +++ b/src/mod/access/accessRule.go @@ -25,18 +25,24 @@ func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool { return true } -// Toggle black list +// Toggle blacklist func (s *AccessRule) ToggleBlacklist(enabled bool) { s.BlacklistEnabled = enabled s.SaveChanges() } -// Toggel white list +// Toggel whitelist func (s *AccessRule) ToggleWhitelist(enabled bool) { s.WhitelistEnabled = enabled s.SaveChanges() } +// Toggle whitelist loopback +func (s *AccessRule) ToggleAllowLoopback(enabled bool) { + s.WhitelistAllowLocalAndLoopback = enabled + s.SaveChanges() +} + /* Check if a IP address is blacklisted, in either country or IP blacklist IsBlacklisted default return is false (allow access) diff --git a/src/mod/access/loopback.go b/src/mod/access/loopback.go new file mode 100644 index 0000000..c17c079 --- /dev/null +++ b/src/mod/access/loopback.go @@ -0,0 +1,134 @@ +package access + +import ( + "errors" + "io" + "net" + "net/http" + "strings" + "time" +) + +const ( + PUBLIC_IP_CHECK_URL = "http://checkip.amazonaws.com/" +) + +// Start the public IP address updater +func (c *Controller) StartPublicIPUpdater() { + stopChan := make(chan bool) + c.publicIpTickerStop = stopChan + ticker := time.NewTicker(time.Duration(c.Options.PublicIpCheckInterval) * time.Second) + go func() { + for { + select { + case <-stopChan: + ticker.Stop() + return + case <-ticker.C: + err := c.UpdatePublicIP() + if err != nil { + c.Options.Logger.PrintAndLog("access", "Unable to update public IP address", err) + } + } + } + }() + + c.publicIpTicker = ticker +} + +// Stop the public IP address updater +func (c *Controller) StopPublicIPUpdater() { + // Stop the public IP address updater + if c.publicIpTickerStop != nil { + c.publicIpTickerStop <- true + } + c.publicIpTicker = nil + c.publicIpTickerStop = nil +} + +// Update the public IP address of the server +func (c *Controller) UpdatePublicIP() error { + req, err := http.NewRequest("GET", PUBLIC_IP_CHECK_URL, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") + req.Header.Set("Accept-Language", "en-US,en;q=0.5") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("sec-ch-ua", `"Chromium";v="91", " Not;A Brand";v="99", "Google Chrome";v="91"`) + req.Header.Set("sec-ch-ua-platform", `"Windows"`) + req.Header.Set("sec-ch-ua-mobile", "?0") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + ip, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // Validate if the returned byte is a valid IP address + pubIP := net.ParseIP(strings.TrimSpace(string(ip))) + if pubIP == nil { + return errors.New("invalid IP address") + } + + c.ServerPublicIP = pubIP.String() + c.Options.Logger.PrintAndLog("access", "Public IP address updated to: "+c.ServerPublicIP, nil) + return nil +} + +func (c *Controller) IsLoopbackRequest(ipAddr string) bool { + loopbackIPs := []string{ + "localhost", + "::1", + "127.0.0.1", + } + + // Check if the request is loopback from public IP + if ipAddr == c.ServerPublicIP { + return true + } + + // Check if the request is from localhost or loopback IPv4 or 6 + for _, loopbackIP := range loopbackIPs { + if ipAddr == loopbackIP { + return true + } + } + + return false +} + +// Check if the IP address is in private IP range +func (c *Controller) IsPrivateIPRange(ipAddr string) bool { + privateIPBlocks := []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16", + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + for _, cidr := range privateIPBlocks { + _, block, err := net.ParseCIDR(cidr) + if err != nil { + continue + } + ip := net.ParseIP(ipAddr) + if block.Contains(ip) { + return true + } + } + + return false +} diff --git a/src/mod/access/typedef.go b/src/mod/access/typedef.go index f81a55b..9b88537 100644 --- a/src/mod/access/typedef.go +++ b/src/mod/access/typedef.go @@ -2,6 +2,7 @@ package access import ( "sync" + "time" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/geodb" @@ -13,14 +14,18 @@ type Options struct { ConfigFolder string //Path for storing config files GeoDB *geodb.Store //For resolving country code Database *database.Database //System key-value database + + /* Public IP monitoring */ + PublicIpCheckInterval int64 //in Seconds } type AccessRule struct { - ID string - Name string - Desc string - BlacklistEnabled bool - WhitelistEnabled bool + ID string + Name string + Desc string + BlacklistEnabled bool + WhitelistEnabled bool + WhitelistAllowLocalAndLoopback bool //Allow local and loopback address to bypass whitelist /* Whitelist Blacklist Table, value is comment if supported */ WhiteListCountryCode *map[string]string @@ -32,7 +37,12 @@ type AccessRule struct { } type Controller struct { + ServerPublicIP string DefaultAccessRule *AccessRule ProxyAccessRule *sync.Map Options *Options + + //Internal + publicIpTicker *time.Ticker + publicIpTickerStop chan bool } diff --git a/src/mod/access/whitelist.go b/src/mod/access/whitelist.go index 17e7f90..dd22760 100644 --- a/src/mod/access/whitelist.go +++ b/src/mod/access/whitelist.go @@ -93,6 +93,13 @@ func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool { } } + //Check for loopback match + if s.WhitelistAllowLocalAndLoopback { + if s.parent.IsLoopbackRequest(ipAddr) || s.parent.IsPrivateIPRange(ipAddr) { + return true + } + } + return false } diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index ce964a5..cdac2d6 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -151,6 +151,7 @@ func (m *Manager) handlePluginSTDOUT(pluginID string, line string) { m.Log("["+thisPlugin.Spec.Name+":"+strconv.Itoa(processID)+"] "+line, nil) } +// StopPlugin stops a plugin, it is garanteed that the plugin is stopped after this function func (m *Manager) StopPlugin(pluginID string) error { plugin, ok := m.LoadedPlugins.Load(pluginID) if !ok { @@ -224,15 +225,3 @@ func (m *Manager) PluginStillRunning(pluginID string) bool { } return plugin.(*Plugin).process.ProcessState == nil } - -// BlockUntilAllProcessExited blocks until all the plugins processes have exited -func (m *Manager) BlockUntilAllProcessExited() { - m.LoadedPlugins.Range(func(key, value interface{}) bool { - plugin := value.(*Plugin) - if m.PluginStillRunning(value.(*Plugin).Spec.ID) { - //Wait for the plugin to exit - plugin.process.Wait() - } - return true - }) -} diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index eb2c8e7..b0cbfda 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -10,6 +10,7 @@ package plugins */ import ( + "encoding/json" "errors" "fmt" "net/http" @@ -34,14 +35,24 @@ func NewPluginManager(options *ManagerOptions) *Manager { os.MkdirAll(options.PluginDir, 0755) } + //Create the plugin config file if not exists + if !utils.FileExists(options.PluginGroupsConfig) { + js, _ := json.Marshal(map[string][]string{}) + err := os.WriteFile(options.PluginGroupsConfig, js, 0644) + if err != nil { + options.Logger.PrintAndLog("plugin-manager", "Failed to create plugin group config file", err) + } + } + //Create database table options.Database.NewTable("plugins") return &Manager{ - LoadedPlugins: sync.Map{}, - tagPluginMap: sync.Map{}, - tagPluginList: make(map[string][]*Plugin), - Options: options, + LoadedPlugins: sync.Map{}, + tagPluginMap: sync.Map{}, + tagPluginListMutex: sync.RWMutex{}, + tagPluginList: make(map[string][]*Plugin), + Options: options, } } @@ -76,6 +87,14 @@ func (m *Manager) LoadPluginsFromDisk() error { } } + if m.Options.PluginGroupsConfig != "" { + //Load the plugin groups from the config file + err = m.LoadPluginGroupsFromConfig() + if err != nil { + m.Log("Failed to load plugin groups", err) + } + } + //Generate the static forwarder radix tree m.UpdateTagsToPluginMaps() @@ -156,9 +175,6 @@ func (m *Manager) Close() { } return true }) - - //Wait until all loaded plugin process are terminated - m.BlockUntilAllProcessExited() } /* Plugin Functions */ diff --git a/src/mod/plugins/static_forwarder.go b/src/mod/plugins/static_forwarder.go index 3acd286..1f20434 100644 --- a/src/mod/plugins/static_forwarder.go +++ b/src/mod/plugins/static_forwarder.go @@ -24,6 +24,7 @@ func (m *Manager) UpdateTagsToPluginMaps() { } //build the plugin list for each tag + m.tagPluginListMutex.Lock() m.tagPluginList = make(map[string][]*Plugin) for tag, pluginIds := range m.Options.PluginGroups { for _, pluginId := range pluginIds { @@ -35,6 +36,7 @@ func (m *Manager) UpdateTagsToPluginMaps() { m.tagPluginList[tag] = append(m.tagPluginList[tag], plugin) } } + m.tagPluginListMutex.Unlock() } // GenerateForwarderRadixTree generates the radix tree for static forwarders diff --git a/src/mod/plugins/tags.go b/src/mod/plugins/tags.go new file mode 100644 index 0000000..9c53c7b --- /dev/null +++ b/src/mod/plugins/tags.go @@ -0,0 +1,99 @@ +package plugins + +import ( + "encoding/json" + "os" +) + +/* + Plugin Tags + + This file contains the tags that are used to match the plugin tag + to the one on HTTP proxy rule. Once the tag is matched, the plugin + will be enabled on that given rule. +*/ + +// LoadTagPluginMap loads the plugin map into the manager +// This will only load the plugin tags to option.PluginGroups map +// to push the changes to runtime, call UpdateTagsToPluginMaps() +func (m *Manager) LoadPluginGroupsFromConfig() error { + m.Options.pluginGroupsMutex.RLock() + defer m.Options.pluginGroupsMutex.RUnlock() + + //Read the config file + rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig) + if err != nil { + return err + } + + var config map[string][]string + err = json.Unmarshal(rawConfig, &config) + if err != nil { + return err + } + + //Reset m.tagPluginList + m.Options.PluginGroups = config + return nil +} + +// AddPluginToTag adds a plugin to a tag +func (m *Manager) AddPluginToTag(tag string, pluginID string) error { + m.Options.pluginGroupsMutex.RLock() + defer m.Options.pluginGroupsMutex.RUnlock() + + //Check if the plugin exists + _, err := m.GetPluginByID(pluginID) + if err != nil { + return err + } + + //Add to m.Options.PluginGroups + pluginList, ok := m.Options.PluginGroups[tag] + if !ok { + pluginList = []string{} + } + pluginList = append(pluginList, pluginID) + m.Options.PluginGroups[tag] = pluginList + + //Update to runtime + m.UpdateTagsToPluginMaps() + + //Save to file + return m.savePluginTagMap() +} + +// RemovePluginFromTag removes a plugin from a tag +func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error { + // Check if the plugin exists in Options.PluginGroups + m.Options.pluginGroupsMutex.RLock() + defer m.Options.pluginGroupsMutex.RUnlock() + pluginList, ok := m.Options.PluginGroups[tag] + if !ok { + return nil + } + + // Remove the plugin from the list + for i, id := range pluginList { + if id == pluginID { + pluginList = append(pluginList[:i], pluginList[i+1:]...) + break + } + } + m.Options.PluginGroups[tag] = pluginList + + // Update to runtime + m.UpdateTagsToPluginMaps() + + // Save to file + return m.savePluginTagMap() +} + +// savePluginTagMap saves the plugin tag map to the config file +func (m *Manager) savePluginTagMap() error { + m.Options.pluginGroupsMutex.RLock() + defer m.Options.pluginGroupsMutex.RUnlock() + + js, _ := json.Marshal(m.Options.PluginGroups) + return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644) +} diff --git a/src/mod/plugins/traffic_router.go b/src/mod/plugins/traffic_router.go index b408a61..1bd8774 100644 --- a/src/mod/plugins/traffic_router.go +++ b/src/mod/plugins/traffic_router.go @@ -45,6 +45,7 @@ func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []str wg.Add(1) go func(thisTag string) { defer wg.Done() + m.tagPluginListMutex.RLock() for _, plugin := range m.tagPluginList[thisTag] { if plugin.Enabled && plugin.Spec.DynamicCaptureSniff != "" && plugin.Spec.DynamicCaptureIngress != "" { mutex.Lock() @@ -52,6 +53,7 @@ func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []str mutex.Unlock() } } + m.tagPluginListMutex.RUnlock() }(tag) } wg.Wait() diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index 8c27721..d28f372 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -29,19 +29,24 @@ type Plugin struct { } type ManagerOptions struct { - PluginDir string //The directory where the plugins are stored - PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs + PluginDir string //The directory where the plugins are stored + PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs + PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file /* Runtime */ SystemConst *zoraxyPlugin.RuntimeConstantValue CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function Database *database.Database `json:"-"` Logger *logger.Logger `json:"-"` + + /* Internal */ + pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups } type Manager struct { - LoadedPlugins sync.Map //Storing *Plugin - tagPluginMap sync.Map //Storing *radix.Tree for each plugin tag - tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed - Options *ManagerOptions + LoadedPlugins sync.Map //Storing *Plugin + tagPluginMap sync.Map //Storing *radix.Tree for each plugin tag + tagPluginListMutex sync.RWMutex //Mutex for the tagPluginList + tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed + Options *ManagerOptions } diff --git a/src/start.go b/src/start.go index f0ac296..5975ebc 100644 --- a/src/start.go +++ b/src/start.go @@ -312,14 +312,14 @@ func startupSequence() { ZoraxyVersion: SYSTEM_VERSION, ZoraxyUUID: nodeUUID, }, - Database: sysdb, - Logger: SystemWideLogger, - //TODO: REMOVE AFTER DEBUG - PluginGroups: map[string][]string{ + Database: sysdb, + Logger: SystemWideLogger, + PluginGroupsConfig: CONF_PLUGIN_GROUPS, + /*PluginGroups: map[string][]string{ "debug": { "org.aroz.zoraxy.debugger", }, - }, + },*/ CSRFTokenGen: func(r *http.Request) string { return csrf.Token(r) }, @@ -377,6 +377,12 @@ func ShutdownSeq() { if acmeAutoRenewer != nil { acmeAutoRenewer.Close() } + + if accessController != nil { + SystemWideLogger.Println("Closing Access Controller") + accessController.Close() + } + //Close the plugin manager SystemWideLogger.Println("Shutting down plugin manager") pluginManager.Close() diff --git a/src/web/components/access.html b/src/web/components/access.html index 4ed89fa..202b2b4 100644 --- a/src/web/components/access.html +++ b/src/web/components/access.html @@ -375,6 +375,21 @@
+This will allow all requests from the selected country. The requester's location is estimated from their IP address and may not be 100% accurate.
@@ -1043,6 +1058,31 @@ enableWhitelist(); }) }); + + $.get("/api/whitelist/allowLocal", function(data){ + if (data == true){ + $('#enableWhitelistLoopback').parent().checkbox("set checked"); + }else{ + $('#enableWhitelistLoopback').parent().checkbox("set unchecked"); + } + + //Register on change event + $("#enableWhitelistLoopback").off("change").on("change", function(){ + enableWhitelistLoopback(); + }) + }); + } + + function enableWhitelistLoopback(){ + var isChecked = $('#enableWhitelistLoopback').is(':checked'); + $.cjax({ + type: 'POST', + url: '/api/whitelist/allowLocal', + data: { enable: isChecked, id: currentEditingAccessRule}, + success: function(data){ + msgbox("Loopback whitelist " + (isChecked ? "enabled" : "disabled"), true); + } + }); } /* @@ -1606,4 +1646,7 @@ function handleUnban(targetIp){ removeIpBlacklist(targetIp); } + + //Bind UI events + $(".advanceSettings").accordion(); \ No newline at end of file