Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 48 additions & 13 deletions apps_script/Code.gs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,62 @@
// and never sees plaintext or holds the key.
//
// Wire: client POSTs base64(encrypted batch). We forward the bytes verbatim
// to RELAY_URL and return its response body verbatim.
// to one of RELAY_URLS and return its response body verbatim.
//
// Replace RELAY_URL with your VPS address before deploying.
// Replace RELAY_URLS with your VPS address(es) before deploying.

const RELAY_URL = 'http://YOUR.VPS.IP:8443/tunnel';
const RELAY_URLS = [
// Replace YOUR_SERVER_PORT with server_config.json's server_port.
// The dist/server_config.json used for the current test listens on 5443.
'http://YOUR.VPS.IP:YOUR_SERVER_PORT/tunnel',
];
const FORWARDER_VERSION = 1;
const PROTOCOL_VERSION = 1;
const ENABLE_INVOCATION_COUNTING = false;
const GAS_RELAY_LOOP_RE = /^https?:\/\/script\.google\.com\/macros\//i;

function doPost(e) {
bumpInvocationCount_();
for (let i = 0; i < RELAY_URLS.length; i++) {
if (GAS_RELAY_LOOP_RE.test(RELAY_URLS[i])) {
return ContentService
.createTextOutput('relay_loop_detected: RELAY_URLS must point to your VPS /tunnel endpoint, not Apps Script')
.setMimeType(ContentService.MimeType.TEXT);
}
}
if (ENABLE_INVOCATION_COUNTING) {
bumpInvocationCount_();
}
const payload = (e && e.postData && e.postData.contents) || '';
const resp = UrlFetchApp.fetch(RELAY_URL, {
method: 'post',
contentType: 'text/plain',
payload: payload,
muteHttpExceptions: true,
followRedirects: false,
deadline: 30, // seconds; long-poll window is kept at 8s for Apps Script stability
});
let lastText = '';
for (let i = 0; i < RELAY_URLS.length; i++) {
try {
const resp = UrlFetchApp.fetch(RELAY_URLS[i], {
method: 'post',
contentType: 'text/plain',
payload: payload,
muteHttpExceptions: true,
followRedirects: false,
deadline: 30, // seconds; long-poll window is kept below this for Apps Script stability
});
const status = resp.getResponseCode();
const text = resp.getContentText();
lastText = text;
if (status === 200) {
return ContentService
.createTextOutput(text)
.setMimeType(ContentService.MimeType.TEXT);
}
lastText = JSON.stringify({
e: 'upstream_status',
status: status,
body: text.slice(0, 1024),
});
} catch (err) {
lastText = String(err);
}
}
return ContentService
.createTextOutput(resp.getContentText())
.createTextOutput(lastText)
.setMimeType(ContentService.MimeType.TEXT);
}

Expand Down
7 changes: 3 additions & 4 deletions client_config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
"socks_port": 1080,
"google_host": "216.239.38.120",
"sni": ["www.google.com", "mail.google.com", "accounts.google.com"],
"_comment_script_keys": "Add more entries here for extra deployments/accounts, e.g. {\"id\":\"AKfycb...\",\"account\":\"acct-b\"}. Deployments with the same account label share one Apps Script daily quota bucket.",
"script_keys": [
{"id": "REPLACE_WITH_DEPLOYMENT_ID", "account": "acct-a"},
{"id": "OPTIONAL_SECOND_DEPLOYMENT_ID", "account": "acct-a"},
{"id": "OPTIONAL_THIRD_DEPLOYMENT_ID", "account": "acct-b"}
{"id": "REPLACE_WITH_DEPLOYMENT_ID", "account": "acct-a"}
],
"tunnel_key": "REPLACE_WITH_OUTPUT_OF_scripts_gen-key.sh",
"tunnel_key": "REPLACE_WITH_64_HEX_CHARACTER_RANDOM_KEY",

"_comment_socks_auth": "Optional: require SOCKS5 username/password (RFC 1929). Both fields must be set together or both omitted.",
"socks_user": "",
Expand Down
8 changes: 4 additions & 4 deletions internal/config/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,15 +323,15 @@ func LoadClient(path string) (*Client, error) {
relayURLs = dedupeStrings(relayURLs)

key := strings.TrimSpace(f.TunnelKey)
if key == "" || key == "REPLACE_WITH_OUTPUT_OF_scripts_gen-key.sh" {
return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: generate a key with 'bash scripts/gen-key.sh' and paste the 64-character output into the tunnel_key field. The same value must be in server_config.json", path)
if key == "" || key == "REPLACE_WITH_64_HEX_CHARACTER_RANDOM_KEY" {
return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: generate a key with 'openssl rand -hex 32' and paste the 64-character output into the tunnel_key field. The same value must be in server_config.json", path)
}
if len(key) != 64 {
return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: generate a fresh key with 'bash scripts/gen-key.sh' and paste the full output. Use the SAME value in client_config.json and server_config.json", len(key), path)
return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: generate a fresh key with 'openssl rand -hex 32' and paste the full output. Use the SAME value in client_config.json and server_config.json", len(key), path)
}
raw, err := hex.DecodeString(key)
if err != nil || len(raw) != 32 {
return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Generate a fresh key with 'bash scripts/gen-key.sh' and copy it carefully — no spaces, quotes, or extra newlines", path)
return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Generate a fresh key with 'openssl rand -hex 32' and copy it carefully — no spaces, quotes, or extra newlines", path)
}

useFronting := len(relayURLs) == 0
Expand Down
3 changes: 2 additions & 1 deletion server_config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"server_host": "0.0.0.0",
"server_port": 8443,
"tunnel_key": "SAME_VALUE_AS_CLIENT_tunnel_key",
"upstream_proxy": "OPTIONAL_socks5://127.0.0.1:40000",
"_comment_upstream_proxy": "Optional: set to socks5://127.0.0.1:40000 only after installing and connecting Cloudflare WARP or another local SOCKS5 proxy on the VPS. Leave empty otherwise.",
"upstream_proxy": "",
"debug_timing": false
}
Loading