Module: Lich::Common::BindHostResolver
- Defined in:
- documented/common/bind_host_resolver.rb
Overview
Resolves keyword bind hosts (+tailscale+, lan, any) into concrete
local addresses so users never have to discover their own interface IPs.
Non-keyword hosts pass through unchanged, gaining only an advisory
warning when they look reachable from untrusted networks.
This is the single bind-token vocabulary for every listener Lich
opens -- the frontend socket, the --game proxy, and the detachable
client all route their --bind-address / host tokens through here.
Each of those is an unauthenticated plaintext socket that can drive
the game session, so every resolution away from loopback carries a
warning for the caller to surface to the user.
Defined Under Namespace
Classes: Resolution
Constant Summary collapse
- Error =
Raised when a keyword host cannot be resolved to a usable address.
Class.new(StandardError)
- TAILSCALE_RANGE =
Tailscale assigns every node an address from the CGNAT range.
IPAddr.new('100.64.0.0/10').freeze
- PRIVATE_RANGES =
RFC1918 ranges in preference order for
lan: household routers hand out 192.168/16 and 10/8, while Docker bridges, WSL adapters, and VM host-only interfaces typically squat on 172.16/12. [ IPAddr.new('192.168.0.0/16').freeze, IPAddr.new('10.0.0.0/8').freeze, IPAddr.new('172.16.0.0/12').freeze ].freeze
- ANY_WARNING =
"binding 0.0.0.0 exposes Lich's unauthenticated listen sockets " \ 'on every network this machine is connected to; anyone who ' \ 'can reach them can control the session'
Class Method Summary collapse
-
.default_route_address ⇒ Object
Learns the source address the OS would pick for outbound traffic by connect()ing a UDP socket to a public IP - no packet is actually sent.
-
.ipv4_addresses(address_list) ⇒ Array<String>
private
Extracts and deduplicates IPv4 addresses from a list of network interfaces.
-
.private_address?(address) ⇒ Boolean
private
Checks whether an address is in a private (RFC1918 or IPv6 ULA) range.
-
.resolve(token, address_list: Socket.ip_address_list, route_probe: method(:default_route_address)) ⇒ Resolution
Resolves a bind host token to a concrete address.
-
.resolve_lan(address_list, route_probe) ⇒ Resolution
private
Finds the machine's LAN address suitable for local-network binding.
-
.resolve_tailscale(address_list) ⇒ Resolution
private
Finds the machine's Tailscale address from the local interface list.
-
.warning_for_explicit(token) ⇒ String?
private
Checks whether an explicitly provided bind address is safe or warns the user.
Class Method Details
.default_route_address ⇒ Object
Learns the source address the OS would pick for outbound traffic by connect()ing a UDP socket to a public IP - no packet is actually sent. This selects the interface holding the default route, which is the address other LAN devices can reach, unlike a naive first-interface scan that may land on a Docker bridge or VM adapter.
122 123 124 125 126 127 128 129 |
# File 'documented/common/bind_host_resolver.rb', line 122 def self.default_route_address UDPSocket.open do |socket| socket.connect('8.8.8.8', 53) socket.addr[3] end rescue StandardError nil end |
.ipv4_addresses(address_list) ⇒ Array<String>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Extracts and deduplicates IPv4 addresses from a list of network interfaces.
161 162 163 |
# File 'documented/common/bind_host_resolver.rb', line 161 def self.ipv4_addresses(address_list) address_list.select(&:ipv4?).map(&:ip_address).uniq end |
.private_address?(address) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Checks whether an address is in a private (RFC1918 or IPv6 ULA) range.
170 171 172 173 174 |
# File 'documented/common/bind_host_resolver.rb', line 170 def self.private_address?(address) IPAddr.new(address).private? rescue IPAddr::InvalidAddressError false end |
.resolve(token, address_list: Socket.ip_address_list, route_probe: method(:default_route_address)) ⇒ Resolution
Resolves a bind host token to a concrete address.
57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'documented/common/bind_host_resolver.rb', line 57 def self.resolve(token, address_list: Socket.ip_address_list, route_probe: method(:default_route_address)) case token.to_s.downcase when 'tailscale' resolve_tailscale(address_list) when 'lan' resolve_lan(address_list, route_probe) when 'any' Resolution.new(host: '0.0.0.0', warning: ANY_WARNING) else Resolution.new(host: token, warning: warning_for_explicit(token)) end end |
.resolve_lan(address_list, route_probe) ⇒ Resolution
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Finds the machine's LAN address suitable for local-network binding.
Probes the default route to identify the preferred interface, then searches for a private IPv4 address in RFC1918 ranges (192.168/16, 10/8, 172.16/12) in preference order. Returns with a warning about network exposure.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'documented/common/bind_host_resolver.rb', line 96 def self.resolve_lan(address_list, route_probe) candidates = ipv4_addresses(address_list).select { |ip| private_address?(ip) } probed = begin route_probe.call rescue StandardError nil end address = if probed && candidates.include?(probed) probed else PRIVATE_RANGES.lazy.map { |range| candidates.find { |ip| range.include?(ip) } }.find(&:itself) end raise Error, 'no private (LAN) IPv4 address found on this machine' unless address Resolution.new( host: address, warning: "Lich's listen sockets are unauthenticated; anyone on your network " \ "can control this session via #{address}. Prefer the tailscale keyword if you use Tailscale" ) end |
.resolve_tailscale(address_list) ⇒ Resolution
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Finds the machine's Tailscale address from the local interface list.
76 77 78 79 80 81 82 83 |
# File 'documented/common/bind_host_resolver.rb', line 76 def self.resolve_tailscale(address_list) address = ipv4_addresses(address_list).find { |ip| TAILSCALE_RANGE.include?(ip) } unless address raise Error, "Tailscale doesn't appear to be running on this machine " \ '(no 100.64.0.0/10 address found); start Tailscale or use the lan keyword instead' end Resolution.new(host: address, warning: nil) end |
.warning_for_explicit(token) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Checks whether an explicitly provided bind address is safe or warns the user.
Returns a warning string if the address looks reachable from untrusted networks. Loopback, private, and Tailscale addresses pass through with no warning. Non-IP hostnames return nil (cannot judge without resolution).
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'documented/common/bind_host_resolver.rb', line 140 def self.warning_for_explicit(token) ip = IPAddr.new(token.to_s) if ip.ipv4? return ANY_WARNING if ip == IPAddr.new('0.0.0.0') return nil if ip.loopback? || ip.private? || TAILSCALE_RANGE.include?(ip) else return ANY_WARNING if ip == IPAddr.new('::') return nil if ip.loopback? || ip.private? || ip.link_local? end "#{token} is not a private address; Lich's unauthenticated listen sockets " \ 'may be reachable from untrusted networks' rescue IPAddr::InvalidAddressError # A hostname - nothing to judge without resolving it here. nil end |