This commit is contained in:
yusing
2026-02-16 08:59:01 +08:00
parent 15b9635ee1
commit e4e6f6b3e8
242 changed files with 3953 additions and 3502 deletions

View File

@@ -14,10 +14,12 @@ import (
"sync"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/agentpool"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/health/monitor"
"github.com/yusing/godoxy/internal/homepage"
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
@@ -33,7 +35,6 @@ import (
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/route/rules"
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
route "github.com/yusing/godoxy/internal/route/types"
@@ -46,7 +47,6 @@ type (
Host string `json:"host,omitempty"`
Port route.Port `json:"port"`
// for TCP and UDP routes, bind address to listen on
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
Root string `json:"root,omitempty"`
@@ -57,7 +57,7 @@ type (
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
Rules rules.Rules `json:"rules,omitempty" extensions:"x-nullable"`
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitzero" extensions:"x-nullable"` // null on load-balancer routes
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
Homepage *homepage.ItemConfig `json:"homepage"`
@@ -108,17 +108,17 @@ type (
)
type lockedError struct {
err gperr.Error
err error
lock sync.Mutex
}
func (le *lockedError) Get() gperr.Error {
func (le *lockedError) Get() error {
le.lock.Lock()
defer le.lock.Unlock()
return le.err
}
func (le *lockedError) Set(err gperr.Error) {
func (le *lockedError) Set(err error) {
le.lock.Lock()
defer le.lock.Unlock()
le.err = err
@@ -131,7 +131,7 @@ func (r Routes) Contains(alias string) bool {
return ok
}
func (r *Route) Validate() gperr.Error {
func (r *Route) Validate() error {
// wait for alias to be set
if r.Alias == "" {
return nil
@@ -150,13 +150,13 @@ func (r *Route) Validate() gperr.Error {
return r.valErr.Get()
}
func (r *Route) validate() gperr.Error {
func (r *Route) validate() error {
// if strings.HasPrefix(r.Alias, "godoxy") {
// log.Debug().Any("route", r).Msg("validating route")
// }
if r.Agent != "" {
if r.Container != nil {
return gperr.Errorf("specifying agent is not allowed for docker container routes")
return errors.New("specifying agent is not allowed for docker container routes")
}
var ok bool
// by agent address
@@ -165,7 +165,7 @@ func (r *Route) validate() gperr.Error {
// fallback to get agent by name
r.agent, ok = agentpool.GetAgent(r.Agent)
if !ok {
return gperr.Errorf("agent %s not found", r.Agent)
return fmt.Errorf("agent %s not found", r.Agent)
}
}
}
@@ -200,7 +200,11 @@ func (r *Route) validate() gperr.Error {
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
wasNotNil := r.Proxmox != nil
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
workingState := config.WorkingState.Load()
var proxmoxProviders []*proxmox.Config
if workingState != nil { // nil in tests
proxmoxProviders = workingState.Value().Providers.Proxmox
}
if len(proxmoxProviders) > 0 {
// it's fine if ip is nil
hostname := r.Host
@@ -208,40 +212,34 @@ func (r *Route) validate() gperr.Error {
for _, p := range proxmoxProviders {
// First check if hostname, IP, or alias matches a node (node-level route)
if nodeName := p.Client().ReverseLookupNode(hostname, ip, r.Alias); nodeName != "" {
zero := 0
zero := uint64(0)
if r.Proxmox == nil {
r.Proxmox = &proxmox.NodeConfig{}
}
r.Proxmox.Node = nodeName
r.Proxmox.VMID = &zero
r.Proxmox.VMName = ""
log.Info().
Str("node", nodeName).
Msgf("found proxmox node for route %q", r.Alias)
log.Info().EmbedObject(r).Msg("found proxmox node")
break
}
// Then check if hostname, IP, or alias matches a VM resource
resource, _ := p.Client().ReverseLookupResource(ip, hostname, r.Alias)
if resource != nil {
vmid := int(resource.VMID)
vmid := resource.VMID
if r.Proxmox == nil {
r.Proxmox = &proxmox.NodeConfig{}
}
r.Proxmox.Node = resource.Node
r.Proxmox.VMID = &vmid
r.Proxmox.VMName = resource.Name
log.Info().
Str("node", resource.Node).
Int("vmid", int(resource.VMID)).
Str("vmname", resource.Name).
Msgf("found proxmox resource for route %q", r.Alias)
log.Info().EmbedObject(r).Msg("found proxmox resource")
break
}
}
}
if wasNotNil && (r.Proxmox.Node == "" || r.Proxmox.VMID == nil) {
log.Warn().Msgf("no proxmox node / resource found for route %q", r.Alias)
log.Warn().EmbedObject(r).Msg("no proxmox node / resource found")
}
}
@@ -260,7 +258,7 @@ func (r *Route) validate() gperr.Error {
switch r.Port.Proxy {
case common.ProxyHTTPPort, common.ProxyHTTPSPort, common.APIHTTPPort:
if r.Scheme.IsReverseProxy() || r.Scheme == route.SchemeTCP {
return gperr.Errorf("localhost:%d is reserved for godoxy", r.Port.Proxy)
return fmt.Errorf("localhost:%d is reserved for godoxy", r.Port.Proxy)
}
}
}
@@ -271,27 +269,19 @@ func (r *Route) validate() gperr.Error {
errs.Add(err)
}
var impl types.Route
var err gperr.Error
switch r.Scheme {
case route.SchemeFileServer:
r.Host = ""
r.Port.Proxy = 0
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
if r.Port.Listening != 0 {
errs.Addf("unexpected listening port for %s scheme", r.Scheme)
}
if r.ShouldExclude() {
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
case route.SchemeTCP, route.SchemeUDP:
if r.ShouldExclude() {
// should exclude, we don't care the scheme here.
} else {
switch r.Scheme {
case route.SchemeFileServer:
r.Host = ""
r.Port.Proxy = 0
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, "https://"+net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening)))
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, "https://"+net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening)))
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
} else {
if r.Bind == "" {
r.Bind = "0.0.0.0"
}
case route.SchemeTCP, route.SchemeUDP:
bindIP := net.ParseIP(r.Bind)
remoteIP := net.ParseIP(r.Host)
toNetwork := func(ip net.IP, scheme route.Scheme) string {
@@ -325,6 +315,8 @@ func (r *Route) validate() gperr.Error {
return errs.Error()
}
var impl types.Route
var err error
switch r.Scheme {
case route.SchemeFileServer:
impl, err = NewFileServer(r)
@@ -360,8 +352,8 @@ func (r *Route) validateRules() error {
return errors.New("rule preset `webui.yml` not found")
}
r.Rules = rules
return nil
}
return nil
}
if r.RuleFile != "" && len(r.Rules) > 0 {
@@ -397,7 +389,7 @@ func (r *Route) validateRules() error {
}
func (r *Route) validateProxmox() {
l := log.With().Str("route", r.Alias).Logger()
l := log.With().EmbedObject(r).Logger()
nodeName := r.Proxmox.Node
vmid := r.Proxmox.VMID
@@ -426,7 +418,7 @@ func (r *Route) validateProxmox() {
} else {
res, err := node.Client().GetResource("lxc", *vmid)
if err != nil { // ErrResourceNotFound
l.Err(err).Msgf("failed to get resource %d", *vmid)
l.Error().Err(err).Msgf("failed to get resource %d", *vmid)
return
}
@@ -445,24 +437,22 @@ func (r *Route) validateProxmox() {
return
}
l = l.With().Str("container", containerName).Logger()
l.Info().Msgf("checking if container is running")
l.Info().Str("container", containerName).Msg("checking if container is running")
running, err := node.LXCIsRunning(ctx, *vmid)
if err != nil {
l.Err(err).Msgf("failed to check container state")
l.Error().Err(err).Msgf("failed to check container state")
return
}
if !running {
l.Info().Msgf("starting container")
l.Info().Msg("starting container")
if err := node.LXCAction(ctx, *vmid, proxmox.LXCStart); err != nil {
l.Err(err).Msgf("failed to start container")
l.Error().Err(err).Msg("failed to start container")
return
}
}
l.Info().Msgf("finding reachable ip addresses")
l.Info().Msg("finding reachable ip addresses")
errs := gperr.NewBuilder("failed to find reachable ip addresses")
for _, ip := range ips {
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
@@ -488,23 +478,23 @@ func (r *Route) Task() *task.Task {
return r.task
}
func (r *Route) Start(parent task.Parent) gperr.Error {
func (r *Route) Start(parent task.Parent) error {
r.onceStart.Do(func() {
r.startErr.Set(r.start(parent))
})
return r.startErr.Get()
}
func (r *Route) start(parent task.Parent) gperr.Error {
func (r *Route) start(parent task.Parent) error {
if r.impl == nil { // should not happen
return gperr.New("route not initialized")
return errors.New("route not initialized")
}
defer close(r.started)
// skip checking for excluded routes
excluded := r.ShouldExclude()
if !excluded {
if err := checkExists(r); err != nil {
if err := checkExists(parent.Context(), r); err != nil {
return err
}
}
@@ -518,15 +508,23 @@ func (r *Route) start(parent task.Parent) gperr.Error {
return err
}
} else {
r.task = parent.Subtask("excluded."+r.Name(), true)
routes.Excluded.Add(r.impl)
ep := entrypoint.FromCtx(parent.Context())
if ep == nil {
return errors.New("entrypoint not initialized")
}
r.task = parent.Subtask("excluded."+r.Name(), false)
r.task.SetValue(monitor.DisplayNameKey{}, r.DisplayName())
ep.ExcludedRoutes().Add(r.impl)
r.task.OnCancel("remove_route_from_excluded", func() {
routes.Excluded.Del(r.impl)
ep.ExcludedRoutes().Del(r.impl)
})
if r.UseHealthCheck() {
r.HealthMon = monitor.NewMonitor(r.impl)
err := r.HealthMon.Start(r.task)
return err
if err != nil {
return err
}
}
}
return nil
@@ -564,6 +562,10 @@ func (r *Route) ProviderName() string {
return r.Provider
}
func (r *Route) ListenURL() *nettypes.URL {
return r.LisURL
}
func (r *Route) TargetURL() *nettypes.URL {
return r.ProxyURL
}
@@ -587,10 +589,9 @@ func (r *Route) References() []string {
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Services[0]}
}
return []string{r.Proxmox.Services[0], aliasRef}
} else {
if r.Proxmox.VMName != aliasRef {
return []string{r.Proxmox.VMName, aliasRef}
}
}
if r.Proxmox.VMName != aliasRef {
return []string{r.Proxmox.VMName, aliasRef}
}
}
return []string{aliasRef}
@@ -678,6 +679,44 @@ func (r *Route) DisplayName() string {
return r.Homepage.Name
}
func (r *Route) MarshalZerologObject(e *zerolog.Event) {
e.Str("alias", r.Alias)
switch r := r.impl.(type) {
case *ReverseProxyRoute:
e.Str("type", "reverse_proxy").
Str("scheme", r.Scheme.String()).
Str("bind", r.LisURL.Host).
Str("target", r.ProxyURL.URL.String())
case *FileServer:
e.Str("type", "file_server").
Str("root", r.Root)
case *StreamRoute:
e.Str("type", "stream").
Str("scheme", r.LisURL.Scheme+"->"+r.ProxyURL.Scheme)
if r.stream != nil {
// listening port could be zero (random),
// use LocalAddr() to get the actual listening host+port.
e.Str("bind", r.stream.LocalAddr().String())
} else {
// not yet started
e.Str("bind", r.LisURL.Host)
}
e.Str("target", r.ProxyURL.URL.String())
}
if r.Proxmox != nil {
e.Str("proxmox", r.Proxmox.Node)
if r.Proxmox.VMID != nil {
e.Uint64("vmid", *r.Proxmox.VMID)
}
if r.Proxmox.VMName != "" {
e.Str("vmname", r.Proxmox.VMName)
}
}
if r.Container != nil {
e.Str("container", r.Container.ContainerName)
}
}
// PreferOver implements pool.Preferable to resolve duplicate route keys deterministically.
// Preference policy:
// - Prefer routes with rules over routes without rules.
@@ -689,7 +728,7 @@ func (r *Route) PreferOver(other any) bool {
switch v := other.(type) {
case *Route:
or = v
case *ReveseProxyRoute:
case *ReverseProxyRoute:
or = v.Route
case *FileServer:
or = v.Route
@@ -932,6 +971,13 @@ func (r *Route) Finalize() {
}
}
switch r.Scheme {
case route.SchemeTCP, route.SchemeUDP:
if r.Bind == "" {
r.Bind = "0.0.0.0"
}
}
r.Port.Listening, r.Port.Proxy = lp, pp
workingState := config.WorkingState.Load()
@@ -942,7 +988,8 @@ func (r *Route) Finalize() {
panic("bug: working state is nil")
}
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
// TODO: default value from context
r.HealthCheck.ApplyDefaults(workingState.Value().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {