Commit c15e7d9e authored by nanahira's avatar nanahira

Merge branch 'master' into gwgroup

parents 5fed9a8c ce4c9b91
......@@ -114,5 +114,7 @@ dist
*.retry
wgfrp-setconf.conf.j2
certs
babeld-reload.conf.j2
__pycache__
......@@ -3,3 +3,4 @@ host_key_checking = False
strategy_plugins = mitogen-0.2.9/ansible_mitogen/plugins/strategy
strategy = mitogen_free
inventory = ../result/inventory.yaml
forks = 12
# MyCard babeld
# debug 1
# router-id 00:00:00:00:00:00:00:03
redistribute local ip 10.198.0.0/24
{% for subnet in localSubnets %}
redistribute ip {{subnet}}
{% endfor %}
redistribute local deny
reflect-kernel-metric true
local-port-readwrite 33123
{% for connection in connections %}
{% if connection.protocol != "null" and not (connection.protocol == "oc" and connection.ocType == "server") and connection.remoteLocalAddress != address %}
{% if connection.inbound %}
interface {{connection.name}} type tunnel rxcost {{connection.metric}} faraway true link-quality true
{% else %}
interface {{connection.name}} type tunnel rxcost 50000 faraway true link-quality true
{% endif %}
{% endif %}
{% endfor %}
{% for interface in lanInterfaces %}
interface {{interface}}
{% endfor %}
# MyCard bird
# Force change in 12.12
# Force change in 12.14
log stderr all;
router id {{address}};
protocol device {
}
#protocol direct {
# disabled;
# ipv4;
# ipv6;
#}
{% for interface in lanInterfaces %}
protocol direct {
ipv4;
ipv6;
interface "{{interface}}";
}
{% endfor %}
protocol device {}
protocol kernel {
learn;
ipv4 {
<<<<<<< HEAD
import where net = 0.0.0.0/0;
export where net != 0.0.0.0/0;
};
#ipv6 { export all; };
=======
import all;
};
>>>>>>> master
}
......@@ -44,54 +35,7 @@ protocol kernel {
table {{plan.name}};
export all;
};
persist;
kernel table {{plan.destMark}};
}
{% endfor %}
protocol ospf v2 {
ipv4 {
import all;
export all; # where source ~ [ RTS_DEVICE, RTS_STATIC ];
};
area 0 {
networks {
{% for subnet in routeLists.mycard %}
{{subnet}};
{% endfor %}
};
# interface "eth*" {
# type broadcast; # Detected by default
# cost 10; # Interface metric
# hello 5; # Default hello perid 10 is too long
# };
{% for connection in connections %}
{% if connection.protocol != "null" and connection.remoteLocalAddress != address %}
interface "{{connection.name}}" {
type ptmp;
{% if connection.outbound %}
cost {{connection.metric}};
{% else %}
cost 50000;
{% endif %}
hello 5;
authentication cryptographic;
password "{{ospfToken}}";
};
{% endif %}
{% endfor %}
{% for interface in lanInterfaces %}
interface "{{interface}}" {
type broadcast;
cost 1;
hello 5;
authentication cryptographic;
password "{{ospfToken}}";
};
{% endfor %}
interface "dummy0" {
stub; # Stub interface, just propagate it
};
};
}
---
- hosts: wg
vars:
services: []
tasks:
- name: load vars
include_vars:
file: '../result/{{item}}.yaml'
with_items:
- global-vars
- vars-{{inventory_hostname_short}}
- name: directory
file:
path: '{{ansible_user_dir}}/nextgen-network/{{item}}'
......@@ -24,17 +28,43 @@
- utility
- switch-rules-up
- switch-rules-down
- ocserv-postup
- ocserv-predown
notify: reload_switch_rules
- name: ipset files
template:
src: scripts/ipset.j2
dest: '{{ansible_user_dir}}/nextgen-network/ipsets/{{item}}.ipset'
with_items: '{{routeListNames}}'
notify: reload_chnroute
- name: global-postup
become: true
shell: '{{ansible_user_dir}}/nextgen-network/scripts/global-postup.sh'
args:
creates: /tmp/mycard_global_postup_done
- name: global-postup systemd
become: true
copy:
content: |
[Unit]
Description=MyCard Network Global Setup
Before=network-online.target
After=network-pre.target
[Service]
Type=oneshot
ExecStart={{ansible_user_dir}}/nextgen-network/scripts/global-postup.sh
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/railgun-global-setup.service
register: global_systemd_result
- name: global-postup systemd enable
become: true
systemd:
name: railgun-global-setup
enabled: true
daemon_reload: '{{global_systemd_result.changed}}'
- name: mycard ipset create
become: true
shell: 'ipset create mycard hash:net maxelem 1000000 || true'
......@@ -50,54 +80,125 @@
become: true
shell: 'ip -4 rule add pref 81 to {{item}} lookup main || true'
with_items: '{{routeLists.mycard}}'
- name: ocserv pre-configure
include_tasks: 'protocols/oc/ocserv-pre.yaml'
when: ocservNeeded and not noBird
- name: disable bug self-link
become: true
ignore_errors: true
systemd:
name: 'wg-quick@{{item}}'
state: stopped
enabled: false
masked: true
with_items:
- mc-{{inventory_hostname_short}}
- mci{{inventory_hostname_short}}
# 为了提高测试时候的性能,不改动wg的时候注释掉这段
- name: 'clean up null connections first'
include_tasks: 'protocols/{{item.protocol}}/configure.yaml'
vars:
conn: '{{item}}'
with_items: '{{ connections }}'
when: "not noUpdateLinks and item.protocol == 'null'"
when: "not noUpdateLinks and item.protocol == 'null' and not item.noUpdate"
- name: 'loop through list from a variable'
include_tasks: 'protocols/{{item.protocol}}/configure.yaml'
vars:
conn: '{{item}}'
with_items: '{{ connections }}'
when: "not noUpdateLinks and item.protocol != 'null'"
when: "not noUpdateLinks and item.protocol != 'null' and not item.noUpdate"
# end
- name: services conf
copy:
content: '{{dockerServices | to_yaml}}'
dest: '{{ansible_user_dir}}/nextgen-network/services/docker-compose.yml'
when: not noBird
- name: bird conf
template:
src: bird.conf.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/bird.conf'
notify: restart_bird
when: not noBird
- name: babeld conf
template:
src: babeld.conf.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/babeld.conf'
#notify: restart_babeld
when: not noBird
- name: babeld reload conf
template:
src: babeld-reload.conf.j2
dest: /tmp/babeld-reload.conf
notify: reload_babeld
- name: frps conf
template:
src: protocols/wgfrp/frps.ini.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/frps.ini'
when: frpsNeeded
when: frpsNeeded and not noBird
notify: restart_frps
- name: start services
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
remove_orphans: true
# pull: true
when: not noBird
- name: systemd bird
become: true
template:
src: bird.conf.j2
dest: '/etc/bird/bird.conf'
notify: restart_bird_systemd
when: systemBird
- name: enable systemd bird
become: true
systemd:
name: bird
state: started
enabled: true
masked: false
when: systemBird
- name: systemd babeld conf
become: true
template:
src: babeld.conf.j2
dest: '/etc/babeld.conf'
#notify: restart_babeld_systemd
when: systemBird
- name: enable systemd babeld
become: true
systemd:
name: babeld
state: started
enabled: true
masked: false
when: systemBird
handlers:
- name: reload_switch_rules
become: true
shell: '{{ansible_user_dir}}/nextgen-network/scripts/switch-rules-down.sh ; {{ansible_user_dir}}/nextgen-network/scripts/switch-rules-up.sh'
- name: restart_bird
shell: 'docker-compose exec bird birdc configure'
args:
chdir: '{{ansible_user_dir}}/nextgen-network/services'
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- bird
when: not noBird
- name: restart_babeld # ocserv would be always restarted whenever key changes..
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- babeld
when: not noBird
- name: reload_babeld
shell: cat /tmp/babeld-reload.conf | timeout 1 nc ::1 33123 || true
- name: restart_frps
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- frps
when: not noBird
- name: restart_frpc
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
......@@ -105,4 +206,35 @@
services:
- frpc-{{item.name}}
with_items: '{{connections}}'
when: 'item.protocol == "wgfrp" and item.frpType == "frpc"'
when: 'item.protocol == "wgfrp" and item.frpType == "frpc" and not noBird and not item.noUpdate'
- name: restart_ocserv
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- ocserv
when: not noBird
- name: restart_openconnect
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- openconnect-{{item.name}}
with_items: '{{connections}}'
when: 'item.protocol == "oc" and item.ocType == "client" and not noBird and not item.noUpdate'
- name: restart_bird_systemd
become: true
systemd:
name: bird
state: restarted
- name: restart_babeld_systemd
become: true
systemd:
name: babeld
state: restarted
when: systemBird
- name: reload_chnroute
become: true
shell: |
ipset flush chnrouter
sed '/^create chnrouter hash:net family inet$/d' {{ansible_user_dir}}/nextgen-network/ipsets/chnrouter.ipset | ipset restore
......@@ -11,3 +11,33 @@
- fatedier/frps:v0.34.2
- fatedier/frpc:v0.34.2
- git-registry.mycard.moe/nanahira/docker-bird
- git-registry.mycard.moe/railgun/babeld
- git-registry.mycard.moe/nanahira/docker-ocserv
- git-registry.mycard.moe/railgun/openconnect
when: not noBird
- name: unstable source
become: true
copy:
content: |
deb http://mirrors.tuna.tsinghua.edu.cn/debian unstable main contrib non-free
dest: /etc/apt/sources.list.d/unstable.list
when: systemBird and ansible_distribution == 'Debian' and ansible_distribution_release != 'sid'
- name: unstable pref 90
become: true
copy:
content: |
Package: *
Pin: release a=unstable
Pin-Priority: 90
dest: /etc/apt/preferences.d/limit-unstable
when: systemBird and ansible_distribution == 'Debian' and ansible_distribution_release != 'sid'
- name: netcat-openbsd
become: true
apt:
update_cache: true
name: netcat-openbsd
- name: install packages for systemd things
become: true
apt:
name: babeld,bird2
when: systemBird
- hosts: wg
remote_user: root
tasks:
- name: load vars
include_vars:
file: '../result/{{item}}.yaml'
with_items:
- global-vars
- vars-{{inventory_hostname_short}}
- name: post scripts
template:
src: scripts/{{item}}.sh.j2
dest: '{{ansible_user_dir}}/nextgen-network/scripts/{{item}}.sh'
mode: a+x
with_items:
- postup
- predown
- global-postup
- utility
- switch-rules-up
- switch-rules-down
- ocserv-postup
- ocserv-predown
- name: chnroute
become: true
shell: |
ipset restore -f {{ansible_user_dir}}/nextgen-network/ipsets/chnrouter.ipset || true
- name: reload_switch_rules
become: true
shell: '{{ansible_user_dir}}/nextgen-network/scripts/switch-rules-down.sh ; {{ansible_user_dir}}/nextgen-network/scripts/switch-rules-up.sh'
---
- hosts: wg
tasks:
- name: ocserv predown
template:
src: scripts/ocserv-predown-old.sh.j2
dest: '{{ansible_user_dir}}/nextgen-network/scripts/ocserv-predown.sh'
mode: a+x
---
- hosts: wg
vars:
services: []
tasks:
- name: global-postup systemd
become: true
copy:
content: |
[Unit]
Description=MyCard Network Global Setup
Before=network-online.target
After=network-pre.target
[Service]
Type=oneshot
ExecStart={{ansible_user_dir}}/nextgen-network/scripts/global-postup.sh
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/railgun-global-setup.service
register: global_systemd_result
- name: global-postup systemd disable
become: true
systemd:
name: railgun-global-setup
enabled: false
daemon_reload: true
- name: global-postup systemd enable
become: true
systemd:
name: railgun-global-setup
enabled: true
daemon_reload: true
- name: remove a rubbish
file:
path: /etc/systemd/system/mutli-user.target.wants
state: absent
- name: up script directory
file:
path: '{{ansible_user_dir}}/nextgen-network/services/client-scripts/{{conn.name}}/post-connect.d'
state: directory
recurse: true
- name: down script directory
file:
path: '{{ansible_user_dir}}/nextgen-network/services/client-scripts/{{conn.name}}/disconnect.d'
state: directory
recurse: true
- name: up script
template:
src: ./openconnect-post-scripts/post-connect.sh.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/client-scripts/{{conn.name}}/post-connect.d/mycard-network-nextgen.sh'
mode: 0755
notify: restart_openconnect
- name: down script
template:
src: ./openconnect-post-scripts/disconnect.sh.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/client-scripts/{{conn.name}}/disconnect.d/mycard-network-nextgen.sh'
mode: 0755
notify: restart_openconnect
- name: per-user config
template:
src: ./ocserv-per-user.conf.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/ocserv/config-per-user/{{conn.name}}'
notify: restart_ocserv
- name: per-user env
template:
src: ./ocserv-user-env.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/ocserv/env-per-user/{{conn.name}}'
notify: restart_ocserv
- name: '{{conn.name}}: stop wireguard'
become: true
ignore_errors: true
systemd:
name: 'wg-quick@{{conn.name}}'
state: stopped
enabled: no
- name: '{{conn.name}}: tasks for {{conn.ocType}}'
include_tasks: './configure-{{conn.ocType}}.yaml'
explicit-ipv4 = {{conn.remoteLocalAddress}}
route = {{conn.localPeerAddress}}/32
mtu = {{conn.mtu}}
- name: directories
file:
name: '{{ansible_user_dir}}/nextgen-network/services/ocserv/{{item}}'
recurse: true
state: directory
with_items:
- config-per-user
- env-per-user
- certs
- name: ocserv.conf
template:
src: ./ocserv.conf.j2
dest: '{{ansible_user_dir}}/nextgen-network/services/ocserv/ocserv.conf'
notify: restart_ocserv
- name: ocpasswd
copy:
content: |
{% for line in ocpasswdLines %}
{{line}}
{% endfor %}
dest: '{{ansible_user_dir}}/nextgen-network/services/ocserv/ocpasswd'
notify: restart_ocserv
- name: ocserv certs
synchronize:
src: ../certs/{{ocservCert}}/
dest: '{{ansible_user_dir}}/nextgen-network/services/ocserv/certs/{{ocservCert}}'
delete: yes
copy_links: yes
verify_host: no
recursive: yes
checksum: yes
archive: no
notify: restart_ocserv
# export dev=
export localAddress={{address}}
export remoteLocalAddress={{conn.remoteLocalAddress}}
export localPeerAddress={{conn.localPeerAddress}}
export remotePeerAddress={{conn.remotePeerAddress}}
export localPeerAddress6={{conn.localPeerAddress6}}
export remotePeerAddress6={{conn.remotePeerAddress6}}
export localRubbishAddress=10.199.{{id}}.1
export remoteNextMark={{conn.remoteNextMark}}
export inbound={{conn.inbound}}
export outbound={{conn.outbound}}
export mtu={{conn.mtu|int - 58}}
{% if conn.inbound %}
export metric={{conn.metric}}
{% else %}
export metric=50000
{% endif %}
auth = "plain[passwd=/etc/ocserv/ocpasswd]"
listen-host-is-dyndns = true
tcp-port = {{ocservPort}}
udp-port = {{ocservPort}}
run-as-user = nobody
run-as-group = daemon
socket-file = /run/ocserv.socket
server-cert = /etc/ssl/certs/{{ocservCert}}/fullchain.pem
server-key = /etc/ssl/certs/{{ocservCert}}/privkey.pem
dh-params = /etc/ssl/certs/{{ocservCert}}/dhparam.pem
isolate-workers = true
server-stats-reset-time = 604800
keepalive = 300
dpd = 60
mobile-dpd = 300
switch-to-tcp-timeout = 25
try-mtu-discovery = false
cert-user-oid = 0.9.2342.19200300.100.1.1
compression = true
no-compress-limit = 256
tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE"
match-tls-dtls-ciphers = false
auth-timeout = 240
idle-timeout = 1200
mobile-idle-timeout = 1800
max-ban-score = 80
ban-reset-time = 300
cookie-timeout = 604800
deny-roaming = false
rekey-time = 172800
rekey-method = ssl
connect-script = {{ansible_user_dir}}/nextgen-network/scripts/ocserv-postup.sh
disconnect-script = {{ansible_user_dir}}/nextgen-network/scripts/ocserv-predown.sh
use-occtl = true
pid-file = /run/ocserv.pid
predictable-ips = true
ipv4-network = 10.199.{{id}}.1/24
ping-leases = false
device = mcoc
config-per-user = /etc/ocserv/config-per-user/
cisco-client-compat = false
dtls-legacy = true
#!/bin/bash
dev="$TUNDEV" localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 58}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
true
#!/bin/bash
# Force reload at 12.23
dev="$TUNDEV" localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 58}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
dev="$TUNDEV" localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 58}} {{ansible_user_dir}}/nextgen-network/scripts/postup.sh
true
......@@ -8,8 +8,8 @@ FwMark = {{conn.localGatewayMark}}
MTU = {{conn.mtu|int - 80}}
Table = off
PostUp = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} link6Address={{conn.link6Address}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/postup.sh
PreDown = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} link6Address={{conn.link6Address}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
PostUp = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/postup.sh
PreDown = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
[Peer]
PublicKey = {{conn.wgPublicKey}}
......@@ -19,4 +19,4 @@ Endpoint = {{conn.remoteAddress}}:{{conn.remotePort}}
PersistentKeepalive = 1
{% endif %}
# forced change 12.12
# forced change 12.23
......@@ -6,8 +6,8 @@ ListenPort = {{conn.localPort}}
{% endif %}
MTU = {{conn.mtu|int - 80}}
Table = off
PostUp = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} link6Address={{conn.link6Address}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/postup.sh
PreDown = dev=%i localPeerAddress={{conn.localPeerAddress}} remotePeerAddress={{conn.remotePeerAddress}} link6Address={{conn.link6Address}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
PostUp = dev=%i localPeerAddress={{conn.localPeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/postup.sh
PreDown = dev=%i localPeerAddress={{conn.localPeerAddress}} localPeerAddress6={{conn.localPeerAddress6}} remotePeerAddress6={{conn.remotePeerAddress6}} remoteNextMark={{conn.remoteNextMark}} inbound={{conn.inbound}} outbound={{conn.outbound}} mtu={{conn.mtu|int - 80}} {{ansible_user_dir}}/nextgen-network/scripts/predown.sh
[Peer]
PublicKey = {{conn.wgPublicKey}}
AllowedIPs = 0.0.0.0/0, ::/0
......
---
- hosts: wg
tasks:
- name: load vars
include_vars:
file: '../result/{{item}}.yaml'
with_items:
# - global-vars
- vars-{{inventory_hostname_short}}
- name: restart_babeld_systemd
become: true
systemd:
name: babeld
state: restarted
when: systemBird
- name: restart_babeld # ocserv would be always restarted whenever key changes..
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- babeld
when: not noBird
- name: restart ocserv
docker_compose:
project_src: '{{ansible_user_dir}}/nextgen-network/services'
restarted: true
services:
- ocserv
when: ocservNeeded and not noBird
#!/usr/bin/env bash
# Forced update 12.24
source {{ansible_user_dir}}/nextgen-network/scripts/utility.sh
echo "running" > /tmp/mycard_global_postup_done
......@@ -14,6 +15,11 @@ ipset add localnet {{subnet}} || true
ip rule add pref 81 to {{subnet}} lookup main || true
{% endfor %}
# MASQ interfaces
{% for interface in masqInterfaces %}
iptables -t nat -A POSTROUTING -o {{interface}} -j MASQUERADE
{% endfor %}
# chain for wg origin
iptables -t mangle -N NEXTGEN_ORIGIN
iptables -t mangle -I PREROUTING -m mark --mark 0x0 ! -p ospf -j NEXTGEN_ORIGIN
......
#!/bin/bash
export dev="$DEVICE"
source /etc/ocserv/env-per-user/$USERNAME
NEW_DEVICE="$USERNAME"
#ip link set $DEVICE down
#ip link set $DEVICE name $NEW_DEVICE
#ip link set $NEW_DEVICE up
#ip link property add altname $DEVICE dev $NEW_DEVICE
#ip link set $NEW_DEVICE alias $DEVICE
ip link property add altname $NEW_DEVICE dev $DEVICE
ip link set $DEVICE alias $NEW_DEVICE
ip addr add "$localAddress/32" dev "$dev"
ip route del "$remoteLocalAddress" dev "$dev" proto kernel scope link src "$localRubbishAddress"
ip addr del "$localRubbishAddress" dev "$dev"
{{ansible_user_dir}}/nextgen-network/scripts/predown.sh
{{ansible_user_dir}}/nextgen-network/scripts/postup.sh
echo -e "interface $dev type tunnel rxcost $metric faraway true rtt-max 500 \nquit" | nc ::1 33123
true
#!/bin/bash
# This script is for old thing only. Will not be used in MyCard Network Gen 2.5
export dev="$DEVICE"
source /etc/ocserv/env-per-user/$USERNAME
{{ansible_user_dir}}/nextgen-network/scripts/predown.sh
true
#!/bin/bash
export dev="$DEVICE"
source /etc/ocserv/env-per-user/$USERNAME
{{ansible_user_dir}}/nextgen-network/scripts/predown.sh
true
#!/usr/bin/env bash
source {{ansible_user_dir}}/nextgen-network/scripts/utility.sh
wait_lock
#set -e
ip addr add "$localPeerAddress" peer "$remotePeerAddress" dev "$dev" scope link
ip -6 addr add "$link6Address" dev "$dev" scope link
ip addr add "$localPeerAddress6" peer "$remotePeerAddress6" dev "$dev" scope link
if [ "$outbound" == True ] ; then
ip route add default dev "$dev" table "$remoteNextMark"
......
File mode changed from 100644 to 100755
#!/bin/bash
source {{ansible_user_dir}}/nextgen-network/scripts/utility.sh
## chnroute
## route plans
{% for plan in routePlans %}
ip rule add pref 400 fwmark {{plan.destMark}} lookup {{plan.destMark}}
interface_switch_chnroute -A u_{{plan.name}}_chnroute {{plan.destMark}}
{% endfor %}
{% for gw in gateways %}
{% if gw.selectionMark > 0 %}
interface_switch_chnroute -A u_{{gw.isp}}_chnroute {{gw.selectionMark}}
{% endif %}
{% endfor %}
## all
{% for plan in routePlans %}
interface_switch -A u_{{plan.name}}_all {{plan.destMark}}
{% endfor %}
{% for gw in gateways %}
{% if gw.selectionMark > 0 %}
interface_switch -A u_{{gw.isp}}_all {{gw.selectionMark}}
{% endif %}
{% endfor %}
## restore mark
{% for plan in routePlans %}
ip rule add pref 400 fwmark {{plan.destMark}} lookup {{plan.destMark}}
restore_mark_switch -A {{plan.destMark}}
{% endfor %}
## local gateways
{% for gw in gateways %}
{% if gw.selectionMark > 0 %}
interface_switch_chnroute -A u_{{gw.isp}}_chnroute {{gw.selectionMark}}
interface_switch -A u_{{gw.isp}}_all {{gw.selectionMark}}
restore_mark_switch -A {{gw.selectionMark}}
{% endif %}
{% endfor %}
......
File mode changed from 100644 to 100755
......@@ -18,10 +18,10 @@ def write_yaml_file(name, data):
universe = IPSet(['0.0.0.0/0'])
special = IPSet([line.strip() for line in open('special.txt')])
chnroutes = IPSet([line.strip() for line in open('chnroutes.txt') if not line.startswith('#')])
result = { 'chnroute_reverse': [] }
result = { 'chnrouter': [] }
chnroute_reverse = universe - special - chnroutes
for route in chnroute_reverse.iter_cidrs():
result['chnroute_reverse'].append(str(route))
chnrouter = universe - special - chnroutes
for route in chnrouter.iter_cidrs():
result['chnrouter'].append(str(route))
write_yaml_file("result.yaml", result)
......@@ -4,7 +4,8 @@
"description": "",
"scripts": {
"build": "tsc",
"start": "node build/inventory.js"
"start": "node build/inventory.js",
"all": "npm run build && npm start"
},
"dependencies": {
"@types/ip": "^1.1.0",
......
......@@ -6,8 +6,16 @@ import YAML from 'yaml';
import _, { add } from 'lodash';
import child_process from 'child_process';
import assert from 'assert';
import ip from "ip";
import { promises as dns } from "dns";
import { promises as dns } from 'dns';
import os from 'os';
async function generateOcpasswdLine(username: string, password: string) {
const tmpName = os.tmpdir() + '/' + Math.floor(Math.random() * 10000000);
await util.promisify(child_process.exec)(`echo "${password}\\n${password}" | ocpasswd -c ${tmpName} ${username}`);
const res = (await fs.promises.readFile(tmpName, 'utf-8')).trim();
await fs.promises.unlink(tmpName);
return res;
}
interface GatewayGroup {
id: number;
......@@ -27,25 +35,33 @@ class InventoryBuilder {
routeLists: any;
resolveCache: Map<string, string>;
resolver: dns.Resolver;
vars: any;
linksOnly: string[];
async resolveDomain(domain: string) {
constructor() {
this.resolveCache = new Map();
this.resolver = new dns.Resolver();
this.resolver.setServers(process.env.DNS ? [process.env.DNS] : ['114.114.114.114', '223.5.5.5']);
}
async resolveDomain(domain: string, ipv6: boolean) {
if (!domain || domain.match(/(\d{1,3}\.){3}\d{1,3}/)) {
return domain;
}
if (this.resolveCache.has(domain)) {
return this.resolveCache.get(domain);
}
const rrtype = domain.includes("-v6") ? "AAAA" : "A";
const rrtype = (domain.includes('-v6') || ipv6) ? 'AAAA' : 'A';
let resolvedIP: string;
while (true) {
try {
[ resolvedIP ] = (await this.resolver.resolve(domain, rrtype)) as string[];
[resolvedIP] = (await this.resolver.resolve(domain, rrtype)) as string[];
break;
} catch (e) {
console.log(`${domain} => FAIL: ${e.toString()}`);
}
}
if (rrtype === "AAAA") {
if (rrtype === 'AAAA') {
resolvedIP = `[${resolvedIP}]`;
}
console.log(`${domain} => ${resolvedIP}`);
......@@ -53,13 +69,7 @@ class InventoryBuilder {
return resolvedIP;
}
constructor() {
this.resolveCache = new Map();
this.resolver = new dns.Resolver();
this.resolver.setServers(process.env.DNS ? [process.env.DNS] : ['114.114.114.114', '223.5.5.5']);
}
async load(sheetName) {
async load(sheetName: string) {
const data = await fs.promises.readFile(path.join('data', `内网互联计划 - ${sheetName}.csv`));
// @ts-ignore
return (await util.promisify(parse)(data, { columns: true, cast: true })).filter(h => h.id);
......@@ -68,7 +78,7 @@ class InventoryBuilder {
async loadGateways() {
const gateways = await this.load('gateways2');
for (let gateway of gateways) {
gateway.isCN = this.hosts[gateway.router] && this.hosts[gateway.router].location.startsWith("CN");
gateway.isCN = this.hosts[gateway.router] && this.hosts[gateway.router].location.startsWith('CN');
if (gateway.mark) {
gateway.selectionMark = gateway.mark + 50;
} else {
......@@ -86,44 +96,55 @@ class InventoryBuilder {
//console.log(this.gateways);
this.connections = _.intersection(Object.keys(this.hosts), Object.keys(_.find(this.hosts)));
const subnets = await this.load('subnets');
for (const [name, router] of Object.entries(this.hosts)) {
router.lanInterfaces = subnets
.filter((subnet) => subnet.router === name)
.map((subnet) => subnet.interface)
.filter((i) => i);
router.subnets = subnets.filter((subnet) => subnet.router === name).map((subnet) => subnet.subnet);
}
for (const host of Object.values(this.hosts)) {
host.wgPublickey = await this.wgPublickey(host.wgPrivateKey);
}
if (process.env.ONLY_LINKS) {
this.linksOnly = process.env.ONLY_LINKS.split(",");
}
this.vars = await this.loadUtilities();
const inventoryValue = { wg: {hosts: Object.fromEntries(Object.values(this.hosts).map(host => [host.name, this.getHostConnectionInfo(host)]))} };
await fs.promises.writeFile('result/inventory.yaml', YAML.stringify(inventoryValue));
// console.log(Object.values(this.hosts));
const rawHosts = await Promise.all(Object.values(this.hosts).map(async(h) => [h.name, await this.host_vars(h)]));
const hosts = Object.fromEntries(rawHosts);
// console.log(hosts);
const vars = await this.loadUtilities();
const rawHostsForSwitch = rawHosts.map(rh => {
const hostVars = JSON.parse(JSON.stringify(rh[1]));
hostVars.ansible_ssh_host = hostVars.address;
return [`switch-${rh[0]}`, hostVars];
});
const switchHosts = Object.fromEntries(rawHostsForSwitch);
const result = YAML.stringify({ wg: { hosts, vars }, switch: { hosts: switchHosts, vars: JSON.parse(JSON.stringify(vars)) } });
return fs.promises.writeFile('result/inventory.yaml', result);
const hosts = await Promise.all(Object.values(this.hosts).map(async (h) => ({
name: h.name,
vars: await this.host_vars(h)
})));
//const hosts = Object.fromEntries(hostsArray);
//await fs.promises.writeFile('result/inventory.yaml', YAML.stringify({
// wg: { hosts }
//}));
await fs.promises.writeFile('result/global-vars.yaml', YAML.stringify(this.vars));
await Promise.all(hosts.map(host => fs.promises.writeFile(`result/vars-${host.name}.yaml`, YAML.stringify(host.vars))));
}
async loadUtilities() {
const raw_utility = _.keyBy(await this.load('configurations'), 'key');
this.routeLists = YAML.parse(fs.readFileSync(path.join('lists', 'result.yaml'), "utf8"));
this.routeLists = YAML.parse(fs.readFileSync(path.join('lists', 'result.yaml'), 'utf8'));
// 所有内网网段
this.routeLists.mycard = ["10.198.0.0/16", "10.200.0.0/15", "172.16.0.0/12"];
this.routeLists.mycard = ['10.198.0.0/16', '10.200.0.0/15', '172.16.0.0/12'];
for (const h in this.hosts) {
const host = this.hosts[h]
for (const c of host.subnets.split(",")) {
const host = this.hosts[h];
for (const c of host.subnets) {
if (!c.length) {
continue;
}
this.routeLists.mycard.push(c);
}
}
// temp user before gateways
this.routeLists.ladder_needed = raw_utility.ladderNeeded.value.split(",").map((m) => { return m.trim() });
const vars = {
routeLists: this.routeLists,
routeListNames: Object.keys(this.routeLists),
noUpdateLinks: !!process.env.NO_LINKS
noUpdateLinks: !!process.env.NO_LINK
};
for (let col in raw_utility) {
vars[col] = raw_utility[col].value;
......@@ -171,24 +192,48 @@ class InventoryBuilder {
});
return routePlans;
}
getHostConnectionInfo(host) {
return {
ansible_ssh_host: host.host,
ansible_ssh_user: host.user,
ansible_ssh_port: host.sshPort || 22,
ansible_python_interpreter: host.python || 'python3',
noBird: !!(host.noBird || host.sysBird),
systemBird: !!host.sysBird
}
}
async host_vars(host) {
const connections = [];
host.dockerServices = {
version: '2.4',
services: {
bird: {
restart: "always",
image: "git-registry.mycard.moe/nanahira/docker-bird",
network_mode: "host",
cap_add: ["NET_ADMIN", "NET_BROADCAST", "NET_RAW"],
volumes: ["./bird.conf:/etc/bird/bird.conf:ro"]
restart: 'always',
image: 'git-registry.mycard.moe/nanahira/docker-bird',
network_mode: 'host',
cap_add: ['NET_ADMIN'],
volumes: ['./bird.conf:/etc/bird/bird.conf:ro']
},
babeld: {
restart: 'always',
image: 'git-registry.mycard.moe/railgun/babeld',
network_mode: 'host',
//cap_add: ['NET_ADMIN'],
privileged: true,
volumes: ['./babeld.conf:/etc/babeld.conf:ro']
}
}
};
host.frpsNeeded = false;
const null_connection = "10000,null";
const lanInterfaces = host.lanInterfaces.length > 0 ? host.lanInterfaces.split(",") : [];
const null_connection = '10000,null';
const lanInterfaces = host.lanInterfaces;
const localSubnets = host.subnets;
//console.log(localSubnets);
const masqInterfaces = host.masqInterfaces.length > 0 ? host.masqInterfaces.split(',') : [];
const routePlans = this.getRoutePlansFromGatewayGroups(host);
for (const h of this.connections) {
if (h != host.name) {
const to = host[h]; // 当前主机的条目
......@@ -220,20 +265,28 @@ class InventoryBuilder {
return {
ansible_ssh_host: host.host,
ansible_ssh_user: host.user,
ansible_ssh_port: host.sshPort || 22,
ansible_python_interpreter: host.python || "python3",
//ansible_ssh_host: host.host,
//ansible_ssh_user: host.user,
//ansible_ssh_port: host.sshPort || 22,
//ansible_python_interpreter: host.python || 'python3',
id: host.id,
address: host.address,
isCN: host.location.startsWith("CN"),
isCN: host.location.startsWith('CN'),
key: host.wgPrivateKey,
frpsNeeded: host.frpsNeeded,
frpsPort: host.frpsPort,
ocservNeeded: host.ocservNeeded || false,
ocservPort: host.ocservPort,
ocservCert: host.ocservCert || null,
ocMetric: host.ocMetric || null,
ocpasswdLines: host.ocpasswdLines || [],
gateways: _.values(this.gateways[host.name]),
connections,
lanInterfaces,
localSubnets,
masqInterfaces,
dockerServices: host.dockerServices,
routePlans
routePlans,
};
}
......@@ -243,61 +296,117 @@ class InventoryBuilder {
const primary = leftbottom ? outbound : inbound; // true 使用 peerAddress、port, false 使用peerAddress2、port2
const connStrSplited = connstr.split(',');
const [_metric, protocol] = connStrSplited;
const paramsString = connStrSplited.slice(2).join("&");
const paramsString = connStrSplited.slice(2).join('&');
const metric = parseInt(_metric);
const params = Object.fromEntries(new URLSearchParams(paramsString).entries());
const name = `mc${!outbound ? 'i' : '-'}${remote.name}`;
const remoteName = `mc${!inbound ? 'i' : '-'}${local.name}`;
const localGatewayName = (cis ? params.lif : params.rif) || params.if;
const localGateway = localGatewayName ? this.gateways[local.name][localGatewayName] : _.find(this.gateways[local.name]);
assert(localGateway, `Gateway ${localGatewayName} for ${local.name} not found.`);
//console.log(local.name, paramsString, params, localGatewayName, localGateway.name)
const localGatewayMark = localGateway.mark || 0;
const remoteGatewayName = (cis ? params.rif : params.lif) || params.if;
const remoteGateway = remoteGatewayName ? this.gateways[remote.name][remoteGatewayName] : _.find(this.gateways[remote.name]);
assert(remoteGateway, `Gateway ${remoteGatewayName} for ${remote.name} not found.`);
//const remoteGatewayMark = remoteGatewayName ? remoteGateway.mark : undefined;
//console.log(remoteGateway.name);
const remoteAddress = remoteGateway.address || null;
const resolvedRemoteAddress = await this.resolveDomain(remoteAddress);
let remoteAddress = remoteGateway.address || null;
if (!localGateway.ipv6 && !remoteGateway.ipv4 && remoteGateway.ipv6) {
remoteAddress = null;
}
const resolvedRemoteAddress = remoteAddress ? await this.resolveDomain(remoteAddress, !remoteGateway.ipv4 && remoteGateway.ipv6) : null;
const remoteLocalAddress = remote.address;
const remoteNextMark = remote.nextMark;
const remoteDestMark = remote.destMark;
const localPort = (primary ? remote.port : remote.port2) + local.offset;
const remotePort = (primary ? local.port : local.port2) + remote.offset;
const remoteFrpsPort = remote.frpsPort;
const remoteOcservPort = remote.ocservPort;
const wgPublicKey = remote.wgPublickey;
const localPeerAddress = primary ? `10.200.${local.id}.${remote.id}` : `10.201.${local.id}.${remote.id}`;
const remotePeerAddress = primary ? `10.200.${remote.id}.${local.id}` : `10.201.${remote.id}.${local.id}`;
const link6Address = `fe80::${primary ? 1 : 2}:${local.id}:${remote.id}/64`;
const localPeerAddress6Block = ((local.id << 8) | remote.id).toString(16);
const remotePeerAddress6Block = ((remote.id << 8) | local.id).toString(16);
const localPeerAddress6 = `fe80::${primary ? 1 : 2}:${localPeerAddress6Block}`;
const remotePeerAddress6 = `fe80::${primary ? 1 : 2}:${remotePeerAddress6Block}`;
const frpType = protocol === 'wgfrp' ? (this.gatewayCompare(localGateway, remoteGateway) ? 'frps' : 'frpc') : undefined;
const ocType = protocol === 'oc' ? (this.gatewayCompareOcserv(local, remote, localGateway, remoteGateway) ? 'server' : 'client') : undefined;
if (frpType === "frps" && !local.dockerServices.services.frps) {
if (frpType === 'frps' && !local.dockerServices.services.frps) {
local.frpsNeeded = true;
local.dockerServices.services.frps = {
restart: "always",
image: "fatedier/frps:v0.34.2",
network_mode: "host",
command: "-c /frps.ini",
volumes: ["./frps.ini:/frps.ini:ro"]
}
restart: 'always',
image: 'fatedier/frps:v0.34.2',
network_mode: 'host',
command: '-c /frps.ini',
volumes: ['./frps.ini:/frps.ini:ro']
};
}
if (frpType === "frpc") {
if (frpType === 'frpc') {
local.dockerServices.services[`frpc-${name}`] = {
restart: "always",
image: "fatedier/frpc:v0.34.2",
network_mode: "host",
command: "-c /frpc.ini",
restart: 'always',
image: 'fatedier/frpc:v0.34.2',
network_mode: 'host',
command: '-c /frpc.ini',
volumes: [`./frpc-${name}.ini:/frpc.ini:ro`]
};
}
if (ocType === 'server') {
if (!local.dockerServices.services.ocserv) {
local.ocservNeeded = true;
local.ocpasswdLines = [];
local.ocMetric = metric;
local.dockerServices.services.ocserv = {
restart: 'always',
image: 'git-registry.mycard.moe/nanahira/docker-ocserv',
network_mode: 'host',
command: 'ocserv -f -d 1',
cap_add: ['NET_ADMIN'],
devices: ['/dev/net/tun:/dev/net/tun'],
volumes: [
'./ocserv/ocserv.conf:/etc/ocserv/ocserv.conf:ro',
'./ocserv/config-per-user:/etc/ocserv/config-per-user:ro',
'./ocserv/env-per-user:/etc/ocserv/env-per-user:ro',
'./ocserv/ocpasswd:/etc/ocserv/ocpasswd:ro',
`./ocserv/certs/${local.ocservCert}:/etc/ssl/certs/${local.ocservCert}:ro`,
'$HOME/nextgen-network/scripts:$HOME/nextgen-network/scripts:ro',
],
depends_on: ['babeld']
};
}
local.ocpasswdLines.push(await generateOcpasswdLine(name, this.vars.ocservPassword));
}
const mtu = Math.min(localGateway ? localGateway.mtu : 1500, remoteGateway ? remoteGateway.mtu : 1500);
if (ocType === 'client') {
local.dockerServices.services[`openconnect-${name}`] = {
restart: 'always',
image: 'git-registry.mycard.moe/railgun/openconnect',
network_mode: 'host',
command: ['bash', '-c', `echo "${this.vars.ocservPassword}" | openconnect --user=${remoteName} --passwd-on-stdin --passtos --interface=${name} --mtu=${mtu} ${remoteAddress}:${remoteOcservPort}`],
cap_add: ['NET_ADMIN'],
devices: ['/dev/net/tun:/dev/net/tun'],
volumes: [
`./client-scripts/${name}:/etc/vpnc:ro`,
'$HOME/nextgen-network/scripts:$HOME/nextgen-network/scripts:ro'
]
};
}
//console.log(local.name, name, mtu);
if (outbound) {
console.log(`${local.name} GW ${localGateway.isp} ${inbound ? "<" : "="}=${frpType === "frps" ? "s" : "="}=[${protocol}]=${frpType === "frpc" ? "s" : "="}=> ${remote.name} GW ${remoteGateway.isp}`);
console.log(`${local.name} GW ${localGateway.isp} ${inbound ? '<' : '='}=${frpType === 'frps' ? 's' : '='}=[${protocol}]=${frpType === 'frpc' ? 's' : '='}=> ${remote.name} GW ${remoteGateway.isp}`);
}
const noUpdate = this.linksOnly && !(this.linksOnly.includes(remote.name) || this.linksOnly.includes(local.name));
return {
name,
metric,
......@@ -314,34 +423,45 @@ class InventoryBuilder {
wgPublicKey,
localPeerAddress,
remotePeerAddress,
link6Address,
localPeerAddress6,
remotePeerAddress6,
remoteFrpsPort,
//remoteOcservPort,
frpType,
ocType,
inbound,
outbound,
mtu
mtu,
noUpdate,
};
}
// frps还是frpc的积分,NAT越有利分越高
gatewayCompareScore(gateway: any): number {
let score: number = 0xff - gateway.id; // 8 bits
let offset = 0;
let score = 0;
score |= (0xff - gateway.id) << offset; // 8 bits
offset += 8;
const isCNScore = gateway.isCN ? 0 : 1; // 1 bit
score |= isCNScore << 8;
score |= isCNScore << offset;
offset += 1;
const ipv4NatScore = ({ // 2 bits
"ports": 0,
"dmz": 1
'ports': 0,
'dmz': 1
})[gateway.ipv4Nat] || 2;
score |= ipv4NatScore << 9;
score |= ipv4NatScore << offset;
offset += 2;
const ipv4Score = ({ // 2 bits
"static": 2,
"dynamic": 1
'static': 2,
'dynamic': 1
})[gateway.ipv4] || 0;
score |= ipv4Score << 11;
score |= ipv4Score << offset;
offset += 2;
const globalSSHScore = ({ // 1 bit
"globalssh": 1
'globalssh': 1
})[gateway.ssh] || 0;
score |= globalSSHScore << 12;
score |= globalSSHScore << offset;
offset += 1;
return score;
}
......@@ -360,6 +480,17 @@ class InventoryBuilder {
return this.gatewayCompareScore(localGateway) > this.gatewayCompareScore(remoteGateway);
}
gatewayCompareOcserv(local: any, remote: any, localGateway: any, remoteGateway: any): boolean {
// 两边至少一个有证书才能连
assert(local.ocservCert || remote.ocservCert);
// 只有一边有证书用有证书的那边
if (!!local.ocservCert !== !!remote.ocservCert) {
return !!local.ocservCert;
}
return this.gatewayCompare(localGateway, remoteGateway);
}
async wgPublickey(privateKey) {
return new Promise((resolve, reject) => {
const child = child_process.execFile('wg', ['pubkey'], { encoding: 'utf8' }, (error, stdout, stderr) => {
......
#!/bin/bash
./update.sh "$@"
cd ansible
ansible-playbook -i ../result/inventory.yaml "$@" restart-babeld.yaml
cd ..
......@@ -28,4 +28,11 @@ _strip_wg_conf() {
# _strip_wg_conf ./protocols/wg/wg.conf.j2 ./protocols/wg/wg-setconf.conf.j2
_strip_wg_conf ./protocols/wgfrp/wgfrp.conf.j2 ./protocols/wgfrp/wgfrp-setconf.conf.j2
sed -r '/^(#.*)?$/d;/^reflect-kernel-metric/d;/^local-port-readwrite/d;/^redistribute/d' babeld.conf.j2 > babeld-reload.conf.j2
echo 'quit' >> babeld-reload.conf.j2
ansible-playbook -i ../result/inventory.yaml "$@" configure.yaml
cd ..
rsync -4cavzP ./data/ nanahira@koishi.yuzurisa.com:~/nginx/railgun-data
ssh -T nanahira@koishi.yuzurisa.com 'cd ~/nginx ; docker-compose restart railgun-enterprise'
#!/bin/bash
rsync -4cavzP ./data/ root@10.198.1.57:~/wg/data
rsync -4cavzP ./data/ nanahira@koishi.yuzurisa.com:~/nginx/railgun-data
ssh -T nanahira@koishi.yuzurisa.com 'cd ~/nginx ; docker-compose restart railgun-enterprise'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment