Skip to content

Proxy Setup

Overview

Shoutrrr supports proxying HTTP requests for notification services, allowing you to route traffic through a proxy server. This can be configured using an environment variable or by customizing the HTTP client in code.

For per-sender egress control and SSRF protection (recommended for untrusted notification URLs), use a custom http.Client and DialContext via SenderOptions.

shoutrrr.Send cannot take these options. Use shoutrrr.NewSenderWithOptions, CreateSenderWithOptions, or router.NewWithOptions.

Usage

Environment Variable

Set the HTTP_PROXY environment variable to the proxy URL. This applies to all HTTP-based services used by Shoutrrr that rely on the default transport.

Set HTTP_PROXY Environment Variable
export HTTP_PROXY="socks5://localhost:1337"

Note

This is a process-global setting and affects http.DefaultClient and clients that inherit http.DefaultTransport. It is not suitable for per-sender SSRF controls.

Supply a custom *http.Client (with custom Transport, DialContext, TLS config, etc.) when creating a sender or router. The client is injected into services that support it and used for all their outbound HTTP.

Use shoutrrr.NewSenderWithOptions (or CreateSenderWithOptions, router.NewWithOptions).

Configure Custom HTTP Client for SSRF Protection
package main

import (
 "context"
 "fmt"
 "log"
 "net"
 "net/http"
 "time"

 "github.com/nicholas-fedor/shoutrrr"
 "github.com/nicholas-fedor/shoutrrr/pkg/types"
)

// isBlockedIP is an example SSRF guard. Implement your own policy.
func isBlockedIP(ip net.IP) bool {
 return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

func dialAllowed(ctx context.Context, network, addr string) (net.Conn, error) {
 host, port, err := net.SplitHostPort(addr)
 if err != nil {
  return nil, err
 }
 ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
 if err != nil {
  return nil, err
 }
 d := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}
 for _, ip := range ips {
  if isBlockedIP(ip.IP) {
   continue
  }
  conn, err := d.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port))
  if err == nil {
   return conn, nil
  }
 }
 return nil, &net.OpError{Op: "dial", Net: network, Err: fmt.Errorf("destination blocked by egress policy")}
}

func main() {
 // Custom Transport with DialContext that performs egress/SSRF checks.
 transport := &http.Transport{
  DialContext: dialAllowed,
  // Proxy: http.ProxyFromEnvironment, // opt-in if you also want env proxies for this client
  ForceAttemptHTTP2:     true,
  MaxIdleConns:          100,
  IdleConnTimeout:       90 * time.Second,
  TLSHandshakeTimeout:   10 * time.Second,
  ExpectContinueTimeout: 1 * time.Second,
 }

 customClient := &http.Client{
  Transport: transport,
  Timeout:   60 * time.Second,
 }

  opts := types.SenderOptions{
   HTTPClient:  customClient,
   DialContext: transport.DialContext,
   // Timeout: 30 * time.Second, // optional per-router override
  }

 url := "discord://abc123@123456789"
 sender, err := shoutrrr.NewSenderWithOptions(nil, opts, url)
 if err != nil {
  log.Fatal(err)
 }

 if errs := sender.Send("Hello via custom client!", nil); len(errs) > 0 {
  for _, e := range errs {
   log.Println("Error:", e)
  }
 }
}

Notes on custom clients and dialers:

  • A non-nil SenderOptions.HTTPClient is propagated by the router to services implementing types.HTTPClientSetter.
  • A non-nil SenderOptions.DialContext is propagated to services implementing types.DialContextSetter (SMTP and MQTT). TLS wrapping still happens after the TCP dial.
  • DialContext must be safe for concurrent use. A custom dialer bypasses MQTT all_proxy; implement proxying in the function if needed.
  • Custom clients usually bypass HTTP_PROXY/HTTPS_PROXY unless their Transport.Proxy is configured to consult the environment.
  • All default timeouts/TLS behavior is preserved when no custom client or dialer is supplied.
  • The same client instance is reused for the lifetime of the sender/router.

Examples

Using Environment Variable for Proxy (Global)

Example

Set Proxy and Send Notification
export HTTP_PROXY="socks5://localhost:1337"
shoutrrr send --url "discord://abc123@123456789" --message "Hello via proxy!"
Expected Output
Notification sent

Using Custom HTTP Client in Go (Per-Sender / SSRF Control)

Example

Send Notification with Custom Client (SSRF-safe)
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "net/http"
    "time"

    "github.com/nicholas-fedor/shoutrrr"
    "github.com/nicholas-fedor/shoutrrr/pkg/types"
)

func isBlockedIP(ip net.IP) bool {
    return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

func main() {
    transport := &http.Transport{
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            host, port, err := net.SplitHostPort(addr)
            if err != nil {
                return nil, err
            }
            ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
            if err != nil {
                return nil, err
            }
            d := &net.Dialer{Timeout: 30 * time.Second}
            for _, ip := range ips {
                if isBlockedIP(ip.IP) {
                    continue
                }
                conn, err := d.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port))
                if err == nil {
                    return conn, nil
                }
            }
            return nil, &net.OpError{Op: "dial", Net: network, Err: fmt.Errorf("destination blocked by egress policy")}
        },
    }
    custom := &http.Client{Transport: transport}

    sender, err := shoutrrr.NewSenderWithOptions(nil, types.SenderOptions{HTTPClient: custom, DialContext: transport.DialContext}, "discord://abc123@123456789")
    if err != nil {
        log.Fatal(err)
    }
    if errs := sender.Send("Hello via custom egress-controlled client!", nil); len(errs) > 0 {
        for _, e := range errs {
            log.Println(e)
        }
    }
}
Expected Output (Success)
(No output on success)
Expected Output (Error)
Error: failed to send message: unexpected response status code

Notes

  • Environment Variable: HTTP_PROXY supports protocols like http, https, or socks5. It affects all HTTP-based services globally.
  • Custom HTTP Client: Provides fine-grained control over proxy settings, suitable for Go applications requiring specific transport configurations.
  • Custom DialContext: Applies the same destination policy to SMTP and MQTT TCP connections. HTTP services continue to use HTTPClient.
  • Service Compatibility: Ensure the proxy supports the protocol used by the service (e.g., HTTPS for Discord, SMTP).
  • Timeouts: The custom client example includes a 30-second dial timeout and 10-second TLS handshake timeout, adjustable as needed.