Publish and add Mastodon post

This commit is contained in:
Bad Manners 2024-09-23 18:02:21 -03:00
parent 4043a2dad9
commit 97ed46af78
Signed by: badmanners
GPG key ID: 8C88292CCB075609
3 changed files with 26 additions and 26 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "gallery.badmanners.xyz",
"version": "1.10.0",
"version": "1.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gallery.badmanners.xyz",
"version": "1.10.0",
"version": "1.10.1",
"hasInstallScript": true,
"dependencies": {
"@astrojs/check": "^0.9.3",

View file

@ -1,7 +1,7 @@
{
"name": "gallery.badmanners.xyz",
"type": "module",
"version": "1.10.0",
"version": "1.10.1",
"scripts": {
"postinstall": "astro sync",
"dev": "astro dev",

View file

@ -6,10 +6,10 @@ isAgeRestricted: false
authors: bad-manners
thumbnail: /src/assets/thumbnails/blog/supercharged_ssh_apps_on_sish.png
description: |
After discovering the joys of a reverse port forwarding-based architecture, I dig even deeper into SSH itself to make the most of it.
After discovering the joys of a reverse port forwarding-based architecture, I dig even deeper into SSH itself to make the most of it through code.
prev: ssh-all-the-way-down
# posts:
# mastodon: https://meow.social/@BadManners/133742069
posts:
mastodon: https://meow.social/@BadManners/113188858859021367
tags:
- technical post
- programming
@ -25,7 +25,7 @@ import imageRusshAxum from "@assets/images/ssh/russh_axum.png";
import imageCheckboxes from "@assets/images/ssh/checkboxes.png";
import imageMultipaintByNumbers from "@assets/images/ssh/multipaint_by_numbers.png";
This is my second post investigating SSH, and learning what it has to offer.
This is my second post investigating SSH, and learning what it has to offer. And this time, with some actual code.
<TocMdx headings={getHeadings()} />
@ -36,7 +36,7 @@ In my [last post](/blog/ssh-all-the-way-down), I went over a saga of trying to s
<figure>
<Image
src={imageSishPublic}
alt="Diagram entitled 'sish public', showing that Eric's machine with a service exposed on localhost port 3000 connects to sish via the command (ssh -R eric:80:localhost:3000 tuns.sh). This creates a bi-directional tunnel and exposes https://eric.tuns.sh to the Internet, which Tony accesses from a separate device."
alt="Diagram entitled 'sish public', showing that Eric's machine with a service exposed on localhost port 3000 connects to sish via the command (ssh -R eric:80:localhost:3000 tuns.sh). This creates a bidirectional tunnel and exposes https://eric.tuns.sh to the Internet, which Tony accesses from a separate device."
/>
<figcaption>
With a simple reverse shell command, and a configured sish instance, we can expose anything to the Internet. ©
@ -52,7 +52,7 @@ In fact, we can host anything TCP-based as long as we can create a secure shell
alt="Diagram showing a VPS host with SSH, HTTP(S), and TCP exposed to the outside world by sish, as it is internally connected through by git.badmanners.xyz via SSH. A Raspberry Pi serving booru.badmanners.xyz connects via SSH as well, while another computer sends an HTTP request for any service. All services connect through SSH and sish handles any reverse proxying within itself."
/>
<figcaption>
The architecture I ended up with. Services inside or outside of the VPS both leverage SSH in order to expose
The architecture I ended up with. Services inside or outside the VPS both leverage SSH in order to expose
themselves.
</figcaption>
</figure>
@ -97,7 +97,7 @@ services:
<figcaption>A basic server connecting to sish via a persistent reverse SSH tunnel.</figcaption>
</figure>
We have two images running for our application. One (`server`) is the actual webserver, while the other (`autosish`) handles the reverse port forwarding for us.
We have two images running for our application. One (`server`) is the actual web server, while the other (`autosish`) handles the reverse port forwarding for us.
It makes sense to have this separation into two images, if our application only uses an HTTP socket, and if it isn't aware of the SSH tunneling shenanigans going on... which is most of the applications.
@ -107,7 +107,7 @@ In this post, we will work on a tunnel-aware application, and finding out more a
## Reversing expectations
But first of all, how does remote port forwarding through an SSH tunnel work?
But first, how does remote port forwarding through an SSH tunnel work?
Better yet, how does _an SSH tunnel_ even work?
@ -129,7 +129,7 @@ For that, I'll be using a comprehensive Rust library called [russh](https://docs
Right, let's get to it!
First of all, we need to connect our client to the server. This is simple enough:
As a starting point, we need to connect our client to the server. This is simple enough:
```rust
use std::path::PathBuf;
@ -161,7 +161,7 @@ async fn main() -> Result<()> {
}
```
If you're unfamiliar with Rust, this might be a lot at once. In summary, we're doing two things: at the top, we import any dependencies we need; and at the bottom, inside of `async fn main()`, we're setting up a client connection with `client::connect()`.
If you're unfamiliar with Rust, this might be a lot at once. In summary, we're doing two things: at the top, we import any dependencies we need; and at the bottom, inside `async fn main()`, we're setting up a client connection with `client::connect()`.
Aside from this code, we also need to define the `Client` struct, which will be responsible for answering messages created by the server. This will implement the `russh::client::Handler` async trait, responsible for exposing our user-defined methods to the ones that the library knows to call.
@ -238,11 +238,11 @@ async fn main() -> Result<()> {
If your key is authorized with the given server, this will print `Public key authentication succeeded!` after connecting, then immediately exit again. Not a lot of progress, but bear with me.
So connecting and authenticating is straightforward enough. You might draw a parallel with first connecting to a website, then logging in with your credentials. What comes after you log in is a bit more freeform, and depends on what you intend to do on the website.
So connecting and authenticating is straightforward enough. You might draw a parallel with first connecting to a website, then logging in with your credentials. What comes after you log in is a bit more free-form, and depends on what you intend to do on the website.
With SSH, the analogy still holds. After creating a session, there are many options for what we can do next ([all of them available in russh](https://docs.rs/russh/0.45.0/russh/client/struct.Session.html)):
- `request_pty()`: Request that an interactive [pseudoterminal](https://en.wikipedia.org/wiki/Pseudoterminal) is created by the server, allowing us to enter commands over a remote shell session. This is the most common usecase for SSH.
- `request_pty()`: Request that an interactive [pseudo-terminal](https://en.wikipedia.org/wiki/Pseudoterminal) is created by the server, allowing us to enter commands over a remote shell session. This is the most common use case for SSH.
- `request_x11()`: Request that an [X11 connection](https://en.wikipedia.org/wiki/X_Window_System) is displayed over the Internet. This lets us access graphical applications through SSH!
- `tcpip_forward()`: Requests TCP/IP forwarding from the server.
@ -338,11 +338,11 @@ That's cool and all, but more important is how it gave us a URL for our service
...
...It just times out after a while with "bad gateway", causing our SSH program to exit.
...And it just times out after a while with "bad gateway", causing our SSH program to exit.
Well, that's less exciting. But at least it's doing _something_. Besides, if we never touch the link that it passed us, we can stay connected indefinitely. And as soon as we open the link on any device, we consistently get disconnected from the SSH server. That's proof that there's a relation between what the browser sees and our little Rust program.
The reason why we get disconnected is because we aren't handling any requests that come in. Right now, there's no way to read HTTP requests, even less sending HTTP responses.
The reason why we get disconnected is that we aren't handling any requests that come in. Right now, there's no way to read HTTP requests, even less sending HTTP responses.
But I thought `channel_open_session()` was already doing that? Not really it only serves to transfer messages between the client and the server. Instead, to handle each new connection, we need to use a new channel.
@ -350,7 +350,7 @@ Sounds simple enough. Then how do we create these channels? The answer is also s
### Changing channels
At this point, it's worth taking a detour from all of the code and explain how an SSH session actually works.
At this point, it's worth taking a detour from all the code and explain how an SSH session actually works.
[RFC 4254](https://datatracker.ietf.org/doc/html/rfc4254) is the document that dictates how SSH connections are supposed to work on a higher level. There are some interesting details, but most importantly for us is the ["5. Channel Mechanism"](https://datatracker.ietf.org/doc/html/rfc4254#section-5) section:
@ -368,7 +368,7 @@ In the next section, ["7.2. TCP/IP Forwarding Channels"](https://datatracker.iet
So a new channel is being created and opened _for_ us. The channel is labeled `forwarded-tcpip`, which corresponds with the `server_channel_open_forwarded_tcpip()` method of `russh::client::Handler`.
Previously, that `Handler` only had one defined method by our `Client` struct, which accepted all of the key fingerprints that the server provides. So we gotta add a second one to handle any received forwarding channels.
Previously, that `Handler` only had one defined method by our `Client` struct, which accepted all the key fingerprints that the server provides. So we gotta add a second one to handle any received forwarding channels.
Remember, forwarded connections are channels, so we can use them just like the channel we get from `session.channel_open_session()`. And thankfully, as you'll see, Tokio has some utilities to make their usage trivial for our case.
@ -478,13 +478,13 @@ To summarize, this is what the code does:
4. Starts an open session to send and receive control data.
5. Upon receiving a newly-created forwarded TCP/IP channel, uses our router to interact with said channel as if it were a TCP socket.
If you want to see the final code, with some additional functionality for running on [localhost.run](https://localhost.run) (by using a PTY session instead of an open session), you can check out the [russh-axum-tcpip-forward](https://github.com/BadMannersXYZ/russh-axum-tcpip-forward) repository on Github.
If you want to see the final code, with some additional functionality for running on [localhost.run](https://localhost.run) (by using a PTY session instead of an open session), you can check out the [russh-axum-tcpip-forward](https://github.com/BadMannersXYZ/russh-axum-tcpip-forward) repository on GitHub.
## Painting the bigger picture
But a simple HTTP server isn't that interesting by itself, even though it's running over SSH instead of a socket. Can we do better?
Yes, we can. We get all of the nice HTTP features that we'd expect, including support for [WebSocket](https://en.wikipedia.org/wiki/WebSocket) but that's beyond the scope of this post.
Yes, we can. We get all the nice HTTP features that we'd expect, including support for [WebSocket](https://en.wikipedia.org/wiki/WebSocket) but that's beyond the scope of this post.
What I'm more interested in is pushing the limits of this solution in terms of simple HTTP. And since I was just starting to learn [htmx](https://htmx.org/), it seemed like the perfect opportunity to put it to the test.
@ -508,7 +508,7 @@ Seeing the checkboxes getting filled and emptied in a grid reminded me a lot of
<figcaption>A screenshot of me playing Multipaint by Numbers together with myself.</figcaption>
</figure>
It's a janky mess, sure, but it definitely works! It has click-and-drag, it has flagging, and it even has cursors that (try to) match those of other people currently playing as well! It definitely feels buggy (rather than _actually_ being buggy) and unresponsive, since HTMX wasn't designed for highly interactive applications like this one. But it was quite a fun learning experience! If you've dabbled in full-stack development, I highly recommend checking out HTMX if you haven't compared to JS, it's like a breath of fresh air.
It's a janky mess, sure, but it definitely works! It has click-and-drag, it has flagging, and it even has cursors that (try to) match those of other people currently playing as well! And I'm the first to admit, it definitely feels buggy (rather than _actually_ being buggy) and unresponsive, since HTMX wasn't designed for highly interactive applications like this one. But it was quite a fun learning experience! If you've dabbled in full-stack development, I highly recommend checking out HTMX if you haven't compared to JS, it's like a breath of fresh air.
But if you just wanna play it yourself, Multipaint by Numbers is available to play on https://multipaint.sish.top, and you can find the source code [here](https://github.com/BadMannersXYZ/htmx-ssh-games).
@ -520,7 +520,7 @@ But there was a reason. Both "400 Checkboxes" and "Multipaint by Numbers" were j
Recall from my previous post the motivation for looking at SSH reverse port forwarding, in the first place: I wanted to expose services from my home network that would otherwise get blocked by NAT.
This ties in with an idea I've had for a game for a while. It's supposed to run on on your computer, but is controlled remotely through the phone (or a separate browser window), with interactivity that connects and synchronizes both ends. They could be running on the same network, but maybe people use their phone on cellular data, therefore having a completely different [ASN](<https://en.wikipedia.org/wiki/Autonomous_system_(Internet)>) backing it up.
This ties in with an idea I've had for a game for a while. It's supposed to run on your computer, but is controlled remotely through the phone (or a separate browser window), with interactivity that connects and synchronizes both ends. They could be running on the same network, but maybe people use their phone on cellular data, therefore having a completely different [ASN](<https://en.wikipedia.org/wiki/Autonomous_system_(Internet)>) backing it up.
Well, what if you could connect your phone to your computer without worrying about NAT? What if it was as simple as accessing a web page? What if the phone interactions were as simple as touching buttons in a mobile-first webapp?
@ -530,10 +530,10 @@ On the other hand, if you are familiar with [WebRTC](https://en.wikipedia.org/wi
- Setting up a new sish instance for my project is not very complicated, whereas WebRTC makes me want to pull my hair.
- I was already planning on using HTML for the mobile interface (instead of, say, [a native app](https://aws.amazon.com/compare/the-difference-between-web-apps-native-apps-and-hybrid-apps/)), so a hypermedia-driven library like HTMX may suit my needs better than translating the plain data that WebRTC sends.
- However, it'd still require some Javascript on the mobile end of it, for things like [rollback netcode](https://en.wikipedia.org/wiki/Netcode#Rollback).
- However, it'd still require some JavaScript on the mobile end of it, for things like [rollback netcode](https://en.wikipedia.org/wiki/Netcode#Rollback).
- SSH already comes with built-in authentication and encryption mechanisms, meaning that I wouldn't have to roll out my own. (In fact, the people behind sish and tuns.sh leverage this feature of SSH plus _forward_ TCP connections to create [tunnel-based logins for services](https://pico.sh/tunnels).)
- The dependency on SSH is transparent, letting me work on the communication channel as if it were a plain webserver or if it were any other application, for that matter. There is no lock-in to a specific technology like there is with WebRTC.
- Since I plan on having a web interface on the mobile device anyway, this scheme avoids adding extra logic for a separate webserver. The sish proxy will only handle upgrading our HTTP connection to HTTPS essentially, and the webserver can be embedded on the computer application, similarly to a [thin client](https://en.wikipedia.org/wiki/Thin_client).
- The dependency on SSH is transparent, letting me work on the communication channel as if it were a plain web server or if it were any other application, for that matter. There is no lock-in to a specific technology like there is with WebRTC.
- Since I plan on having a web interface on the mobile device anyway, this scheme avoids adding extra logic for a separate web server. The sish proxy will only handle upgrading our HTTP connection to HTTPS essentially, and the web server can be embedded on the computer application, similarly to a [thin client](https://en.wikipedia.org/wiki/Thin_client).
With that said, there's nothing inherently wrong with WebRTC though (other than it being [a complex mess of protocols](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols)), and I'm not dismissing it straight away for this project.