Removed GAN

- Removed UI and module of GAN
- this feature is moved to plugin
This commit is contained in:
Toby Chui 2025-03-02 09:12:07 +08:00
parent 549e492ffd
commit 75c351e7e2
14 changed files with 7 additions and 2604 deletions

View File

@ -144,24 +144,6 @@ func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
}
// Register the APIs for Global Area Network management functions, Will be moving to plugin soon
func RegisterGANAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
}
// Register the APIs for Stream (TCP / UDP) Proxy management functions
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
@ -344,7 +326,6 @@ func initAPIs(targetMux *http.ServeMux) {
RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter)
RegisterStatisticalAPIs(authRouter)
RegisterGANAPIs(authRouter)
RegisterStreamProxyAPIs(authRouter)
RegisterMDNSAPIs(authRouter)
RegisterNetworkUtilsAPIs(authRouter)

View File

@ -1,80 +0,0 @@
package ganserv
import (
"errors"
"log"
"os"
"runtime"
"strings"
)
func TryLoadorAskUserForAuthkey() (string, error) {
//Check for zt auth token
value, exists := os.LookupEnv("ZT_AUTH")
if !exists {
log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
} else {
return value, nil
}
authKey := ""
if runtime.GOOS == "windows" {
if isAdmin() {
//Read the secret file directly
b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
}
} else {
//Elavate the permission to admin
ak, err := readAuthTokenAsAdmin()
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = ak
} else {
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
}
}
} else if runtime.GOOS == "linux" {
if isAdmin() {
//Try to read from source using sudo
ak, err := readAuthTokenAsAdmin()
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = strings.TrimSpace(ak)
} else {
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
}
} else {
//Try read from source
b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
}
}
} else if runtime.GOOS == "darwin" {
b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
}
}
authKey = strings.TrimSpace(authKey)
if authKey == "" {
return "", errors.New("Unable to load authkey from file")
}
return authKey, nil
}

View File

@ -1,37 +0,0 @@
//go:build linux
// +build linux
package ganserv
import (
"os"
"os/exec"
"os/user"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
func readAuthTokenAsAdmin() (string, error) {
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
}
cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret")
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func isAdmin() bool {
currentUser, err := user.Current()
if err != nil {
return false
}
return currentUser.Username == "root"
}

View File

@ -1,73 +0,0 @@
//go:build windows
// +build windows
package ganserv
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"golang.org/x/sys/windows"
"imuslab.com/zoraxy/mod/utils"
)
// Use admin permission to read auth token on Windows
func readAuthTokenAsAdmin() (string, error) {
//Check if the previous startup already extracted the authkey
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
}
verb := "runas"
exe := "cmd.exe"
cwd, _ := os.Getwd()
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
os.WriteFile(output, []byte(""), 0775)
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
verbPtr, _ := syscall.UTF16PtrFromString(verb)
exePtr, _ := syscall.UTF16PtrFromString(exe)
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
argPtr, _ := syscall.UTF16PtrFromString(args)
var showCmd int32 = 1 //SW_NORMAL
err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
if err != nil {
return "", err
}
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
retry := 0
time.Sleep(3 * time.Second)
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
time.Sleep(3 * time.Second)
log.Println("Waiting for ZeroTier authtoken extraction...")
retry++
}
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err != nil {
return "", err
}
return strings.TrimSpace(string(authKey)), nil
}
// Check if admin on Windows
func isAdmin() bool {
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
if err != nil {
return false
}
return true
}

View File

@ -1,130 +0,0 @@
package ganserv
import (
"log"
"net"
"imuslab.com/zoraxy/mod/database"
)
/*
Global Area Network
Server side implementation
This module do a few things to help manage
the system GANs
- Provide DHCP assign to client
- Provide a list of connected nodes in the same VLAN
- Provide proxy of packet if the target VLAN is online but not reachable
Also provide HTTP Handler functions for management
- Create Network
- Update Network Properties (Name / Desc)
- Delete Network
- Authorize Node
- Deauthorize Node
- Set / Get Network Prefered Subnet Mask
- Handle Node ping
*/
type Node struct {
Auth bool //If the node is authorized in this network
ClientID string //The client ID
MAC string //The tap MAC this client is using
Name string //Name of the client in this network
Description string //Description text
ManagedIP net.IP //The IP address assigned by this network
LastSeen int64 //Last time it is seen from this host
ClientVersion string //Client application version
PublicIP net.IP //Public IP address as seen from this host
}
type Network struct {
UID string //UUID of the network, must be a 16 char random ASCII string
Name string //Name of the network, ASCII only
Description string //Description of the network
CIDR string //The subnet masked use by this network
Nodes []*Node //The nodes currently attached in this network
}
type NetworkManagerOptions struct {
Database *database.Database
AuthToken string
ApiPort int
}
type NetworkMetaData struct {
Desc string
}
type MemberMetaData struct {
Name string
}
type NetworkManager struct {
authToken string
apiPort int
ControllerID string
option *NetworkManagerOptions
networksMetadata map[string]NetworkMetaData
}
// Create a new GAN manager
func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
option.Database.NewTable("ganserv")
//Load network metadata
networkMeta := map[string]NetworkMetaData{}
if option.Database.KeyExists("ganserv", "networkmeta") {
option.Database.Read("ganserv", "networkmeta", &networkMeta)
}
//Start the zerotier instance if not exists
//Get controller info
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
if err != nil {
log.Println("ZeroTier connection failed: ", err.Error())
return &NetworkManager{
authToken: option.AuthToken,
apiPort: option.ApiPort,
ControllerID: "",
option: option,
networksMetadata: networkMeta,
}
}
return &NetworkManager{
authToken: option.AuthToken,
apiPort: option.ApiPort,
ControllerID: instanceInfo.Address,
option: option,
networksMetadata: networkMeta,
}
}
func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData {
md, ok := m.networksMetadata[netid]
if !ok {
return &NetworkMetaData{}
}
return &md
}
func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) {
m.networksMetadata[netid] = *meta
m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata)
}
func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData {
thisMemberData := MemberMetaData{}
m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData)
return &thisMemberData
}
func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) {
m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta)
}

View File

@ -1,504 +0,0 @@
package ganserv
import (
"encoding/json"
"net"
"net/http"
"regexp"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {
if m.ControllerID == "" {
//Node id not exists. Check again
instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort)
if err != nil {
utils.SendErrorResponse(w, "unable to access node id information")
return
}
m.ControllerID = instanceInfo.Address
}
js, _ := json.Marshal(m.ControllerID)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) {
networkInfo, err := m.createNetwork()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Network created. Assign it the standard network settings
err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
// Return the new network ID
js, _ := json.Marshal(networkInfo.Nwid)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) {
networkID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty network id given")
return
}
if !m.networkExists(networkID) {
utils.SendErrorResponse(w, "network id not exists")
return
}
err = m.deleteNetwork(networkID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
}
utils.SendOK(w)
}
func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) {
netid, _ := utils.GetPara(r, "netid")
if netid != "" {
targetNetInfo, err := m.getNetworkInfoById(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(targetNetInfo)
utils.SendJSONResponse(w, string(js))
} else {
// Return the list of networks as JSON
networkIds, err := m.listNetworkIds()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
networkInfos := []*NetworkInfo{}
for _, id := range networkIds {
thisNetInfo, err := m.getNetworkInfoById(id)
if err == nil {
networkInfos = append(networkInfos, thisNetInfo)
}
}
js, _ := json.Marshal(networkInfos)
utils.SendJSONResponse(w, string(js))
}
}
func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "network id not given")
return
}
if !m.networkExists(netid) {
utils.SendErrorResponse(w, "network not eixsts")
}
newName, _ := utils.PostPara(r, "name")
newDesc, _ := utils.PostPara(r, "desc")
if newName != "" && newDesc != "" {
//Strip away html from name and desc
re := regexp.MustCompile("<[^>]*>")
newName := re.ReplaceAllString(newName, "")
newDesc := re.ReplaceAllString(newDesc, "")
//Set the new network name and desc
err = m.setNetworkNameAndDescription(netid, newName, newDesc)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else {
//Get current name and description
name, desc, err := m.getNetworkNameAndDescription(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal([]string{name, desc})
utils.SendJSONResponse(w, string(js))
}
}
func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid not given")
return
}
targetNetwork, err := m.getNetworkInfoById(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(targetNetwork)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid not given")
return
}
cidr, err := utils.PostPara(r, "cidr")
if err != nil {
utils.SendErrorResponse(w, "cidr not given")
return
}
ipstart, err := utils.PostPara(r, "ipstart")
if err != nil {
utils.SendErrorResponse(w, "ipstart not given")
return
}
ipend, err := utils.PostPara(r, "ipend")
if err != nil {
utils.SendErrorResponse(w, "ipend not given")
return
}
//Validate the CIDR is real, the ip range is within the CIDR range
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
utils.SendErrorResponse(w, "invalid cidr string given")
return
}
startIP := net.ParseIP(ipstart)
endIP := net.ParseIP(ipend)
if startIP == nil || endIP == nil {
utils.SendErrorResponse(w, "invalid start or end ip given")
return
}
withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP)
if !withinRange {
utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range")
return
}
err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle listing of network members. Set details=true for listing all details
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
netid, err := utils.GetPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid is empty")
return
}
details, _ := utils.GetPara(r, "detail")
memberIds, err := m.getNetworkMembers(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if details == "" {
//Only show client ids
js, _ := json.Marshal(memberIds)
utils.SendJSONResponse(w, string(js))
} else {
//Show detail members info
detailMemberInfo := []*MemberInfo{}
for _, thisMemberId := range memberIds {
memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId)
if err == nil {
detailMemberInfo = append(detailMemberInfo, memInfo)
}
}
js, _ := json.Marshal(detailMemberInfo)
utils.SendJSONResponse(w, string(js))
}
}
// Handle Authorization of members
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
//Check if the target memeber exists
if !m.memberExistsInNetwork(netid, memberid) {
utils.SendErrorResponse(w, "member not exists in given network")
return
}
setAuthorized, err := utils.PostPara(r, "auth")
if err != nil || setAuthorized == "" {
//Get the member authorization state
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(memberInfo.Authorized)
utils.SendJSONResponse(w, string(js))
} else if setAuthorized == "true" {
m.AuthorizeMember(netid, memberid, true)
} else if setAuthorized == "false" {
m.AuthorizeMember(netid, memberid, false)
} else {
utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized)
}
}
// Handle Delete or Add IP for a member in a network
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
opr, err := utils.PostPara(r, "opr")
if err != nil {
utils.SendErrorResponse(w, "opr not defined")
return
}
targetip, _ := utils.PostPara(r, "ip")
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if opr == "add" {
if targetip == "" {
utils.SendErrorResponse(w, "ip not set")
return
}
if !isValidIPAddr(targetip) {
utils.SendErrorResponse(w, "ip address not valid")
return
}
newIpList := append(memberInfo.IPAssignments, targetip)
err = m.setAssignedIps(netid, memberid, newIpList)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else if opr == "del" {
if targetip == "" {
utils.SendErrorResponse(w, "ip not set")
return
}
//Delete user ip from the list
newIpList := []string{}
for _, thisIp := range memberInfo.IPAssignments {
if thisIp != targetip {
newIpList = append(newIpList, thisIp)
}
}
err = m.setAssignedIps(netid, memberid, newIpList)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else if opr == "get" {
js, _ := json.Marshal(memberInfo.IPAssignments)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "unsupported opr type: "+opr)
}
}
// Handle naming for members
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
if !m.memberExistsInNetwork(netid, memberid) {
utils.SendErrorResponse(w, "target member not exists in given network")
return
}
//Read memeber data
targetMemberData := m.GetMemberMetaData(netid, memberid)
newname, err := utils.PostPara(r, "name")
if err != nil {
//Send over the member data
js, _ := json.Marshal(targetMemberData)
utils.SendJSONResponse(w, string(js))
} else {
//Write member data
targetMemberData.Name = newname
m.WriteMemeberMetaData(netid, memberid, targetMemberData)
utils.SendOK(w)
}
}
// Handle delete of a given memver
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
//Check if that member is authorized.
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, "member not exists in given GANet")
return
}
if memberInfo.Authorized {
//Deauthorized this member before deleting
m.AuthorizeMember(netid, memberid, false)
}
//Remove the memeber
err = m.deleteMember(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Check if a given network id is a network hosted on this zoraxy node
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
networks, err := m.listNetworkIds()
if err != nil {
return false
}
for _, network := range networks {
if network == networkId {
return true
}
}
return false
}
// Handle server instant joining a given network
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
if m.memberExistsInNetwork(netid, m.ControllerID) {
utils.SendErrorResponse(w, "controller already inside network")
return
}
//Join the network
err = m.joinNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle server instant leaving a given network
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
//Leave the network
err = m.leaveNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove it from target network if it is authorized
err = m.deleteMember(netid, m.ControllerID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -1,39 +0,0 @@
package ganserv
import (
"fmt"
"math/rand"
"net"
"time"
)
//Get a random free IP from the pool
func (n *Network) GetRandomFreeIP() (net.IP, error) {
// Get all IP addresses in the subnet
ips, err := GetAllAddressFromCIDR(n.CIDR)
if err != nil {
return nil, err
}
// Filter out used IPs
usedIPs := make(map[string]bool)
for _, node := range n.Nodes {
usedIPs[node.ManagedIP.String()] = true
}
availableIPs := []string{}
for _, ip := range ips {
if !usedIPs[ip] {
availableIPs = append(availableIPs, ip)
}
}
// Randomly choose an available IP
if len(availableIPs) == 0 {
return nil, fmt.Errorf("no available IP")
}
rand.Seed(time.Now().UnixNano())
randIndex := rand.Intn(len(availableIPs))
pickedFreeIP := availableIPs[randIndex]
return net.ParseIP(pickedFreeIP), nil
}

View File

@ -1,55 +0,0 @@
package ganserv_test
import (
"fmt"
"net"
"strconv"
"testing"
"imuslab.com/zoraxy/mod/ganserv"
)
func TestGetRandomFreeIP(t *testing.T) {
n := ganserv.Network{
CIDR: "172.16.0.0/12",
Nodes: []*ganserv.Node{
{
Name: "nodeC1",
ManagedIP: net.ParseIP("172.16.1.142"),
},
{
Name: "nodeC2",
ManagedIP: net.ParseIP("172.16.5.174"),
},
},
}
// Call the function for 10 times
for i := 0; i < 10; i++ {
freeIP, err := n.GetRandomFreeIP()
fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP)
// Assert that no error occurred
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
}
// Assert that the returned IP is a valid IPv4 address
if freeIP.To4() == nil {
t.Errorf("Invalid IP address format: %s", freeIP.String())
}
// Assert that the returned IP is not already used by a node
for _, node := range n.Nodes {
if freeIP.Equal(node.ManagedIP) {
t.Errorf("Returned IP is already in use: %s", freeIP.String())
}
}
n.Nodes = append(n.Nodes, &ganserv.Node{
Name: "NodeT" + strconv.Itoa(i),
ManagedIP: freeIP,
})
}
}

View File

@ -1,55 +0,0 @@
package ganserv
import (
"net"
)
//Generate all ip address from a CIDR
func GetAllAddressFromCIDR(cidr string) ([]string, error) {
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
var ips []string
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
ips = append(ips, ip.String())
}
// remove network address and broadcast address
return ips[1 : len(ips)-1], nil
}
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func isValidIPAddr(ipAddr string) bool {
ip := net.ParseIP(ipAddr)
if ip == nil {
return false
}
return true
}
func ipWithinCIDR(ipAddr string, cidr string) bool {
// Parse the CIDR string
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return false
}
// Parse the IP address
ip := net.ParseIP(ipAddr)
if ip == nil {
return false
}
// Check if the IP address is in the CIDR range
return ipNet.Contains(ip)
}

View File

@ -1,669 +0,0 @@
package ganserv
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
)
/*
zerotier.go
This hold the functions that required to communicate with
a zerotier instance
See more on
https://docs.zerotier.com/self-hosting/network-controllers/
*/
type NodeInfo struct {
Address string `json:"address"`
Clock int64 `json:"clock"`
Config struct {
Settings struct {
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
HomeDir string `json:"homeDir,omitempty"`
ListeningOn []string `json:"listeningOn,omitempty"`
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
PrimaryPort int `json:"primaryPort,omitempty"`
SecondaryPort int `json:"secondaryPort,omitempty"`
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
TertiaryPort int `json:"tertiaryPort,omitempty"`
} `json:"settings"`
} `json:"config"`
Online bool `json:"online"`
PlanetWorldID int `json:"planetWorldId"`
PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"`
PublicIdentity string `json:"publicIdentity"`
TCPFallbackActive bool `json:"tcpFallbackActive"`
Version string `json:"version"`
VersionBuild int `json:"versionBuild"`
VersionMajor int `json:"versionMajor"`
VersionMinor int `json:"versionMinor"`
VersionRev int `json:"versionRev"`
}
type ErrResp struct {
Message string `json:"message"`
}
type NetworkInfo struct {
AuthTokens []interface{} `json:"authTokens"`
AuthorizationEndpoint string `json:"authorizationEndpoint"`
Capabilities []interface{} `json:"capabilities"`
ClientID string `json:"clientId"`
CreationTime int64 `json:"creationTime"`
DNS []interface{} `json:"dns"`
EnableBroadcast bool `json:"enableBroadcast"`
ID string `json:"id"`
IPAssignmentPools []interface{} `json:"ipAssignmentPools"`
Mtu int `json:"mtu"`
MulticastLimit int `json:"multicastLimit"`
Name string `json:"name"`
Nwid string `json:"nwid"`
Objtype string `json:"objtype"`
Private bool `json:"private"`
RemoteTraceLevel int `json:"remoteTraceLevel"`
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
Revision int `json:"revision"`
Routes []interface{} `json:"routes"`
Rules []struct {
Not bool `json:"not"`
Or bool `json:"or"`
Type string `json:"type"`
} `json:"rules"`
RulesSource string `json:"rulesSource"`
SsoEnabled bool `json:"ssoEnabled"`
Tags []interface{} `json:"tags"`
V4AssignMode struct {
Zt bool `json:"zt"`
} `json:"v4AssignMode"`
V6AssignMode struct {
SixPlane bool `json:"6plane"`
Rfc4193 bool `json:"rfc4193"`
Zt bool `json:"zt"`
} `json:"v6AssignMode"`
}
type MemberInfo struct {
ActiveBridge bool `json:"activeBridge"`
Address string `json:"address"`
AuthenticationExpiryTime int `json:"authenticationExpiryTime"`
Authorized bool `json:"authorized"`
Capabilities []interface{} `json:"capabilities"`
CreationTime int64 `json:"creationTime"`
ID string `json:"id"`
Identity string `json:"identity"`
IPAssignments []string `json:"ipAssignments"`
LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"`
LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"`
LastAuthorizedTime int `json:"lastAuthorizedTime"`
LastDeauthorizedTime int `json:"lastDeauthorizedTime"`
NoAutoAssignIps bool `json:"noAutoAssignIps"`
Nwid string `json:"nwid"`
Objtype string `json:"objtype"`
RemoteTraceLevel int `json:"remoteTraceLevel"`
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
Revision int `json:"revision"`
SsoExempt bool `json:"ssoExempt"`
Tags []interface{} `json:"tags"`
VMajor int `json:"vMajor"`
VMinor int `json:"vMinor"`
VProto int `json:"vProto"`
VRev int `json:"vRev"`
}
// Get the zerotier node info from local service
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
//Read from zerotier service instance
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
//Parse the payload into struct
thisInstanceInfo := NodeInfo{}
err = json.Unmarshal(payload, &thisInstanceInfo)
if err != nil {
return nil, err
}
return &thisInstanceInfo, nil
}
/*
Network Functions
*/
//Create a zerotier network
func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID)
data := []byte(`{}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
networkInfo := NetworkInfo{}
err = json.Unmarshal(payload, &networkInfo)
if err != nil {
return nil, err
}
return &networkInfo, nil
}
// List network details
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
thisNetworkInfo := NetworkInfo{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &thisNetworkInfo)
if err != nil {
return nil, err
}
return &thisNetworkInfo, nil
}
func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error {
payloadBytes, err := json.Marshal(newNetworkInfo)
if err != nil {
return err
}
payloadBuffer := bytes.NewBuffer(payloadBytes)
// Create the HTTP request
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/"
req, err := http.NewRequest("POST", url, payloadBuffer)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
req.Header.Set("Content-Type", "application/json")
// Send the HTTP request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// List network IDs
func (m *NetworkManager) listNetworkIds() ([]string, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
if err != nil {
return []string{}, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return []string{}, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return []string{}, errors.New("network error")
}
networkIds := []string{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return []string{}, err
}
err = json.Unmarshal(payload, &networkIds)
if err != nil {
return []string{}, err
}
return networkIds, nil
}
// wrapper for checking if a network id exists
func (m *NetworkManager) networkExists(networkId string) bool {
networkIds, err := m.listNetworkIds()
if err != nil {
return false
}
for _, thisid := range networkIds {
if thisid == networkId {
return true
}
}
return false
}
// delete a network
func (m *NetworkManager) deleteNetwork(networkID string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
client := &http.Client{}
// Create a new DELETE request
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
// Add the required authorization header
req.Header.Set("X-Zt1-Auth", m.authToken)
// Send the request and get the response
resp, err := client.Do(req)
if err != nil {
return err
}
// Close the response body when we're done
defer resp.Body.Close()
s, err := io.ReadAll(resp.Body)
fmt.Println(string(s), err, resp.StatusCode)
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Configure network
// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
data := map[string]interface{}{
"ipAssignmentPools": []map[string]string{
{
"ipRangeStart": ipRangeStart,
"ipRangeEnd": ipRangeEnd,
},
},
"routes": []map[string]interface{}{
{
"target": routeTarget,
"via": nil,
},
},
"v4AssignMode": "zt",
"private": true,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid
data := map[string]interface{}{
"ipAssignments": newIps,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error {
// Convert string to rune slice
r := []rune(name)
// Loop over runes and remove non-ASCII characters
for i, v := range r {
if v > 127 {
r[i] = ' '
}
}
// Convert back to string and trim whitespace
name = strings.TrimSpace(string(r))
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/"
data := map[string]interface{}{
"name": name,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
meta := m.GetNetworkMetaData(netid)
if meta != nil {
meta.Desc = desc
m.WriteNetworkMetaData(netid, meta)
}
return nil
}
func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) {
//Get name from network info
netinfo, err := m.getNetworkInfoById(netid)
if err != nil {
return "", "", err
}
name := netinfo.Name
//Get description from meta
desc := ""
networkMeta := m.GetNetworkMetaData(netid)
if networkMeta != nil {
desc = networkMeta.Desc
}
return name, desc, nil
}
/*
Member functions
*/
func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member"
reqBody := bytes.NewBuffer([]byte{})
req, err := http.NewRequest("GET", url, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to get network members")
}
memberList := map[string]int{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &memberList)
if err != nil {
return nil, err
}
members := make([]string, 0, len(memberList))
for k := range memberList {
members = append(members, k)
}
return members, nil
}
func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool {
//Get a list of member
memberids, err := m.getNetworkMembers(netid)
if err != nil {
return false
}
for _, thisMemberId := range memberids {
if thisMemberId == memid {
return true
}
}
return false
}
// Get a network memeber info by netid and memberid
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
thisMemeberInfo := &MemberInfo{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &thisMemeberInfo)
if err != nil {
return nil, err
}
return thisMemeberInfo, nil
}
// Set the authorization state of a member
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
payload := []byte(`{"authorized": true}`)
if !setAuthorized {
payload = []byte(`{"authorized": false}`)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Delete a member from the network
func (m *NetworkManager) deleteMember(netid string, memid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Make the host to join a given network
func (m *NetworkManager) joinNetwork(netid string) error {
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Make the host to leave a given network
func (m *NetworkManager) leaveNetwork(netid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}

View File

@ -20,7 +20,6 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
@ -156,6 +155,7 @@ func startupSequence() {
if err != nil {
panic(err)
}
statisticCollector.SetAutoSave(STATISTIC_AUTO_SAVE_INTERVAL)
//Start the static web server
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
@ -247,24 +247,6 @@ func startupSequence() {
}
}
/*
Global Area Network
Require zerotier token to work
*/
usingZtAuthToken := *ztAuthToken
if usingZtAuthToken == "" {
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
if err != nil {
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
}
}
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
AuthToken: usingZtAuthToken,
ApiPort: *ztAPIPort,
Database: sysdb,
})
//Create WebSSH Manager
webSshManager = sshprox.NewSSHProxyManager()
@ -332,6 +314,12 @@ func startupSequence() {
},
Database: sysdb,
Logger: SystemWideLogger,
//TODO: REMOVE AFTER DEBUG
PluginGroups: map[string][]string{
"debug": {
"org.aroz.zoraxy.debugger",
},
},
CSRFTokenGen: func(r *http.Request) string {
return csrf.Token(r)
},

View File

@ -1,231 +0,0 @@
<div id="ganetWindow" class="standardContainer">
<div class="ui basic segment">
<h2>Global Area Network</h2>
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
</div>
<div class="ui yellow message">
<b>Deprecation Notice</b>
<p>Global Area Network will be deprecating in v3.2.x and moved to Plugin</p>
</div>
<div class="gansnetworks">
<div class="ganstats ui basic segment">
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
<h1 class="ui header" style="text-align: right;">
<span class="ganControllerID"></span>
<div class="sub header">Network Controller ID</div>
</h1>
</div>
<div class="ui list">
<div class="item">
<i class="exchange icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganetCount">0</div>
<div class="description">Networks</div>
</div>
</div>
<div class="item">
<i class="desktop icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
</div>
</div>
</div>
</div>
<div class="ganlist">
<button class="ui basic orange button" onclick="addGANet();">Create New Network</button>
<div class="ui divider"></div>
<!--
<div class="ui icon input" style="margin-bottom: 1em;">
<input type="text" placeholder="Search a Network">
<i class="circular search link icon"></i>
</div>-->
<div style="width: 100%; overflow-x: auto;">
<table class="ui celled basic unstackable striped table">
<thead>
<tr>
<th>Network ID</th>
<th>Name</th>
<th>Description</th>
<th>Subnet (Assign Range)</th>
<th>Nodes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="GANetList">
<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
/*
Network Management Functions
*/
function handleAddNetwork(){
let networkName = $("#networkName").val().trim();
if (networkName == ""){
msgbox("Network name cannot be empty", false, 5000);
return;
}
//Add network with default settings
addGANet(networkName, "192.168.196.0/24");
$("#networkName").val("");
}
function initGANetID(){
$.get("/api/gan/network/info", function(data){
if (data.error !== undefined){
msgbox(data.error, false, 5000)
}else{
if (data != ""){
$(".ganControllerID").text(data);
}
}
})
}
function addGANet() {
$.cjax({
url: "/api/gan/network/add",
type: "POST",
dataType: "json",
data: {},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false, 5000);
}else{
msgbox("Network added successfully");
}
console.log("Network added successfully:", response);
listGANet();
},
error: function(xhr, status, error) {
console.log("Error adding network:", error);
}
});
}
function listGANet(){
$("#connectedNodes").attr("count", "0");
$.get("/api/gan/network/list", function(data){
$("#GANetList").empty();
if (data.error != undefined){
console.log(data.error);
msgbox("Unable to load auth token for GANet", false, 5000);
//token error or no zerotier found
$(".gansnetworks").addClass("disabled");
$("#GANetList").append(`<tr>
<td colspan="6"><i class="red times circle icon"></i> Auth token access error or not found</td>
</tr>`);
$(".ganControllerID").text('Access Denied');
}else{
var nodeCount = 0;
data.forEach(function(gan){
$("#GANetList").append(`<tr class="ganetEntry" addr="${gan.nwid}">
<td><a href="#" onclick="event.preventDefault(); openGANetDetails('${gan.nwid}');">${gan.nwid}</a></td>
<td>${gan.name}</td>
<td class="gandesc" addr="${gan.nwid}"></td>
<td class="ganetSubnet"></td>
<td class="ganetNodes"></td>
<td>
<button onclick="openGANetDetails('${gan.nwid}');" class="ui tiny basic icon button" title="Edit Network"><i class="edit icon"></i></button>
<button onclick="removeGANet('${gan.nwid}');" class="ui tiny basic icon button" title="Remove Network"><i class="red remove icon"></i></button>
</td>
</tr>`);
nodeCount += 0;
});
if (data.length == 0){
$("#GANetList").append(`<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>`);
}
$("#ganodeCount").text(nodeCount);
$("#ganetCount").text(data.length);
//Load description
$(".gandesc").each(function(){
let addr = $(this).attr("addr");
let domEle = $(this);
$.get("/api/gan/network/name?netid=" + addr, function(data){
$(domEle).text(data[1]);
});
});
$(".ganetEntry").each(function(){
let addr = $(this).attr("addr");
let subnetEle = $(this).find(".ganetSubnet");
let nodeEle = $(this).find(".ganetNodes");
$.get("/api/gan/network/list?netid=" + addr, function(data){
if (data.routes != undefined && data.routes.length > 0){
if (data.ipAssignmentPools != undefined && data.ipAssignmentPools.length > 0){
$(subnetEle).html(`${data.routes[0].target} <br> (${data.ipAssignmentPools[0].ipRangeStart} - ${data.ipAssignmentPools[0].ipRangeEnd})`);
}else{
$(subnetEle).html(`${data.routes[0].target}<br>(Unassigned Range)`);
}
}else{
$(subnetEle).text("Unassigned");
}
//console.log(data);
});
$.get("/api/gan/members/list?netid=" + addr, function(data){
$(nodeEle).text(data.length);
let currentNodesCount = parseInt($("#connectedNodes").attr("count"));
currentNodesCount += data.length;
$("#connectedNodes").attr("count", currentNodesCount);
$("#ganodeCount").text($("#connectedNodes").attr("count"));
})
});
}
})
}
//Remove the given GANet
function removeGANet(netid){
if (confirm("Confirm remove Network " + netid + " PERMANENTLY ?"))
$.cjax({
url: "/api/gan/network/remove",
type: "POST",
dataType: "json",
data: {
id: netid,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("Net " + netid + " removed");
}
listGANet();
}
});
}
function openGANetDetails(netid){
$("#ganetWindow").load("./components/gandetails.html", function(){
setTimeout(function(){
initGanetDetails(netid);
});
});
}
//Bind event to tab switch
tabSwitchEventBind["gan"] = function(){
//On switch over to this page, load info
listGANet();
initGANetID();
}
</script>

View File

@ -1,687 +0,0 @@
<div class="standardContainer">
<button onclick="exitToGanList();" class="ui large circular black icon button"><i class="angle left icon"></i></button>
<div style="max-width: 300px; margin-top: 1em;">
<button onclick='$("#gannetDetailEdit").slideToggle("fast");' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
<h1 class="ui header">
<span class="ganetID"></span>
<div class="sub header ganetName"></div>
</h1>
<div class="ui divider"></div>
<p><span class="ganetDesc"></span></p>
</div>
<div id="gannetDetailEdit" class="ui form" style="margin-top: 1em; display:none;">
<div class="ui divider"></div>
<p>You can change the network name and description below. <br>The name and description is only for easy management purpose and will not effect the network operation.</p>
<div class="field">
<label>Network Name</label>
<input type="text" id="gaNetNameInput" placeholder="">
</div>
<div class="field">
<label>Network Description</label>
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
</div>
<br><br>
</div>
<div class="ui divider"></div>
<h2>Settings</h2>
<div class="" style="overflow-x: auto;">
<table class="ui basic celled unstackable table" style="min-width: 560px;">
<thead>
<tr>
<th colspan="4">IPv4 Auto-Assign</th>
</tr>
</thead>
<tbody id="ganetRangeTable">
</tbody>
</table>
</div>
<br>
<div class="ui form">
<h3>Custom IP Range</h3>
<p>Manual IP Range Configuration. The IP range must be within the selected CIDR range.
<br>Use <code>Utilities > IP to CIDR tool</code> if you are not too familiar with CIDR notations.</p>
<div class="two fields">
<div class="field">
<label>IP Start</label>
<input type="text" class="ganIpStart" placeholder="">
</div>
<div class="field">
<label>IP End</label>
<input type="text" class="ganIpEnd" placeholder="">
</div>
</div>
</div>
<button onclick="setNetworkRange();" class="ui basic button"><i class="ui blue save icon"></i> Save Settings</button>
<div class="ui divider"></div>
<h2>Members</h2>
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
<div class="ui checkbox" style="margin-bottom: 1em;">
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);" checked>
<label>Show Unauthorized Members</label>
</div>
<div class="" style="overflow-x: auto;">
<table class="ui celled unstackable table">
<thead>
<tr>
<th>Auth</th>
<th>Address</th>
<th>Name</th>
<th>Managed IP</th>
<th>Authorized Since</th>
<th>Version</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="networkMemeberTable">
<tr>
</tr>
</tbody>
</table>
</div>
<div class="ui divider"></div>
<h4>Add Controller as Member</h4>
<p>Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.</p>
<button class="ui basic small button addControllerToNetworkBtn" onclick="ganAddControllerToNetwork(this);"><i class="green add icon"></i> Add Controller as Member</button>
<button class="ui basic small button removeControllerFromNetworkBtn" onclick="ganRemoveControllerFromNetwork(this);"><i class="red sign-out icon"></i> Remove Controller from Member</button>
<br><br>
</div>
<script>
$(".checkbox").checkbox();
var currentGANetID = "";
var currentGANNetMemeberListener = undefined;
var currentGaNetDetails = {};
var currentGANMemberList = [];
var netRanges = {
"10.147.17.*": "10.147.17.0/24",
"10.147.18.*": "10.147.18.0/24",
"10.147.19.*": "10.147.19.0/24",
"10.147.20.*": "10.147.20.0/24",
"10.144.*.*": "10.144.0.0/16",
"10.241.*.*": "10.241.0.0/16",
"10.242.*.*": "10.242.0.0/16",
"10.243.*.*": "10.243.0.0/16",
"10.244.*.*": "10.244.0.0/16",
"172.22.*.*": "172.22.0.0/15",
"172.23.*.*": "172.23.0.0/16",
"172.24.*.*": "172.24.0.0/14",
"172.25.*.*": "172.25.0.0/16",
"172.26.*.*": "172.26.0.0/15",
"172.27.*.*": "172.27.0.0/16",
"172.28.*.*": "172.28.0.0/15",
"172.29.*.*": "172.29.0.0/16",
"172.30.*.*": "172.30.0.0/15",
"192.168.191.*": "192.168.191.0/24",
"192.168.192.*": "192.168.192.0/24",
"192.168.193.*": "192.168.193.0/24",
"192.168.194.*": "192.168.194.0/24",
"192.168.195.*": "192.168.195.0/24",
"192.168.196.*": "192.168.196.0/24"
}
function generateIPRangeTable(netRanges) {
$("#ganetRangeTable").empty();
const tableBody = document.getElementById('ganetRangeTable');
const cidrs = Object.values(netRanges);
// Set the number of rows and columns to display in the table
const numRows = 6;
const numCols = 4;
let row = document.createElement('tr');
let col = 0;
for (let i = 0; i < cidrs.length; i++) {
if (col >= numCols) {
tableBody.appendChild(row);
row = document.createElement('tr');
col = 0;
}
const td = document.createElement('td');
td.setAttribute('class', `clickable iprange`);
td.setAttribute('CIDR', cidrs[i]);
td.innerHTML = cidrs[i];
let thisCidr = cidrs[i];
td.onclick = function(){
selectNetworkRange(thisCidr, td);
};
row.appendChild(td);
col++;
}
// Add any remaining cells to the table
if (col > 0) {
for (let i = col; i < numCols; i++) {
row.appendChild(document.createElement('td'));
}
tableBody.appendChild(row);
}
}
function highlightCurrentGANetCIDR(){
var currentCIDR = currentGaNetDetails.routes[0].target;
$(".iprange").each(function(){
if ($(this).attr("CIDR") == currentCIDR){
$(this).addClass("active");
populateStartEndIpByCidr(currentCIDR);
}
})
}
function populateStartEndIpByCidr(cidr){
function cidrToRange(cidr) {
var range = [2];
cidr = cidr.split('/');
var start = ip2long(cidr[0]);
range[0] = long2ip(start);
range[1] = long2ip(Math.pow(2, 32 - cidr[1]) + start - 1);
return range;
}
var cidrRange = cidrToRange(cidr);
$(".ganIpStart").val(cidrRange[0]);
$(".ganIpEnd").val(cidrRange[1]);
}
function selectNetworkRange(cidr, object){
populateStartEndIpByCidr(cidr);
$(".iprange.active").removeClass("active");
$(object).addClass("active");
}
function setNetworkRange(){
var ipstart = $(".ganIpStart").val().trim();
var ipend = $(".ganIpEnd").val().trim();
if (ipstart == ""){
$(".ganIpStart").parent().addClass("error");
}else{
$(".ganIpStart").parent().removeClass("error");
}
if (ipend == ""){
$(".ganIpEnd").parent().addClass("error");
}else{
$(".ganIpEnd").parent().removeClass("error");
}
//Get CIDR from selected range group
var cidr = $(".iprange.active").attr("cidr");
$.cjax({
url: "/api/gan/network/setRange",
metohd: "POST",
data:{
netid: currentGANetID,
cidr: cidr,
ipstart: ipstart,
ipend: ipend
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000)
}else{
msgbox("Network Range Updated")
}
}
})
}
function saveNameAndDesc(object=undefined){
var name = $("#gaNetNameInput").val();
var desc = $("#gaNetDescInput").val();
if (object != undefined){
$(object).addClass("loading");
}
$.cjax({
url: "/api/gan/network/name",
method: "POST",
data: {
netid: currentGANetID,
name: name,
desc: desc,
},
success: function(data){
initNetNameAndDesc();
if (object != undefined){
$(object).removeClass("loading");
msgbox("Network Metadata Updated");
}
$("#gannetDetailEdit").slideUp("fast");
}
});
}
function initNetNameAndDesc(){
//Get the details of the net
$.get("/api/gan/network/name?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
$("#gaNetNameInput").val(data[0]);
$(".ganetName").html(data[0]);
$("#gaNetDescInput").val(data[1]);
$(".ganetDesc").text(data[1]);
}
});
}
function initNetDetails(){
//Get the details of the net
$.get("/api/gan/network/list?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
currentGaNetDetails = data;
highlightCurrentGANetCIDR();
}
});
}
//Handle delete IP from memeber
function deleteIpFromMemeber(memberid, ip){
$.cjax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "del",
ip: ip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP removed from member " + memberid)
}
renderMemeberTable();
}
});
}
function addIpToMemeberFromInput(memberid, newip){
function isValidIPv4Address(address) {
// Split the address into its 4 components
const parts = address.split('.');
// Check that there are 4 components
if (parts.length !== 4) {
return false;
}
// Check that each component is a number between 0 and 255
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (isNaN(part) || part < 0 || part > 255) {
return false;
}
}
// The address is valid
return true;
}
if (!isValidIPv4Address(newip)){
msgbox(newip + " is not a valid IPv4 address", false, 5000)
return
}
$.cjax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "add",
ip: newip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP added to member " + memberid)
}
renderMemeberTable();
}
})
}
//Member table populate
function renderMemeberTable(forceUpdate = false) {
$.ajax({
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
type: 'GET',
success: function(data) {
let tableBody = $('#networkMemeberTable');
if (tableBody.length == 0){
return;
}
data.sort((a, b) => a.address.localeCompare(b.address));
//Check if the new object equal to the old one
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
//Do not need to update it
return;
}
tableBody.empty();
currentGANMemberList = data;
var authroziedCount = 0;
data.forEach((member) => {
let lastAuthTime = new Date(member.lastAuthorizedTime).toLocaleString();
if (member.lastAuthorizedTime == 0){
lastAuthTime = "Never";
}
let version = `${member.vMajor}.${member.vMinor}.${member.vProto}.${member.vRev}`;
if (member.vMajor == -1){
version = "Unknown";
}
let authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);">
<label></label>
</div>`;
if (member.authorized){
authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);" checked="">
<label></label>
</div>`
}
let rowClass = "authorized";
let unauthorizedStyle = "";
if (!$("#showUnauthorizedMembers")[0].checked && !member.authorized){
unauthorizedStyle = "display:none;";
}
if (!member.authorized){
rowClass = "unauthorized";
}else{
authroziedCount++;
}
let assignedIp = "";
if (member.ipAssignments.length == 0){
assignedIp = "Not assigned"
}else{
assignedIp = `<div class="ui list">`
member.ipAssignments.forEach(function(thisIp){
assignedIp += `<div class="item" style="width: 100%;">${thisIp} <a style="cursor:pointer; float: right;" title="Remove IP" onclick="deleteIpFromMemeber('${member.address}','${thisIp}');"><i class="red remove icon"></i></a></div>`;
})
assignedIp += `</div>`
}
const row = $(`<tr class="GANetMemberEntity ${rowClass}" style="${unauthorizedStyle}">`);
row.append($(`<td class="GANetMember ${rowClass}" style="text-align: center;">`).html(authorizedCheckbox));
row.append($('<td>').text(member.address));
row.append($('<td>').html(`<span class="memberName" addr="${member.address}"></span> <a style="cursor:pointer; float: right;" title="Edit Memeber Name" onclick="renameMember('${member.address}');"><i class="grey edit icon"></i></a>`));
row.append($('<td>').html(`${assignedIp}
<div class="ui action mini fluid input" style="min-width: 200px;">
<input type="text" placeholder="IPv4" onchange="$(this).val($(this).val().trim());">
<button onclick="addIpToMemeberFromInput('${member.address}',$(this).parent().find('input').val());" class="ui basic icon button">
<i class="add icon"></i>
</button>
</div>`));
row.append($('<td>').text(lastAuthTime));
row.append($('<td>').text(version));
row.append($(`<td title="Deauthorize & Delete Memeber" style="text-align: center;" onclick="handleMemberDelete('${member.address}');">`).html(`<button class="ui basic mini icon button"><i class="red remove icon"></i></button>`));
tableBody.append(row);
});
if (data.length == 0){
tableBody.append(`<tr>
<td colspan="7"><i class="green check circle icon"></i> No member has joined this network yet.</td>
</tr>`);
}
if (data.length > 0 && authroziedCount == 0 && !$("#showUnauthorizedMembers")[0].checked){
//All nodes are unauthorized. Show tips to enable unauthorize display
tableBody.append(`<tr>
<td colspan="7"><i class="yellow exclamation circle icon"></i> Unauthorized nodes detected. Enable "Show Unauthorized Member" to change member access permission.</td>
</tr>`);
}
initNameForMembers();
},
error: function(xhr, status, error) {
console.log('Error:', error);
}
});
}
function initNameForMembers(){
$(".memberName").each(function(){
let addr = $(this).attr("addr");
let targetDOM = $(this);
$.cjax({
url: "/api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
$(targetDOM).text("N/A");
}else{
$(targetDOM).text(data.Name);
}
}
});
})
}
function renameMember(targetMemberAddr){
if (targetMemberAddr == ""){
msgbox("Member address cannot be empty", false, 5000)
return
}
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
if (newname != null && newname.trim() != "") {
$.cjax({
url: "/api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
memid: targetMemberAddr,
name: newname
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Name Updated");
}
renderMemeberTable(true);
}
})
}
}
//Helper function to check if two objects are equal recursively
function objectEqual(obj1, obj2) {
// compare types
if (typeof obj1 !== typeof obj2) {
return false;
}
// compare values
if (typeof obj1 !== 'object' || obj1 === null) {
return obj1 === obj2;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// compare keys
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false;
}
// recursively compare values
if (!objectEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
function changeUnauthorizedVisibility(visable){
if(visable){
$(".GANetMemberEntity.unauthorized").show();
}else{
$(".GANetMemberEntity.unauthorized").hide();
}
}
function handleMemberAuth(object){
let targetMemberAddr = $(object).attr("addr");
let isAuthed = object.checked;
$.cjax({
url: "/api/gan/members/authorize",
method: "POST",
data: {
netid:currentGANetID,
memid: targetMemberAddr,
auth: isAuthed
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
if (isAuthed){
msgbox("Member Authorized");
}else{
msgbox("Member Deauthorized");
}
}
renderMemeberTable(true);
}
})
}
function handleMemberDelete(addr){
if (confirm("Confirm delete member " + addr + " ?")){
$.cjax({
url: "/api/gan/members/delete",
method: "POST",
data: {
netid:currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Deleted");
}
renderMemeberTable(true);
}
});
}
}
//Add and remove this controller node to network as member
function ganAddControllerToNetwork(){
$(".addControllerToNetworkBtn").addClass("disabled");
$(".addControllerToNetworkBtn").addClass("loading");
$.cjax({
url: "/api/gan/network/join",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
$(".addControllerToNetworkBtn").removeClass("disabled");
$(".addControllerToNetworkBtn").removeClass("loading");
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller joint " + currentGANetID);
}
setTimeout(function(){
renderMemeberTable(true);
}, 3000)
}
});
}
function ganRemoveControllerFromNetwork(){
$(".removeControllerFromNetworkBtn").addClass("disabled");
$(".removeControllerFromNetworkBtn").addClass("loading");
$.cjax({
url: "/api/gan/network/leave",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller left " + currentGANetID);
}
renderMemeberTable(true);
$(".removeControllerFromNetworkBtn").removeClass("disabled");
$(".removeControllerFromNetworkBtn").removeClass("loading");
}
});
}
//Entry points
function initGanetDetails(ganetId){
currentGANetID = ganetId;
$(".ganetID").text(ganetId);
initNetNameAndDesc(ganetId);
generateIPRangeTable(netRanges);
initNetDetails();
renderMemeberTable(true);
//Setup a listener to listen for member list change
if (currentGANNetMemeberListener == undefined){
currentGANNetMemeberListener = setInterval(function(){
if ($('#networkMemeberTable').length > 0 && currentGANetID){
renderMemeberTable();
}
}, 3000);
}
}
//Switch from other tabs back to this, exit to GAN list
tabSwitchEventBind["gan"] = function(){
exitToGanList();
}
//Exit point
function exitToGanList(){
$("#gan").load("./components/gan.html", function(){
if (tabSwitchEventBind["gan"]){
tabSwitchEventBind["gan"]();
}
});
}
</script>

View File

@ -67,9 +67,6 @@
<a class="item" tag="access">
<i class="simplistic ban icon"></i> Access Control
</a>
<a class="item" tag="gan">
<i class="simplistic globe icon"></i> Global Area Network
</a>
<div class="ui divider menudivider">Security</div>
<a class="item" tag="cert">
<i class="simplistic lock icon"></i> TLS / SSL certificates
@ -135,9 +132,6 @@
<!-- Blacklist -->
<div id="access" class="functiontab" target="access.html"></div>
<!-- Global Area Networking -->
<div id="gan" class="functiontab" target="gan.html"></div>
<!-- SSO / Oauth services -->
<div id="sso" class="functiontab" target="sso.html"></div>