Add direct proxying

master
paradust7 2023-08-26 14:24:41 +00:00
parent 1ee93c5715
commit dbb4c0cfe1
5 changed files with 243 additions and 3 deletions

117
ConnectProxy.js Normal file
View File

@ -0,0 +1,117 @@
'use strict';
import assert from 'assert';
import { sanitize } from './util.js';
import { DIRECT_PROXY } from './settings.js';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const CONNECTION_ESTABLISHED_REPLY = textEncoder.encode('HTTP/1.0 200 Connection Established\r\nProxy-agent: Apache/2.4.41 (Ubuntu)\r\n\r\n');
// Not sure what this does, but it seems irrelevant.
const GEOIP_RESPONSE = `HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: %NOW%
Content-Type: application/json
Content-Length: 19
Connection: keep-alive
Cache-Control: max-age=604800, private
Access-Control-Allow-Origin: *
{"continent":"NA"}
`;
const LIST_RESPONSE = `HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: %NOW%
Content-Type: application/json
Content-Length: %LENGTH%
Last-Modified: %NOW%
Connection: keep-alive
Access-Control-Allow-Origin: *
%PAYLOAD%
`;
const PAYLOAD = {
'total': { 'servers': DIRECT_PROXY.length, 'clients': 0 },
'total_max': { 'server': DIRECT_PROXY.length, 'clients': 0 },
'list': DIRECT_PROXY.map(([vip, ip, port]) => {
return {
'address': vip,
'ip': vip,
'port': port,
'proto_min': 37,
'proto_max': 42,
}}),
};
// Fake a CONNECT proxy to simulate servers.minetest.net response
export class ConnectProxy {
constructor(client) {
this.client = client;
this.firstLine = true;
this.conn = null;
}
forward(data) {
if (this.firstLine) {
this.firstLine = false;
this.handle_handshake(data);
return;
}
if (!(data instanceof ArrayBuffer)) {
throw new Error("ConnectProxy received non-binary messages");
}
data = textDecoder.decode(data);
assert(data.endsWith('\r\n\r\n'));
let lines = data.split('\r\n');
assert(lines.length >= 1);
let tokens = lines[0].split(' ');
assert(tokens[0] == 'GET');
let url = sanitize(tokens[1]);
const now = (new Date()).toUTCString();
let response;
if (url.startsWith('/geoip')) {
response = GEOIP_RESPONSE.replace(/%NOW%/g, now);
} else if (url.startsWith('/list')) {
const payload = JSON.stringify(PAYLOAD);
response = LIST_RESPONSE.replace(/%NOW%/g, now).replace('%LENGTH%', payload.length + 1).replace('%PAYLOAD%', payload);
this.client.log("Sending virtual server list")
} else {
this.client.log(`Invalid GET request for ${url}`);
this.client.close();
return;
}
this.client.send(textEncoder.encode(response));
}
handle_handshake(data) {
// The CONNECT line and it's headers could be split among several packets.
// In a real server, this would aggregate data until it sees \r\n\r\n
// But minetest-wasm always sends it as one packet, so just assume that.
data = textDecoder.decode(data);
assert(data.endsWith('\r\n\r\n'));
let lines = data.split('\r\n');
assert(lines.length >= 1);
let tokens = lines[0].split(' ');
assert.strictEqual(tokens.length, 3);
assert.strictEqual(tokens[0], 'CONNECT');
assert.strictEqual(tokens[2], 'HTTP/1.1');
let host_port = tokens[1].split(':');
assert.strictEqual(host_port.length, 2);
let host = host_port[0];
let port = parseInt(host_port[1]);
if (host != 'servers.minetest.net' || port != 80) {
this.client.log(`Ignoring request to proxy to ${host}:${port}`);
this.client.close();
return;
}
this.client.log('Connected for server list');
this.client.send(CONNECTION_ESTABLISHED_REPLY);
}
close() {
this.client.close();
}
}

75
UDPProxy.js Normal file
View File

@ -0,0 +1,75 @@
'use strict';
import dgram from 'dgram';
export class UDPProxy {
constructor(client, ip, port) {
const socket = dgram.createSocket('udp4');
this.client = client;
this.socket = socket;
this.ip = ip;
this.port = port;
this.sendok = false;
this.sendqueue = [];
socket.on('listening', this.handle_listening.bind(this));
socket.on('error', this.handle_error.bind(this));
socket.on('message', this.handle_message.bind(this));
socket.bind();
}
forward(data) {
// This creates a view of the ArrayBuffer
data = new Uint8Array(data);
if (data.byteLength < 4 ||
data[0] != 0x4f ||
data[1] != 0x45 ||
data[2] != 0x74 ||
data[3] != 0x03) {
throw new Error('Client sent packet with invalid protocol.');
}
if (this.sendok) {
// data must be a typed array here
this.socket.send(data, this.port, this.ip);
} else {
this.sendqueue.push(data);
}
}
handle_listening() {
const sourcePort = this.socket.address().port;
this.log(`Bound ${sourcePort} -> ${this.ip}:${this.port}`);
this.sendok = true;
if (this.sendqueue.length > 0) {
for (const data of this.sendqueue) {
this.socket.send(data, this.port, this.ip);
}
this.sendqueue = [];
}
}
handle_error(err) {
this.log("Socket error: " + err);
this.close();
}
handle_message(msg, rinfo) {
if (rinfo.address != this.ip || rinfo.port != this.port) {
this.log("Ignoring unsolicited packet from " + rinfo.address + " port " + rinfo.port);
return;
}
this.client.send(msg);
}
log(msg) {
this.client.log(msg);
}
close() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
this.client.close();
}
}

View File

@ -1,8 +1,14 @@
'use strict';
import assert from 'assert';
import { isIPv4 } from 'net';
import { format } from 'util'; // node.js built-in
import { DIRECT_PROXY } from './settings.js';
import { ConnectProxy } from './ConnectProxy.js';
import { UDPProxy } from './UDPProxy.js';
import { extract_ip_chain, sanitize } from './util.js';
import { vpn_make, vpn_connect} from './vpn.js';
import { format } from 'util'; // node.js built-in
const textDecoder = new TextDecoder();
let lastlog = null;
@ -99,6 +105,20 @@ export class Client {
return;
}
response = 'BIND OK';
} else if (command == 'PROXY') {
assert(tokens[2] == 'TCP' || tokens[2] == 'UDP');
const isUDP = (tokens[2] == 'UDP');
const ip = sanitize(tokens[3]);
const port = parseInt(sanitize(tokens[4]));
assert(isIPv4(ip));
assert(port >= 1 && port < 65536);
this.target = route(this, isUDP, ip, port);
if (!this.target) {
this.log(`Proxy to udp=${isUDP}, ip=${ip}, port=${port} rejected`);
response = 'PROXY FAIL';
} else {
response = 'PROXY OK';
}
} else {
this.log('Unhandled command: ', data);
this.close();
@ -108,3 +128,17 @@ export class Client {
}
}
const PROXY_MAP = new Map(DIRECT_PROXY.map(([vip,ip,port]) => [vip, [ip, port]]));
function route(client, isUDP, ip, port) {
if (!isUDP && ip == '10.0.0.1' && port == 8080) {
return new ConnectProxy(client);
}
if (isUDP && PROXY_MAP.has(ip)) {
let [real_ip, real_port] = PROXY_MAP.get(ip);
return new UDPProxy(client, real_ip, real_port);
}
return null;
}

View File

@ -1,7 +1,6 @@
'use strict';
const PROXY_PORT = 8888;
import { PROXY_PORT } from './settings.js';
import { WebSocketServer } from 'ws';
import { Client } from './client.js';

15
settings.js Normal file
View File

@ -0,0 +1,15 @@
export const PROXY_PORT = 8888;
// [virtual_ip, real_ip, real_port]
//
// The virtual IP is the one that minetest-wasm sees.
// The virtual port is the same as the real port.
//
export const DIRECT_PROXY = [
// This allows clients to connect to a server running on the proxy itself.
['192.168.0.1', '127.0.0.1', 30000],
// This would allow clients to connect to 1.2.3.4, port 40000
//['192.168.0.2', '1.2.3.4', 40000],
];