Skip to content

Commit 4e2326b

Browse files
authored
Merge pull request #14 from Dstack-TEE/ssh-over-tproxy
Add example ssh-over-tproxy
2 parents 9f39acf + bbd118f commit 4e2326b

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

ssh-over-tproxy/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SSH Over TPROXY Example
2+
3+
This guide illustrates how to set up an SSH server within a tapp and access it using a public tproxy endpoint.
4+
5+
## Installation Steps
6+
7+
1. **Deploy the Docker Compose File**
8+
Start by deploying the provided `docker-compose.yaml` on Dstack or Phala Cloud. Adjust the workload section as needed, and remember to set the root password using the `ROOT_PW` environment variable.
9+
10+
2. **Install `httpsconnect`**
11+
Ensure that the `httpsconnect` executable is in your system's `PATH`. For instance, you can run:
12+
```
13+
chmod +x httpsconnect && sudo cp httpsconnect /usr/local/bin/
14+
```
15+
16+
3. **Configure Your SSH Client**
17+
Add the following configuration block to your `~/.ssh/config` file:
18+
```
19+
Host my-tapp
20+
HostName localhost
21+
Port 1022
22+
ProxyCommand httpsconnect --proxy <app-id>-8080.<tproxy-serv-domain> -u user -p pass %h %p
23+
```
24+
Be sure to replace `<app-id>` with your tapp's application ID and `<tproxy-serv-domain>` with your tproxy server's domain.
25+
26+
4. **Connect via SSH command**
27+
Finally, initiate the connection by running:
28+
```
29+
ssh root@my-tapp
30+
```
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
services:
2+
ssh-server:
3+
build:
4+
context: .
5+
dockerfile_inline: |
6+
FROM ubuntu:latest
7+
RUN apt-get update && apt-get install -y openssh-server sudo
8+
RUN mkdir /run/sshd
9+
RUN echo 'root:${ROOT_PW}' | chpasswd
10+
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
11+
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
12+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
13+
RUN sed -i 's/#Port 22/Port 1022/' /etc/ssh/sshd_config
14+
EXPOSE 1022
15+
CMD ["/usr/sbin/sshd", "-D"]
16+
restart: unless-stopped
17+
privileged: true
18+
network_mode: host
19+
volumes:
20+
- /:/host/
21+
- /var/run/tappd.sock:/var/run/tappd.sock
22+
- /var/run/docker.sock:/var/run/docker.sock
23+
https-proxy:
24+
build:
25+
context: .
26+
dockerfile_inline: |
27+
FROM alpine:latest
28+
RUN apk add --no-cache wget unzip
29+
RUN wget https://github.com/jthomperoo/simple-proxy/releases/download/v1.3.0/simple-proxy_linux_amd64.zip
30+
RUN unzip -d simple-proxy simple-proxy_linux_amd64.zip
31+
RUN cp simple-proxy/simple-proxy /usr/bin/simple-proxy
32+
RUN rm -r simple-proxy/ simple-proxy_linux_amd64.zip
33+
CMD ["simple-proxy", "-port", "8080", "-basic-auth", "user:pass"]
34+
network_mode: host
35+
workload:
36+
image: nginx

ssh-over-tproxy/httpsconnect

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import socket
5+
import ssl
6+
import argparse
7+
import base64
8+
import select
9+
10+
def create_proxy_tunnel(proxy_host, proxy_port, target_host, target_port, proxy_username=None, proxy_password=None, verify_ssl=True):
11+
# Create SSL context
12+
ssl_context = ssl.create_default_context()
13+
if not verify_ssl:
14+
ssl_context.check_hostname = False
15+
ssl_context.verify_mode = ssl.CERT_NONE
16+
17+
# Create initial socket
18+
sock = socket.create_connection((proxy_host, proxy_port))
19+
20+
# Wrap socket with SSL
21+
ssl_sock = ssl_context.wrap_socket(sock, server_hostname=proxy_host)
22+
23+
# Prepare proxy authorization if credentials are provided
24+
auth_header = ""
25+
if proxy_username and proxy_password:
26+
auth = f"{proxy_username}:{proxy_password}"
27+
auth_bytes = base64.b64encode(auth.encode()).decode()
28+
auth_header = f"Proxy-Authorization: Basic {auth_bytes}\r\n"
29+
30+
# Send CONNECT request
31+
connect_request = (
32+
f"CONNECT {target_host}:{target_port} HTTP/1.1\r\n"
33+
f"Host: {target_host}:{target_port}\r\n"
34+
f"{auth_header}"
35+
"Proxy-Connection: Keep-Alive\r\n"
36+
"\r\n"
37+
)
38+
39+
ssl_sock.send(connect_request.encode())
40+
41+
# Read response
42+
response = ""
43+
while "\r\n\r\n" not in response:
44+
response += ssl_sock.recv(1).decode()
45+
46+
# Check if connection was successful
47+
status_line = response.split("\r\n")[0]
48+
if not status_line.startswith("HTTP/1.1 200"):
49+
raise Exception(f"Proxy connection failed: {status_line}")
50+
51+
return ssl_sock
52+
53+
def main():
54+
parser = argparse.ArgumentParser(description='SSH ProxyCommand for HTTPS proxy')
55+
parser.add_argument('--proxy', required=True, help='Proxy address (host:port)')
56+
parser.add_argument('-u', '--username', help='Proxy username')
57+
parser.add_argument('-p', '--password', help='Proxy password')
58+
parser.add_argument('--no-verify-ssl', action='store_true', help='Disable SSL verification')
59+
parser.add_argument('target_host', help='Target SSH host')
60+
parser.add_argument('target_port', type=int, help='Target SSH port')
61+
62+
args = parser.parse_args()
63+
64+
# Parse proxy address
65+
proxy_host, proxy_port = args.proxy.split(':')
66+
proxy_port = int(proxy_port)
67+
68+
try:
69+
# Create tunnel
70+
tunnel_socket = create_proxy_tunnel(
71+
proxy_host,
72+
proxy_port,
73+
args.target_host,
74+
args.target_port,
75+
args.username,
76+
args.password,
77+
not args.no_verify_ssl
78+
)
79+
80+
# Forward data between stdin/stdout and the tunnel
81+
while True:
82+
r, w, e = select.select([sys.stdin, tunnel_socket], [], [])
83+
84+
if tunnel_socket in r:
85+
data = tunnel_socket.recv(4096)
86+
if not data:
87+
break
88+
sys.stdout.buffer.write(data)
89+
sys.stdout.buffer.flush()
90+
91+
if sys.stdin in r:
92+
data = sys.stdin.buffer.read1(4096)
93+
if not data:
94+
break
95+
tunnel_socket.sendall(data)
96+
97+
except Exception as e:
98+
sys.stderr.write(f"Error: {str(e)}\n")
99+
sys.exit(1)
100+
finally:
101+
if 'tunnel_socket' in locals():
102+
tunnel_socket.close()
103+
104+
if __name__ == "__main__":
105+
main()

0 commit comments

Comments
 (0)