Skip to content

Instantly share code, notes, and snippets.

@MDCore
Created July 9, 2026 23:03
Show Gist options
  • Select an option

  • Save MDCore/5c6821b4f71289e3afa7f24e32ee854d to your computer and use it in GitHub Desktop.

Select an option

Save MDCore/5c6821b4f71289e3afa7f24e32ee854d to your computer and use it in GitHub Desktop.
koreader patch to allow http proxies when using tailscale
-- 2-https-proxy.lua
-- KOReader userpatch: enable HTTPS requests through an HTTP CONNECT proxy.
--
-- Stock LuaSec (ssl.https) refuses to use a proxy for HTTPS ("proxy not
-- supported"). This is a problem when reaching tailnet services via a
-- userspace Tailscale HTTP-CONNECT proxy (e.g. 127.0.0.1:1056) on devices
-- with no TUN module, where DNS for *.ts.net can't be resolved locally.
--
-- This patch reimplements ssl.https' internal `tcp` connector to open a
-- CONNECT tunnel through the proxy (so the proxy resolves the hostname and
-- relays the encrypted stream), and rewrites `request` to stop bailing out
-- when a proxy is set. Ported from DhruvaSambrani's LuaSec patch
-- (add-proxy-redirect-custom); see koreader/koreader#14693.
--
-- Drop this file in koreader/patches/ . It runs at startup (priority 2,
-- after base modules are available).
local logger = require("logger")
local ok, https = pcall(require, "ssl.https")
if not ok or type(https) ~= "table" then
logger.warn("[https-proxy patch] ssl.https not available, skipping")
return
end
local socket = require("socket")
local ssl = require("ssl")
local http = require("socket.http")
local url = require("socket.url")
local mime = require("mime")
local try = socket.try
-- Default TLS config used by LuaSec's https.lua.
-- Mirror the stock module's cfg so we don't weaken/alter TLS behaviour.
local cfg = {
protocol = "any",
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1" },
verify = "none",
}
local PORT = https.PORT or 443
local TIMEOUT = https.TIMEOUT or 60
-- Re-register the SSL socket's methods onto the connection wrapper,
-- same as stock https.lua's reg().
local function reg(conn)
local mt = getmetatable(conn.sock).__index
for name, method in pairs(mt) do
if type(method) == "function" then
conn[name] = function(self, ...)
return method(self.sock, ...)
end
end
end
end
-- Check if a host matches a no_proxy list (comma-separated; "*", exact,
-- or leading-dot suffix match).
local function should_bypass_proxy(host, no_proxy_str)
if not no_proxy_str or no_proxy_str == "" then return false end
host = string.lower(host)
for pattern in string.gmatch(no_proxy_str, "([^,]+)") do
pattern = string.gsub(pattern, "^%s*(.-)%s*$", "%1")
pattern = string.lower(pattern)
if pattern == "*" then return true end
if host == pattern then return true end
if string.sub(pattern, 1, 1) == "." then
if string.sub(host, -#pattern) == pattern then
return true
end
end
end
return false
end
-- Return a 'create' function that opens the (optionally proxied) connection.
local function tcp(params, custom_create)
params = params or {}
for k, v in pairs(cfg) do
params[k] = params[k] or v
end
params.mode = "client"
local create = custom_create or params.create or socket.tcp
local proxy_url = params.proxy or http.PROXY
local proxy = nil
local target_host = params.host
local target_port = tonumber(params.port) or PORT
local no_proxy_env = params.noproxy or http.NOPROXY
if proxy_url then
if should_bypass_proxy(target_host, no_proxy_env) then
proxy = nil
else
proxy = url.parse(proxy_url)
end
end
return function()
local conn = {}
conn.sock = try(create())
local st = getmetatable(conn.sock).__index.settimeout
function conn:settimeout()
return st(self.sock, TIMEOUT)
end
function conn:connect(host, port)
if proxy then
try(self.sock:connect(proxy.host, proxy.port))
local msg = "CONNECT " .. target_host .. ":" .. target_port .. " HTTP/1.1\r\n"
msg = msg .. "Host: " .. target_host .. ":" .. target_port .. "\r\n"
if proxy.user and proxy.password then
local auth = mime.b64(proxy.user .. ":" .. proxy.password)
msg = msg .. "Proxy-Authorization: Basic " .. auth .. "\r\n"
end
msg = msg .. "\r\n"
try(self.sock:send(msg))
local line = try(self.sock:receive())
local code = string.match(line or "", "HTTP/%d%.%d (%d%d%d)")
repeat
line = try(self.sock:receive())
until line == "" or line == nil
if code ~= "200" then
return nil, "proxy connection failed: " .. tostring(code)
end
else
try(self.sock:connect(host, port))
end
self.sock = try(ssl.wrap(self.sock, params))
self.sock:sni(target_host)
self.sock:settimeout(TIMEOUT)
try(self.sock:dohandshake())
reg(self)
return 1
end
return conn
end
end
local function default_https_port(u)
return url.build(url.parse(u, { port = PORT }))
end
local function urlstring_totable(u, body, result_table)
local ltn12 = require("ltn12")
u = {
url = default_https_port(u),
method = body and "POST" or "GET",
sink = ltn12.sink.table(result_table),
}
if body then
u.source = ltn12.source.string(body)
u.headers = {
["content-length"] = #body,
["content-type"] = "application/x-www-form-urlencoded",
}
end
return u
end
-- Patched request: don't reject proxies; wire up the CONNECT-aware create fn.
https.request = function(reqt, body)
local result_table = {}
local stringrequest = type(reqt) == "string"
if stringrequest then
reqt = urlstring_totable(reqt, body, result_table)
else
reqt.url = default_https_port(reqt.url)
end
local t_url = url.parse(reqt.url)
reqt.host = t_url.host
reqt.port = t_url.port
local custom_create = reqt.create
reqt.create = tcp(reqt, custom_create)
reqt.proxy = nil
local res, code, headers, status = http.request(reqt)
if res and stringrequest then
return table.concat(result_table), code, headers, status
end
return res, code, headers, status
end
logger.info("[https-proxy patch] ssl.https.request patched for CONNECT-proxy support")

KOReader userpatch: HTTPS through an HTTP CONNECT proxy

A drop-in KOReader userpatch that lets KOReader's own plugins (Readeck, OPDS, sync, etc.) reach services over an HTTP CONNECT proxy — e.g. the koreader-tailscale plugin's proxy on 127.0.0.1:1056 when Tailscale is stuck in userspace mode with no TUN. LuaSec's https.lua normally refuses HTTPS over a proxy ("proxy not supported") and never opens a CONNECT tunnel (koreader/koreader#14693); this patch reimplements the connector to tunnel through it. Ported from DhruvaSambrani's luasec patch (write-up) — all credit to him for the fix; this is just the userspace, no-rebuild packaging as a patches/ file that survives OTA updates.

Install: drop 2-https-proxy.lua in your KOReader patches/ dir, set NETWORK_PROXY = "http://127.0.0.1:1056" in defaults.custom.lua, and restart.

Tested on PocketBook Color (PB633), KOReader 2026.03, userspace Tailscale; reports from other devices welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment