-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add transparent server mode based on WireGuard #5562
Conversation
Re: CI failures:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, very excited for this to land! 😃
- there's a bit of overlap with Unify proxy modes, introduce UDP protocol detection. #5556, which I'd like to get in first (@meitinger already had the pleasure of fixing merge conflicts with my stuff). That shouldn't be a substantial obstacle though. :)
- if possible I'd like to reuse the ProxyConnectionHandler instead of subclassing it. That should become more straightforward with @meitinger's PR, which changes the location of transparent mode address resolution
- The macOS failure we should tackle at its root, i.e. provide matching binary wheels. Building additional Rust code in CI takes too much time.
I figured as much. I still wanted to file this PR to get some initial feedback rather than waiting for more stuff to land.
Ok, sounds good to me. Hope resolving those merge conflicts won't be too bad.
Yeah, I'll try to figure out how to use Github Actions to publish binary wheels to PyPI ... looks like there's something I can use here: https://github.com/messense/maturin-action |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cross compilation is a can of worms. Let's just spawn |
PS: I managed to build binary wheels for Windows (x64, x86), mac OS (x64, aarch64), and linux (x86_64, i686) by using a GitHub Action: https://github.com/decathorpe/mitmproxy_wireguard/actions/runs/2958218239 Once I figure out how to upload those files to PyPI, that should make things considerably easier. Are there any targets that are important for mitmproxy which are missing from this list? |
Targets are good! Not sure if we even want or need Windows x86. This job here is pretty much best practice: https://github.com/mitmproxy/pdoc/blob/021cbec1424f91caf9dbd35105a7d8a6fbda8f8c/.github/workflows/main.yml#L63 Instead of the mitmproxy/.github/workflows/main.yml Line 139 in 1706a9b
|
Thanks for the link, looks like exactly what I needed.
Looks like this works fine as-is. When downloading the artifact, I got a ZIP archive that's called "wheels" that contained all the files from the different https://pypi.org/project/mitmproxy-wireguard/#files Since that's now squared away, I'll try to actually rebase this PR tomorrow, so we can see whether the binary wheels work for the CI here. |
Awesome! This used to not work, great to see that they apparently fixed it. 😃 |
I pushed a rebased version of the "add a wireguard mode spec" commit, and tried to address feedback wrt/ how to locate configuration files. Instead of specifying a "name" (which seemed to be both ambiguous to the user and not clear how to implement for us), the mode spec can optionally specify the directory from which configuration can be loaded from if it's already present, or in which configuration files will be written if they don't exist yet (or if the So, for example, when using
I think this should address the problem of "where to place files by default" and "how to expose this setting to the user". What do you think? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! Maybe I have missed this, but have we found a definite use case for a peers
argument? Do we need to make a distinction betwen load and generate, or could we just do generate-if-not-exists? I'd be happy to keep things as simple as possible for now.
If the Keeping valid configurations for existing peers around when adding configuration for more peers would mean I need to write parsing code for WireGuard client configuration files in addition to the already existing server configuration file parser and/or make the code for configuration file handling in
Yeah, keeping it simple was my intention. Right now, there are three cases:
These three cases should cover a) a sane default (load or generate with defaults), b) an explicit "use this configuration file" case, and c) an explicit "generate new configuration files even if they already exist" case. I think this is a good balance between simplicity and UX. |
Second draft of WireGuard server mode:
|
PS: It looks like mitmproxy does not use the "logging" module from the python standard library? mitmproxy_wireguard is currently explicitly compatible with and reliant on Python's "logging" module (with |
Yeah, we need to fix interop here. I think the correct way is to actually deprecate our custom logging shenanigans and switch to the builtin logging module. Let's keep using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The custom DatagramTransport looks good! The lack of incoming data is a WireGuard handshake problem. If I hardcode the configuration:
self._wireguard_conf = wg.WireGuardServerConf.custom(51820, "EG47ZWjYjr+Y97TQ1A7sVl7Xn3mMWDnvjU/VxU769ls=", ["Test1sbpTFmJULgSlJ5hJ1RdzsXWrl3Mg7k9UTN//jE="])
I get tons of output. I haven't tracked down what the actual issue is, but I suspect we should vastly simplify configuration parsing.
- Do we really need to create or parse WireGuard ini files? I don't think we need to. It's perfectly fine for us to have our own custom file format which just contains two private keys as JSON:
~/.mitmproxy/wireguard.conf
We only support one peer until we find a reason later on why we would need more peers on the same port.{ "mitmproxy_key": "...", "client_key": "...", }
- When
wireguard:~/.mitmproxy/wireguard.conf
is passed to mitmproxy, we do the following:- Check if
~/.mitmproxy/wireguard.conf
exists, and create one if it doesn't.
(mitm-wg only provides agenkey()
method for this) - Read keys from
foo.conf
, fail if file format is invalid.
- Check if
- In
WireGuardServerInstance.start
, we- Invoke
WireGuardServerConf.custom
with the keys we have (using mitm-wg'spubkey(key)
to get public keys). This basically becomes our only interface - we don't even need a WireGuardServerConf object really, we can just pass keys directly tostart_server
. - Log a WireGuard client configuration file to mitmproxy's event log like we do here1. This means that
mitmdump --mode wireguard
will basically print the configuration to stdout on startup.
- Invoke
This means we don't need to maintain the ~750 lines of conf parsing that are currently in mitm-wg. Does that make sense? Or is that too simple and I'm overlooking something?
Footnotes
-
We later add
WireGuardServerInstance.client_config()
which returns the config as a string. Then we can reuse the same code forWireGuardServerInstance.to_json()
, which adds the config so that it's available in mitmweb. ↩
This is a very good point. If you look at
Either is fine with me. The standalone thingy doesn't sound too bad though. :) |
Oh oh ... ok, that explains why "nothing is happening". I'll look into it.
That would be simpler, yes ... but (cont'd below)
Supporting variable number of peers is easy, not sure it's worth ripping it out again. The default is already one peer, so unless somebody would explicitly do something different, this is already what happens.
My intention with the configuration parser / writer code was to be compatible with official / other WireGuard servers and clients, which all (?) understand the same file format. For example, it should be possible to just drop the generated client configuration file into If we don't provide a config file, the user would have to either put together a config file themselves, or copy public / private keys and settings into their WireGuard client app to manually configure the tunnel. I thought that providing a ready-to-use / importable config file would be much nicer - and since the biggest benefit of using the WireGuard mode over the transparent mode would be better UX, I thought that this would be important.
Yeah, extending the |
Writing a standalone test client in Rust shouldn't be hard, but how would you expect to ship it / install it in CI? I don't think it can be included in the sdist or the binary wheel that are built by maturin, so it would have to be a separate "test client binary" built by Cargo, and it would then probably need to be built + installed with "cargo install" in CI (which would take ~20 minutes) which is probably not what you want. So for now I'm leading towards option 1. YOLO ... |
I found the problem that prevented handshakes in mitmproxy_wireguard (the generated config file included the wrong public key for the server). With the latest code, I can use the official WireGuard Android VPN to connect to mitmproxy, view |
To be clear, I want mitmdump to print out a valid configuration file on startup which can then be pasted into
Supporting more than one peer means:
I don'd mind keeping support for multiple peers in the Rust bits, but outside of that it's still quite a bit of unnecessary complexity.
Here's the kind of UX I'm envisioning:
The benefit of printing this to stdout is that the configuration is right in your face and you don't need to go search for a file/consult the docs. For other devices, the configuration file needs to adjust the local IP address of the endpoint in the configuration, so we want that to be dynamic anyways (and not just written once to a file). Taking things one step further, it would be awesome to display a QR code in mitmweb which can then just be scanned on Android. Does that make more sense now?
Compile it once for Linux and include the compiled binary in the repo. Skip the tests on other OSes.
🎉 🎉 🎉 |
I really just skimmed over the discussion, I'm very excited for WireGuard support!
Or in mitmproxy as well? 👀 https://pythonhosted.org/PyQRCode/rendering.html#terminal-rendering (was the first thing I found) |
Aaaaaah ... I get it now. When writing the original implementation, my first thought was to re-use the existing / standard WireGuard configuration file format instead of reinventing the wheel. But we can use a simpler serialization format (i.e. JSON), since we don't actually need to store files on disk that are compatible with other WireGuard tools, and only need to display / print client configuration as necessary. I'll need to think about what to rip out of the current implementation and how to implement the new approach ... |
I think this is ready for review now. Configuration handling is much simpler now. The I also tested generating QR codes from this pretty-printed configuration file (with something like |
They've been there before :) Protocol and Transport are indeed different things, but we really need neither here except for sending. No particular opinions on how we solve it as long as it works. :) |
No, I'm very sure they weren't. The CI was all-green before you pushed the "simplify configuration" commits. I'm very careful not to push changes that fail
🤷🏼♂️ Well, if you don't care, then I probably shouldn't either. |
This release fixes TCP connections which were broken in v0.1.1.
I bumped the dependency to the just-released mitmproxy_wireguard v0.1.2 which reverts the |
Seems like I can't GitHub. Sorry. 🫣
From mitmproxy's perspective we only really care about DatagramReader/DatagramWriter, which happen to have some internal dependencies on transport/protocol. We shouldn't try to copy all that asyncio complexity, if Reader/Writer are happy we're good. :)
Can you elaborate on what exactly was broken without checksums? I'm surprised to see packets with in valid checksums outside of the test client. If we need checksums we should revert 80b63bf991e26e2aedde53f3d2ea0cd99a72f801 entirely, otherwise we'll have a sneaky memory leak. Finally we need to adjust the test client to compute proper checksums, but that should be doable. |
I am not sure why, but the TCP stack just didn't establish connections at all when adding Note that the virtual network device operates on IP packets, so ignoring checksums there should only affect verification of the IP header checksum (it appears that verifying IP header checksum is the default behaviour when sending and receiving packets in smoltcp). Checking TCP packet payload checksum is independent of this, IIUC. I added the "early fail if TCP checksum mismatch" check back to mitmproxy_wireguard. |
Ah. Random hypothesis: With
I think smoltcp uses the device capabilities for all checksums, including TCP: I'll adjust the test client to emit correct checksums. One unrelated question: I noticed the MTU in |
Great, thanks!
If I remember correctly, this happened when I ran The TCP buffer sizes are already 64 KiB: The WireGuard and UDP packet buffer sizes are 1500 bytes: I can increase those to 64 * 1024 as well. That should work around the problem with truncated packets when using MTU > 1420? EDIT: I don't think that will work. The TCP handling code passes incoming packets to the virtual network device, which cannot handle receiving / sending packets larger than its MTU, and the maximum value accepted by smoltcp is only 9216 bytes (9 KiB ?) anyway: So I think even for "local" connections the MTU needs to be <= 1420 (which is also recommended by the WireGuard spec, except when using PPPoE, then the maximum MTU is 1412). |
Yes, could you try that and see if it fixes things? :-) |
Sorry, the edit to my previous commit was probably not the most visible place to put this:
So, if we start to allow packets that are larger than the MTU set in the virtual network device, they will be truncated by smoltcp. It might be possible to dynamically set MTU when initializing the virtual network device, but I don't see an obvious way to determine the "safe" MTU at runtime. So setting it to 1420 is probably the safest and easiest solution. I also pushed a new release of mitmproxy_wireguard to GitHub (with binaries attached as an asset) for the test client built for linux-x86_64, macos-{x86_64,aarch64}, and windows-x64. These should be usable for testing the WireGuard server mode here? https://github.com/decathorpe/mitmproxy_wireguard/releases/tag/0.1.4 |
Absolutely, Honestly I don't see much change for the test binary at all, just take a random x64 Linux binary, copy it into the repo here and build a simple test with it. :) |
Looks like the test almost works. But somehow the 6c22471#diff-1ebfcdb52f4d132251ed110e13c2ce7bf39f0e2d9d39b38ed9f4c742d8ef070dR123 CI output: |
I finally managed to fix the test client (tracked down a packet that didn't have correct checksums). Not sure what your commits changed about the binaries though? Either way, pushed the latest version of the test client binaries as of 0.1.6. |
Looks like the CI is finally happy, with the exception of some missed lines in test coverage? |
this allows us to exclude it from individual coverage, which makes no sense. Also improve type checking to make sure that it's a full replacement.
🎉 |
This is a draft for a transparent mode implementation based on WireGuard. I'm filing this "early" even though some things aren't finished yet, but I'm looking for feedback on whether things are generally looking "OK" or if some things should be done differently.
New mode spec for a WireGuard mode:
listen_port
to override the default WireGuard port (51820),name
to override the default filename prefix for generated WireGuard configuration files (mitmproxy_wireguard
), andpeers
to override the default number of peers (1) for which a configuration will be generatedNew WireGuard server mode implementation:
WireGuardConnectionHandler
(based onProxyConnectionHandler
but usingmitmproxy_wireguard.TcpStream
instead of `asyncio.StreamReader/StreamWriter)WireGuardInstance
: based onTcpServerInstance
/TransparentInstance
for TCP, andUdpServerInstance
for UDP (not hooked up yet, better support for UDP connections needs to be implemented inmitmproxy_wireguard
first)reader
/writer
inConnectionIO
(might need to be refactored to be generic, and / or require further adaptations oncemitmproxy_wireguard.UdpStream
is ready for handling UDP packets)test_transparent
test intests/mitmproxy/proxy/test_mode_servers.py
?)What's not working yet:
TcpStream.get_extra_info("original_addr")
is not implemented yet, but exposing the original destination in this manner should be relatively straightforward inmitmproxy_wireguard
.mitmdump --mode wireguard:[spec]
correctly generates WireGuard configuration files with the given settings, but then crashes withinvalid IP address syntax
and I cannot find where this error message is coming from (grepping mitmproxy source code doesn't yield any hits for this string and relevant substrings of it).