mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Add Guard.IsAdminAuthorized, a fail-closed variant of IsWhiteListed, and use it to gate destructive volume admin RPCs. IsWhiteListed keeps its allow-all-when-empty semantics for HTTP compatibility. For TCP peers with an empty whitelist, off-host callers are rejected but loopback (127.0.0.0/8, ::1) is still trusted. A volume server commonly cohabits with the master/filer on a single host and in integration-test clusters; the loopback exception keeps cluster-internal admin traffic working without -whiteList while still locking out off-host attackers. Non-TCP peers (in-process / bufconn / unix-socket) bypass the host check entirely. When `weed server` runs master+volume+filer in a single process the master dials the volume server in-process and the peer address surfaces as "@", which has no parseable IP. Such a caller shares our OS process and cannot be spoofed by a remote attacker, so we treat it as trusted by construction. The gate also tolerates a nil guard (developmental / embedded path) and only enforces once a guard is wired up. UpdateWhiteList skips entries whose CIDR fails to parse so the IP-iteration path can no longer hit a nil *net.IPNet.
116 lines
4.4 KiB
Go
116 lines
4.4 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"testing"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/peer"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
)
|
|
|
|
// fakeAddr is a net.Addr that is not *net.TCPAddr. We use it to fake a
|
|
// gRPC peer for the in-process / bufconn / unix-socket case.
|
|
type fakeAddr struct{ s string }
|
|
|
|
func (a fakeAddr) Network() string { return "fake" }
|
|
func (a fakeAddr) String() string { return a.s }
|
|
|
|
// TestCheckGrpcAdminAuthNonTCPPeerTrusted pins the in-process trust contract.
|
|
// When `weed server` runs master+volume+filer in the same process, the master
|
|
// dials the volume server via an in-process / bufconn / unix-socket transport.
|
|
// The peer Addr is not a *net.TCPAddr (often surfaces as "@"), so we cannot
|
|
// extract an IP — but the caller is in the same OS process, so we trust it
|
|
// unconditionally. Without this, an empty-whitelist fail-closed guard would
|
|
// break every embedded-cluster deployment.
|
|
func TestCheckGrpcAdminAuthNonTCPPeerTrusted(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard(nil, "", 0, "", 0)}
|
|
cases := []net.Addr{
|
|
fakeAddr{s: "@"},
|
|
fakeAddr{s: ""},
|
|
fakeAddr{s: "bufconn"},
|
|
&net.UnixAddr{Name: "/tmp/weed.sock", Net: "unix"},
|
|
}
|
|
for _, addr := range cases {
|
|
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: addr})
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
t.Errorf("non-TCP peer %T(%q) should be trusted, got %v", addr, addr.String(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCheckGrpcAdminAuthTCPLoopbackTrusted covers the common single-host
|
|
// deployment where a separate `weed` process on the same host dials over
|
|
// real TCP. With no whitelist configured, loopback peers are still allowed
|
|
// (delegated to Guard.IsAdminAuthorized).
|
|
func TestCheckGrpcAdminAuthTCPLoopbackTrusted(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard(nil, "", 0, "", 0)}
|
|
for _, ip := range []string{"127.0.0.1", "127.0.0.5", "::1"} {
|
|
ctx := peer.NewContext(context.Background(), &peer.Peer{
|
|
Addr: &net.TCPAddr{IP: net.ParseIP(ip), Port: 50000},
|
|
})
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
t.Errorf("loopback TCP peer %s should be trusted, got %v", ip, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCheckGrpcAdminAuthTCPOffHostDenied is the fail-closed half of the
|
|
// contract: an off-host TCP peer with no matching whitelist entry is denied
|
|
// with PermissionDenied.
|
|
func TestCheckGrpcAdminAuthTCPOffHostDenied(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard(nil, "", 0, "", 0)}
|
|
ctx := peer.NewContext(context.Background(), &peer.Peer{
|
|
Addr: &net.TCPAddr{IP: net.ParseIP("10.0.0.5"), Port: 50000},
|
|
})
|
|
err := vs.checkGrpcAdminAuth(ctx)
|
|
if err == nil {
|
|
t.Fatalf("off-host TCP peer with empty whitelist should be denied")
|
|
}
|
|
if got, want := status.Code(err), codes.PermissionDenied; got != want {
|
|
t.Errorf("status code = %v want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestCheckGrpcAdminAuthTCPWhitelistedAllowed verifies that an off-host TCP
|
|
// peer present in the configured whitelist is allowed through.
|
|
func TestCheckGrpcAdminAuthTCPWhitelistedAllowed(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard([]string{"10.0.0.5"}, "", 0, "", 0)}
|
|
ctx := peer.NewContext(context.Background(), &peer.Peer{
|
|
Addr: &net.TCPAddr{IP: net.ParseIP("10.0.0.5"), Port: 50000},
|
|
})
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
t.Errorf("whitelisted TCP peer should be allowed, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCheckGrpcAdminAuthNoPeerInfoDenied confirms we still deny when gRPC
|
|
// gives us no peer info at all (should not happen with real transports but
|
|
// we cannot prove a positive identity, so we refuse).
|
|
func TestCheckGrpcAdminAuthNoPeerInfoDenied(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard(nil, "", 0, "", 0)}
|
|
err := vs.checkGrpcAdminAuth(context.Background())
|
|
if err == nil {
|
|
t.Fatalf("missing peer info should be denied")
|
|
}
|
|
if got, want := status.Code(err), codes.PermissionDenied; got != want {
|
|
t.Errorf("status code = %v want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestCheckGrpcAdminAuthNoGuardIsNoop preserves the developmental / embedded
|
|
// path: with no Guard wired up at all, the gate is a no-op regardless of
|
|
// peer information.
|
|
func TestCheckGrpcAdminAuthNoGuardIsNoop(t *testing.T) {
|
|
vs := &VolumeServer{guard: nil}
|
|
ctx := peer.NewContext(context.Background(), &peer.Peer{
|
|
Addr: &net.TCPAddr{IP: net.ParseIP("10.0.0.5"), Port: 50000},
|
|
})
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
t.Errorf("no guard should be a no-op, got %v", err)
|
|
}
|
|
}
|