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.
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).
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.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 HTTP_PROXY="socks5://localhost:1337"
shoutrrr send --url "discord://abc123@123456789" --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://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)
}
}
}
(No output on success)
Error: failed to send message: unexpected response status code
Notes¶
- Environment Variable:
HTTP_PROXYsupports protocols likehttp,https, orsocks5. 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.