Documentation
¶
Overview ¶
Package remote handles Ether's network presence: LAN address enumeration, WAN IP detection, Plex connection URIs (plain IP and *.plex.direct), UPnP IGD and NAT-PMP port mapping, GDM LAN discovery and reachability probes.
It has no dependency on plex.tv; internal/plextv drives it.
Index ¶
- Constants
- Variables
- func DefaultGateway() (net.IP, error)
- func GDMHello(i GDMInfo, bye bool) []byte
- func GDMResponse(i GDMInfo) []byte
- func IsPublicIP(ip net.IP) bool
- func IsVirtualInterface(name string) bool
- func LANIPs(opts LANOptions) ([]net.IP, error)
- func ParsePlexDirectHost(host string) (ip net.IP, hash string, ok bool)
- func PlexDirectHost(ip net.IP, hash string) string
- func PlexDirectLabel(ip net.IP) string
- func URIs(cs []Connection) []string
- func UsableLANIP(ip net.IP) bool
- type BuildOptions
- type Connection
- type DiscoverOptions
- type GDMInfo
- type GDMOptions
- type GDMReply
- type GDMResponder
- type IGD
- func (g *IGD) AddPortMapping(ctx context.Context, protocol string, external, internal int, ...) error
- func (g *IGD) DeletePortMapping(ctx context.Context, protocol string, external int) error
- func (g *IGD) ExternalIP(ctx context.Context) (net.IP, error)
- func (g *IGD) GetSpecificPortMapping(ctx context.Context, protocol string, external int) (*PortMappingEntry, error)
- type InterfaceAddr
- type LANOptions
- type MappingStatus
- type NATPMP
- type NATPMPError
- type PortMapper
- type PortMapperOptions
- type PortMappingEntry
- type ProbeResult
- type Reachability
- type ReachabilityChecker
- type UPnPError
- type WANDetector
Constants ¶
const ( GDMMulticastGroup = "239.0.0.250" GDMServerPort = 32414 // GDMAnnouncePort receives HELLO/BYE announcements. GDMAnnouncePort = 32413 )
GDM ("G'Day Mate") is Plex's LAN discovery protocol. Clients send "M-SEARCH * HTTP/1.1" to UDP 32414 on multicast group 239.0.0.250 (and by broadcast); every PMS answers unicast with an "HTTP/1.0 200 OK" block of headers describing itself. PMS listens on 32410, 32412, 32413 and 32414.
const ( MethodUPnP = "upnp" MethodNATPMP = "nat-pmp" )
Port mapping methods.
const ( ServiceWANIPConnection2 = "urn:schemas-upnp-org:service:WANIPConnection:2" ServiceWANIPConnection1 = "urn:schemas-upnp-org:service:WANIPConnection:1" ServiceWANPPPConnection1 = "urn:schemas-upnp-org:service:WANPPPConnection:1" )
UPnP IGD service types, best first.
const ( UPnPErrConflictInMappingEntry = 718 UPnPErrOnlyPermanentLeasesSupported = 725 UPnPErrExternalPortOnlySupportsWild = 727 )
Well-known UPnP error codes.
const ( SecureRequired = "required" SecurePreferred = "preferred" SecureDisabled = "disabled" )
Secure-connection modes, matching config.Plex.SecureConnections and PMS's "Secure connections" preference.
const NATPMPPort = 5351
NATPMPPort is the gateway port NAT-PMP (RFC 6886) listens on.
const SSDPMulticast = "239.255.255.250:1900"
SSDPMulticast is the standard SSDP group.
Variables ¶
var DefaultGDMPorts = []int{32410, 32412, 32413, 32414}
DefaultGDMPorts are the UDP ports PMS answers GDM on.
var DefaultWANEndpoints = []string{
"https://v4.plex.tv/pms/:/ip",
"https://api.ipify.org",
"https://icanhazip.com",
"https://ifconfig.me/ip",
}
DefaultWANEndpoints are queried in order. The first is the endpoint PMS itself uses (GET https://v4.plex.tv/pms/:/ip in observed PMS traffic); the rest are well-known plain-text fallbacks.
var ErrNoIGD = errors.New("remote: no UPnP internet gateway found")
ErrNoIGD is returned when no usable gateway answered.
var ErrNoWANIP = errors.New("remote: could not detect public IP")
ErrNoWANIP is returned when no endpoint produced a public address.
Functions ¶
func DefaultGateway ¶
DefaultGateway returns the IPv4 default gateway. On Linux it reads /proc/net/route; elsewhere it guesses the ".1" address of the first private LAN subnet, which is right for the vast majority of home networks. NAT-PMP is only a fallback after UPnP, so a wrong guess just means the fallback fails.
func GDMHello ¶
GDMHello renders an announcement ("HELLO * HTTP/1.0") or, with bye, a departure ("BYE * HTTP/1.0").
func IsPublicIP ¶
IsPublicIP reports whether ip is a globally routable unicast address.
func IsVirtualInterface ¶
IsVirtualInterface reports whether an interface name looks like a container bridge, VPN tunnel or hypervisor adapter.
func LANIPs ¶
func LANIPs(opts LANOptions) ([]net.IP, error)
LANIPs is LANAddresses reduced to the IPs.
func ParsePlexDirectHost ¶
ParsePlexDirectHost reverses PlexDirectHost. ok is false for hosts that are not "<ip-label>.<hash>.plex.direct".
func PlexDirectHost ¶
PlexDirectHost returns the plex.direct host for ip under a certificate hash: 192.168.1.5 -> "192-168-1-5.<hash>.plex.direct". IPv6 addresses use their expanded colon groups joined by dashes, as plex.tv does.
func PlexDirectLabel ¶
PlexDirectLabel is the IP-encoding first label of a plex.direct host.
func UsableLANIP ¶
UsableLANIP reports whether ip can be published as a LAN connection.
Types ¶
type BuildOptions ¶
type BuildOptions struct {
LAN []net.IP
WAN net.IP // nil when unknown or remote access is off
LocalPort int // port the server listens on (32400)
PublicPort int // external port on the WAN; 0 = LocalPort
// CertHash is the plex.direct hash of a valid plex.direct certificate.
// Without it no https plex.direct URIs are produced.
CertHash string
// Secure is SecureRequired, SecurePreferred (default) or SecureDisabled.
Secure string
// CustomURLs are owner-supplied URLs (reverse proxies, DDNS names).
CustomURLs []string
// HTTPSOnPlainIP adds https://<ip>:port URIs (self-signed setups). Off
// by default because clients cannot validate a certificate for a bare IP.
HTTPSOnPlainIP bool
}
BuildOptions describes the server's addresses for BuildConnections.
type Connection ¶
type Connection struct {
Protocol string `json:"protocol"` // "http" or "https"
Address string `json:"address"` // IP or host
Port int `json:"port"`
URI string `json:"uri"`
Local bool `json:"local"`
Relay bool `json:"relay,omitempty"`
IPv6 bool `json:"IPv6,omitempty"`
Custom bool `json:"custom,omitempty"` // owner-supplied custom URL
}
Connection is one way to reach the server, in the shape plex.tv's resources API returns it.
func BuildConnections ¶
func BuildConnections(o BuildOptions) []Connection
BuildConnections returns the connection list for the server, best first: LAN before WAN, https plex.direct before plain http (unless secure connections are disabled), then custom URLs.
func ParseCustomURL ¶
func ParseCustomURL(raw string) (Connection, error)
ParseCustomURL validates an owner-supplied connection URL.
type DiscoverOptions ¶
type DiscoverOptions struct {
// Target is where M-SEARCH is sent; SSDPMulticast when empty. Tests
// point it at a loopback responder.
Target string
// Wait bounds how long responses are collected (default 3s).
Wait time.Duration
Client *http.Client
}
DiscoverOptions tunes SSDP discovery.
type GDMInfo ¶
type GDMInfo struct {
Name string
MachineIdentifier string
Port int
Version string
UpdatedAt time.Time
// ContentType defaults to "plex/media-server".
ContentType string
}
GDMInfo is what the responder advertises.
type GDMOptions ¶
type GDMOptions struct {
// Info is called per request so name/version changes apply live.
Info func() GDMInfo
// Ports to listen on; DefaultGDMPorts when empty.
Ports []int
// ListenAddrs, when set, replaces Ports/multicast with plain unicast
// sockets on these addresses (e.g. "127.0.0.1:0" in tests).
ListenAddrs []string
// Announce sends HELLO on start and every AnnounceInterval (default
// 15m) to the multicast group, and BYE on stop. Off by default: clients
// discover servers by searching, and current PMS builds rely on that.
Announce bool
AnnounceInterval time.Duration
Logger *slog.Logger
// contains filtered or unexported fields
}
GDMOptions configures a GDMResponder.
type GDMReply ¶
type GDMReply struct {
From *net.UDPAddr
ContentType string
Name string
MachineIdentifier string
Port int
Version string
UpdatedAt time.Time
Headers http.Header
}
GDMReply is one server's answer to a GDM search.
func DiscoverGDM ¶
DiscoverGDM sends one M-SEARCH to target ("239.0.0.250:32414" when empty) and collects replies until ctx is done or wait elapses.
func ParseGDMReply ¶
ParseGDMReply parses an "HTTP/1.0 200 OK" GDM reply.
type GDMResponder ¶
type GDMResponder struct {
// contains filtered or unexported fields
}
GDMResponder answers GDM discovery on the LAN.
func NewGDMResponder ¶
func NewGDMResponder(o GDMOptions) *GDMResponder
NewGDMResponder returns a responder; call Start.
func (*GDMResponder) Addrs ¶
func (g *GDMResponder) Addrs() []net.Addr
Addrs returns the bound local addresses.
func (*GDMResponder) Start ¶
func (g *GDMResponder) Start(ctx context.Context) error
Start opens the sockets and serves until ctx is done or Stop is called. Ports that cannot be bound (e.g. a real PMS on the same host) are logged and skipped; Start fails only when no socket could be opened.
func (*GDMResponder) Stop ¶
func (g *GDMResponder) Stop()
Stop closes the sockets and waits for the serve loops.
type IGD ¶
type IGD struct {
Location string // device description URL
FriendlyName string
ServiceType string
ControlURL string
// LocalIP is this host's address on the interface that reaches the
// gateway: the internal client for port mappings.
LocalIP net.IP
Client *http.Client
}
IGD is a discovered UPnP Internet Gateway Device connection service.
func DiscoverIGD ¶
func DiscoverIGD(ctx context.Context, o DiscoverOptions) (*IGD, error)
DiscoverIGD finds a UPnP gateway with a WAN connection service.
func (*IGD) AddPortMapping ¶
func (g *IGD) AddPortMapping(ctx context.Context, protocol string, external, internal int, internalClient net.IP, description string, lease time.Duration) error
AddPortMapping maps external TCP/UDP port to internalClient:internalPort for lease (0 = permanent).
func (*IGD) DeletePortMapping ¶
DeletePortMapping removes a mapping.
func (*IGD) ExternalIP ¶
ExternalIP asks the gateway for its WAN address.
func (*IGD) GetSpecificPortMapping ¶
func (g *IGD) GetSpecificPortMapping(ctx context.Context, protocol string, external int) (*PortMappingEntry, error)
GetSpecificPortMapping looks up the mapping for an external port.
type InterfaceAddr ¶
InterfaceAddr is one usable LAN address.
func LANAddresses ¶
func LANAddresses(opts LANOptions) ([]InterfaceAddr, error)
LANAddresses lists the addresses Plex clients on the local network can reach this machine on: up, non-loopback, non-link-local, non-virtual interfaces. Private IPv4 addresses sort first.
type LANOptions ¶
type LANOptions struct {
// IncludeIPv6 adds global/ULA IPv6 addresses. PMS publishes IPv4 LAN
// addresses by default; plex.tv only returns IPv6 connections when the
// client asks with includeIPv6=1.
IncludeIPv6 bool
// IncludeVirtual keeps docker/VPN/hypervisor interfaces.
IncludeVirtual bool
}
LANOptions tunes LAN address enumeration.
type MappingStatus ¶
type MappingStatus struct {
Active bool `json:"active"`
Method string `json:"method,omitempty"` // "upnp" or "nat-pmp"
Gateway string `json:"gateway,omitempty"` // IGD friendly name or NAT-PMP address
ExternalIP string `json:"externalIp,omitempty"`
ExternalPort int `json:"externalPort,omitempty"`
InternalIP string `json:"internalIp,omitempty"`
InternalPort int `json:"internalPort,omitempty"`
Lease string `json:"lease,omitempty"` // "permanent" or a duration
RenewAt time.Time `json:"renewAt,omitempty"`
LastError string `json:"lastError,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
}
MappingStatus is the port mapper's state for the admin dashboard.
type NATPMP ¶
type NATPMP struct {
// Gateway is "ip:port" of the gateway's NAT-PMP service.
Gateway string
// Tries bounds retransmissions (RFC: 250ms doubling); default 4.
Tries int
}
NATPMP is a minimal RFC 6886 client.
func (*NATPMP) ExternalIP ¶
ExternalIP asks the gateway for its public address.
type NATPMPError ¶
type NATPMPError struct{ Code uint16 }
NATPMPError is a non-zero NAT-PMP result code.
func (*NATPMPError) Error ¶
func (e *NATPMPError) Error() string
type PortMapper ¶
type PortMapper struct {
// contains filtered or unexported fields
}
PortMapper keeps one TCP port mapping alive on the LAN gateway, via UPnP IGD first and NAT-PMP second. Ensure is idempotent and cheap to call on an interval; Close removes the mapping.
func NewPortMapper ¶
func NewPortMapper(o PortMapperOptions) *PortMapper
NewPortMapper returns a mapper; nothing happens until Ensure.
func (*PortMapper) Close ¶
func (m *PortMapper) Close(ctx context.Context)
Close removes the mapping from the gateway (on shutdown or when remote access is turned off).
func (*PortMapper) Ensure ¶
func (m *PortMapper) Ensure(ctx context.Context, internal, external int) (MappingStatus, error)
Ensure makes sure external TCP port maps to this host's internal port, creating or renewing the mapping when needed. It returns the current status. After a failure it waits a minute before trying again.
func (*PortMapper) Status ¶
func (m *PortMapper) Status() MappingStatus
Status returns a snapshot of the mapping state.
type PortMapperOptions ¶
type PortMapperOptions struct {
// Description appears in the router's mapping table.
Description string
// Lease requested from the gateway (default 2h). Renewed at half-life.
Lease time.Duration
// DiscoverIGD finds a UPnP gateway; nil uses SSDP multicast discovery.
DiscoverIGD func(ctx context.Context) (*IGD, error)
// Gateway returns the NAT-PMP gateway; nil uses DefaultGateway. Return
// an error to disable NAT-PMP.
Gateway func() (net.IP, error)
// NATPMPPort overrides the NAT-PMP port (tests).
NATPMPPort int
Logger *slog.Logger
Now func() time.Time
}
PortMapperOptions configures a PortMapper.
type PortMappingEntry ¶
type PortMappingEntry struct {
InternalPort int
InternalClient string
Enabled bool
Description string
Lease time.Duration
}
PortMappingEntry is an existing mapping on the gateway.
type ProbeResult ¶
type ProbeResult struct {
URI string `json:"uri"`
OK bool `json:"ok"`
Latency string `json:"latency,omitempty"`
Error string `json:"error,omitempty"`
}
ProbeResult is one URI's outcome.
type Reachability ¶
type Reachability struct {
Checked time.Time `json:"checked"`
Reachable bool `json:"reachable"`
// URI is the first URI that answered with this server's identity.
URI string `json:"uri,omitempty"`
// Results has one entry per URI tried.
Results []ProbeResult `json:"results"`
}
Reachability is the result of a remote-access self-test.
type ReachabilityChecker ¶
ReachabilityChecker probes connection URIs for this server's /identity.
Probing the WAN address from inside the LAN relies on the router's NAT loopback (hairpinning). Routers without it make a correctly forwarded port look unreachable, so a negative result is reported as such and callers should present it as "could not verify", not as a hard failure.
func NewReachabilityChecker ¶
func NewReachabilityChecker(client *http.Client) *ReachabilityChecker
NewReachabilityChecker uses client (a 10s-timeout client when nil).
func (*ReachabilityChecker) Check ¶
func (r *ReachabilityChecker) Check(ctx context.Context, uris []string, machineID string) Reachability
Check probes uris concurrently and reports whether any answered with machineID.
type WANDetector ¶
WANDetector finds this network's public IPv4 address.
func NewWANDetector ¶
NewWANDetector returns a detector over endpoints (DefaultWANEndpoints when empty) with a short per-request timeout.