diff --git a/changelog.md b/changelog.md index a59d1559..ab3cedb2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ ## Version 0.22 - Support for mounting network disk + - Fix issue with Constellation sometime not connecting right away on new network + +## Version 0.21.7 + - Fix issue with Constellation routing HTTP tunnels to the wrong protocol ## Version 0.21.6 - Prevent potential double counting of server by wrong timing diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx index 4c54ed86..0a78c297 100644 --- a/client/src/pages/config/users/proxyman.jsx +++ b/client/src/pages/config/users/proxyman.jsx @@ -117,7 +117,8 @@ const ProxyManagement = () => { API.constellation.tunnels().then((res) => { setTunnels((res.data || []).map(r => { let route = r.Route; - route._from = r.From; + route._from = (r.Targets || []).map(t => t.deviceName); + route._targets = r.Targets || []; return route; })); }); @@ -276,7 +277,10 @@ const ProxyManagement = () => { :
}, { title: t('mgmt.config.proxy.originTitle'), screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => }, - { title: t('global.target'), screenMin: 'md', search: (r) => r.Target, field: (r) => <> }, + { title: t('global.target'), screenMin: 'md', search: (r) => r._IsTunnel ? (r._targets || []).map(t => t.targetURL).join(' ') : r.Target, field: (r) => r._IsTunnel && r._targets && r._targets.length > 0 + ? <> {r._targets.map((t, i) => )} + : <> + }, { title: t('global.securityTitle'), screenMin: 'lg', field: (r) => , style: {minWidth: '70px'} }, { title: '', clickable:true, field: (r, k) => r._IsTunnel ? diff --git a/client/src/pages/constellation/vpn.jsx b/client/src/pages/constellation/vpn.jsx index a5515b7e..d242889f 100644 --- a/client/src/pages/constellation/vpn.jsx +++ b/client/src/pages/constellation/vpn.jsx @@ -438,12 +438,16 @@ export const ConstellationVPN = ({ freeVersion }) => { } />} - {!freeVersion && !config.ConstellationConfig.ThisDeviceName && + {!freeVersion && !config.ConstellationConfig.ThisDeviceName && <> + + {t('mgmt.constellation.setup.lighthouseInfo')} + + {t('mgmt.constellation.setup.isLighthouse.label')} - } />} + } />} {constellationEnabled && formik.values.IsLighthouse && <> {t('mgmt.constellation.setup.relayRequests.label')} diff --git a/client/src/utils/locales/en/translation.json b/client/src/utils/locales/en/translation.json index e3fedc74..4a0683eb 100644 --- a/client/src/utils/locales/en/translation.json +++ b/client/src/utils/locales/en/translation.json @@ -272,6 +272,7 @@ "mgmt.constellation.resetLabel": "Reset Network", "mgmt.constellation.resetText": "This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone.", "mgmt.constellation.restartButton": "Restart VPN Service", + "mgmt.constellation.setup.lighthouseInfo": "This is important! If you are behind CGNAT or do not wish to open any port on your server, you have to uncheck this!", "mgmt.constellation.setup.createConstellation": "Create Constellation", "mgmt.constellation.setup.deviceName.validationError": "Device must only contain lowercase letters, numbers and hyphens", "mgmt.constellation.setup.addDeviceSuccess": "Device added successfully! Download scan the QR Code from the Cosmos app or download the relevant files to your device along side the config and network certificate to connect:", diff --git a/package.json b/package.json index 9fac7c6f..319561c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.21.7", + "version": "0.22.0-unstable01", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/authorizationserver/oauth2.go b/src/authorizationserver/oauth2.go index 7b3a8e34..af591e1f 100644 --- a/src/authorizationserver/oauth2.go +++ b/src/authorizationserver/oauth2.go @@ -80,7 +80,7 @@ func Init() { oauth2 = compose.ComposeAllEnabled(foconfig, store, AuthPrivateKey) routes := config.HTTPConfig.ProxyConfig.Routes - routes = append(routes, config.ConstellationConfig.Tunnels...) + routes = append(routes, utils.GetConstellationTunnelRoutes()...) // Add proxy route clients for _, route := range routes { diff --git a/src/constellation/NATS.go b/src/constellation/NATS.go index 9ad5c7b5..1382c03f 100644 --- a/src/constellation/NATS.go +++ b/src/constellation/NATS.go @@ -226,6 +226,11 @@ func StartNATS() { utils.Error("[NATS] Failed to get cluster IPs", err) } + utils.Debug("[NATS] Cluster IPs: ") + for _, cip := range cips { + utils.Debug("[NATS] Cluster IP: " + cip.String()) + } + // if only one, abort if (len(cips) == 0) { utils.Warn("[NATS] No cluster IPs found, NATS server will not start") @@ -296,6 +301,8 @@ func StartNATS() { utils.Debug("[NATS] Retrying to start NATS server") } + NATSStarted = true + if err != nil { utils.MajorError("[NATS] Error starting NATS server", err) } else { @@ -312,6 +319,7 @@ func StopNATS() { ns.WaitForShutdown() ns = nil } + NATSStarted = false } // sync lock - RWMutex allows multiple readers, single writer @@ -320,6 +328,11 @@ var NATSClientTopic = "" var nc *nats.Conn var js nats.JetStreamContext func InitNATSClient() error { + if !NATSStarted { + utils.Warn("[NATS] NATS server not started, cannot initialize client") + return errors.New("NATS server not started, cannot initialize client") + } + clientConfigLock.Lock() defer clientConfigLock.Unlock() diff --git a/src/constellation/api_devices_block.go b/src/constellation/api_devices_block.go index 972799df..b90930b6 100644 --- a/src/constellation/api_devices_block.go +++ b/src/constellation/api_devices_block.go @@ -92,7 +92,7 @@ func DeviceBlock(w http.ResponseWriter, req *http.Request) { } go func() { - SendNewDBSyncMessage() + go SendNewDBSyncMessage() time.Sleep(2 * time.Second) RestartNebula() }() diff --git a/src/constellation/api_devices_create.go b/src/constellation/api_devices_create.go index d15453c3..52f12ea3 100644 --- a/src/constellation/api_devices_create.go +++ b/src/constellation/api_devices_create.go @@ -168,7 +168,7 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) { }) go func() { - SendNewDBSyncMessage() + go SendNewDBSyncMessage() time.Sleep(2 * time.Second) RestartNebula() }() diff --git a/src/constellation/api_devices_edit.go b/src/constellation/api_devices_edit.go index 7ec34e97..750ab307 100644 --- a/src/constellation/api_devices_edit.go +++ b/src/constellation/api_devices_edit.go @@ -89,7 +89,7 @@ func DeviceEdit_API(w http.ResponseWriter, req *http.Request) { }) go func() { - SendNewDBSyncMessage() + go SendNewDBSyncMessage() time.Sleep(2 * time.Second) RestartNebula() }() diff --git a/src/constellation/index.go b/src/constellation/index.go index e41a91c8..ccfd7b2e 100644 --- a/src/constellation/index.go +++ b/src/constellation/index.go @@ -7,6 +7,7 @@ import ( var NebulaStarted = false var NebulaHasStarted = false +var NATSStarted = false var CachedDeviceNames = map[string]string{} var CachedDevices = map[string]utils.ConstellationDevice{} var needToSyncCA = false @@ -15,6 +16,15 @@ func resyncConstellationNodes() { SendNewDBSyncMessage() } +func getConstellationTunnelRoutes() []utils.ProxyRouteConfig { + tunnels := GetLocalTunnelCache() + routes := make([]utils.ProxyRouteConfig, len(tunnels)) + for i, t := range tunnels { + routes[i] = t.Route + } + return routes +} + func GetDefaultHostnames() []string { hostnames, _ := utils.ListIps(true) httpHostname := utils.GetMainConfig().HTTPConfig.Hostname @@ -71,8 +81,10 @@ func Init() { utils.IsConstellationIP = IsConstellationIP utils.ResyncConstellationNodes = resyncConstellationNodes + utils.GetConstellationTunnelRoutes = getConstellationTunnelRoutes NebulaStarted = false + NATSStarted = false var err error diff --git a/src/constellation/nebula.go b/src/constellation/nebula.go index 54fc3f08..4bf1d153 100644 --- a/src/constellation/nebula.go +++ b/src/constellation/nebula.go @@ -81,7 +81,10 @@ func startNebula() error { UpdateFirewallBlockedClients() AdjustDNS(logBuffer) - ExportLighthouseFromDB() + e := ExportLighthouseFromDB() + if e != nil { + utils.Error("Failed to export lighthouse config from DB", e) + } // Handle existing PID file pidFile := utils.CONFIGFOLDER + "nebula.pid" @@ -216,6 +219,7 @@ func monitorNebulaProcess(proc *exec.Cmd) { defer ProcessMux.Unlock() process = nil NebulaStarted = false + NATSStarted = false } func stop() { @@ -244,6 +248,7 @@ func stop() { } NebulaStarted = false + NATSStarted = false cachedCurrentDevice = nil CachedDevices = map[string]utils.ConstellationDevice{} CachedDeviceNames = map[string]string{} @@ -293,6 +298,7 @@ func ResetNebula() error { config.ConstellationConfig.DNSDisabled = false config.ConstellationConfig.FirewallBlockedClients = []string{} config.ConstellationConfig.ThisDeviceName = "" + config.ConstellationConfig.IPRange = "" config.ConstellationConfig.ConstellationHostname = strings.Join(GetDefaultHostnames(), ",") if config.Licence == "" { diff --git a/src/constellation/tunnels.go b/src/constellation/tunnels.go index 11bb095f..0c3ab1ed 100644 --- a/src/constellation/tunnels.go +++ b/src/constellation/tunnels.go @@ -54,7 +54,12 @@ func GetAllTunneledRoutes() []utils.ProxyRouteConfig { protocol = "http://" } - route.Target = protocol + thisIp + ":" + port + route.Target = protocol + thisIp + + if port != "" { + route.Target += ":" + port + } + route.Mode = "PROXY" if route.TunneledHost != "" { @@ -126,7 +131,7 @@ func ClientHeartbeatInit() { utils.Debug("[NATS] Key-Value store 'constellation-nodes' ready") - go UpdateLocalTunnelCache(); + UpdateLocalTunnelCache() heartbeatStopChan = make(chan struct{}) heartbeatTicker = time.NewTicker(2 * time.Second) @@ -135,6 +140,55 @@ func ClientHeartbeatInit() { stopChan := heartbeatStopChan ticker := heartbeatTicker + // Watch KV for changes and refresh tunnel cache + go func() { + err := ClientConnectToJS() + if err != nil { + utils.Warn("[NATS] Error connecting to JetStream for KV watcher: " + err.Error()) + return + } + + clientConfigLock.RLock() + if js == nil { + clientConfigLock.RUnlock() + utils.Warn("[NATS] JetStream context is nil for KV watcher") + return + } + kv, err := js.KeyValue("constellation-nodes") + clientConfigLock.RUnlock() + if err != nil { + utils.Warn("[NATS] Error getting KV store for watcher: " + err.Error()) + return + } + + watcher, err := kv.WatchAll() + if err != nil { + utils.Warn("[NATS] Error creating KV watcher: " + err.Error()) + return + } + defer watcher.Stop() + + utils.Log("[NATS] KV watcher started for tunnel cache updates") + + for { + select { + case <-stopChan: + utils.Log("[NATS] KV watcher stopped") + return + case entry, ok := <-watcher.Updates(): + if !ok { + utils.Warn("[NATS] KV watcher channel closed") + return + } + if entry == nil { + // nil marks end of initial values + continue + } + GetLocalTunnelCache() + } + } + }() + go func() { for { select { @@ -288,6 +342,13 @@ func UpdateLocalTunnelCache() { tunnels := make([]utils.ConstellationTunnel, 0, len(byName)) for _, t := range byName { + // Ensure local node is always first in targets + for i, target := range t.Targets { + if target.DeviceName == currentDeviceName && i != 0 { + t.Targets[0], t.Targets[i] = t.Targets[i], t.Targets[0] + break + } + } tunnels = append(tunnels, *t) } diff --git a/src/proxy/buildFromConfig.go b/src/proxy/buildFromConfig.go index fc3ef547..658a9893 100644 --- a/src/proxy/buildFromConfig.go +++ b/src/proxy/buildFromConfig.go @@ -10,7 +10,6 @@ import ( ) func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router { - router.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) diff --git a/src/proxy/routeTo.go b/src/proxy/routeTo.go index c0b6f097..330ef067 100644 --- a/src/proxy/routeTo.go +++ b/src/proxy/routeTo.go @@ -241,16 +241,29 @@ func TunnelRouteTo(tunnel utils.ConstellationTunnel, lb *TunnelLoadBalancer) htt handler http.Handler } + // For self-tunnel, use the original route handler to avoid proxy loop + currentDeviceName, _ := constellation.GetCurrentDeviceName() + proxies := make([]targetProxy, 0, len(tunnel.Targets)) for _, t := range tunnel.Targets { - route := tunnel.Route - route.Target = t.TargetURL - proxy, err := NewProxy(t.TargetURL, route.AcceptInsecureHTTPSTarget, route.DisableHeaderHardening, route) - if err != nil { - utils.Error("Create Tunnel Route for "+t.DeviceName, err) - continue + if t.DeviceName == currentDeviceName { + // Find the original config route and use RouteTo directly + for _, cfgRoute := range utils.GetMainConfig().HTTPConfig.ProxyConfig.Routes { + if cfgRoute.Name == tunnel.Route.Name { + proxies = append(proxies, targetProxy{t, RouteTo(cfgRoute)}) + break + } + } + } else { + route := tunnel.Route + route.Target = t.TargetURL + proxy, err := NewProxy(t.TargetURL, route.AcceptInsecureHTTPSTarget, route.DisableHeaderHardening, route) + if err != nil { + utils.Error("Create Tunnel Route for "+t.DeviceName, err) + continue + } + proxies = append(proxies, targetProxy{t, proxy}) } - proxies = append(proxies, targetProxy{t, proxy}) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/src/proxy/tunnel_lb.go b/src/proxy/tunnel_lb.go index e597dceb..93e98704 100644 --- a/src/proxy/tunnel_lb.go +++ b/src/proxy/tunnel_lb.go @@ -3,6 +3,7 @@ package proxy import ( "sync" "sync/atomic" + "strings" "github.com/azukaar/cosmos-server/src/constellation" "github.com/azukaar/cosmos-server/src/utils" @@ -45,6 +46,7 @@ func (lb *TunnelLoadBalancer) SelectTarget(targets []utils.TunnelTarget, routeNa // Mode selection var selected *utils.TunnelTarget + mode = strings.ToLower(mode) switch mode { case "round_robin": c := lb.getCounter(routeName) diff --git a/src/utils/types.go b/src/utils/types.go index 32a88bac..ca5c38a2 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -262,9 +262,9 @@ type ProxyRouteConfig struct { ExtraHeaders map[string]string `yaml:"extra_headers,omitempty"` SkipURLClean bool `yaml:"skip_url_clean"` UseH2C bool `yaml:"use_h2c"` - LBMode string `yaml:"lb_mode" json:"lb_mode,omitempty"` - LBStickyMode bool `yaml:"lb_sticky_mode" json:"lb_sticky_mode,omitempty"` - Const_IsTunneled bool `yaml:"-", json:"-"` + LBMode string `yaml:"lb_mode" json:"LBMode,omitempty"` + LBStickyMode bool `yaml:"lb_sticky_mode" json:"LBStickyMode,omitempty"` + Const_IsTunneled bool `yaml:"-" json:"-"` } type EmailConfig struct { diff --git a/src/utils/utils.go b/src/utils/utils.go index 4065a47b..b6a902c6 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -66,6 +66,7 @@ var StopAllRCloneProcess func(bool) var InitPremiumFeatures func() var ResyncConstellationNodes = func() {} +var GetConstellationTunnelRoutes = func() []ProxyRouteConfig { return []ProxyRouteConfig{} } var LetsEncryptErrors = []string{} @@ -560,8 +561,8 @@ func GetAllHostnames(applyWildCard bool, removePorts bool) []string { mainHostname, } - proxies := GetMainConfig().ConstellationConfig.Tunnels - proxies = append(proxies, GetMainConfig().HTTPConfig.ProxyConfig.Routes...) + proxies := GetMainConfig().HTTPConfig.ProxyConfig.Routes + proxies = append(proxies, GetConstellationTunnelRoutes()...) for _, proxy := range proxies { if proxy.UseHost && proxy.Host != "" && !strings.Contains(proxy.Host, ",") && !strings.Contains(proxy.Host, " ") {