tcp隧道 udp隧道
总览(Overview)
This post is for anyone interested in writing performant and safe applications in Rust quickly. It walks the reader through designing and implementing an HTTP Tunnel and basic, language-agnostic, principles of creating robust, scalable, observable, and evolvable network applications.
这篇文章供有兴趣在Rust中快速编写高性能和安全应用程序的任何人使用。 它引导读者设计和实现HTTP隧道以及创建健壮,可伸缩,可观察和可发展的网络应用程序的基本,与语言无关的原理。
防锈:性能,可靠性,生产率。 选择三个。 (Rust: performance, reliability, productivity. Pick three.)
About a year ago, I started to learn Rust. The first two weeks were quite painful. Nothing compiled, I didn’t know how to do basic operations, I couldn’t make a simple program run. But step by step, I started to understand what the compiler wanted. Even more, I realized that it forces the right thinking and correct behaviour.
大约一年前,我开始学习Rust。 前两个星期非常痛苦。 什么都没编译,我不知道如何进行基本操作,我无法运行简单的程序。 但是逐步地,我开始理解编译器想要什么。 更重要的是,我意识到这迫使正确的思考和正确的行为。
Yes, sometimes, you have to write seemingly redundant constructs. But it’s better not to compile a correct program than to compile an incorrect one. This makes making mistakes more difficult.
是的,有时候,您必须编写看似多余的结构。 但是,最好不要编译正确的程序,而不要编译不正确的程序。 这使犯错误变得更加困难。
Anyway, soon after, I became more or less productive and finally could do what I wanted. Well, most of the time.
无论如何,不久之后,我或多或少地变得有生产力,终于可以做我想要的。 好吧,大多数时候。
Recently out of curiosity, I decided to take on a slightly more complex challenge: implement an HTTP Tunnel in Rust. It turned out to be surprisingly easy to do and took about a day, which is quite impressive. Basically, I stitched together tokio, clap, serde, and several other very useful crates. Okay, enough of the introduction. Let me share the knowledge I gained during this exciting challenge and elaborate on why I organized the app this way. I hope you’ll enjoy it.
最近出于好奇,我决定接受一个稍微复杂的挑战:在Rust中实现HTTP隧道。 事实证明,它非常容易完成,并且花费了大约一天的时间,这非常令人印象深刻。 基本上,我将tokio , clap , serde和其他几个非常有用的板条缝在一起。 好的,足够的介绍。 让我分享在这个激动人心的挑战中获得的知识,并详细说明为什么我以这种方式组织该应用程序。 希望您会喜欢。
什么是HTTP隧道? (What is an HTTP Tunnel?)
Simply put, it’s a lightweight VPN that you can set up with your browser so your Internet provider cannot block or track your activity, and web-servers won’t see your IP address.
简而言之,它是一种轻量级VPN,您可以使用浏览器对其进行设置,以使Internet提供商无法阻止或跟踪您的活动,并且Web服务器不会看到您的IP地址。
If you’d like, you can test it with your browser locally, e.g., with Firefox (otherwise just skip this section for now).
如果需要,可以在本地浏览器(例如,Firefox)上对其进行测试(否则,请暂时跳过此部分)。
$ cargo install http-tunnel
2. Start:
2.开始:
$ http-tunnel --bind 0.0.0.0:8080 http
You can also check the http-tunnel GitHub repository for build/installation instructions.
您还可以检查http-tunnel GitHub存储库以获取构建/安装说明。
Now you can go to your browser and set the HTTP Proxy
to localhost:8080
. For instance, in Firefox just search for proxy
in the preferences section:
现在,您可以转到浏览器并将HTTP Proxy
设置为localhost:8080
。 例如,在Firefox中,只需在首选项部分中搜索proxy
:
and then specify it for HTTP Proxy
and also check it for HTTPS:
然后为HTTP Proxy
指定它,并为HTTPS:
检查它HTTPS:
You can visit several web-pages and check the ./logs/application.log
file — all your traffic was going via the tunnel. For example:
您可以访问几个网页并检查./logs/application.log
文件-您的所有流量都通过隧道。 例如:
Okay, let’s walk through the process from the beginning.
好的,让我们从头开始。
设计应用 (Design the app)
Each application starts with design, which means we need to define the following:
每个应用程序都从设计开始,这意味着我们需要定义以下内容:
- Functional requirements. 功能要求。
- Non-functional requirements. 非功能性要求。
- Application abstractions and components. 应用程序抽象和组件。
步骤1.功能要求 (Step 1. Functional requirements)
We need to follow the specification outlined here: https://en.wikipedia.org/wiki/HTTP_tunnel :
我们需要遵循此处概述的规范: https : //en.wikipedia.org/wiki/HTTP_tunnel :
Negotiate target with an HTTP CONNECT
request. E.g., if the client wants to create a tunnel to www.wikipedia.org, the request will look like:
与HTTP CONNECT
请求协商目标。 例如,如果客户端要创建到www.wikipedia.org的隧道,则请求将如下所示:
CONNECT www.wikipedia.org:443 HTTP/1.1...
followed by a response, e.g.
随后是回应,例如
HTTP/1.1 200 OK
After this point, just relay TCP traffic both ways until one of the sides closes it, or an I/O error happens.
此后,只需双向中继TCP通信,直到双方之一将其关闭,否则将发生I / O错误。
The HTTP Tunnel should work for both HTTP and HTTPS.
HTTP隧道应同时适用于HTTP和HTTPS。
We also should be able to manage access/block targets (e.g., to block-list trackers).
我们还应该能够管理访问/阻止目标(例如,阻止列表跟踪器)。
步骤2.非功能需求 (Step 2. Non-functional requirements)
The service shouldn’t log any information that identifies users.
该服务不应记录任何可识别用户的信息。
It should have high throughput and low-latency (it should be unnoticeable for users and relatively cheap to run).
它应该具有高吞吐量和低延迟(对于用户来说应该是不明显的,并且运行起来应该相对便宜)。
Ideally, we want it to be resilient to traffic spikes, provide noisy neighbor isolation, and resist basic DDoS attacks.
理想情况下,我们希望它能够应对流量高峰,提供嘈杂的邻居隔离以及抵御基本的DDoS攻击。
Error messaging should be developer-friendly. We want the system to be observable to troubleshoot and tune it in production at a massive scale.
错误消息应该对开发人员友好。 我们希望该系统能够在大规模生产中进行故障排除和调试。
步骤3.组件 (Step 3. Components)
When designing components, we need to first breakdown the app to a set of responsibilities. First, let’s see how our flow diagram looks like:
在设计组件时,我们需要首先将应用分解为一组职责。 首先,让我们看一下流程图的样子:
To implement this, we can introduce the following main components:
为此,我们可以介绍以下主要组件:
- TCP/TLS Acceptor TCP / TLS接受器
- HTTP CONNECT NegotiatorHTTP CONNECT协商器
- Target Connector目标连接器
- Full-Duplex Relay全双工中继
实作(Implementation)
TCP / TLS接受器(TCP/TLS Acceptor)
When we roughly know how to organize the app, it’s time to decide which dependencies we should use. For Rust, the best I/O library I know is tokio. In the tokio
family, there are many libraries, including tokio-tls
, which makes things much simpler. So the TCP acceptor code would look like:
当我们大致了解如何组织应用程序时,就该决定应该使用哪个依赖项了。 对于Rust,我知道的最好的I / O库是tokio 。 在tokio
家族中,有许多库,包括tokio-tls
,这使事情变得简单得多。 因此, TCP接受器代码如下所示:
And then the whole acceptor loop + launching asynchronous connection handlers would be:
然后整个接收器循环+启动异步连接处理程序将是:
Let’s break down what’s happening here. We accept a connection. If the operation was successful, use tokio::spawn
to create a new task that will handle that connection. Memory/thread-safety management happens behind the scenes. Handling futures is hidden by async/await
syntax sugar.
让我们分解一下这里发生的事情。 我们接受连接。 如果操作成功,请使用tokio::spawn
创建一个新任务来处理该连接。 内存/线程安全管理在后台进行。 async/await
语法糖隐藏了处理期货的方法。
However, there is one question. TcpStream
and TlsStream
are different objects, but handling both is precisely the same. Can we re-use the same code? In Rust, abstraction is achieved via Traits
, which are super handy:
但是,有一个问题。 TcpStream
和TlsStream
是不同的对象,但是两者的处理完全相同。 我们可以重用相同的代码吗? 在Rust中,通过Traits
实现抽象,这非常方便:
The stream must implement:
流必须实现:
AsyncRead /Write
— so we can read/write it asynchronouslyAsyncRead /Write
—因此我们可以异步读取/写入它Send
— to be able to send between threadsSend
-能够在线程之间发送Unpin
— to be moveable (otherwise we won’t be able to doasync move
andtokio::spawn
to create anasync
task)Unpin
-可以移动(否则我们将无法执行async move
和tokio::spawn
来创建async
任务)'static
—to denote that it may live until application shutdown and doesn’t depend on any other object’s destruction.'static
-表示它可以一直存在直到应用程序关闭,并且不依赖于任何其他对象的破坏。
Which our TCP/TLS
streams exactly are. However, now we can see that it doesn’t have to be TCP/TLS
streams. This code would work for UDP
or QUIC
or ICMP
. I.e., it can wrap any protocol within any other protocol, or itself. In other words, this code is reusable, extendable, and ready for migration (which happens sooner or later).
我们的TCP/TLS
流到底是哪个。 但是,现在我们可以看到它不必是TCP/TLS
流。 此代码适用于UDP
或QUIC
或ICMP
。 即,它可以将任何协议包装在任何其他协议中,或者本身。 换句话说,此代码是可重用,可扩展的,并准备进行迁移(迟早会发生)。
HTTP连接协商器 (HTTP Connect Negotiator)
Let’s pause for a second and think at a higher level. What if we can abstract from HTTP Tunnel, and just need to implement a generic tunnel?
让我们稍停片刻,再思考一下。 如果我们可以从HTTP隧道中抽象出来,而只需要实现一个通用隧道呢?
- We need to establish some transport-level connections (L4). 我们需要建立一些传输级别的连接(L4)。
- Negotiate a target (doesn’t really matter how: HTTP, PPv2, etc.). 协商目标(实际上并不重要:HTTP,PPv2等)。
- Establish an L4 connection to the target. 建立与目标的L4连接。
- Report success and start relaying data. 报告成功并开始中继数据。
A target could be, for instance, another tunnel. Also, we can support different protocols. The core would stay the same.
目标可以是例如另一个隧道。 此外,我们可以支持不同的协议。 核心将保持不变。
We already saw that tunnel_stream
method already works with any L4 Client<->Tunnel
connection.
我们已经看到tunnel_stream
方法已经适用于任何L4 Client<->Tunnel
连接。
Here, we specify two abstractions:
在这里,我们指定两个抽象:
TunnelTarget
is just something that has anAddr
— whatever it is.TunnelTarget
就是具有Addr
东西-无论它是什么。TargetConnector
— can connect to thatAddr
and needs to return a stream that supports async I/O.TargetConnector
—可以连接到该Addr
并且需要返回支持异步I / O的流。
Okay, but what about the target negotiation? The tokio-utils
crate already has an abstraction for that, named Framed
streams (with corresponding Encoder/Decoder
traits). We need to implement them for HTTP CONNECT
(or any other proxy protocol). You can find the implementation here.
好的,但是目标协商呢? tokio-utils
板条箱对此已经有了一个抽象,称为Framed
流(具有相应的Encoder/Decoder
特性)。 我们需要为HTTP CONNECT
(或任何其他代理协议)实现它们。 您可以在此处找到实现。
中继 (Relay)
We only have one major component remaining — that which relays data after the tunnel negotiation is done. tokio
provides a method to split a stream into two halves: ReadHalf
and WriteHalf
. We can split both client and target connections and relay them in both directions:
我们只剩下一个主要组件,即在隧道协商完成后中继数据的组件。 tokio
提供了一种将流分成两半的方法: ReadHalf
和WriteHalf
。 我们可以拆分客户端和目标连接,并在两个方向上中继它们:
Where the relay_data(…)
definition requires nothing more than implementing abstractions mentioned above. I.e., it can connect any two halves of a stream:
其中relay_data(…)
定义只需要实现上述抽象即可。 即,它可以连接流的任何两半:
And finally, instead of a simple HTTP Tunnel, we have an engine that can be used to build any type of tunnels or a chain of tunnels (e.g., for onion routing), over any transport and proxy protocols:
最后,除了简单的HTTP隧道,我们还有一个引擎,可用于通过任何传输和代理协议构建任何类型的隧道或隧道链(例如,用于洋葱路由):
The implementation is almost trivial in basic cases, but we want our app to handle failures, and that’s the focus of the next section.
在基本情况下,实现几乎是微不足道的,但是我们希望我们的应用程序能够处理故障,这是下一部分的重点。
处理失败 (Dealing with failures)
The amount of time engineers deal with failures is proportional to the scale of a system. It’s easy to write happy-case code. Still, if it enters an irrecoverable state on the very first error, it’s painful to use. Besides that, your app will be used by other engineers, and there are very few things more irritating than cryptic/misleading error messages. If your code runs as a part of a large service, some people need to monitor and support it (e.g., SREs or DevOps), and it should be a pleasure for them to deal with your service.
工程师处理故障的时间与系统规模成正比。 编写幸福案例代码很容易。 但是,如果它在第一个错误时进入不可恢复的状态,则使用起来很痛苦。 除此之外,您的应用程序还将由其他工程师使用,并且几乎没有什么比含糊不清/误导性的错误消息更令人讨厌了。 如果您的代码作为大型服务的一部分运行,则有些人需要对其进行监视和支持(例如SRE或DevOps),因此他们应该很高兴处理您的服务。
What kind of failures may an HTTP Tunnel encounter?
HTTP隧道可能遇到什么样的故障?
It’s a good idea to enumerate all error codes that your app returns to the client. So it’s clear why a request failed if the operation can be tried again (or shouldn’t), if it’s an integration bug or just network noise.
枚举应用返回给客户端的所有错误代码是一个好主意。 因此很明显,如果再次尝试该操作(或者不应该尝试该操作),集成错误或仅仅是网络噪音,为什么请求失败。
Dealing with delays is crucial for a network app. If your operations don’t have timeouts, it’s a matter of time until all of your threads will be Waiting for Godot, or your app will exhaust all available resources and become unavailable. Here we delegate timeout definition to RelayPolicy
:
处理延迟对于网络应用程序至关重要。 如果您的操作没有超时,那么所有线程将在等待Waitot还是一个时间问题,否则您的应用程序将耗尽所有可用资源而变得不可用。 在这里,我们将超时定义委托给RelayPolicy
:
Relay policy can be configured like this:
中继策略可以这样配置:
relay_policy: idle_timeout: 10s min_rate_bpm: 1000 max_rate_bps: 10000 max_lifetime: 100smax_total_payload: 100mb
So we can limit activity per connection with max_rate_bps
and detecting idle clients with min_rate_bpm
(so they don’t consume system resources than can be utilized more productively). A connection lifetime and total traffic may be bounded as well.
因此,我们可以使用max_rate_bps
限制每个连接的活动,并使用min_rate_bpm
来检测空闲客户端(这样,它们不会消耗系统资源,而无法更有效地利用它们)。 连接寿命和总流量也可能受到限制。
It goes without saying that each failure mode needs to be tested. It’s straightforward to do that in Rust in general and with tokio-test
in particular:
不言而喻,每种故障模式都需要进行测试。 通常,在Rust中,特别是在tokio-test
中,这样做很简单:
The same goes for I/O errors:
I / O错误也是如此:
记录和指标 (Logging and metrics)
I haven’t seen an application that failed only in ways anticipated by its developers. I’m not saying there are no such applications. Still, chances are that your app is going to encounter something you didn’t expect: data races, specific traffic patterns, dealing with traffic bursts, legacy clients.
我还没有看到仅以开发人员预期的方式失败的应用程序。 我并不是说没有这样的应用程序。 尽管如此,您的应用仍有可能遇到意想不到的事情:数据争用,特定的流量模式,处理流量突发,旧客户端。
But probably one of the most common types of failures is human failures, such as pushing bad code or configuration, which are inevitable in large projects. Anyway, we need to be able to deal with something we didn’t foresee. So we emit enough information that would allow us to detect failures and troubleshoot.
但是,最常见的故障类型之一可能是人为故障,例如推送错误的代码或配置,这在大型项目中是不可避免的。 无论如何,我们需要能够处理我们未曾预见的事情。 因此,我们发出了足够的信息,使我们能够检测到故障并进行故障排除。
So we’d better log every error and important events with meaningful information and relevant context as well as statistics.
因此,我们最好使用有意义的信息,相关的上下文以及统计信息来记录每个错误和重要事件。
Please note the tunnel_ctx: TunnelCtx
field, which can be used to correlate metric records with log messages:
请注意tunnel_ctx: TunnelCtx
字段,该字段可用于将度量标准记录与日志消息相关联:
error!( "{} failed to write {} bytes. Err = {:?}, CTX={}", self.name, n, e, self.tunnel_ctx);
配置和参数 (Configuration and parameters)
Last but not least. We’d like to be able to run our tunnel in different modes with different parameters. Here’s where serde
and clap
become handy.
最后但并非最不重要的。 我们希望能够在具有不同参数的不同模式下运行隧道。 这是serde
和clap
变得很方便的地方。
In my opinion, clap
makes dealing with command line parameters pleasant. Extraordinarily expressive and easy to maintain.
我认为, clap
使处理命令行参数变得愉快。 极富表现力,易于维护。
Configuration files can be easily handled with serde-yaml
:
配置文件可以使用serde-yaml
轻松处理:
target_connection: dns_cache_ttl: 60s allowed_targets: "(?i)(wikipedia|rust-lang)\\.org:443$" connect_timeout: 10s relay_policy: idle_timeout: 10s min_rate_bpm: 1000 max_rate_bps: 10000
Which just corresponds to Rust structs:
刚对应于Rust结构:
It doesn’t need any additional comments to make it readable and maintainable, and that is beautiful.
它不需要任何其他注释即可使其可读性和可维护性,这很漂亮。
结论 (Conclusion)
As you could see from this quick overview, the Rust ecosystem already provides many building blocks so you can focus on what you need to do rather than how. You didn’t see any memory/resources management or explicit thread-safety (which often comes at the expense of concurrency). Abstraction mechanisms are fantastic, so your code can be highly reusable. This task was a lot of fun, so I’ll try to take on the next challenge.
正如你可以从这个简要概述看到,锈生态系统已经提供了很多积木这样你就可以专注于你需要做的,而不是怎么样。 您没有看到任何内存/资源管理或显式的线程安全性(通常以并发为代价)。 抽象机制非常棒,因此您的代码可以高度重用。 这项任务很有趣,因此我将尝试应对下一个挑战。
翻译自: https://medium.com/swlh/writing-a-modern-http-s-tunnel-in-rust-56e70d898700
tcp隧道 udp隧道