diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..b814354 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,30 @@ +name: Docker image build + +on: + push: + paths-ignore: + - '**.md' + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: linux/amd64 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Docker Image + uses: docker/build-push-action@v3 + with: + context: . + file: docker/Dockerfile + tags: phantun + platforms: linux/amd64 diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..83e3d09 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,2 @@ +README.md +docker-compose.yml diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c3c2a63 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +# +# Dockerfile for phantun +# + +# +# Build stage +# +FROM rust:alpine AS builder + +COPY . /phantun + +RUN apk update \ + && apk add --no-cache --virtual .build-deps musl-dev \ + && cd phantun \ + && cargo build --release \ + && strip target/release/server target/release/client \ + && install target/release/server /usr/local/bin/phantun-server \ + && install target/release/client /usr/local/bin/phantun-client \ + && cd - \ + && rm -r phantun \ + && apk del .build-deps \ + && rm -rf /var/cache/apk/* + +# +# Runtime stage +# +FROM alpine:3.16 + +RUN apk update \ + && apk add --no-cache iptables ip6tables tzdata \ + && rm -rf /var/cache/apk/* + +COPY --from=builder /usr/local/bin/phantun-server /usr/local/bin/ +COPY --from=builder /usr/local/bin/phantun-client /usr/local/bin/ +COPY docker/phantun.sh /usr/local/bin/ + +ENV USE_IPTABLES_NFT_BACKEND=0 +ENV RUST_LOG=INFO + +ENTRYPOINT ["phantun.sh"] +CMD ["phantun-server", "--help"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..c4dd3f6 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,11 @@ +# phantun (docker) + +## Build + +```sh +docker build -t phantun -f docker/Dockerfile . +``` + +## Usage + +It is recommended to use docker-compose, see [docker-compose.yml](docker-compose.yml) for details. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..e3c2a44 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.9' + +services: + phantun-server: + image: phantun + container_name: phantun-server + restart: unless-stopped + network_mode: host + privileged: true + environment: + USE_IPTABLES_NFT_BACKEND: 0 + RUST_LOG: INFO + command: > + phantun-server --local 1985 --remote 127.0.0.1:1984 --ipv4-only + + phantun-client: + image: phantun + container_name: phantun-client + restart: unless-stopped + network_mode: host + privileged: true + environment: + USE_IPTABLES_NFT_BACKEND: 0 + RUST_LOG: INFO + command: > + phantun-client --local 127.0.0.1:1984 --remote 11.22.33.44:1985 --ipv4-only diff --git a/docker/phantun.sh b/docker/phantun.sh new file mode 100755 index 0000000..36be794 --- /dev/null +++ b/docker/phantun.sh @@ -0,0 +1,209 @@ +#!/bin/sh + +# alias ​​settings must be global, and must be defined before the function being called with the alias +if [ "$USE_IPTABLES_NFT_BACKEND" = 1 ]; then + alias iptables=iptables-nft + alias iptables-save=iptables-nft-save + alias ip6tables=ip6tables-nft + alias ip6tables-save=ip6tables-nft-save +fi + +info() { + local green='\e[0;32m' + local clear='\e[0m' + local time=$(date '+%Y-%m-%d %T') + printf "${green}[${time}] [INFO]: ${clear}%s\n" "$*" +} + +warn() { + local yellow='\e[1;33m' + local clear='\e[0m' + local time=$(date '+%Y-%m-%d %T') + printf "${yellow}[${time}] [WARN]: ${clear}%s\n" "$*" >&2 +} + +error() { + local red='\e[0;31m' + local clear='\e[0m' + local time=$(date '+%Y-%m-%d %T') + printf "${red}[${time}] [ERROR]: ${clear}%s\n" "$*" >&2 +} + +_get_default_iface() { + ip -4 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}' +} + +_get_default6_iface() { + ip -6 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}' +} + +_get_addr_by_iface() { + ip -4 addr show dev "$1" | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1 +} + +_get_addr6_by_iface() { + ip -6 addr show dev "$1" | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1 +} + +_check_rule_by_comment() { + iptables-save | grep -q "$1" +} + +_check_rule6_by_comment() { + ip6tables-save | grep -q "$1" +} + +_is_server_mode() { + [ "$1" = "phantun-server" ] +} + +_is_ipv4_only() { + case "$@" in + *-4*|*--ipv4-only*) + return 0 + ;; + *\ -4*|*\ --ipv4-only*) + return 0 + ;; + esac + return 1 +} + +_get_tun_from_args() { + local tun=$(echo "$@" | awk -F '--tun' '{print $2}' | awk '{print $1}') + echo ${tun:=tun0} +} + +_get_peer_from_args() { + local peer=$(echo "$@" | awk -F '--tun-peer' '{print $2}' | awk '{print $1}') + _is_server_mode "$1" && echo ${peer:=192.168.201.2} || echo ${peer:=192.168.200.2} +} + +_get_peer6_from_args() { + local peer=$(echo "$@" | awk -F '--tun-peer6' '{print $2}' | awk '{print $1}') + _is_server_mode "$1" && echo ${peer:=fcc9::2} || echo ${peer:=fcc8::2} +} + +_get_port_from_args() { + local value=$(echo "$@" | awk -F '-l|--local' '{print $2}' | awk '{print $1}') + _is_server_mode "$1" && echo $value || echo $value | awk -F ':' '{print $2}' +} + +_iptables() { + iptables -w 10 "$@" +} + +_ip6tables() { + ip6tables -w 10 "$@" +} + +apply_sysctl() { + info "apply sysctl: $(sysctl -w net.ipv4.ip_forward=1)" + ! _is_ipv4_only "$@" || return + info "apply sysctl: $(sysctl -w net.ipv6.conf.all.forwarding=1)" +} + +apply_iptables() { + local interface=$(_get_default_iface) + local address=$(_get_addr_by_iface "${interface}") + local tun=$(_get_tun_from_args "$@") + local peer=$(_get_peer_from_args "$@") + local port=$(_get_port_from_args "$@") + local comment="phantun_${tun}_${port}" + + if _check_rule_by_comment "${comment}"; then + warn "iptables rules already exist, maybe needs to check." + else + _iptables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed." + _iptables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed." + if _is_server_mode "$1"; then + info "iptables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}" + _iptables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \ + -m comment --comment "${comment}" || error "iptables DNAT rule add failed." + else + info "iptables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}" + _iptables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \ + -m comment --comment "${comment}" || error "iptables SNAT rule add failed." + fi + fi +} + +apply_ip6tables() { + ! _is_ipv4_only "$@" || return + + local interface=$(_get_default6_iface) + local address=$(_get_addr6_by_iface "${interface}") + local tun=$(_get_tun_from_args "$@") + local peer=$(_get_peer6_from_args "$@") + local port=$(_get_port_from_args "$@") + local comment="phantun_${tun}_${port}" + + if _check_rule6_by_comment "${comment}"; then + warn "ip6tables rules already exist, maybe needs to check." + else + _ip6tables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed." + _ip6tables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed." + if _is_server_mode "$1"; then + info "ip6tables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}" + _ip6tables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \ + -m comment --comment "${comment}" || error "ip6tables DNAT rule add failed." + else + info "ip6tables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}" + _ip6tables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \ + -m comment --comment "${comment}" || error "ip6tables SNAT rule add failed." + fi + fi +} + +stop_process() { + kill $(pidof phantun-server phantun-client) + info "terminate phantun process." +} + +revoke_iptables() { + local tun=$(_get_tun_from_args "$@") + local port=$(_get_port_from_args "$@") + local comment="phantun_${tun}_${port}" + + iptables-save -t filter | grep "${comment}" | while read rule; do + _iptables -t filter ${rule/-A/-D} || error "iptables filter rule remove failed." + done + iptables-save -t nat | grep "${comment}" | while read rule; do + _iptables -t nat ${rule/-A/-D} || error "iptables nat rule remove failed." + done + info "iptables rule: [${comment}] removed." +} + +revoke_ip6tables() { + ! _is_ipv4_only "$@" || return + + local tun=$(_get_tun_from_args "$@") + local port=$(_get_port_from_args "$@") + local comment="phantun_${tun}_${port}" + + ip6tables-save -t filter | grep "${comment}" | while read rule; do + _ip6tables -t filter ${rule/-A/-D} || error "ip6tables filter rule remove failed." + done + ip6tables-save -t nat | grep "${comment}" | while read rule; do + _ip6tables -t nat ${rule/-A/-D} || error "ip6tables nat rule remove failed." + done + info "ip6tables rule: [${comment}] removed." +} + +graceful_stop() { + warn "caught SIGTERM or SIGINT signal, graceful stopping..." + stop_process + revoke_iptables "$@" + revoke_ip6tables "$@" +} + +start_phantun() { + trap 'graceful_stop "$@"' SIGTERM SIGINT + apply_sysctl "$@" + apply_iptables "$@" + apply_ip6tables "$@" + "$@" & + wait +} + +start_phantun "$@"