Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,138 changes: 513 additions & 625 deletions .plan/metrics/dae Transparent Proxy-Grafana_dashboard.json

Large diffs are not rendered by default.

3,306 changes: 0 additions & 3,306 deletions .plan/metrics/dae-dashboard.json

This file was deleted.

471 changes: 471 additions & 0 deletions .plan/metrics/pr38-upstream-1015-review.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions cmd/endpoint_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func validateEndpointTLSFiles(cfg metricshttp.EndpointConfig) error {
if err != nil {
return fmt.Errorf("cannot stat endpoint_tls_certificate '%s': %w", cfg.TlsCertificate, err)
}
if err = common.ValidateFilePermissionAllowed(cfg.TlsCertificate, certFi, 0o640, 0o644); err != nil {
// The certificate is public: reject only group or other write access.
if err = common.ValidateFilePermissionForbidden(cfg.TlsCertificate, certFi, 0o022); err != nil {
return fmt.Errorf("invalid endpoint_tls_certificate: %w", err)
}

Expand All @@ -67,7 +68,8 @@ func validateEndpointTLSFiles(cfg metricshttp.EndpointConfig) error {
if err != nil {
return fmt.Errorf("cannot stat endpoint_tls_key '%s': %w", cfg.TlsKey, err)
}
if err = common.ValidateFilePermissionAllowed(cfg.TlsKey, keyFi, 0o600); err != nil {
// The private key must not be accessible by group or others.
if err = common.ValidateFilePermissionForbidden(cfg.TlsKey, keyFi, 0o077); err != nil {
return fmt.Errorf("invalid endpoint_tls_key: %w", err)
}
return nil
Expand Down
5 changes: 5 additions & 0 deletions cmd/management_servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func resolveManagementServers(conf *config.Config, log *logrus.Logger) (manageme
}

cfg := endpointConfigFromGlobal(conf, log)
// BasicAuth is keyed on the username; a password alone would leave the
// endpoint open while looking protected.
if (cfg.Username == "") != (cfg.Password == "") {
return managementPlan{}, fmt.Errorf("endpoint_username and endpoint_password must be configured together")
}
if err := validateEndpointTLSFiles(cfg); err != nil {
return managementPlan{}, fmt.Errorf("invalid endpoint tls config: %w", err)
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/management_servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@ func TestResolveManagementServers(t *testing.T) {
}
})

t.Run("username and password must be configured together", func(t *testing.T) {
for _, tc := range []struct{ username, password string }{
{username: "", password: "secret"},
{username: "admin", password: ""},
} {
conf := &config.Config{}
conf.Global.EndpointListenAddress = "127.0.0.1:5556"
conf.Global.EndpointUsername = tc.username
conf.Global.EndpointPassword = tc.password
_, err := resolveManagementServers(conf, log)
if err == nil || !strings.Contains(err.Error(), "endpoint_username and endpoint_password") {
t.Fatalf("username=%q password=%q: err = %v, want pairing error", tc.username, tc.password, err)
}
}
conf := &config.Config{}
conf.Global.EndpointListenAddress = "127.0.0.1:5556"
conf.Global.EndpointUsername = "admin"
conf.Global.EndpointPassword = "secret"
if _, err := resolveManagementServers(conf, log); err != nil {
t.Fatalf("paired credentials: %v", err)
}
})

t.Run("tls validation error", func(t *testing.T) {
conf := &config.Config{}
conf.Global.EndpointListenAddress = "127.0.0.1:5556"
Expand Down
18 changes: 18 additions & 0 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,22 @@ func TestValidateEndpointTLSFilesChecksPermissions(t *testing.T) {
if err == nil {
t.Fatal("expected too-open certificate permissions to fail")
}

// Stricter modes than the common defaults must keep passing.
strictCert := writeEndpointFile(t, "cert-strict.pem", 0o600)
strictKey := writeEndpointFile(t, "key-strict.pem", 0o400)
if err := validateEndpointTLSFiles(metricshttp.EndpointConfig{
TlsCertificate: strictCert,
TlsKey: strictKey,
}); err != nil {
t.Fatalf("expected 0600 certificate and 0400 key to pass: %v", err)
}

groupReadableKey := writeEndpointFile(t, "key-open.pem", 0o640)
if err := validateEndpointTLSFiles(metricshttp.EndpointConfig{
TlsCertificate: cert,
TlsKey: groupReadableKey,
}); err == nil {
t.Fatal("expected group-readable key to fail")
}
}
31 changes: 5 additions & 26 deletions common/file_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,16 @@ package common
import (
"fmt"
"os"
"sort"
"strings"
)

func ValidateFilePermissionNotTooOpen(path string, fi os.FileInfo) error {
// ValidateFilePermissionForbidden rejects a directory, or a file whose
// permission bits include any of forbidden.
func ValidateFilePermissionForbidden(path string, fi os.FileInfo, forbidden os.FileMode) error {
if fi.IsDir() {
return fmt.Errorf("cannot read a directory: %v", path)
}
if fi.Mode()&0o037 > 0 {
return fmt.Errorf("permissions %04o for '%v' are too open; requires the file is NOT writable by the same group and NOT accessible by others; suggest 0640 or 0600", fi.Mode()&0o777, path)
if perm := fi.Mode().Perm(); perm&forbidden != 0 {
return fmt.Errorf("permissions %04o for '%v' are too open; bits %04o must not be set", perm, path, forbidden.Perm())
}
return nil
}

func ValidateFilePermissionAllowed(path string, fi os.FileInfo, allowedModes ...os.FileMode) error {
if fi.IsDir() {
return fmt.Errorf("cannot read a directory: %v", path)
}
perm := fi.Mode().Perm()
for _, mode := range allowedModes {
if perm == mode.Perm() {
return nil
}
}
if len(allowedModes) == 0 {
return fmt.Errorf("permissions %04o for '%v' are invalid", perm, path)
}
allowed := make([]string, 0, len(allowedModes))
for _, mode := range allowedModes {
allowed = append(allowed, fmt.Sprintf("%04o", mode.Perm()))
}
sort.Strings(allowed)
return fmt.Errorf("permissions %04o for '%v' are invalid; allowed: %s", perm, path, strings.Join(allowed, ", "))
}
61 changes: 30 additions & 31 deletions common/file_permission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,38 @@ func writeTempFile(t *testing.T, mode os.FileMode) (string, os.FileInfo) {
return path, fi
}

func TestValidateFilePermissionNotTooOpen(t *testing.T) {
_, fi0600 := writeTempFile(t, 0o600)
if err := ValidateFilePermissionNotTooOpen("test-0600", fi0600); err != nil {
t.Fatalf("0600 should pass: %v", err)
func TestValidateFilePermissionForbidden(t *testing.T) {
for _, tc := range []struct {
mode os.FileMode
forbidden os.FileMode
wantErr bool
}{
// Private key: no group/other access.
{mode: 0o600, forbidden: 0o077},
{mode: 0o400, forbidden: 0o077},
{mode: 0o640, forbidden: 0o077, wantErr: true},
{mode: 0o604, forbidden: 0o077, wantErr: true},
// Certificate: no group/other write.
{mode: 0o644, forbidden: 0o022},
{mode: 0o640, forbidden: 0o022},
{mode: 0o600, forbidden: 0o022},
{mode: 0o444, forbidden: 0o022},
{mode: 0o664, forbidden: 0o022, wantErr: true},
{mode: 0o646, forbidden: 0o022, wantErr: true},
} {
path, fi := writeTempFile(t, tc.mode)
err := ValidateFilePermissionForbidden(path, fi, tc.forbidden)
if (err != nil) != tc.wantErr {
t.Fatalf("mode %04o forbidden %04o: err = %v, wantErr %v", tc.mode, tc.forbidden, err, tc.wantErr)
}
}

_, fi0640 := writeTempFile(t, 0o640)
if err := ValidateFilePermissionNotTooOpen("test-0640", fi0640); err != nil {
t.Fatalf("0640 should pass: %v", err)
}

_, fi0644 := writeTempFile(t, 0o644)
if err := ValidateFilePermissionNotTooOpen("test-0644", fi0644); err == nil {
t.Fatal("0644 should fail as too open")
}
}

func TestValidateFilePermissionAllowed(t *testing.T) {
_, fi0600 := writeTempFile(t, 0o600)
if err := ValidateFilePermissionAllowed("key", fi0600, 0o600); err != nil {
t.Fatalf("0600 should pass for key: %v", err)
}
if err := ValidateFilePermissionAllowed("key", fi0600, 0o640, 0o644); err == nil {
t.Fatal("0600 should fail for cert-only allowed modes")
}

_, fi0640 := writeTempFile(t, 0o640)
if err := ValidateFilePermissionAllowed("cert", fi0640, 0o640, 0o644); err != nil {
t.Fatalf("0640 should pass for cert: %v", err)
dir := t.TempDir()
fi, err := os.Stat(dir)
if err != nil {
t.Fatalf("stat dir: %v", err)
}

_, fi0644 := writeTempFile(t, 0o644)
if err := ValidateFilePermissionAllowed("cert", fi0644, 0o640, 0o644); err != nil {
t.Fatalf("0644 should pass for cert: %v", err)
if err := ValidateFilePermissionForbidden(dir, fi, 0o077); err == nil {
t.Fatal("a directory should be rejected")
}
}
49 changes: 49 additions & 0 deletions component/outbound/dialer/check_counters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2026, daeuniverse Organization <dae@v2raya.org>
*/

package dialer

import (
"context"
"fmt"
"testing"
)

// TestCheck_CountersCountOnlyVerdicts pins the health-check counters exported
// as dae_health_check_total / dae_health_check_failure_total: a skip or a
// probe-infrastructure failure carries no node evidence and must not dilute
// the failure ratio.
func TestCheck_CountersCountOnlyVerdicts(t *testing.T) {
d := newNamedTestDialer(t, "counter-node")
typ := newTestNetworkType()

steps := []struct {
name string
result func() (bool, error)
wantTotal uint64
wantFailure uint64
}{
{"success", func() (bool, error) { return true, nil }, 1, 0},
{"node failure", func() (bool, error) { return false, fmt.Errorf("connection refused") }, 2, 1},
{"check option unavailable", func() (bool, error) {
return false, wrapCheckOptionError(fmt.Errorf("resolve refused"))
}, 2, 1},
{"plain skip", func() (bool, error) { return false, nil }, 2, 1},
}
for _, step := range steps {
result := step.result
opts := &CheckOption{
networkType: typ,
CheckFunc: func(context.Context, *NetworkType) (bool, error) {
return result()
},
}
_, _ = d.check(opts, false, nil)
total, failure := d.GetCollectionCounters(typ)
if total != step.wantTotal || failure != step.wantFailure {
t.Fatalf("after %s: total=%d failure=%d, want %d and %d", step.name, total, failure, step.wantTotal, step.wantFailure)
}
}
}
5 changes: 4 additions & 1 deletion component/outbound/dialer/connectivity_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -1410,7 +1410,6 @@ func (d *Dialer) check(opts *CheckOption, isResuscitation bool, cycle *cycleResu
const maxAttempts = 2
var bestLatency time.Duration
checkedAt := time.Now()
d.mustGetCollection(opts.networkType).CheckTotal.Add(1)

for range maxAttempts {
ctx, cancel := context.WithTimeout(d.ctx, Timeout)
Expand Down Expand Up @@ -1440,6 +1439,9 @@ func (d *Dialer) check(opts *CheckOption, isResuscitation bool, cycle *cycleResu
case ok && err == nil:
d.collectionFineMu.Lock()
collection := d.mustGetCollection(opts.networkType)
// CheckTotal counts only checks that produced a verdict; skips,
// teardown and probe-infrastructure failures carry no health evidence.
collection.CheckTotal.Add(1)
collection.LastProbe = DialerProbeObservationSnapshot{
CheckedAt: checkedAt,
Alive: true,
Expand Down Expand Up @@ -1483,6 +1485,7 @@ func (d *Dialer) check(opts *CheckOption, isResuscitation bool, cycle *cycleResu
Alive: false,
Message: err.Error(),
}
collection.CheckTotal.Add(1)
collection.CheckFailureTotal.Add(1)
d.collectionFineMu.Unlock()

Expand Down
4 changes: 0 additions & 4 deletions component/outbound/dialer_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,6 @@ func (g *DialerGroup) MinCheckInterval() time.Duration {
return min
}

func (g *DialerGroup) AliveDialerSets() [8]*dialer.AliveDialerSet {
return g.currentSelectionState().aliveDialerSets
}

func (d *DialerGroup) MustGetAliveDialerSet(typ *dialer.NetworkType) *dialer.AliveDialerSet {
return d.currentSelectionState().aliveDialerSets[typ.Index()]
}
Expand Down
4 changes: 0 additions & 4 deletions control/node_latency.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
// NodeLatencySnapshot describes the latest observable latency status for one node link.
type NodeLatencySnapshot struct {
Link string
Name string // human-readable dialer name, e.g. "香港标准 IEPL 专线 1"
Group string // outbound group name, e.g. "FC_HK"
LatencyMs *int32
Alive bool
Message string
Expand Down Expand Up @@ -73,8 +71,6 @@ func (c *ControlPlane) SnapshotNodeLatencies() []NodeLatencySnapshot {
}

snapshot := bestNodeLatencySnapshotForDialer(d)
snapshot.Name = d.Property().Name
snapshot.Group = group.Name
if existing, ok := latenciesByLink[snapshot.Link]; !ok || preferNodeLatencySnapshot(snapshot, existing) {
latenciesByLink[snapshot.Link] = snapshot
}
Expand Down
8 changes: 6 additions & 2 deletions example.dae
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ global {

# Endpoint configuration for metrics and diagnostics.
# Set a listen address to enable the endpoint server (disabled by default).
#endpoint_listen_address: '0.0.0.0:5556'
# Metrics reveal node, group and DNS upstream names: keep it on loopback or a
# LAN address, and set endpoint_username/endpoint_password (and TLS) first.
#endpoint_listen_address: '127.0.0.1:5556'
# Username and password must be set together.
#endpoint_username: ''
#endpoint_password: ''
# TLS permission policy: certificate must be 0640 or 0644; private key must be 0600.
# TLS permission policy: the certificate must not be writable by group or
# others; the private key must not be accessible by group or others (e.g. 0600).
#endpoint_tls_certificate: ''
#endpoint_tls_key: ''
#endpoint_prometheus_enabled: true
Expand Down
Loading
Loading