Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ env:

on:
push:
branches:
- development
- main
Comment thread
swarna1101 marked this conversation as resolved.
tags:
- v*

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ COPY --from=builder /optimum-gateway/LICENSE /optimum-gateway/NOTICE /optimum-ga

# USER gateway

EXPOSE 33212 33213 48123
EXPOSE 33212 33213 33213/udp 48123

ENTRYPOINT ["/optimum-gateway"]
2 changes: 1 addition & 1 deletion Dockerfile.bench
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ RUN mkdir -p /gateway/logs

COPY --from=builder /gateway/optimum-gateway /optimum-gateway

EXPOSE 33212 33213 48123
EXPOSE 33212 33213 33213/udp 48123

ENTRYPOINT ["/optimum-gateway"]
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ To run the gateway image directly:
docker run --name optimum-gateway --rm \
-p 33212:33212/tcp \
-p 33213:33213/tcp \
-p 33213:33213/udp \
-p 48123:48123/tcp \
-v $(pwd)/config:/app/config \
-v $(pwd)/data/libp2p:/tmp/libp2p \
Expand All @@ -101,7 +102,8 @@ docker run --name optimum-gateway --rm \
-config=/app/config/app_conf.yml
```

`agent_mump2p_port` (default `33213`) must be reachable by other gateways in the mesh.
`agent_mump2p_port` (default `33213`) must be reachable by other gateways in
the mesh over both TCP and UDP (QUIC-v1).

### Run from source

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The gateway has three trust anchors, in roughly increasing blast radius:
firewall the libp2p port to the intended CL client.
2. **The mump2p fleet** the gateway joins via the embedded mump2p node on
the port configured by `agent_mump2p_port` / `OPT_AGENT_MUMP2P_PORT`
(default `33213`). When auth is enabled (`OPT_ENABLE_AUTH=true` with
(default `33213`) over both TCP and UDP (QUIC-v1). When auth is enabled (`OPT_ENABLE_AUTH=true` with
`OPT_API_KEY` set), the handshake is authenticated by the operator's
gateway JWT and verified against the upstream JWKS; when auth is
disabled, `VerifyToken` is a no-op and only `ClusterID` is enforced.
Expand All @@ -86,7 +86,7 @@ except the libp2p (`OPT_AGENT_LIB_P2P_PORT`) and Optimum
| Port (default) | Service | Bind | Auth | Notes |
| -------------------------------- | ----------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| 33212 (`OPT_AGENT_LIB_P2P_PORT`) | libp2p host (CL gossip) | all interfaces (`/ip4/0.0.0.0/tcp/...`, `/ip6/::/tcp/...`); Noise transport | libp2p-pubsub direct peering with `OPT_DIRECT_CL_PEERS`; connect-time peer ID allowlist when that list is non-empty (not a `ConnectionGater`) | Firewall to the local CL client when `OPT_DIRECT_CL_PEERS` is empty. |
| 33213 (`OPT_AGENT_MUMP2P_PORT`) | Optimum (mump2p) | all interfaces | `ClusterID` always enforced in handshake; JWT verified by `authMgr.VerifyToken` (no-op when `OPT_ENABLE_AUTH=false` or `OPT_API_KEY` empty) | No mainnet/hoodi refusal if auth is disabled — operators are responsible for setting `OPT_ENABLE_AUTH=true` in production. |
| 33213 (`OPT_AGENT_MUMP2P_PORT`) | Optimum (mump2p) | all interfaces (`/ip4/0.0.0.0/tcp/...`, `/ip6/::/tcp/...`, and `/ip{4,6}/.../udp/.../quic-v1`) | `ClusterID` always enforced in handshake; JWT verified by `authMgr.VerifyToken` (no-op when `OPT_ENABLE_AUTH=false` or `OPT_API_KEY` empty) | Open this port for both TCP and UDP; no mainnet/hoodi refusal if auth is disabled — operators are responsible for setting `OPT_ENABLE_AUTH=true` in production. |
| 48123 (`OPT_TELEMETRY_PORT`) | HTTP telemetry (Fiber) | all interfaces | **none** — `/`, `/health`, `/api/v1/self_info` are unauthenticated; `/metrics` is registered only when `OPT_ENABLE_TELEMETRY=true` and is also unauthenticated | Put behind a reverse proxy / firewall if exposed. |
| 6060 (`OPT_PPROF_ADDR`) | pprof HTTP | `OPT_PPROF_ADDR` (default `127.0.0.1:6060`) | none | Only started when `OPT_ENABLE_PPROF=true`. Remote exposure is controlled by setting `OPT_PPROF_ADDR` to a non-loopback address. |
| 9600 (`OPT_STREAM_ADDR`) | Consumer block stream (WebSocket) | `OPT_STREAM_ADDR` (default `127.0.0.1:9600`) | Consumer JWT (`aud=stream`) when `OPT_STREAM_REQUIRE_AUTH=true`; auth-off rejected on non-loopback | Only started when `OPT_STREAM_ENABLE=true`. No native TLS; a public bind must sit behind a trusted terminating proxy. |
Expand Down
1 change: 1 addition & 0 deletions docker-compose-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
ports:
- "33212:33212"
- "33213:33213"
- "33213:33213/udp"
- "48123:48123"
volumes:
- "./config:/app/config:ro"
Expand Down
3 changes: 2 additions & 1 deletion docker-compose-sidecar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ services:
- mump2p_cluster
ports:
- "33212:33212" # libp2p port
- "33213:33213" # opt port
- "33213:33213" # mump2p TCP port
- "33213:33213/udp" # mump2p QUIC port
- "48123:48123" # telemetry + API
volumes:
- "./config:/app/config:ro"
Expand Down
7 changes: 5 additions & 2 deletions guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The gateway accepts configuration through environment variables as well. Environ
- `OPT_IDENTITY_LIBP2P_DIR`: Directory for the CL-facing libp2p identity
- `OPT_IDENTITY_MUMP2P_DIR`: Directory for the mump2p mesh identity
- `OPT_AGENT_LIB_P2P_PORT`: TCP port used for the CL-facing libp2p listener
- `OPT_AGENT_MUMP2P_PORT`: TCP port used for the gateway-to-gateway mesh
- `OPT_AGENT_MUMP2P_PORT`: TCP and UDP (QUIC-v1) port used for the gateway-to-gateway mesh
- `OPT_ENABLE_TELEMETRY`: Enable local API and Prometheus metrics
- `OPT_TELEMETRY_PORT`: HTTP port for `/health`, `/metrics`, `/api/v1/self_info`
- `OPT_REMOTE_PUSH_ENABLE`: Push metrics/logs to Optimum (requires telemetry + API key)
Expand Down Expand Up @@ -77,6 +77,8 @@ make run
```sh
docker run --name optimum-gateway --rm \
-p 33212:33212/tcp \
-p 43213:43213/tcp \
-p 43213:43213/udp \
-p 127.0.0.1:48123:48123/tcp \
-e OPT_API_KEY=ogw_live_xxx \
-v $(pwd)/config:/app/config \
Expand All @@ -86,7 +88,8 @@ docker run --name optimum-gateway --rm \
-config=/app/config/app_conf.yml
```

`agent_mump2p_port` (sample `43213`) is used for gateway-to-gateway mesh egress.
`agent_mump2p_port` (sample `43213`) must be reachable by other gateways over
both TCP and UDP (QUIC-v1).

## Connect Your CL Client

Expand Down
2 changes: 1 addition & 1 deletion pkg/service/bootstrapper/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func (s *Service) predictMumP2PAddrInfo() (peerInfo peer.AddrInfo, publicIP stri
}
return peer.AddrInfo{
ID: identityKey.ID,
Addrs: commonnet.MustBuildAdvertisedAddresses(s.log, publicIPV4, publicIPV6, s.cfg.AgentMumP2PPort),
Addrs: utils.BuildQUICTCPAddr(s.log, publicIPV4, publicIPV6, s.cfg.AgentMumP2PPort),
}, publicHost, nil
}

Expand Down
12 changes: 10 additions & 2 deletions pkg/service/mum_p2p/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/net/connmgr"
"github.com/libp2p/go-libp2p/p2p/security/noise"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
gomplex "github.com/libp2p/go-mplex"
"github.com/multiformats/go-multiaddr"
Expand Down Expand Up @@ -56,6 +57,10 @@ type Node struct {
oncer sync.Once
}

// getExternalIPs is a seam for tests around NewNode. Production uses the
// common-net implementation; tests can avoid depending on external discovery.
var getExternalIPs = commonnet.GetExternalIPs

// NewNode creates a new P2P node instance using the provided config.
// It sets up the libp2p host with a listen address and initializes Optimum pub-sub.
func NewNode(
Expand All @@ -70,7 +75,7 @@ func NewNode(
return nil, fmt.Errorf("failed ensuring identity from %s: %w", identityDir, err)
}

publicIPV4, publicIPV6, err := commonnet.GetExternalIPs()
publicIPV4, publicIPV6, err := getExternalIPs()
if err != nil {
return nil, fmt.Errorf("failed to get public IP address: %w", err)
}
Expand All @@ -87,16 +92,19 @@ func NewNode(
return nil, fmt.Errorf("failed to create connection manager: %w", err)
}

cachedAddrs := commonnet.MustBuildAdvertisedAddresses(log, publicIPV4, publicIPV6, cfg.ListenPort)
cachedAddrs := utils.BuildQUICTCPAddr(log, publicIPV4, publicIPV6, cfg.ListenPort)

libP2POpts := []libp2p.Option{
libp2p.ConnectionManager(cn),
libp2p.ListenAddrStrings(
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", cfg.ListenPort),
fmt.Sprintf("/ip6/::/tcp/%d", cfg.ListenPort),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", cfg.ListenPort),
fmt.Sprintf("/ip6/::/udp/%d/quic-v1", cfg.ListenPort),
),
libp2p.Ping(false), // Disable Ping Service.
libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(libp2pquic.NewTransport),
libp2p.DefaultMuxers,
libp2p.Muxer("/mplex/6.7.0", mplex.DefaultTransport),
libp2p.Security(noise.ID, noise.New),
Expand Down
95 changes: 95 additions & 0 deletions pkg/service/mum_p2p/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package mum_p2p

import (
"context"
"fmt"
"net"
"testing"
"time"

"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"

commonconfig "github.com/getoptimum/optimum-common/pkg/config"
commonentities "github.com/getoptimum/optimum-common/pkg/entities"
commonlogger "github.com/getoptimum/optimum-common/pkg/logger"
cfgpkg "github.com/getoptimum/optimum-gateway/pkg/config"
)

func TestNewNodeAcceptsInboundTCPAndQUIC(t *testing.T) {
// NewNode calls GetExternalIPs for advertised addrs. Stub it so this
// only exercises listen + transport registration.
oldGetExternalIPs := getExternalIPs
getExternalIPs = func() (string, string, error) { return "127.0.0.1", "", nil }
t.Cleanup(func() { getExternalIPs = oldGetExternalIPs })

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
log := commonlogger.NewAppSLogger(commonlogger.Error)
cfg := newTransportTestConfig(ctx, t, log)

target, err := NewNode(ctx, log, cfg, t.TempDir())
require.NoError(t, err)
t.Cleanup(target.Stop)

id := target.GetHost().ID()
port := cfg.ListenPort

dial := func(t *testing.T, transport, addrFmt string, opts ...libp2p.Option) {
t.Helper()
dialer, err := libp2p.New(opts...)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, dialer.Close()) })

addr := multiaddr.StringCast(fmt.Sprintf(addrFmt, port, id))
require.NoError(t, dialer.Connect(ctx, peer.AddrInfo{ID: id, Addrs: []multiaddr.Multiaddr{addr}}))

conns := target.GetHost().Network().ConnsToPeer(dialer.ID())
require.NotEmpty(t, conns)
require.Equal(t, transport, conns[0].ConnState().Transport)
require.Equal(t, network.DirInbound, conns[0].Stat().Direction)
}

t.Run("tcp", func(t *testing.T) {
dial(t, "tcp", "/ip4/127.0.0.1/tcp/%d/p2p/%s", libp2p.Transport(tcp.NewTCPTransport))
})
t.Run("quic", func(t *testing.T) {
dial(t, "quic-v1", "/ip4/127.0.0.1/udp/%d/quic-v1/p2p/%s", libp2p.Transport(libp2pquic.NewTransport))
})
}

func newTransportTestConfig(ctx context.Context, t *testing.T, log commonlogger.AppLogger) *Config {
t.Helper()
return &Config{
ClusterID: "transport-test",
ListenPort: freeTCPUDPPort(t),
MaxMessageSize: cfgpkg.DefaultMaxMessageSize,
Rotator: commonconfig.NewConfigRotator(ctx, log, &commonentities.OptimumConfig{
MaxMessageSize: cfgpkg.DefaultMaxMessageSize,
}, "hoodi", "transport-test", func(*commonentities.DynamicConfig) {}),
}
}

func freeTCPUDPPort(t *testing.T) int {
t.Helper()
for range 20 {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := ln.Addr().(*net.TCPAddr).Port
pc, err := net.ListenPacket("udp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
_ = ln.Close()
continue
}
require.NoError(t, ln.Close())
require.NoError(t, pc.Close())
return port
}
t.Fatal("could not allocate a port free on both TCP and UDP")
return 0
}
22 changes: 22 additions & 0 deletions pkg/utils/addresses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package utils

import (
"github.com/multiformats/go-multiaddr"

"github.com/getoptimum/optimum-common/pkg/logger"
commonnet "github.com/getoptimum/optimum-common/pkg/net"
)

func BuildQUICTCPAddr(
log logger.AppLogger,
publicIPV4,
publicIPV6 string,
listenPort int,
) []multiaddr.Multiaddr {
addrsTCP := commonnet.MustBuildAdvertisedAddresses(log, publicIPV4, publicIPV6, listenPort)
addrsQUIC := commonnet.MustBuildAdvertisedQUICAddresses(log, publicIPV4, publicIPV6, listenPort)
result := make([]multiaddr.Multiaddr, 0, len(addrsTCP)+len(addrsQUIC))
result = append(result, addrsTCP...)
result = append(result, addrsQUIC...)
return result
Comment thread
swarna1101 marked this conversation as resolved.
}
Loading