文档菜单

HTTPS代理端点

Litport的每个HTTP代理端点在同一主机和端口上也接受TLS。将代理URL中的协议从http://改为https://,客户端与节点之间的连接即被加密。凭证、端口和目标均保持不变。TLS开销计入流量(见成本),握手失败会直接关闭连接,没有可供检查的X-Proxy-Error-Code(见确认TLS确实在使用)。

协议 客户端到节点 示例
http:// 明文 http://USER:PASSWORD@hub-eu-1.litport.net:PORT
https:// TLS https://USER:PASSWORD@hub-eu-1.litport.net:PORT

PORT是控制台中为您的令牌显示的HTTP代理端口(默认为1337和31337;见代理入口)。SOCKS5端点没有TLS变体。

选择安全级别#

与代理通信有三种方式。选择与客户端运行位置相符的一种;本页其余部分介绍如何设置每一种。

级别 代理URL与设置 被动监听者(Wi-Fi、ISP、共享网络) 主动攻击者(重定向您的连接) 客户端支持
0. 明文 http://,无需其他设置 可读取您的凭证、每个CONNECT主机名以及明文HTTP的正文 同左 所有客户端
1. 加密,不验证 https://,加上客户端的“跳过代理验证”选项 什么也看不到 可冒充节点并截获您的凭证 大多数库;不支持浏览器;Requests除非同时关闭目标验证,否则也不支持
2. 加密并验证(推荐) https://,加上Litport根证书文件 什么也看不到 验证失败即中止连接:客户端会拒绝任何不是Litport节点的对象 所有支持https://代理的客户端

建议:使用级别2。它只需要一个公开文件和一个选项,并且是唯一能让可用于消费的凭证同时免受两类攻击者威胁的级别。下载一次根证书,并放在代码旁边:

curl --fail --remote-name https://litport.net/certificates/litport-cache-root.crt

对于快速手动测试,或无法给其提供文件的客户端,级别1是比明文更好的合理选择。级别0只是因为所有客户端都支持才成为默认;只要您与节点之间的网络不属于您,就应改用其他级别。

被称为“HTTPS代理”的两件不同的事#

  • 代理HTTPS目标是HTTP代理一直以来的工作方式:客户端发送CONNECT example.com:443,节点建立隧道,您的TLS会话在隧道内端到端运行。http://代理端点支持这种方式;除非您启用缓存模式,否则隧道内容对Litport不透明。
  • HTTPS代理端点加密的是客户端与节点本身之间的这一跳。您发送给代理的所有内容,包括CONNECT行和您的凭证,都在这个TLS会话内传输。

本页讲的是第二种。两者相互独立:任何目标都可以使用任一协议。

明文端点会暴露什么(级别0)#

使用http://时,客户端与节点之间路径上的任何环节都能读取到:

线路上暴露的内容 后果
Proxy-Authorization请求头 每次请求都会暴露您的代理用户名和密码。按流量计费的凭证,谁截获谁就能用它消费。
CONNECT host:port行 您连接的每个主机名,即使隧道本身已加密。
明文http://目标请求 完整的请求体和响应体。

“路径上”包括您所在的本地网络、您的ISP、共享云网络、企业出口代理,以及任何记录流量的环节。两种情况下,目标站点看到的都一样:所选的出口IP,绝不是您的IP。

何时使用https://#

在以下情况使用HTTPS端点:

  • 客户端所在网络不受您控制。公共Wi-Fi下的笔记本电脑、共享办公室、酒店和机场网络,以及任何带有透明检测设备的环境。
  • 客户端运行在共享或第三方基础设施上。CI运行器、无服务器函数、共享云VPC,以及运行在您无权管理的主机上的容器。在这些环境中,凭证被截获既不易察觉,又会长期有效。
  • 目标列表本身属于敏感信息。即使内容已加密,CONNECT行也会暴露您在研究哪些主机。
  • 您发送的是明文http://目标请求。否则这些正文在第一跳上可被完整读取。
  • 普通代理流量被屏蔽或限速。有些网络会标记非标准端口上的CONNECT;而指向主机名的TLS看起来就像普通HTTPS。

在以下情况继续使用http://:

  • 客户端与节点之间已有私密的加密通路,例如接入您所控制网络的VPN,并且您希望每个连接的开销尽可能小。
  • 客户端库根本无法与代理协商TLS(见下文的客户端说明)。
  • 您会打开非常多的短连接,而且每个连接额外握手的代价比暴露风险更重要。

成本#

  • 每个连接多一次TLS握手。支持keep-alive的客户端每个连接只需握手一次,而不是每个请求一次。
  • TLS开销计入流量。记录封装和握手都是经过节点的字节,因此普通(非缓存)请求的计量会略高:小请求通常高出几个百分点,大文件传输则可忽略不计。缓存模式的计量不变。
  • 您的客户端必须信任Litport根证书,才能建立到代理的连接。这是下一节的主题,也是curl -k无济于事的原因。

节点证书,以及-k为什么无效#

级别2意味着验证节点的证书。节点出示的是为其自身主机名(例如hub-eu-1.litport.net)签发的证书,签发者为Litport缓存根证书。该根证书不在操作系统的信任库中,因此如果客户端用默认证书包验证代理证书,就会失败:

curl: (60) SSL certificate problem: unable to get local issuer certificate

curl -k不能解决这个问题。客户端有两条独立的TLS链路,设置也各自独立:

TLS链路 保护对象 curl选项
客户端到代理 您的凭证和CONNECT行 --proxy-cacert, --proxy-insecure
客户端到目标(隧道内) 您正在获取的站点 --cacert, -k / --insecure

-k只放宽目标链路的验证。代理链路仍会用系统证书包验证并中止。应改为让代理链路使用Litport根证书:

curl --fail --remote-name https://litport.net/certificates/litport-cache-root.crt
curl --proxy-cacert litport-cache-root.crt \
  -x 'https://USER:[email protected]:PORT' \
  'https://example.com/'

在更广泛地安装该根证书之前,有两点需要了解:

  • 为代理这一跳信任该根证书不会启用缓存模式。缓存模式要求凭证带有_cache-密码后缀;没有该后缀,隧道保持不透明。
  • 同一个根证书也可用于签发缓存模式拦截所用的证书。如果不想把它加入系统级证书库,可以将其限定在使用代理的进程或容器范围内,下面的每个示例都是这样做的。

始终使用节点的主机名,不要使用其IP地址。证书是为主机名签发的。

级别2:已验证的客户端示例#

替换USER、PASSWORD和PORT;凭证中的保留字符需进行百分号编码。所有示例都会用litport-cache-root.crt验证代理证书,并对目标保持常规的公共CA验证。每一节末尾都给出级别1的切换方式,它只跳过代理链路的验证;使用前先阅读级别1,了解其代价。

curl#

已验证(推荐):

curl --proxy-cacert litport-cache-root.crt \
  -x 'https://hub-eu-1.litport.net:PORT' --proxy-user 'USER:PASSWORD' \
  'https://example.com/'

添加--verbose可以看到两次握手:第一次是与代理(hub-eu-1.litport.net)的握手,然后是其内部的CONNECT,最后是与目标的握手。

未验证,仅用于一次性测试(凭证仍然加密,但客户端无法区分真实节点和冒充者):

curl --proxy-insecure -x 'https://USER:[email protected]:PORT' 'https://example.com/'

--proxy-cacert需要curl 7.52或更高版本,当前所有发行版都已自带。

Python Requests#

Requests对两条链路使用同一个CA证书包,而verify=会替换其默认证书包。先构建一次合并后的证书包,这样目标仍保持公共CA验证:

cat "$(python -c 'import certifi; print(certifi.where())')" litport-cache-root.crt > litport-bundle.pem
import requests

proxy = 'https://USER:[email protected]:PORT'
response = requests.get(
    'https://example.com/',
    proxies={'http': proxy, 'https': proxy},
    verify='litport-bundle.pem',
    timeout=30,
)

在http和https两个键下都要列出代理;Requests按目标的协议选择对应条目。通过https://代理实现TLS-in-TLS需要urllib3 1.26或更高版本,Requests 2.26及以上版本依赖它。

Requests没有仅针对代理的开关:verify=False会同时关闭节点和目标两条链路的验证,并且每次请求都会打印警告。仅用于一次性测试。

response = requests.get('https://example.com/', proxies={'http': proxy, 'https': proxy}, verify=False, timeout=30)

Python httpx#

httpx将代理的TLS设置与目标的TLS设置分开:

import ssl
import httpx

context = ssl.create_default_context()
context.load_verify_locations(cafile='litport-cache-root.crt')
proxy = httpx.Proxy('https://USER:[email protected]:PORT', ssl_context=context)
with httpx.Client(proxy=proxy, timeout=30) as client:
    response = client.get('https://example.com/')

Proxy上的ssl_context只验证节点;客户端的默认上下文仍会验证目标。要在保持目标验证的同时跳过代理链路,给Proxy传入一个关闭了验证的上下文:

insecure = ssl.create_default_context()
insecure.check_hostname = False
insecure.verify_mode = ssl.CERT_NONE
proxy = httpx.Proxy('https://USER:[email protected]:PORT', ssl_context=insecure)

Node#

https-proxy-agent接受https://代理URL,并使用Node的信任库验证节点,因此需通过NODE_EXTRA_CA_CERTS追加根证书。它不会在任何地方关闭验证。

npm install https-proxy-agent
NODE_EXTRA_CA_CERTS=./litport-cache-root.crt node fetch.mjs
import https from 'node:https'
import { HttpsProxyAgent } from 'https-proxy-agent'

const agent = new HttpsProxyAgent('https://USER:[email protected]:PORT')
https.get('https://example.com/', { agent }, response => response.resume())

对于undici(也是Node 18及更高版本中全局fetch的底层引擎),代理链路有自己的TLS选项,因此不需要环境变量:

import { readFileSync } from 'node:fs'
import { ProxyAgent, fetch } from 'undici'

const dispatcher = new ProxyAgent({
  uri: 'https://hub-eu-1.litport.net:PORT',
  token: 'Basic ' + Buffer.from('USER:PASSWORD').toString('base64'),
  proxyTls: { ca: [readFileSync('litport-cache-root.crt')] },
})
const response = await fetch('https://example.com/', { dispatcher })

这两个库都可以只跳过代理链路。对于https-proxy-agent,第二个参数会传给到节点的TLS连接;对于undici则是proxyTls。目标验证保持开启。不要使用NODE_TLS_REJECT_UNAUTHORIZED=0,它会关闭进程内所有位置的验证。

const agent = new HttpsProxyAgent('https://USER:[email protected]:PORT', { rejectUnauthorized: false })
const dispatcher = new ProxyAgent({ uri: 'https://hub-eu-1.litport.net:PORT', token, proxyTls: { rejectUnauthorized: false } })

Go#

Go的http.Transport拨号https://代理时,使用的TLSClientConfig与访问目标时相同,因此应把根证书追加到系统证书池,而不是替换它:

pool, err := x509.SystemCertPool()
if err != nil || pool == nil { pool = x509.NewCertPool() }
pem, err := os.ReadFile("litport-cache-root.crt")
if err != nil || !pool.AppendCertsFromPEM(pem) { log.Fatal("could not add Litport root") }
proxyURL, err := url.Parse("https://USER:[email protected]:PORT")
if err != nil { log.Fatal(err) }
client := &http.Client{Transport: &http.Transport{
  Proxy: http.ProxyURL(proxyURL),
  TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12},
}}
response, err := client.Get("https://example.com/")

Go会把TLSClientConfig同时用于两条链路,因此InsecureSkipVerify: true也会停止验证目标。没有仅针对代理的开关;应改为把根证书保留在证书池中。

PHP#

curl扩展提供与命令行相同的代理链路选项:

$ch = curl_init('https://example.com/');
curl_setopt_array($ch, [
    CURLOPT_PROXY => 'https://hub-eu-1.litport.net:PORT',
    CURLOPT_PROXYUSERPWD => 'USER:PASSWORD',
    CURLOPT_PROXY_CAINFO => __DIR__ . '/litport-cache-root.crt',
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);

要只跳过代理链路,将CURLOPT_PROXY_CAINFO替换为CURLOPT_PROXY_SSL_VERIFYPEER => false和CURLOPT_PROXY_SSL_VERIFYHOST => 0;目标的CURLOPT_SSL_VERIFYPEER保持默认值。

Java#

JDK内置的java.net.http.HttpClient只支持明文代理。使用Apache HttpClient 5,它接受https代理主机,并为JVM提供一个信任库,其中除默认根证书外还包含Litport根证书:

keytool -importcert -noprompt -alias litport-cache -file litport-cache-root.crt \
  -keystore ./litport-truststore.p12 -storetype PKCS12 -storepass changeit
java -Djavax.net.ssl.trustStore=./litport-truststore.p12 \
  -Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStorePassword=changeit App
HttpHost proxy = new HttpHost("https", "hub-eu-1.litport.net", PORT);
BasicCredentialsProvider credentials = new BasicCredentialsProvider();
credentials.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("USER", "PASSWORD".toCharArray()));
try (CloseableHttpClient client = HttpClients.custom()
        .setProxy(proxy)
        .setDefaultCredentialsProvider(credentials)
        .build()) {
    client.execute(new HttpGet("https://example.com/"), response -> EntityUtils.toString(response.getEntity()));
}

专用信任库会替换JVM的默认证书包,因此还需要把所需的公共根证书也导入其中,或者将Litport根证书导入JVM的cacerts副本。Java没有仅针对代理的开关:信任所有证书的TrustManager也会停止验证目标,因此应使用信任库。

浏览器与浏览器自动化#

浏览器使用操作系统证书库验证代理证书(Firefox使用自己的证书库)。在所用配置文件对应的证书库中安装根证书,然后让浏览器指向HTTPS代理:

  • Chrome和Edge:启动时加上--proxy-server="https://hub-eu-1.litport.net:PORT",或使用返回HTTPS hub-eu-1.litport.net:PORT的PAC文件。浏览器会提示输入代理凭证。
  • Firefox:使用带有相同HTTPS host:port指令的PAC文件,或在连接设置中将代理类型设为HTTPS。
  • Playwright和Puppeteer:传入server: 'https://hub-eu-1.litport.net:PORT'以及用户名和密码。浏览器的证书库中仍需有该根证书;ignoreHTTPSErrors和--ignore-certificate-errors只影响目标证书。浏览器从不会跳过对代理证书的验证,因此安装根证书是唯一的办法。

各操作系统和Firefox证书库的安装说明见证书页面。

不支持代理TLS的客户端#

Scrapy的默认下载器、JDK的HttpClient以及许多较旧的HTTP库只能与明文代理通信。对这些客户端,要么继续使用http://,要么在127.0.0.1上运行一个小型本地转发器:它接收明文,并通过TLS连接到节点;这样敏感的那一跳就留在本机上。

级别1:加密但不验证#

上面每个已验证的示例都有对应的未验证变体。跳过验证仍会保持这一跳加密,因此网络上的被动监听者依然什么也看不到。但它去掉了“您正在与Litport节点通信”这一保证:能重定向您连接的主动攻击者可以出示任意证书并截获您的凭证。这与级别0面对主动攻击者时的暴露程度相同,因此应把级别1视为比http://更好的一步,而不是等同于级别2。

客户端 仅代理链路 两条链路
curl --proxy-insecure --proxy-insecure -k
Python Requests 不可用 verify=False
Python httpx Proxy(..., ssl_context=insecure) 在客户端上添加verify=False
Node https-proxy-agent agent选项{ rejectUnauthorized: false } NODE_TLS_REJECT_UNAUTHORIZED=0(避免使用)
Node undici proxyTls: { rejectUnauthorized: false } 添加requestTls: { rejectUnauthorized: false }
Go 不可用 InsecureSkipVerify: true
PHP curl CURLOPT_PROXY_SSL_VERIFYPEER / CURLOPT_PROXY_SSL_VERIFYHOST 添加CURLOPT_SSL_VERIFYPEER => false
Java 不可用 信任所有证书的TrustManager(避免使用)
浏览器 不可用 不可用;安装根证书

无人值守运行的场景应优先使用级别2。验证只需要一个文件和一个选项,而它保护的凭证可用于消费。

确认TLS确实在使用#

  • curl --verbose会在CONNECT行之前打印代理握手,包括证书主题CN=hub-eu-1.litport.net。
  • 通过https://使用错误密码时,仍会返回407和X-Proxy-Error-Code: 4,与通过http://时完全一样;错误响应头在TLS内部传输。
  • 如果连接立即关闭且没有HTTP响应,说明节点无法完成TLS握手。检查:使用的是主机名而不是IP地址;https://用在HTTP端口而不是SOCKS5端口上;客户端在代理链路上信任Litport根证书。参见代理错误参考。