- Added whitelist loopback quick toggle
- Fixed plugin exit stuck bug
This commit is contained in:
Toby Chui 2025-03-09 17:02:48 +08:00
parent 23d4df1ed7
commit 3e57a90bb6
17 changed files with 417 additions and 52 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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 */

View File

@ -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()
}

View File

@ -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)

134
src/mod/access/loopback.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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 */

View File

@ -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

99
src/mod/plugins/tags.go Normal file
View File

@ -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)
}

View File

@ -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()

View File

@ -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
}

View File

@ -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()

View File

@ -375,6 +375,21 @@
<div class="toggleSucc" style="float: right; display:none; color: #2abd4d;" >
<i class="ui green checkmark icon"></i> Setting Saved
</div>
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<div class="ui toggle checkbox">
<input type="checkbox" id="enableWhitelistLoopback">
<label>Enable LAN and Loopback<br>
<small>Allowing loopback request from your public IP address and local area network devices</small></label>
</div>
</div>
</div>
</div>
<h4>Country Whitelist</h4>
<p><i class="yellow exclamation triangle icon"></i>
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.</p>
@ -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();
</script>