Files
seaweedfs/weed/command/iam.go
T
Chris LuandDevin 88b91cccba command: no extra localhost listener when -ip.bind is specified (#12055)
* util: add listenOnLocalhost parameter to NewIpAndLocalListeners

Nil keeps the current behavior of also binding localhost when the host
is a specific address; all callers pass nil for now.

* command: add -ip.bind.localhost flag to opt out of the extra localhost listener

Binding a command to a specific -ip.bind address also opened a second
listener on localhost for every port, with no way to turn it off. Each
command that calls NewIpAndLocalListeners (master, master.follower,
filer, s3, iam, volume, sftp, mq.broker, mq.agent) now accepts
-ip.bind.localhost=false to bind only the requested address. Embedded
services get prefixed flags (s3.ip.bind.localhost, sftp.ip.bind.localhost,
iam.ip.bind.localhost), and the command-level flag also disables them so
a single switch turns off every extra listener the command opens.

* mini: only require all-interface port availability while the loopback listener is on

With -ip.bind.localhost=false each service binds just -ip.bind, so a port
held on another address no longer collides; the all-interface precheck
would reject or relocate usable ports. WebDAV, the Admin UI, and the
Admin worker gRPC (which binds -admin.worker.ip, not -ip.bind) never open
a loopback companion listener, so their checks now use the address they
actually bind.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mq.broker: bind the gRPC listener to -ip.bind instead of all interfaces

The listener used an empty host so -ip was ignored for binding and the
localhost companion was never created, leaving -ip.bind.localhost with
nothing to control. Give the broker the same -ip/-ip.bind pair the other
servers use; embedded mode binds to -ip.bind like its peers.

* server: add -sftp.ip.bind.localhost and -iam.ip.bind.localhost switches

weed filer already exposes the prefixed opt-outs for its embedded sftp
and iam; give weed server the same pair so each extra listener can be
disabled individually while -ip.bind.localhost=false still turns them
all off.

* mq.broker: keep the default all-interface bind unless localhost is off

An unset -ip.bind used to mean the standalone broker listened on every
interface; defaulting it to -ip silently cut off multi-homed clients. Only
fall back to -ip when -ip.bind.localhost=false, where a specific bind is
needed for the opt-out to take effect.

* mini: validate the Admin HTTP port on the address it will bind

The port check always used -ip.bind while startMiniAdminWithWorker falls
back to loopback when Admin has no credential, so a loopback-held port
passed validation and killed the admin at startup.

* mq.broker: keep the all-interface default bind when -ip.bind is unset

-ip only becomes the fallback once -ip.bind.localhost=false needs a
specific address for the listener.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mini: validate the Admin HTTP port on its effective bind IP

When the Admin listener falls back to loopback (no credential or mTLS
configured), the pre-flight port check now probes that same address
instead of -ip.bind.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: keep NewIpAndLocalListeners source-compatible for 3-arg callers

* command: drop the -ip.bind.localhost flag; an explicit -ip.bind drops the extra listener

Instead of an opt-out flag, the loopback companion listener now simply
follows whether the bind address was chosen: it is created only while
-ip.bind is unset and the bind address falls back to -ip. Passing
-ip.bind=<addr> binds each port to exactly that address.

Each service options struct carries a listenOnLocalhost *bool that
embedders (weed server, weed mini, embedded s3/sftp/iam in weed filer)
set from the parent's -ip.bind state, captured before the bind flag is
repointed or filled in by the -ip fallback; standalone commands derive
it from their own flag before the fallback runs.

iam and mq.agent gain a real -ip.bind flag so they have a way to pin a
single address. mq.broker keeps its new -ip.bind flag but drops the
-ip.bind.localhost fallback hack: empty still listens on all interfaces.

* server: default embedded mq.agent brokers to the broker's bind address

With an explicit -ip.bind the embedded broker no longer listens on
localhost, so an unset -mq.agent.brokers defaulting to localhost:17777
could not reach it. Default to <serverBindIp>:<mq.broker.port> (the
address the embedded broker binds), falling back to localhost when the
bind address is a wildcard. An explicit -mq.agent.brokers still wins.

Also fix the NewIpAndLocalListeners skip message to say localhost.

* server: bind embedded mq.agent to serverBindIp

The embedded agent's ipBind was never set, so startQueueAgent fell back
to the advertised -ip address and an explicit -ip.bind did not pin the
agent listener.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 06:52:16 -07:00

157 lines
6.0 KiB
Go

package command
import (
"context"
"fmt"
"github.com/seaweedfs/seaweedfs/weed/util/version"
"time"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iamapi"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/util/grace"
// Import credential stores to register them
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc"
_ "github.com/seaweedfs/seaweedfs/weed/credential/memory"
_ "github.com/seaweedfs/seaweedfs/weed/credential/postgres"
)
var (
iamStandaloneOptions IamOptions
)
type IamOptions struct {
filer *string
masters *string
ip *string
ipBind *string
listenOnLocalhost *bool
port *int
selfServiceRequiresGrant *bool
}
func init() {
cmdIam.Run = runIam // break init cycle
iamStandaloneOptions.filer = cmdIam.Flag.String("filer", "localhost:8888", "comma-separated filer server addresses for high availability")
iamStandaloneOptions.masters = cmdIam.Flag.String("master", "localhost:9333", "comma-separated master servers")
iamStandaloneOptions.ip = cmdIam.Flag.String("ip", util.DetectedHostAddress(), "iam server http listen ip address")
iamStandaloneOptions.ipBind = cmdIam.Flag.String("ip.bind", "", "ip address to bind to. If empty, default to same as -ip option.")
iamStandaloneOptions.port = cmdIam.Flag.Int("port", 8111, "iam server http listen port")
iamStandaloneOptions.selfServiceRequiresGrant = cmdIam.Flag.Bool("selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
}
var cmdIam = &Command{
UsageLine: "iam [-port=8111] [-filer=<ip:port>[,<ip:port>]...] [-master=<ip:port>,<ip:port>]",
Short: "[DEPRECATED] start a standalone iam API compatible server",
Long: `[DEPRECATED] start a standalone iam API compatible server.
DEPRECATION NOTICE:
The standalone 'weed iam' command is deprecated and will be removed in a future release.
The IAM API is now embedded in the S3 server by default. Simply use 'weed s3' instead,
which provides both S3 and IAM APIs on the same port (enabled by default with -iam=true).
This simplifies deployment by running a single server instead of two separate servers,
following the pattern used by MinIO and Ceph RGW.
To use the embedded IAM API:
weed s3 -port=8333 # IAM API is available on the same port
To disable the embedded IAM API (if you prefer the old behavior):
weed s3 -iam=false # Run S3 without IAM
weed iam -port=8111 # Run IAM separately (deprecated)
Multiple filer addresses can be specified for high availability, separated by commas.`,
}
func runIam(cmd *Command, args []string) bool {
glog.Warningf("================================================================================")
glog.Warningf("DEPRECATION WARNING: 'weed iam' is deprecated and will be removed in a future release.")
glog.Warningf("The IAM API is now embedded in 'weed s3' by default (use -iam=true, which is the default).")
glog.Warningf("Please migrate to using 'weed s3' which provides both S3 and IAM APIs on the same port.")
glog.Warningf("================================================================================")
return iamStandaloneOptions.startIamServer()
}
func (iamopt *IamOptions) startIamServer() bool {
filerAddresses := pb.ServerAddresses(*iamopt.filer).ToAddresses()
util.LoadSecurityConfiguration()
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
for {
err := pb.WithOneOfGrpcFilerClients(false, filerAddresses, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{})
if err != nil {
return fmt.Errorf("get filer configuration: %v", err)
}
glog.V(0).Infof("IAM read filer configuration: %s", resp)
return nil
})
if err != nil {
glog.V(0).Infof("wait to connect to filers %v", filerAddresses)
time.Sleep(time.Second)
} else {
glog.V(0).Infof("connected to filers %v", filerAddresses)
break
}
}
masters := pb.ServerAddresses(*iamopt.masters).ToAddressMap()
router := mux.NewRouter().SkipClean(true)
iamApiServer, iamApiServer_err := iamapi.NewIamApiServer(router, &iamapi.IamServerOption{
Masters: masters,
Filers: filerAddresses,
Port: *iamopt.port,
GrpcDialOption: grpcDialOption,
IamSelfServiceRequiresGrant: *iamopt.selfServiceRequiresGrant,
})
glog.V(0).Info("NewIamApiServer created")
if iamApiServer_err != nil {
glog.Fatalf("IAM API Server startup error: %v", iamApiServer_err)
}
// Register shutdown handler to prevent goroutine leak
grace.OnInterrupt(func() {
iamApiServer.Shutdown()
})
// The loopback companion listener is only kept while -ip.bind is unset and
// the bind address falls back to -ip. An explicit -ip.bind pins the port to
// exactly that address.
bindIp := *iamopt.ip
listenOnLocalhost := iamopt.ipBind == nil || *iamopt.ipBind == ""
if iamopt.ipBind != nil && *iamopt.ipBind != "" {
bindIp = *iamopt.ipBind
}
if iamopt.listenOnLocalhost != nil {
listenOnLocalhost = *iamopt.listenOnLocalhost
}
listenAddress := fmt.Sprintf(":%d", *iamopt.port)
iamApiListener, iamApiLocalListener, err := util.NewIpAndLocalListeners(bindIp, *iamopt.port, time.Duration(10)*time.Second, listenOnLocalhost)
if err != nil {
glog.Fatalf("IAM API Server listener on %s error: %v", listenAddress, err)
}
glog.V(0).Infof("Start Seaweed IAM API Server %s at http port %d", version.Version(), *iamopt.port)
if iamApiLocalListener != nil {
go func() {
if err = newHttpServer(router, nil).Serve(iamApiLocalListener); err != nil {
glog.Errorf("IAM API Server Fail to serve: %v", err)
}
}()
}
if err = newHttpServer(router, nil).Serve(iamApiListener); err != nil {
glog.Fatalf("IAM API Server Fail to serve: %v", err)
}
return true
}