Merge pull request #275 from Jigsaw-Code/fortuna-go-only

Remove ss-libev
This commit is contained in:
Vinicius Fortuna 2018-12-10 11:22:53 -05:00 committed by GitHub
commit 907faae795
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 11 additions and 1281 deletions

View file

@ -33,7 +33,6 @@ COPY src/shadowbox/scripts/update_mmdb.sh /etc/periodic/weekly/update_mmdb
# TODO: remove the ACL file, used to prevent access to localhost and LAN, when migrating to shadowsocks-go.
COPY src/shadowbox/shadowbox.acl /root/shadowbox/shadowsocks.acl
RUN sh ./scripts/install_shadowsocks.sh 3.2.0
RUN /etc/periodic/weekly/update_mmdb
WORKDIR /root/shadowbox

View file

@ -1,45 +0,0 @@
// Copyright 2018 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ip_location from './ip_location';
function testIpLocationService(name: string, service: ip_location.IpLocationService) {
describe(name, () => {
it('returns ZZ on unknown country', (done) => {
service.countryForIp('127.0.0.1')
.then((countryCode) => {
expect(countryCode).toEqual('ZZ');
done();
})
.catch((e) => {
done.fail(e);
});
});
it('returns AU for 1.0.0.1', (done) => {
service.countryForIp('1.0.0.1')
.then((countryCode) => {
expect(countryCode).toEqual('AU');
done();
})
.catch((e) => {
done.fail(e);
});
});
});
}
testIpLocationService(
'MmdbLocationService',
new ip_location.MmdbLocationService(
'third_party/maxmind/GeoLite2-Country_20180327/GeoLite2-Country.mmdb'));

View file

@ -1,55 +0,0 @@
// Copyright 2018 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as maxmind from 'maxmind';
export interface IpLocationService {
// Returns the 2-digit country code for the IP address.
countryForIp(ipAddress: string): Promise<string>;
}
// An IpLocationService that uses the node-maxmind package.
// The database is downloaded by scripts/update_mmdb.sh.
// The Dockerfile runs this script on boot and configures the system to run it weekly.
export class MmdbLocationService implements IpLocationService {
private readonly db: Promise<maxmind.Reader>;
constructor(filename: string) {
this.db = new Promise<maxmind.Reader>((fulfill, reject) => {
// TODO: Change type to maxmind.Options once the type definition is updated
// with these fields.
const options: {} = {watchForUpdates: true, watchForUpdatesNonPersistent: true};
maxmind.open(filename, options, (err, reader) => {
if (err) {
reject(err);
} else {
fulfill(reader);
}
});
});
}
countryForIp(ipAddress) {
return this.db.then((reader) => {
if (!reader) {
throw new Error('MMDB reader is not valid');
}
if (!maxmind.validate(ipAddress)) {
throw new Error('Invalid IP address');
}
const result = reader.get(ipAddress);
return (result && result.country && result.country.iso_code) || 'ZZ';
});
}
}

View file

@ -11,9 +11,7 @@
],
"dependencies": {
"ShadowsocksConfig": "Jigsaw-Code/outline-shadowsocksconfig#^v0.0.8",
"ipaddr.js": "^1.4.0",
"js-yaml": "^3.12.0",
"maxmind": "^2.7.0",
"prom-client": "^11.1.3",
"randomstring": "^1.1.5",
"request-lite": "^2.40.1",
@ -22,7 +20,6 @@
},
"devDependencies": {
"@types/js-yaml": "^3.11.2",
"@types/maxmind": "^2",
"@types/node": "^8",
"@types/randomstring": "^1.1.6",
"@types/restify": "^2.0.41"

View file

@ -1,71 +0,0 @@
#!/bin/bash -eu
#
# Copyright 2018 The Outline Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# TODO(fortuna): Delete once outline-ss-server rolls out
VERSION=$1
DOWNLOAD_URL=https://github.com/shadowsocks/shadowsocks-libev/releases/download/v${VERSION}/shadowsocks-libev-${VERSION}.tar.gz
BUILD_DIR=/src/shadowsocks-libev
set -ex
# Install runtime dependencies
apk add --no-cache libev c-ares libsodium mbedtls pcre
# Install build dependencies
apk add --no-cache --virtual BUILD_DEPS \
autoconf automake build-base gettext-dev libev-dev libsodium-dev libtool \
linux-headers mbedtls-dev openssl-dev pcre-dev tar c-ares-dev
# Build.
mkdir -p $BUILD_DIR
cd $BUILD_DIR
curl -sSL $DOWNLOAD_URL | tar xz --strip 1
./configure --disable-documentation
make install
# Other licenses and/or source.
# Alpine does not always include LICENSE files and has no equivalent of
# Debian's "apt source" command. So, we have to manually roll something.
# We'll place licenses in the root folder of the image, named LICENSE.xxx,
# and sources under /src.
# libev (BSD or GPL2):
# http://software.schmorp.de/pkg/libev.html
curl -sS http://cvs.schmorp.de/libev/LICENSE > /LICENSE.libev
# c-ares (MIT):
# https://c-ares.haxx.se/
curl -sS https://c-ares.haxx.se/license.html > /LICENSE.c-ares.html
# libsodium (ISC):
# https://libsodium.org/
curl -sS https://raw.githubusercontent.com/jedisct1/libsodium/master/LICENSE > /LICENSE.libsodium
# mbedtls (Apache):
# https://tls.mbed.org/
curl -sS https://raw.githubusercontent.com/ARMmbed/mbedtls/development/apache-2.0.txt > /LICENSE.mbedtls
# pcre (BSD):
# http://www.pcre.org/
curl -sS http://www.pcre.org/licence.txt > /LICENSE.pcre
# Clean shadowsocks-libev's folder, leaving the source in the image.
make clean
# Remove build dependencies.
apk del BUILD_DEPS

View file

@ -1,231 +0,0 @@
// Copyright 2018 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as child_process from 'child_process';
import * as dgram from 'dgram';
import * as dns from 'dns';
import {IpLocationService} from '../infrastructure/ip_location';
import * as logging from '../infrastructure/logging';
import {AccessKey, ShadowsocksServer} from '../model/shadowsocks_server';
import {UsageMetricsWriter} from './shared_metrics';
const SHADOWSOCKS_ACL_PATH = '/root/shadowbox/shadowsocks.acl';
export async function createLibevShadowsocksServer(
publicAddress: string, metricsSocketPort: number, ipLocation: IpLocationService,
usageWriter: UsageMetricsWriter, verbose: boolean) {
const metricsSocket = await createBoundUdpSocket(metricsSocketPort);
return new LibevShadowsocksServer(publicAddress, metricsSocket, ipLocation, usageWriter, verbose);
}
// Runs shadowsocks-libev server instances.
// TODO(fortuna): Delete once outline-ss-server rolls out
export class LibevShadowsocksServer implements ShadowsocksServer {
private portId = new Map<number, string>();
private portInboundBytes = new Map<number, number>();
private portIps = new Map<number, string[]>();
private keyProcess = new Map<string, child_process.ChildProcess>();
private keys = new Map<string, AccessKey>();
constructor(
private readonly publicAddress: string, private readonly metricsSocket: dgram.Socket,
ipLocation: IpLocationService, usageWriter: UsageMetricsWriter,
private readonly verbose: boolean) {
metricsSocket.on('message', (buf: Buffer) => {
let metricsMessage;
try {
metricsMessage = parseMetricsMessage(buf);
} catch (err) {
logging.error(`Error parsing metrics message ${buf}: ${err.stack}`);
return;
}
const accessKeyId = this.portId.get(metricsMessage.portNumber);
if (accessKeyId === undefined) {
// Access key has been deleted, and we no longer have the portInboundBytes. Ignore.
return;
}
let previousTotalInboundBytes = this.portInboundBytes.get(metricsMessage.portNumber) || 0;
if (previousTotalInboundBytes > metricsMessage.totalInboundBytes) {
// totalInboundBytes is a counter that monotonically increases. A drop means
// ss-server got restarted, so we set the previous value to zero.
previousTotalInboundBytes = 0;
}
const dataDelta = metricsMessage.totalInboundBytes - previousTotalInboundBytes;
if (dataDelta === 0) {
return;
}
this.portInboundBytes.set(metricsMessage.portNumber, metricsMessage.totalInboundBytes);
getConnectedClientIPAddresses(metricsMessage.portNumber)
.catch((e) => {
logging.error(
`Unable to get client IP for port ${metricsMessage.portNumber}: ${e.stack}`);
return [];
})
.then((ipAddresses: string[]) => {
// We keep using the same IP addresses if we don't see any IP for a port.
// This may happen if getConnectedClientIPAddresses runs when there's no TCP
// connection open at that moment.
if (ipAddresses && ipAddresses.length > 0) {
this.portIps.set(metricsMessage.portNumber, ipAddresses);
} else {
ipAddresses = this.portIps.get(metricsMessage.portNumber) || [];
}
return Promise.all(ipAddresses.map((ipAddress) => {
return ipLocation.countryForIp(ipAddress).catch((e) => {
logging.error(`failed to get country for IP: ${e.stack}`);
return 'ZZ';
});
}));
})
.then((countries: string[]) => {
const dedupedCountries = [...new Set(countries)].sort();
usageWriter.writeBytesTransferred(accessKeyId || '', dataDelta, dedupedCountries);
})
.catch((err: Error) => {
logging.error(`Unable to write bytes transferred: ${err.stack}`);
});
});
}
// Update spawns the ss-libev subprocess and returns, likely before the instances
// are ready and serving.
update(newKeys: AccessKey[]): Promise<void> {
return new Promise((resolve, reject) => {
const oldKeyIds = this.keys.keys();
const newKeyIds = new Set(newKeys.map(k => k.id));
// Start keys that were added
for (const key of newKeys) {
if (this.keys.has(key.id)) {
continue;
}
this.startInstance(key);
}
// Stop keys that were removed.
for (const oldKeyId of oldKeyIds) {
if (!newKeyIds.has(oldKeyId)) {
this.stopInstance(oldKeyId);
}
}
resolve();
});
}
private startInstance(key: AccessKey): child_process.ChildProcess {
logging.info(`Starting server on port ${key.port}`);
this.keys.set(key.id, key);
this.portId.set(key.port, key.id);
const metricsAddress = this.metricsSocket.address();
const commandArguments = [
'-m', key.cipher, // Encryption method
'-u', // Allow UDP
'--fast-open', // Allow TCP fast open
'-p', key.port.toString(), '-k', key.secret, '--manager-address',
`${metricsAddress.address}:${metricsAddress.port}`, '--acl', SHADOWSOCKS_ACL_PATH
];
logging.info('starting ss-server with args: ' + commandArguments.join(' '));
// Add the system DNS servers.
// TODO(fortuna): Add dns.getServers to @types/node.
for (const dnsServer of dns.getServers()) {
commandArguments.push('-d');
commandArguments.push(dnsServer);
}
if (this.verbose) {
// Make the Shadowsocks output verbose in debug mode.
commandArguments.push('-v');
}
const childProcess = child_process.spawn('ss-server', commandArguments);
this.keyProcess.set(key.id, childProcess);
childProcess.on('error', (error) => {
logging.error(`Error spawning server on port ${key.port}: ${error}`);
});
// TODO(fortuna): Add restart logic.
childProcess.on('exit', (code, signal) => {
logging.info(`Server on port ${key.port} has exited. Code: ${code}, Signal: ${signal}`);
});
// This exposes the ss-server output on the docker logs.
// TODO(fortuna): Consider saving the output and expose it through the manager service.
childProcess.stdout.pipe(process.stdout);
childProcess.stderr.pipe(process.stderr);
return childProcess;
}
private stopInstance(keyId: string) {
this.keyProcess.get(keyId).kill();
this.keyProcess.delete(keyId);
const key = this.keys.get(keyId);
this.keys.delete(keyId);
this.portId.delete(key.port);
this.portIps.delete(key.port);
this.portInboundBytes.delete(key.port);
}
}
function getConnectedClientIPAddresses(portNumber: number): Promise<string[]> {
const lsofCommand = `lsof -i tcp:${portNumber} -n -P -Fn ` +
' | grep \'\\->\'' + // only look at connection lines (e.g. skips "p8855" and "f60")
' | sed \'s/:\\d*$//g\'' + // remove p
' | sed \'s/n\\S*->//g\'' + // remove first part of address
' | sed \'s/\\[//g\'' + // remove [] (used by ipv6)
' | sed \'s/\\]//g\'' + // remove ] (used by ipv6)
' | sort | uniq'; // remove duplicates
return execCmd(lsofCommand).then((output: string) => {
return output.trim().split('\n').map((e) => e.trim()).filter(Boolean);
});
}
function execCmd(cmd: string): Promise<string> {
return new Promise((fulfill, reject) => {
child_process.exec(cmd, (error: child_process.ExecError, stdout: string, stderr: string) => {
if (error) {
reject(error);
} else {
fulfill(stdout.trim());
}
});
});
}
interface MetricsMessage {
portNumber: number;
totalInboundBytes: number;
}
function parseMetricsMessage(buf): MetricsMessage {
const jsonString = buf.toString()
.substr('stat: '.length) // remove leading "stat: "
.replace(/\0/g, ''); // remove trailing null terminator
// statObj is in the form {"port#": totalInboundBytes}, where
// there is always only 1 port# per JSON object. If there are multiple
// ss-servers communicating to the same manager, we will get multiple
// message events.
const statObj = JSON.parse(jsonString);
// Object.keys is used here because node doesn't support Object.values.
const portNumber = parseInt(Object.keys(statObj)[0], 10);
const totalInboundBytes = statObj[portNumber];
return {portNumber, totalInboundBytes};
}
// Creates a bound UDP socket on a random unused port.
function createBoundUdpSocket(portNumber: number): Promise<dgram.Socket> {
const socket = dgram.createSocket('udp4');
return new Promise((fulfill, reject) => {
socket.bind(portNumber, 'localhost', () => {
return fulfill(socket);
});
});
}

View file

@ -21,21 +21,18 @@ import * as restify from 'restify';
import {RealClock} from '../infrastructure/clock';
import {PortProvider} from '../infrastructure/get_port';
import * as ip_location from '../infrastructure/ip_location';
import * as json_config from '../infrastructure/json_config';
import * as logging from '../infrastructure/logging';
import {PrometheusClient, runPrometheusScraper} from '../infrastructure/prometheus_scraper';
import {RolloutTracker} from '../infrastructure/rollout';
import {AccessKeyId} from '../model/access_key';
import {ShadowsocksServer} from '../model/shadowsocks_server';
import {createLibevShadowsocksServer} from './libev_shadowsocks_server';
import {LegacyManagerMetrics, LegacyManagerMetricsJson, PrometheusManagerMetrics} from './manager_metrics';
import {bindService, ShadowsocksManagerService} from './manager_service';
import {OutlineShadowsocksServer} from './outline_shadowsocks_server';
import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key';
import * as server_config from './server_config';
import {createPrometheusUsageMetricsWriter, OutlineSharedMetricsPublisher, PrometheusUsageMetrics, RestMetricsCollectorClient, SharedMetricsPublisher} from './shared_metrics';
import {OutlineSharedMetricsPublisher, PrometheusUsageMetrics, RestMetricsCollectorClient, SharedMetricsPublisher} from './shared_metrics';
const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state';
const MAX_STATS_FILE_AGE_MS = 5000;
@ -81,6 +78,7 @@ function reserveAccessKeyPorts(
}
}
// TODO: Get rid of this after 30 days of everyone's migration to Prometheus.
function createLegacyManagerMetrics(configFilename: string): LegacyManagerMetrics {
const metricsConfig = readMetricsConfig(configFilename);
return new LegacyManagerMetrics(
@ -160,23 +158,14 @@ async function main() {
]
};
const rollouts = createRolloutTracker(serverConfig);
let shadowsocksServer: ShadowsocksServer;
if (rollouts.isRolloutEnabled('outline-ss-server', 100)) {
const ssMetricsLocation = `localhost:${ssMetricsPort}`;
logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`);
prometheusConfigJson.scrape_configs.push(
{job_name: 'outline-server-ss', static_configs: [{targets: [ssMetricsLocation]}]});
shadowsocksServer =
new OutlineShadowsocksServer(
getPersistentFilename('outline-ss-server/config.yml'), verbose, ssMetricsLocation)
.enableCountryMetrics(MMDB_LOCATION);
} else {
const ipLocation = new ip_location.MmdbLocationService(MMDB_LOCATION);
const metricsWriter = createPrometheusUsageMetricsWriter(prometheus.register);
shadowsocksServer = await createLibevShadowsocksServer(
proxyHostname, await portProvider.reserveNewPort(), ipLocation, metricsWriter, verbose);
}
const ssMetricsLocation = `localhost:${ssMetricsPort}`;
logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`);
prometheusConfigJson.scrape_configs.push(
{job_name: 'outline-server-ss', static_configs: [{targets: [ssMetricsLocation]}]});
const shadowsocksServer =
new OutlineShadowsocksServer(
getPersistentFilename('outline-ss-server/config.yml'), verbose, ssMetricsLocation)
.enableCountryMetrics(MMDB_LOCATION);
runPrometheusScraper(
[
'--storage.tsdb.retention', '31d', '--storage.tsdb.path',

855
yarn.lock

File diff suppressed because it is too large Load diff