Documentation
¶
Overview ¶
Package dmsghttp pkg/dmsg/dmsghttp/debug.go c1-net-dmsg
Package dmsghttp pkg/dmsg/dmsghttp/debug_pprof.go c1-net-dmsg
Package dmsghttp pkg/dmsg/dmsghttp/http.go c1-net-dmsg
Package dmsghttp pkg/dmsg/dmsghttp/http_transport.go c1-net-dmsg
Package dmsghttp pkg/dmsg/dmsghttp/recursion_guard.go c1-net-dmsg
Recursion guard for dmsg-over-HTTP transports.
HTTPTransport.RoundTrip dials a fresh dmsg stream via the caller's dmsg.Client whenever there's no idle pooled stream. When the dmsg client's own discovery is itself wrapped with dmsgfirst (DMSG- first, plain-HTTP fallback), the dmsg-side path of dmsgfirst goes over THIS transport. So a discovery lookup triggered from inside RoundTrip — to obtain entries needed for the very dial that's in progress — re-enters RoundTrip on the same goroutine and recurses indefinitely until the stack overflows.
The cycle is structural to the dmsgfirst design (using dmsghttp for the primary path is what makes it dmsg-first), so the break has to happen at the inner layer: dmsgfirst notices it's been called from within a RoundTrip frame and routes the lookup through its plain-HTTP fallback for that one call only. The outer caller's flow continues on dmsg as normal.
The marker is a context value because contexts already propagate through the entire dmsg DialStream / EnsureAndObtainSession path. No extra threading required at any of the intermediate layers.
Package dmsghttp pkg/dmsg/dmsghttp/util.go c1-net-dmsg
Index ¶
- Constants
- func DebugMux(logSource func() []byte) *http.ServeMux
- func GetServers(ctx context.Context, dmsgDisc string, dmsgServerType string, ...) (entries []*disc.Entry)
- func IsInsideTransport(ctx context.Context) bool
- func ListenAndServe(ctx context.Context, _ cipher.SecKey, a http.Handler, _ disc.APIClient, ...) error
- func RemoteIsWhitelisted(r *http.Request, whitelistedPKs []cipher.PubKey) bool
- func ServeDebug(ctx context.Context, dmsgC *dmsg.Client, log *logging.Logger, ...) errordeprecated
- func UpdateServers(ctx context.Context, dClient disc.APIClient, dmsgDisc string, ...) (entries []*disc.Entry)
- func WhitelistMiddleware(whitelistedPKs []cipher.PubKey, next http.Handler) http.Handler
- func WithDebug(next http.Handler, whitelistPKs []cipher.PubKey, logSource func() []byte) http.Handler
- func WithRecursionGuard(ctx context.Context) context.Context
- type HTTPTransport
Examples ¶
Constants ¶
const DefaultDebugPort = uint16(81)
DefaultDebugPort is the dmsg port historically used for serving debug/pprof on its own listener. Prefer WithDebug to fold the same endpoints onto the service's main dmsg :80 handler (one port for /health + /metrics + /debug/*).
Variables ¶
This section is empty.
Functions ¶
func DebugMux ¶
DebugMux returns an http.ServeMux with the standard pprof endpoints, plus /debug/log serving logSource() when it is non-nil — the service's recent log output from an in-memory ring buffer (no disk file, no -s flag).
func GetServers ¶
func GetServers(ctx context.Context, dmsgDisc string, dmsgServerType string, log *logging.Logger) (entries []*disc.Entry)
GetServers is used to get all the available servers from the dmsg-discovery.
func IsInsideTransport ¶
IsInsideTransport reports whether ctx was tagged by WithRecursionGuard somewhere up the stack. dmsgfirst's fallbackClient checks this in each of its APIClient methods; when true, it short-circuits to the plain-HTTP fallback rather than calling its dmsghttp-backed primary.
func ListenAndServe ¶
func ListenAndServe(ctx context.Context, _ cipher.SecKey, a http.Handler, _ disc.APIClient, dmsgPort uint16, dmsgC *dmsg.Client, log *logging.Logger) error
ListenAndServe serves http over dmsg
func RemoteIsWhitelisted ¶
RemoteIsWhitelisted reports whether the request's remote public key is in the whitelist. When serving over dmsg, RemoteAddr is "<pk>:<port>".
For a handler that serves a REDUCED response to non-whitelisted callers rather than denying them — see the setup node's /stats, which is public in aggregate and gated only for the per-destination detail. An empty whitelist, or a RemoteAddr that will not parse, is not whitelisted: the caller then gets the public view, never the privileged one.
Deliberately not shared with WhitelistMiddleware below, which must keep distinguishing an unparseable RemoteAddr (500) from a denied one (401). Here both simply mean "not privileged".
func ServeDebug
deprecated
func ServeDebug(ctx context.Context, dmsgC *dmsg.Client, log *logging.Logger, whitelistPKs []cipher.PubKey) error
ServeDebug serves pprof endpoints over dmsg on DefaultDebugPort, gated by the provided whitelist public keys. It blocks until the context is canceled or an error occurs.
Deprecated: prefer WithDebug, which folds pprof + /debug/log onto the service's main dmsg :80 handler instead of a separate :81 listener.
An empty whitelist is logged at WARN and the middleware will reject every request — see WhitelistMiddleware for rationale.
func UpdateServers ¶
func UpdateServers(ctx context.Context, dClient disc.APIClient, dmsgDisc string, dmsgC *dmsg.Client, dmsgServerType string, log *logging.Logger) (entries []*disc.Entry)
UpdateServers is used to update the servers in the direct client.
func WhitelistMiddleware ¶
WhitelistMiddleware wraps an http.Handler with public-key-based access control. When serving over dmsg, RemoteAddr is in the format "<pk>:<port>".
An empty whitelist denies every request with 401. Pprof endpoints expose process internals (heap, goroutine, cmdline) and let any allowed caller pin CPU via /debug/pprof/profile, so fail-open on misconfiguration is not acceptable. The stock production binary embeds a non-empty deployment.Prod.SurveyWhitelist; only forks or SKYDEPLOY-overridden services configs without a survey_whitelist field hit this path, and for those denying access is the safe default.
func WithDebug ¶
func WithDebug(next http.Handler, whitelistPKs []cipher.PubKey, logSource func() []byte) http.Handler
WithDebug returns a handler that serves the survey-gated debug endpoints (/debug/pprof/* and, when logSource != nil, /debug/log) on the SAME listener as next, delegating every other path to next. This consolidates pprof + logs onto the service's main dmsg :80 instead of a separate debug port (matching the visor's logserver). Only /debug/* is whitelist-gated; next is unchanged.
func WithRecursionGuard ¶
WithRecursionGuard returns ctx tagged so dmsg-discovery clients downstream of HTTPTransport.RoundTrip (notably dmsgfirst) can tell they're being called recursively from within a transport dial and avoid re-entering the same transport.
Types ¶
type HTTPTransport ¶
type HTTPTransport struct {
// contains filtered or unexported fields
}
HTTPTransport implements http.RoundTripper. It dials fresh dmsg streams via dmsgC.DialStream, runs HTTP/1.1 request/response over the stream directly, and pools idle streams per destination so follow-up requests skip the noise handshake.
Pre-pool every HTTP request opened a brand-new dmsg stream and paid a full noise handshake — every visor heartbeat, every TPD/AR/SD poll. With ~50 visor heartbeats/sec across the network this drove dmsg-server CPU into the ground (production pprof showed handshakeResponder + secp256k1 ops at ~50 % cum on dmsg-server-1). Pooling collapses sustained per-host traffic onto a small number of long-lived streams.
We don't use the standard net/http.Transport with a custom DialContext because dmsg.Stream.Read auto-extends the read deadline to StreamIdleTimeout (a porter-cleanup safety net) on every successful read, which fights net/http's idle-conn detection in ways that produce flaky concurrent behavior under load. The pool here is small, focused, and uses one stream per in-flight request.
Do not confuse with a Skywire Transport implementation.
func MakeHTTPTransport ¶
func MakeHTTPTransport(_ context.Context, dmsgC *dmsg.Client) *HTTPTransport
MakeHTTPTransport returns an HTTPTransport with a per-destination stream pool. The ctx parameter is retained for API compatibility with pre-pool callers; pool lifecycle is driven by dmsgC.
Returns a pointer because the transport carries pool state that must be shared across RoundTrip calls. Callers that hold the returned value will share one pool — copying it via assignment silently divides the pool, which is why we return *HTTPTransport rather than HTTPTransport.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/net/nettest"
"github.com/skycoin/skywire/pkg/cipher"
"github.com/skycoin/skywire/pkg/dmsg/disc"
"github.com/skycoin/skywire/pkg/dmsg/dmsg"
"github.com/skycoin/skywire/pkg/dmsg/dmsghttp"
"github.com/skycoin/skywire/pkg/httputil"
)
func main() {
logrus.SetLevel(logrus.FatalLevel)
const maxSessions = 100
const dmsgHTTPPort = 80
// Create a mock dmsg discovery.
dc := disc.NewMock(0)
// Create a dmsg server to relay all requests/responses.
srvPK, srvSK := cipher.GenerateKeyPair()
srvConf := dmsg.ServerConfig{
MaxSessions: maxSessions,
UpdateInterval: 0,
}
srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil)
defer func() {
if err := srv.Close(); err != nil {
panic(err)
}
}()
go func() {
lis, err := nettest.NewLocalListener("tcp")
if err != nil {
panic(err)
}
if err := srv.Serve(lis, ""); err != nil {
panic(err)
}
}()
<-srv.Ready()
// Create a dmsg client to host http server.
c1PK, c1SK := cipher.GenerateKeyPair()
dmsgC1 := dmsg.NewClient(c1PK, c1SK, dc, nil)
defer func() {
if err := dmsgC1.Close(); err != nil {
panic(err)
}
}()
go dmsgC1.Serve(context.Background())
<-dmsgC1.Ready()
// Allow discovery entry to propagate
time.Sleep(time.Second)
// Host HTTP server via dmsg client 1.
lis, err := dmsgC1.Listen(dmsgHTTPPort)
if err != nil {
panic(err)
}
defer func() {
if err := lis.Close(); err != nil {
panic(err)
}
}()
r := httputil.NewRouter()
// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("<html><body><h1>Hello World!</h1></body></html>")) //nolint:errcheck
})
go func() { _ = http.Serve(lis, r) }() //nolint
// Create dmsg client to run http client.
c2PK, c2SK := cipher.GenerateKeyPair()
dmsgC2 := dmsg.NewClient(c2PK, c2SK, dc, nil)
defer func() {
if err := dmsgC2.Close(); err != nil {
panic(err)
}
}()
go dmsgC2.Serve(context.Background())
<-dmsgC2.Ready()
// Allow all sessions and discovery entries to stabilize.
time.Sleep(2 * time.Second)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Run HTTP client.
httpC := http.Client{
Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgC2),
Timeout: 30 * time.Second,
}
resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/", c1PK.String(), dmsgHTTPPort))
if err != nil {
panic(err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
panic(err)
}
}()
readB, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
// Print read data.
fmt.Println("READ:", string(readB))
}
Output: READ: <html><body><h1>Hello World!</h1></body></html>
func (*HTTPTransport) CloseIdleConnections ¶
func (t *HTTPTransport) CloseIdleConnections()
CloseIdleConnections closes every idle stream the transport currently holds. Useful for tests and for callers that want to force a clean restart of connectivity (e.g. after a dmsg session reset). Pre-existing in-flight requests on streams that are not yet idle are unaffected.
func (*HTTPTransport) RoundTrip ¶
RoundTrip implements http.RoundTripper. Reuses an idle stream to the destination if one is available; otherwise dials a fresh one. After the response body is fully read and closed, the stream goes back into the idle pool. On any I/O error the stream is discarded.
func (*HTTPTransport) SetSelfDialer ¶ added in v1.3.98
SetSelfDialer makes requests addressed to pk, the owner of this transport, go to dial instead of over dmsg: dial returns an in-process connection to whatever the owner serves on that dmsg port. A visor that hosts a deployment service under its own key reaches it this way.