This commit is contained in:
Toby Chui
2025-10-16 21:50:04 +08:00
29 changed files with 652 additions and 259 deletions

View File

@@ -210,9 +210,10 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
}
resulst := []string{}
if bltype == "country" {
switch bltype {
case "country":
resulst = rule.GetAllBlacklistedCountryCode()
} else if bltype == "ip" {
case "ip":
resulst = rule.GetAllBlacklistedIp()
}

View File

@@ -74,11 +74,13 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
//Global certificate settings
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/tlsMinVersion", handleSetTlsMinVersion)
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
//Certificate store functions
authRouter.HandleFunc("/api/cert/setDefault", tlsCertManager.SetCertAsDefault)
authRouter.HandleFunc("/api/cert/getCommonName", tlsCertManager.HandleGetCertCommonName)
authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload)
authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate)

View File

@@ -45,32 +45,49 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
}
}
// Handle the GET and SET of reverse proxy TLS versions
func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
newState, err := utils.PostPara(r, "set")
if err != nil {
//GET
var reqLatestTLS bool = false
if sysdb.KeyExists("settings", "forceLatestTLS") {
sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS)
}
js, _ := json.Marshal(reqLatestTLS)
utils.SendJSONResponse(w, string(js))
} else {
switch newState {
case "true":
sysdb.Write("settings", "forceLatestTLS", true)
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true)
case "false":
sysdb.Write("settings", "forceLatestTLS", false)
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false)
default:
utils.SendErrorResponse(w, "invalid state given")
}
func minTlsVersionStringToUint16(version string) uint16 {
// Update the setting
var tlsVersionUint16 uint16
switch version {
case "1.0":
tlsVersionUint16 = 0x0301
case "1.1":
tlsVersionUint16 = 0x0302
case "1.2":
tlsVersionUint16 = 0x0303
case "1.3":
tlsVersionUint16 = 0x0304
}
return tlsVersionUint16
}
// Handle the GET and SET of reverse proxy minimum TLS version
func handleSetTlsMinVersion(w http.ResponseWriter, r *http.Request) {
newVersion, err := utils.PostPara(r, "set")
if err != nil {
// GET
var minTLSVersion string = "1.2" // Default to 1.2
if sysdb.KeyExists("settings", "minTLSVersion") {
sysdb.Read("settings", "minTLSVersion", &minTLSVersion)
}
js, _ := json.Marshal(minTLSVersion)
utils.SendJSONResponse(w, string(js))
return
}
// Validate input
allowed := map[string]bool{"1.0": true, "1.1": true, "1.2": true, "1.3": true}
if !allowed[newVersion] {
utils.SendErrorResponse(w, "invalid TLS version")
return
}
sysdb.Write("settings", "minTLSVersion", newVersion)
tlsVersionUint16 := minTlsVersionStringToUint16(newVersion)
// Update the setting
SystemWideLogger.PrintAndLog("TLS", "Updating minimum TLS version to v"+newVersion+" or above", nil)
dynamicProxyRouter.SetTlsMinVersion(tlsVersionUint16)
utils.SendOK(w)
}
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {

View File

@@ -44,7 +44,7 @@ import (
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.7"
SYSTEM_VERSION = "3.2.8"
DEVELOPMENT_BUILD = false
/* System Constants */

View File

@@ -17,6 +17,7 @@ require (
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.26
github.com/monperrus/crawler-user-agents v1.1.0
github.com/pires/go-proxyproto v0.8.1
github.com/shirou/gopsutil/v4 v4.25.1
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0

View File

@@ -610,6 +610,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@@ -27,6 +27,7 @@ import (
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
)
@@ -432,18 +433,18 @@ func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Req
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
//Clean each domain
cleanedDomains := []string{}
if (domainPara != "") {
if domainPara != "" {
for _, d := range strings.Split(domainPara, ",") {
// Apply normalization on each domain
nd, err := NormalizeDomain(d)
nd, err := netutils.NormalizeDomain(d)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
cleanedDomains = append(cleanedDomains, nd)
}
cleanedDomains = append(cleanedDomains, nd)
}
}
@@ -507,7 +508,6 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
dns = true
}
// Default propagation timeout is 300 seconds
propagationTimeout := 300
if dns {
@@ -549,7 +549,6 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
a.Logf("Could not extract SANs from PEM, using domainPara only", err)
}
// Extract DNS servers from the request
var dnsServers []string
dnsServersPara, err := utils.PostPara(r, "dnsServers")

View File

@@ -7,8 +7,6 @@ import (
"fmt"
"os"
"time"
"strings"
"unicode"
)
// Get the issuer name from pem file
@@ -42,8 +40,6 @@ func ExtractDomains(certBytes []byte) ([]string, error) {
return []string{}, errors.New("decode cert bytes failed")
}
func ExtractIssuerName(certBytes []byte) (string, error) {
// Parse the PEM block
block, _ := pem.Decode(certBytes)
@@ -73,9 +69,9 @@ func ExtractDomainsFromPEM(pemFilePath string) ([]string, error) {
certBytes, err := os.ReadFile(pemFilePath)
if err != nil {
return nil, err
return nil, err
}
domains,err := ExtractDomains(certBytes)
domains, err := ExtractDomains(certBytes)
if err != nil {
return nil, err
}
@@ -116,48 +112,3 @@ func CertExpireSoon(certBytes []byte, numberOfDays int) bool {
}
return false
}
// NormalizeDomain cleans and validates a domain string.
// - Trims spaces around the domain
// - Converts to lowercase
// - Removes trailing dot (FQDN canonicalization)
// - Checks that the domain conforms to standard rules:
// * Each label ≤ 63 characters
// * Only letters, digits, and hyphens
// * Labels do not start or end with a hyphen
// * Full domain ≤ 253 characters
// Returns an empty string if the domain is invalid.
func NormalizeDomain(d string) (string, error) {
d = strings.TrimSpace(d)
d = strings.ToLower(d)
d = strings.TrimSuffix(d, ".")
if len(d) == 0 {
return "", errors.New("domain is empty")
}
if len(d) > 253 {
return "", errors.New("domain exceeds 253 characters")
}
labels := strings.Split(d, ".")
for _, label := range labels {
if len(label) == 0 {
return "", errors.New("Domain '" + d + "' not valid: Empty label")
}
if len(label) > 63 {
return "", errors.New("Domain not valid: label exceeds 63 characters")
}
for i, r := range label {
if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-') {
return "", errors.New("Domain '" + d + "' not valid: Invalid character '" + string(r) + "' in label")
}
if (i == 0 || i == len(label)-1) && r == '-' {
return "", errors.New("Domain '" + d + "' not valid: label '"+ label +"' starts or ends with hyphen")
}
}
}
return d, nil
}

View File

@@ -92,7 +92,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
//Plugin routing
if h.Parent.Option.PluginManager != nil && h.Parent.Option.PluginManager.HandleRoute(w, r, sep.Tags) {
//Request handled by subroute
return

View File

@@ -438,7 +438,15 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (in
if !strings.Contains(host, ":") {
host += ":443"
}
serverName := req.URL.Hostname()
serverName := ""
//if p.Transport != nil {
// if tr, ok := p.Transport.(*http.Transport); ok && tr.TLSClientConfig != nil && tr.TLSClientConfig.ServerName != "" {
// serverName = tr.TLSClientConfig.ServerName
// }
//}
if serverName == "" {
serverName = req.URL.Hostname()
}
// Connect with SNI offload
tlsConfig := &tls.Config{

View File

@@ -48,8 +48,8 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
// Update TLS Version in runtime. Will restart proxy server if running.
// Set this to true to force TLS 1.2 or above
func (router *Router) UpdateTLSVersion(requireLatest bool) {
router.Option.ForceTLSLatest = requireLatest
func (router *Router) SetTlsMinVersion(minTlsVersion uint16) {
router.Option.MinTLSVersion = minTlsVersion
router.Restart()
}
@@ -77,9 +77,9 @@ func (router *Router) StartProxyService() error {
return errors.New("reverse proxy router root not set")
}
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
minVersion := tls.VersionTLS12 //Default to TLS 1.2
if router.Option.MinTLSVersion != 0 {
minVersion = int(router.Option.MinTLSVersion)
}
config := &tls.Config{

View File

@@ -272,6 +272,11 @@ func (ep *ProxyEndpoint) Remove() error {
return nil
}
// Check if the proxy endpoint is enabled
func (ep *ProxyEndpoint) IsEnabled() bool {
return !ep.Disabled
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest

View File

@@ -12,6 +12,7 @@ import (
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic"
@@ -95,27 +96,47 @@ func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoi
return targetSubdomainEndpoint
}
// Clearn URL Path (without the http:// part) replaces // in a URL to /
func (router *Router) clearnURL(targetUrlOPath string) string {
return strings.ReplaceAll(targetUrlOPath, "//", "/")
}
// Rewrite URL rewrite the prefix part of a virtual directory URL with /
func (router *Router) rewriteURL(rooturl string, requestURL string) string {
rewrittenURL := requestURL
rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/"))
if strings.Contains(rewrittenURL, "//") {
rewrittenURL = router.clearnURL(rewrittenURL)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
}
return rewrittenURL
}
// upstreamHostSwap check if this loopback to one of the proxy rule in the system. If yes, do a shortcut target swap
// this prevents unnecessary external DNS lookup and connection, return true if swapped and request is already handled
// by the loopback handler. Only continue if return is false
func (h *ProxyHandler) upstreamHostSwap(w http.ResponseWriter, r *http.Request, selectedUpstream *loadbalance.Upstream) bool {
upstreamHostname := selectedUpstream.OriginIpOrDomain
if strings.Contains(upstreamHostname, ":") {
upstreamHostname = strings.Split(upstreamHostname, ":")[0]
}
loopbackProxyEndpoint := h.Parent.GetProxyEndpointFromHostname(upstreamHostname)
if loopbackProxyEndpoint != nil {
//This is a loopback request. Swap the target to the loopback target
//h.Parent.Option.Logger.PrintAndLog("proxy", "Detected a loopback request to self. Swap the target to "+loopbackProxyEndpoint.RootOrMatchingDomain, nil)
if loopbackProxyEndpoint.IsEnabled() {
h.hostRequest(w, r, loopbackProxyEndpoint)
} else {
//Endpoint disabled, return 503
http.ServeFile(w, r, "./web/rperror.html")
h.Parent.logRequest(r, false, 521, "host-http", r.Host, upstreamHostname)
}
return true
}
return false
}
// Handle host request
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
reqHostname := r.Host
/* Load balancing */
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil {
@@ -125,6 +146,12 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
return
}
/* Upstream Host Swap (use to detect loopback to self) */
if h.upstreamHostSwap(w, r, selectedUpstream) {
//Request handled by the loopback handler
return
}
/* WebSocket automatic proxy */
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {

View File

@@ -49,7 +49,7 @@ type RouterOption struct {
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
MinTLSVersion uint16 //Minimum TLS version
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint

View File

@@ -13,6 +13,25 @@ import (
CIDR and IPv4 / v6 validations
*/
// Get the requester IP without trusting any proxy headers
func GetRequesterIPUntrusted(r *http.Request) string {
// If the request is from an untrusted IP, we should not trust the X-Real-IP and X-Forwarded-For headers
ip := r.RemoteAddr
// Trim away the port number
reqHost, _, err := net.SplitHostPort(ip)
if err == nil {
ip = reqHost
}
// Check if the IP is a valid IPv4 or IPv6 address
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return ""
}
return ip
}
// Get the requester IP, trust the X-Real-IP and X-Forwarded-For headers
func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {

View File

@@ -2,10 +2,13 @@ package netutils
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"unicode"
"github.com/likexian/whois"
"imuslab.com/zoraxy/mod/utils"
@@ -167,3 +170,53 @@ func CheckIfPortOccupied(portNumber int) bool {
listener.Close()
return false
}
// NormalizeDomain cleans and validates a domain string.
// - Trims spaces around the domain
// - Converts to lowercase
// - Removes trailing dot (FQDN canonicalization)
// - Checks that the domain conforms to standard rules:
// - Each label ≤ 63 characters
// - Only letters, digits, and hyphens
// - Labels do not start or end with a hyphen
// - Full domain ≤ 253 characters
//
// Returns an empty string if the domain is invalid.
func NormalizeDomain(d string) (string, error) {
d = strings.TrimSpace(d)
d = strings.ToLower(d)
d = strings.TrimSuffix(d, ".")
if len(d) == 0 {
return "", errors.New("domain is empty")
}
if len(d) > 253 {
return "", errors.New("domain exceeds 253 characters")
}
labels := strings.Split(d, ".")
for index, label := range labels {
if index == 0 {
if len(label) == 1 && label == "*" {
continue
}
}
if len(label) == 0 {
return "", errors.New("Domain '" + d + "' not valid: Empty label")
}
if len(label) > 63 {
return "", errors.New("Domain not valid: label exceeds 63 characters")
}
for i, r := range label {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' {
return "", errors.New("Domain '" + d + "' not valid: Invalid character '" + string(r) + "' in label")
}
if (i == 0 || i == len(label)-1) && r == '-' {
return "", errors.New("Domain '" + d + "' not valid: label '" + label + "' starts or ends with hyphen")
}
}
}
return d, nil
}

View File

@@ -47,19 +47,19 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
ProxyProtocolVersion, _ := utils.PostInt(r, "proxyProtocolVersion")
enableLogging, _ := utils.PostBool(r, "enableLogging")
//Create the target config
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
Name: name,
ListeningAddr: strings.TrimSpace(listenAddr),
ProxyAddr: strings.TrimSpace(proxyAddr),
Timeout: timeout,
UseTCP: useTCP,
UseUDP: useUDP,
UseProxyProtocol: useProxyProtocol,
EnableLogging: enableLogging,
Name: name,
ListeningAddr: strings.TrimSpace(listenAddr),
ProxyAddr: strings.TrimSpace(proxyAddr),
Timeout: timeout,
UseTCP: useTCP,
UseUDP: useUDP,
ProxyProtocolVersion: convertIntToProxyProtocolVersion(ProxyProtocolVersion),
EnableLogging: enableLogging,
})
js, _ := json.Marshal(newConfigUUID)
@@ -79,7 +79,7 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
proxyProtocolVersion, _ := utils.PostInt(r, "proxyProtocolVersion")
enableLogging, _ := utils.PostBool(r, "enableLogging")
newTimeoutStr, _ := utils.PostPara(r, "timeout")
@@ -94,15 +94,15 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
// Create a new ProxyRuleUpdateConfig with the extracted parameters
newConfig := &ProxyRuleUpdateConfig{
InstanceUUID: configUUID,
NewName: newName,
NewListeningAddr: listenAddr,
NewProxyAddr: proxyAddr,
UseTCP: useTCP,
UseUDP: useUDP,
UseProxyProtocol: useProxyProtocol,
EnableLogging: enableLogging,
NewTimeout: newTimeout,
InstanceUUID: configUUID,
NewName: newName,
NewListeningAddr: listenAddr,
NewProxyAddr: proxyAddr,
UseTCP: useTCP,
UseUDP: useUDP,
ProxyProtocolVersion: proxyProtocolVersion,
EnableLogging: enableLogging,
NewTimeout: newTimeout,
}
// Call the EditConfig method to modify the configuration

View File

@@ -15,50 +15,59 @@ import (
)
/*
TCP Proxy
Stream Proxy
Forward port from one port to another
Also accept active connection and passive
connection
*/
// ProxyProtocolVersion enum type
type ProxyProtocolVersion int
const (
ProxyProtocolDisabled ProxyProtocolVersion = 0
ProxyProtocolV1 ProxyProtocolVersion = 1
ProxyProtocolV2 ProxyProtocolVersion = 2
)
type ProxyRelayOptions struct {
Name string
ListeningAddr string
ProxyAddr string
Timeout int
UseTCP bool
UseUDP bool
UseProxyProtocol bool
EnableLogging bool
Name string
ListeningAddr string
ProxyAddr string
Timeout int
UseTCP bool
UseUDP bool
ProxyProtocolVersion ProxyProtocolVersion
EnableLogging bool
}
// ProxyRuleUpdateConfig is used to update the proxy rule config
type ProxyRuleUpdateConfig struct {
InstanceUUID string //The target instance UUID to update
NewName string //New name for the instance, leave empty for no change
NewListeningAddr string //New listening address, leave empty for no change
NewProxyAddr string //New proxy target address, leave empty for no change
UseTCP bool //Enable TCP proxy, default to false
UseUDP bool //Enable UDP proxy, default to false
UseProxyProtocol bool //Enable Proxy Protocol, default to false
EnableLogging bool //Enable Logging TCP/UDP Message, default to true
NewTimeout int //New timeout for the connection, leave -1 for no change
InstanceUUID string //The target instance UUID to update
NewName string //New name for the instance, leave empty for no change
NewListeningAddr string //New listening address, leave empty for no change
NewProxyAddr string //New proxy target address, leave empty for no change
UseTCP bool //Enable TCP proxy, default to false
UseUDP bool //Enable UDP proxy, default to false
ProxyProtocolVersion int //Enable Proxy Protocol v1/v2, default to disabled
EnableLogging bool //Enable Logging TCP/UDP Message, default to true
NewTimeout int //New timeout for the connection, leave -1 for no change
}
type ProxyRelayInstance struct {
/* Runtime Config */
UUID string //A UUIDv4 representing this config
Name string //Name of the config
Running bool //Status, read only
AutoStart bool //If the service suppose to started automatically
ListeningAddress string //Listening Address, usually 127.0.0.1:port
ProxyTargetAddr string //Proxy target address
UseTCP bool //Enable TCP proxy
UseUDP bool //Enable UDP proxy
UseProxyProtocol bool //Enable Proxy Protocol
EnableLogging bool //Enable logging for ProxyInstance
Timeout int //Timeout for connection in sec
UUID string //A UUIDv4 representing this config
Name string //Name of the config
Running bool //Status, read only
AutoStart bool //If the service suppose to started automatically
ListeningAddress string //Listening Address, usually 127.0.0.1:port
ProxyTargetAddr string //Proxy target address
UseTCP bool //Enable TCP proxy
UseUDP bool //Enable UDP proxy
ProxyProtocolVersion ProxyProtocolVersion //Proxy Protocol v1/v2
EnableLogging bool //Enable logging for ProxyInstance
Timeout int //Timeout for connection in sec
/* Internal */
tcpStopChan chan bool //Stop channel for TCP listener
@@ -178,7 +187,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
ProxyTargetAddr: config.ProxyAddr,
UseTCP: config.UseTCP,
UseUDP: config.UseUDP,
UseProxyProtocol: config.UseProxyProtocol,
ProxyProtocolVersion: config.ProxyProtocolVersion,
EnableLogging: config.EnableLogging,
Timeout: config.Timeout,
tcpStopChan: nil,
@@ -203,6 +212,30 @@ func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayInstance, error
return nil, errors.New("config not found")
}
// ConvertIntToProxyProtocolVersion converts an int to ProxyProtocolVersion type
func convertIntToProxyProtocolVersion(v int) ProxyProtocolVersion {
switch v {
case 1:
return ProxyProtocolV1
case 2:
return ProxyProtocolV2
default:
return ProxyProtocolDisabled
}
}
// convertProxyProtocolVersionToInt converts ProxyProtocolVersion type back to int
func convertProxyProtocolVersionToInt(v ProxyProtocolVersion) int {
switch v {
case ProxyProtocolV1:
return 1
case ProxyProtocolV2:
return 2
default:
return 0
}
}
// Edit the config based on config UUID, leave empty for unchange fields
func (m *Manager) EditConfig(newConfig *ProxyRuleUpdateConfig) error {
// Find the config with the specified UUID
@@ -224,7 +257,7 @@ func (m *Manager) EditConfig(newConfig *ProxyRuleUpdateConfig) error {
foundConfig.UseTCP = newConfig.UseTCP
foundConfig.UseUDP = newConfig.UseUDP
foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol
foundConfig.ProxyProtocolVersion = convertIntToProxyProtocolVersion(newConfig.ProxyProtocolVersion)
foundConfig.EnableLogging = newConfig.EnableLogging
if newConfig.NewTimeout != -1 {

View File

@@ -11,6 +11,8 @@ import (
"sync"
"sync/atomic"
"time"
proxyproto "github.com/pires/go-proxyproto"
)
func isValidIP(ip string) bool {
@@ -44,20 +46,22 @@ func (c *ProxyRelayInstance) connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.W
wg.Done()
}
func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error {
func WriteProxyProtocolHeader(dst net.Conn, src net.Conn, version ProxyProtocolVersion) error {
clientAddr, ok1 := src.RemoteAddr().(*net.TCPAddr)
proxyAddr, ok2 := src.LocalAddr().(*net.TCPAddr)
if !ok1 || !ok2 {
return errors.New("invalid TCP address for proxy protocol")
}
header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n",
clientAddr.IP.String(),
proxyAddr.IP.String(),
clientAddr.Port,
proxyAddr.Port)
header := proxyproto.Header{
Version: byte(convertProxyProtocolVersionToInt(version)),
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: clientAddr,
DestinationAddr: proxyAddr,
}
_, err := dst.Write([]byte(header))
_, err := header.WriteTo(dst)
return err
}
@@ -161,9 +165,9 @@ func (c *ProxyRelayInstance) Port2host(allowPort string, targetAddress string, s
}
c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil)
if c.UseProxyProtocol {
if c.ProxyProtocolVersion != ProxyProtocolDisabled {
c.LogMsg("[+] write proxy protocol header to target address ["+targetAddress+"]", nil)
err = writeProxyProtocolHeaderV1(target, conn)
err = WriteProxyProtocolHeader(target, conn, c.ProxyProtocolVersion)
if err != nil {
c.LogMsg("[x] Write proxy protocol header failed: "+err.Error(), nil)
target.Close()

View File

@@ -1,11 +1,14 @@
package streamproxy
import (
"bytes"
"errors"
"log"
"net"
"strings"
"time"
proxyproto "github.com/pires/go-proxyproto"
)
/*
@@ -82,6 +85,24 @@ func (c *ProxyRelayInstance) CloseAllUDPConnections() {
})
}
// Write Proxy Protocol v2 header to UDP connection
func WriteProxyProtocolHeaderUDP(conn *net.UDPConn, srcAddr, dstAddr *net.UDPAddr) error {
header := proxyproto.Header{
Version: byte(convertProxyProtocolVersionToInt(ProxyProtocolV2)),
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.UDPv4,
SourceAddr: srcAddr,
DestinationAddr: dstAddr,
}
var buf bytes.Buffer
_, err := header.WriteTo(&buf)
if err != nil {
return err
}
_, err = conn.Write(buf.Bytes())
return err
}
func (c *ProxyRelayInstance) ForwardUDP(address1, address2 string, stopChan chan bool) error {
//By default the incoming listen Address is int
//We need to add the loopback address into it
@@ -142,6 +163,10 @@ func (c *ProxyRelayInstance) ForwardUDP(address1, address2 string, stopChan chan
// Fire up routine to manage new connection
go c.RunUDPConnectionRelay(conn, lisener)
// Send Proxy Protocol header if enabled
if c.ProxyProtocolVersion == ProxyProtocolV2 {
_ = WriteProxyProtocolHeaderUDP(conn.ServerConn, cliaddr, targetAddr)
}
} else {
c.LogMsg("[UDP] Found connection for client "+saddr, nil)
conn = rawConn.(*udpClientServerConn)

View File

@@ -75,6 +75,50 @@ func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) {
}
}
// Set the selected certificate as the default / fallback certificate
func (m *Manager) SetCertAsDefault(w http.ResponseWriter, r *http.Request) {
certname, err := utils.PostPara(r, "certname")
if err != nil {
utils.SendErrorResponse(w, "invalid certname given")
return
}
//Check if the previous default cert exists. If yes, get its hostname from cert contents
defaultPubKey := filepath.Join(m.CertStore, "default.key")
defaultPriKey := filepath.Join(m.CertStore, "default.pem")
if utils.FileExists(defaultPubKey) && utils.FileExists(defaultPriKey) {
//Move the existing default cert to its original name
certBytes, err := os.ReadFile(defaultPriKey)
if err == nil {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
os.Rename(defaultPubKey, filepath.Join(m.CertStore, domainToFilename(cert.Subject.CommonName, "key")))
os.Rename(defaultPriKey, filepath.Join(m.CertStore, domainToFilename(cert.Subject.CommonName, "pem")))
}
}
}
}
//Check if the cert exists
certname = filepath.Base(certname) //prevent path escape
pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key")
priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem")
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
os.Rename(pubKey, filepath.Join(m.CertStore, "default.key"))
os.Rename(priKey, filepath.Join(m.CertStore, "default.pem"))
utils.SendOK(w)
//Update cert list
m.UpdateLoadedCertList()
} else {
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
return
}
}
// Handle upload of the certificate
func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST
@@ -124,6 +168,13 @@ func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
defer file.Close()
// create file in upload directory
// Read file contents for validation
fileBytes, err := io.ReadAll(file)
if err != nil {
http.Error(w, "Failed to read file", http.StatusBadRequest)
return
}
os.MkdirAll(m.CertStore, 0775)
f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename))
if err != nil {
@@ -138,6 +189,11 @@ func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
_, err = f.Write(fileBytes)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
//Update cert list
m.UpdateLoadedCertList()
@@ -215,11 +271,13 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request)
showDate, _ := utils.GetBool(r, "date")
if showDate {
type CertInfo struct {
Domain string
Domain string // Domain name of the certificate
Filename string // Filename that stores the certificate
LastModifiedDate string
ExpireDate string
RemainingDays int
UseDNS bool
UseDNS bool // Whether this cert is obtained via DNS challenge
IsFallback bool // Whether this cert is the fallback/default cert
}
results := []*CertInfo{}
@@ -248,7 +306,7 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
duration := cert.NotAfter.Sub(time.Now())
duration := time.Until(cert.NotAfter)
// Convert the duration to days
expiredIn = int(duration.Hours() / 24)
@@ -262,12 +320,23 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request)
useDNSValidation = certInfo.UseDNS
}
certDomain := ""
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certDomain = cert.Subject.CommonName
}
}
thisCertInfo := CertInfo{
Domain: filename,
Domain: certDomain,
Filename: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
UseDNS: useDNSValidation,
IsFallback: (filename == "default"), //TODO: figure out a better implementation
}
results = append(results, &thisCertInfo)
@@ -350,3 +419,25 @@ func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Requ
}
utils.SendOK(w)
}
// Extract the common name from a PEM encoded certificate
func (m *Manager) HandleGetCertCommonName(w http.ResponseWriter, r *http.Request) {
certContents, err := utils.PostPara(r, "cert")
if err != nil {
utils.SendErrorResponse(w, "Certificate content not provided")
return
}
block, _ := pem.Decode([]byte(certContents))
if block == nil {
utils.SendErrorResponse(w, "Failed to decode PEM block")
return
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
utils.SendErrorResponse(w, "Failed to parse certificate: "+err.Error())
return
}
js, _ := json.Marshal(cert.Subject.CommonName)
utils.SendJSONResponse(w, string(js))
}

View File

@@ -29,21 +29,6 @@ func getCertPairs(certFiles []string) []string {
return result
}
// Get the cloest subdomain certificate from a list of domains
func matchClosestDomainCertificate(subdomain string, domains []string) string {
var matchingDomain string = ""
maxLength := 0
for _, domain := range domains {
if strings.HasSuffix(subdomain, "."+domain) && len(domain) > maxLength {
matchingDomain = domain
maxLength = len(domain)
}
}
return matchingDomain
}
// Convert a domain name to a filename format
func domainToFilename(domain string, ext string) string {
// Replace wildcard '*' with '_'
@@ -52,6 +37,10 @@ func domainToFilename(domain string, ext string) string {
domain = "_" + strings.TrimPrefix(domain, "*")
}
if strings.HasPrefix(".", ext) {
ext = strings.TrimPrefix(ext, ".")
}
// Add .pem extension
ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot
return domain + "." + ext

View File

@@ -211,7 +211,6 @@ func getWebsiteStatus(url string) (int, error) {
}
resp, err := client.Do(req)
//resp, err := client.Get(url)
if err != nil {
//Try replace the http with https and vise versa
rewriteURL := ""

View File

@@ -199,4 +199,4 @@ func ValidateListeningAddress(address string) bool {
}
return true
}
}

View File

@@ -58,13 +58,9 @@ func ReverseProxyInit() {
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
}
forceLatestTLSVersion := false
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
if forceLatestTLSVersion {
SystemWideLogger.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
} else {
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
}
minTLSVersion := "1.2" // default
sysdb.Read("settings", "minTLSVersion", &minTLSVersion)
SystemWideLogger.Println("Minimum TLS version set to v" + minTLSVersion)
developmentMode := false
sysdb.Read("settings", "devMode", &developmentMode)
@@ -106,7 +102,7 @@ func ReverseProxyInit() {
HostVersion: SYSTEM_VERSION,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
MinTLSVersion: minTlsVersionStringToUint16(minTLSVersion),
NoCache: developmentMode,
ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect,
@@ -125,6 +121,7 @@ func ReverseProxyInit() {
DevelopmentMode: *development_build,
Logger: SystemWideLogger,
})
if err != nil {
SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err)
return
@@ -238,6 +235,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
if bypassGlobalTLS == "" {
bypassGlobalTLS = "false"
}
// Enable uptime monitor?
enableUtm, err := utils.PostBool(r, "enableUtm")
if err != nil {
enableUtm = true
}
useBypassGlobalTLS := bypassGlobalTLS == "true"
@@ -410,7 +414,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
RequireRateLimit: requireRateLimit,
RateLimit: int64(proxyRateLimit),
Tags: tags,
Tags: tags,
DisableUptimeMonitor: !enableUtm,
}
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)

View File

@@ -7,7 +7,12 @@
.valid.certdate{
color: #31c071;
}
#certifiedDomainList .ui.basic.button{
margin-top: 0.1em;
}
</style>
<script src="script/jsrsasign-all-min.js"></script>
<div class="standardContainer">
<div class="ui basic segment">
<h2>TLS / SSL Certificates</h2>
@@ -21,7 +26,7 @@
<div class="three fields">
<div class="field">
<label>Server Name (Domain)</label>
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
<input type="text" id="certdomain" placeholder="">
<small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
</div>
<div class="field">
@@ -59,23 +64,25 @@
</div>
</div>
<p>Current list of loaded certificates</p>
<div tourstep="certTable">
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<div tourstep="certTable" style="width: 100%; overflow-x: auto; padding-bottom: 1em;">
<div style="min-width: 960px; max-width: 100%; ">
<table class="ui unstackable basic celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th>DNS Challenge</th>
<th class="no-sort">Renew</th>
<th class="no-sort">Remove</th>
<tr>
<th>Domain</th>
<th>Filename</th>
<th>Last Update</th>
<th>Expire At</th>
<th>Fallback</th>
<th>DNS Challenge</th>
<th class="no-sort">Actions</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
</div>
<br>
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui divider"></div>
@@ -103,6 +110,8 @@
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
</div>
<div class="ui yellow message"><i class="exclamation triangle icon"></i> We will be removing the fallback certificate section soon. <br>
Please use "<i class="ui blue home icon"></i>Set Fallback" button in the certificate list above to set the fallback certificate.</div>
</div>
<div class="ui divider"></div>
<div tourstep="acmeSettings">
@@ -150,6 +159,58 @@
$("#defaultCA").dropdown();
function getPossibleCommonNameFromSelectedCertificate(){
const fileInput = document.getElementById('pubkeySelector');
const file = fileInput.files[0];
if (!file) {
msgbox("No certificate file selected", false, 4000);
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const certContent = e.target.result;
$.cjax({
url: '/api/cert/getCommonName',
method: 'POST',
data: { cert: certContent },
success: function(data) {
if (data.error !== undefined) {
//Ignore error
$("#certdomain").attr("placeholder", "");
} else if (data) {
if (typeof data === "string" && data.startsWith("*.")) {
data = data.substring(2);
}
$("#certdomain").attr("placeholder", data);
}
},
error: function(xhr) {
//Ignore error
}
});
};
reader.readAsText(file);
}
function setSelectedCertAsFallbackCertificate(certDomain){
$.cjax({
url: '/api/cert/setDefault',
method: 'POST',
data: { certname: certDomain },
success: function(data) {
if (data.error !== undefined) {
msgbox(data.error, false, 5000);
} else {
msgbox('Fallback certificate set successfully!');
initManagedDomainCertificateList();
initDefaultKeypairCheck();
}
},
error: function(xhr) {
msgbox('Failed to set fallback certificate', false, 5000);
}
});
}
//Renew certificate by button press
function renewCertificate(domain, dns, btn=undefined){
@@ -378,17 +439,22 @@
});
data.forEach(entry => {
let isExpired = entry.RemainingDays <= 0;
let entryDomainRenewKey = entry.Domain;
let entryDomainRenewKey = entry.Filename;
if (entryDomainRenewKey.includes("_.")){
entryDomainRenewKey = entryDomainRenewKey.replace("_.","*.");
}
$("#certifiedDomainList").append(`<tr>
<td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Domain}');">${entry.Domain}</a></td>
<td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Filename}');">${entry.Domain}</a></td>
<td>${entry.Filename}</td>
<td>${entry.LastModifiedDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td>${entry.IsFallback?"<i class='green check icon'></i>":""}</td>
<td><i class="${entry.UseDNS?"green check": "red times"} icon"></i></td>
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entryDomainRenewKey}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button></td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
<td>
<button title="Set as Default / Fallback Certificate" class="ui mini basic button ${(entry.IsFallback?"disabled":"")} " onclick="setSelectedCertAsFallbackCertificate('${entry.Filename}');"><i class="ui blue home icon"></i> Set Fallback</button>
<button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entryDomainRenewKey}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button>
<button title="Delete key-pair" class="ui mini basic icon red button" onclick="deleteCertificate('${entry.Filename}');"><i class="ui red trash icon"></i></button>
</td>
</tr>`);
});
@@ -413,7 +479,7 @@
document.getElementById('pubkeySelector').value = '';
document.getElementById('prikeySelector').value = '';
document.getElementById('certdomain').value = '';
$("#certdomain").attr("placeholder", "");
uploadPendingPublicKey = undefined;
uploadPendingPrivateKey = undefined;
@@ -439,8 +505,17 @@
function handleDomainKeysUpload(callback=undefined){
let domain = $("#certdomain").val();
if (domain.trim() == ""){
msgbox("Missing domain", false, 5000);
return;
//Check if placeholder has value
if ($("#certdomain").attr("placeholder").trim() != ""){
domain = $("#certdomain").attr("placeholder").trim();
}else{
domain = undefined;
}
if (domain == undefined || domain.trim() == "") {
msgbox("Missing domain", false, 5000);
return;
}
}
if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
const publicKeyForm = new FormData();
@@ -493,6 +568,7 @@
const file = event.target.files[0];
if (ktype == "pub"){
uploadPendingPublicKey = file;
getPossibleCommonNameFromSelectedCertificate();
}else if (ktype == "pri"){
uploadPendingPrivateKey = file;
}

View File

@@ -62,6 +62,13 @@
<input type="checkbox" id="useStickySessionLB">
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="enableUtm" checked>
<label>Enable uptime monitor<br><small>Automatically check upstream status and switch to another if offline</small>
</label>
</div>
</div>
<div class="field">
<label>Tags</label>
@@ -168,22 +175,78 @@
</div>
<div class="six wide column">
<div class="ui basic segment rulesInstructions">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
Example of domain matching keyword:<br>
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
Example of subdomain matching keyword:<br>
<code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
Example of wildcard matching keyword:<br>
<code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
<div class="ui list">
<div class="item"><code>www.aroz.org</code></div>
<div class="item"><code>foo.bar.aroz.org</code></div>
<div class="ui fluid styled accordion" id="matchingKeywordExamplesAccordion" style="background-color: transparent !important;">
<div class="title active" style="color: white;">
<i class="dropdown icon"></i>
Matching Keyword Examples
</div>
<div class="content active">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
Example of domain matching keyword:<br>
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
Example of subdomain matching keyword:<br>
<code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
Example of wildcard matching keyword:<br>
<code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
<div class="ui list">
<div class="item"><code>www.aroz.org</code></div>
<div class="item"><code>foo.bar.aroz.org</code></div>
</div>
<br>
</div>
<div class="title" style="color: white;">
<i class="dropdown icon"></i>
Remote Target Require TLS
</div>
<div class="content">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui green lock icon"></i> Upstream TLS Requirement</span><br>
<p>
When you enable <b>Proxy Target require TLS Connection</b>, it means the <b>upstream server</b> (the target you are proxying to) requires a secure (HTTPS) connection.<br>
<b>This does not affect whether clients connect to this proxy endpoint using HTTP or HTTPS.</b>
</p>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Example</span><br>
<code>Matching Keyword: mydomain.com<br>
Target: example.com:443 (TLS enabled)</code><br>
<ul>
<li>Client connects to <b>mydomain.com</b> (HTTP or HTTPS, depending on your proxy setup)</li>
<li>Proxy forwards requests to <b>example.com:443</b> using <b>HTTPS</b></li>
</ul>
<small>
Use this option if your upstream server only accepts secure connections.<br>
If your upstream uses a self-signed certificate, check the <b>Ignore TLS/SSL Verification Error</b> option in Advance Settings.
</small>
</div>
<div class="title" style="color: white;">
<i class="dropdown icon"></i>
What is Sticky Session?
</div>
<div class="content">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui green sync icon"></i> Sticky Session (Session Affinity)</span><br>
<p>
Sticky session ensures that requests from the same client are always forwarded to the same upstream server. This is useful for applications that store session data locally and require the client to consistently connect to the same backend.<br>
</p>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> How to Add Multiple Upstreams</span><br>
<ul>
<li>Go to <b>HTTP Proxy</b> in the sidebar.</li>
<li>Click <b>Edit</b> on your proxy rule.</li>
<li>Use the <b>Upstreams</b> section to add more upstream endpoints for load balancing.</li>
</ul>
<small>
Sticky session will only work if you have more than one upstream endpoint configured.
</small>
</div>
</div>
<br>
<script>
$('#matchingKeywordExamplesAccordion').accordion();
</script>
</div>
</div>
</div>
@@ -204,6 +267,7 @@
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
let tags = $("#proxyTags").val().trim();
let enableUtm = $("#enableUtm")[0].checked;
if (rootname.trim() == ""){
$("#rootname").parent().addClass("error");
@@ -238,6 +302,7 @@
access: accessRuleToUse,
stickysess: useStickySessionLB,
tags: tags,
enableUtm: enableUtm,
},
success: function(data){
if (data.error != undefined){

View File

@@ -110,10 +110,19 @@
Advance Settings
</div>
<div class="content">
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
<div id="tlsMinVer" class="ui notloopbackOnly tlsEnabledOnly" style="margin-top: 0.6em;">
<div style="display: flex; align-items: center; gap: 1em;">
<select id="tlsVersionSelect" class="ui dropdown">
<option value="1.0">TLS v1.0</option>
<option value="1.1">TLS v1.1</option>
<option value="1.2">TLS v1.2</option>
<option value="1.3">TLS v1.3</option>
</select>
<p for="tlsVersionSelect" style="margin: 0;">
Minimum TLS Version<br>
<small>(Enhance security, but may not be compatible with legacy browsers)</small>
</p>
</div>
</div>
<br>
<div id="developmentMode" class="ui toggle checkbox" style="margin-top: 0.6em;">
@@ -466,31 +475,28 @@
initHTTPtoHTTPSRedirectSetting();
function initTlsVersionSetting(){
$.get("/api/cert/tlsRequireLatest", function(data){
if (data == true){
$("#tlsMinVer").checkbox("set checked");
}else{
$("#tlsMinVer").checkbox("set unchecked");
$.get("/api/cert/tlsMinVersion", function(data){
// Set dropdown value
if (data && typeof data === "string") {
$("#tlsVersionSelect").val(data);
}
//Bind events to the checkbox
$("#tlsMinVer").find("input").on("change", function(){
let thisValue = $("#tlsMinVer").checkbox("is checked");
// Bind change event
$("#tlsVersionSelect").off("change").on("change", function(){
var selectedVersion = $(this).val();
$.cjax({
url: "/api/cert/tlsRequireLatest",
data: {"set": thisValue},
url: "/api/cert/tlsMinVersion",
method: "POST",
data: {set: selectedVersion},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("TLS Version Setting Updated");
msgbox("TLS minimum version updated");
}
}
})
});
});
});
}
initTlsVersionSetting();

View File

@@ -74,14 +74,6 @@
<small>Forward UDP request on this listening socket</small></label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useProxyProtocol" class="hidden">
<label>Enable Proxy Protocol V1<br>
<small>Enable TCP Proxy Protocol header V1</small>
</label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="enableLogging" class="hidden">
@@ -90,6 +82,18 @@
</label>
</div>
</div>
<div class="field">
<label>Proxy Protocol</label>
<select name="proxyProtocolVersion" class="ui dropdown">
<option value="0">Disabled</option>
<option value="1">Proxy Protocol V1</option>
<option value="2">Proxy Protocol V2</option>
</select>
<small>Select Proxy Protocol v1 / v2 to use (if any)</small>
<div id="proxyProtocolUdpWarning" style="display:none; color:#bd7100; margin-top:0.5em;">
<i class="exclamation triangle icon"></i> Proxy Protocol V1 is not supported for UDP. The proxy protocol header will not be included in UDP packets if selected.
</div>
</div>
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
@@ -99,6 +103,21 @@
<script>
let editingStreamProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
// Show/hide warning for Proxy Protocol V1 + UDP
function updateProxyProtocolUdpWarning() {
const proxyProtocolVersion = $("#streamProxyForm select[name=proxyProtocolVersion]").val();
const useUDP = $("#streamProxyForm input[name=useUDP]")[0].checked;
if (proxyProtocolVersion === "1" && useUDP) {
$("#proxyProtocolUdpWarning").show();
} else {
$("#proxyProtocolUdpWarning").hide();
}
}
$("#streamProxyForm select[name=proxyProtocolVersion]").on("change", updateProxyProtocolUdpWarning);
$("#streamProxyForm input[name=useUDP]").on("change", updateProxyProtocolUdpWarning);
$(document).ready(updateProxyProtocolUdpWarning);
$("#streamProxyForm .dropdown").dropdown();
$('#streamProxyForm').on('submit', function(event) {
event.preventDefault();
@@ -138,7 +157,7 @@
function clearStreamProxyAddEditForm(){
$('#streamProxyForm').find('input:not([type=checkbox]), select').val('');
$('#streamProxyForm select').dropdown('clear');
$('#streamProxyForm select[name=proxyProtocolVersion]').dropdown('set selected', '0');
$("#streamProxyForm input[name=timeout]").val(10);
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
}
@@ -212,8 +231,10 @@
modeText.push("UDP")
}
if (config.UseProxyProtocol){
modeText.push("ProxyProtocol V1")
if (config.ProxyProtocolVersion === 1) {
modeText.push("ProxyProtocol V1");
} else if (config.ProxyProtocolVersion === 2) {
modeText.push("ProxyProtocol V2");
}
modeText = modeText.join(" & ")
@@ -277,13 +298,8 @@
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "UseProxyProtocol"){
let checkboxEle = $("#streamProxyForm input[name=useProxyProtocol]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
}else if (key == "ProxyProtocolVersion") {
$("#streamProxyForm select[name=proxyProtocolVersion]").dropdown("set selected", value);
return;
}else if (key == "EnableLogging"){
let checkboxEle = $("#streamProxyForm input[name=enableLogging]").parent();
@@ -342,7 +358,7 @@
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
useProxyProtocol: $("#streamProxyForm input[name=useProxyProtocol]")[0].checked ,
proxyProtocolVersion: parseInt($("#streamProxyForm select[name=proxyProtocolVersion]").val(), 10),
enableLogging: $("#streamProxyForm input[name=enableLogging]")[0].checked ,
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
},