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 HTTPS_PROXY environment variable to the proxy URL for services that use HTTPS, which is most of them, and HTTP_PROXY for services configured to use plain HTTP. Hosts listed in NO_PROXY are reached directly.
Every HTTP-based service honors these variables when it uses its own default client.
export HTTPS_PROXY="socks5://localhost:1337"
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.
Custom HTTP Client (Per-Sender, Recommended for SSRF/Egress Control)¶
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).
The examples on this page send to a Discord webhook.
Replace TOKEN and WEBHOOK_ID with its token and ID, which the Discord page shows how to find.
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://TOKEN@WEBHOOK_ID"
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.HTTPClientis propagated by the router to services implementingtypes.HTTPClientSetter. - A non-nil
SenderOptions.DialContextis propagated to services implementingtypes.DialContextSetter(SMTP and MQTT). TLS wrapping still happens after the TCP dial. DialContextmust be safe for concurrent use. A custom dialer bypasses MQTTall_proxy; implement proxying in the function if needed.- Custom clients usually bypass
HTTP_PROXY/HTTPS_PROXYunless theirTransport.Proxyis 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
export HTTPS_PROXY="socks5://localhost:1337"
shoutrrr send --url "discord://TOKEN@WEBHOOK_ID" --message "Hello via proxy!"
Notification sent
Using Custom HTTP Client in Go (Per-Sender / SSRF Control)¶
Example
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://TOKEN@WEBHOOK_ID")
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)
}
}
}
(No output on success)
Error: failed to send message: unexpected response status code
Notes¶
- Environment Variables:
HTTPS_PROXYapplies to HTTPS requests andHTTP_PROXYto plain HTTP requests. Both accepthttp,https, orsocks5proxy URLs, andNO_PROXYlists hosts to reach directly. Go reads them once, when the process sends its first HTTP request, so set them before starting the application. They affect every HTTP-based service that uses its own default client. - 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.