Skip to content

Commit

Permalink
Merge pull request #9 from clajiness/v0.8
Browse files Browse the repository at this point in the history
V0.8
  • Loading branch information
clajiness authored Dec 23, 2024
2 parents 42840f4 + a7f580f commit 759dd71
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 75 deletions.
3 changes: 0 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,5 @@ Metrics/BlockLength:
Naming/AccessorMethodName:
Enabled: false

Style/Documentation:
Enabled: false

Style/FrozenStringLiteralComment:
Enabled: false
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
source 'https://rubygems.org'

gem 'faraday', '~> 2.12', '>= 2.12.2'
gem 'json', '~> 2.9', '>= 2.9.1'
gem 'logger', '~> 1.6', '>= 1.6.4'
gem 'open3', '~> 0.2.1'
gem 'rubocop', '~> 1.69', '>= 1.69.2', require: false
gem 'uri', '~> 1.0', '>= 1.0.2'
gem 'yaml', '~> 0.4.0'
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ GEM
logger (1.6.4)
net-http (0.6.0)
uri
open3 (0.2.1)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
Expand All @@ -37,14 +38,20 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
yaml (0.4.0)

PLATFORMS
arm64-darwin-24
ruby

DEPENDENCIES
faraday (~> 2.12, >= 2.12.2)
json (~> 2.9, >= 2.9.1)
logger (~> 1.6, >= 1.6.4)
open3 (~> 0.2.1)
rubocop (~> 1.69, >= 1.69.2)
uri (~> 1.0, >= 1.0.2)
yaml (~> 0.4.0)

BUNDLED WITH
2.5.22
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@ A tool for keeping ProtonVPN, OPNsense, and qBittorrent forwarded ports in sync.
> This is beta software. I'm not responsible for any issues you may encounter.
## Purpose
This tool helps automate port forwarding from ProtonVPN to qBittorrent via OPNsense. The tool polls ProtonVPN for the given forwarded port, checks the port set in OPNsense and qBittorrent, and updates it if necessary. You can ignore qBittorrent by using the `QBIT_SKIP` environment variable.
This tool helps automate port forwarding from ProtonVPN to qBittorrent via OPNsense. The tool polls ProtonVPN for the given forwarded port, checks the port set in OPNsense and qBittorrent, and updates it if necessary.

Version v0.5 and later allows you to skip qBittorrent and just sync Proton's forwarded port to OPNsense.
You can ignore qBittorrent by using the `QBIT_SKIP` environment variable.

## Installation
I recommend using the provided Docker Compose file to simplify the set up of qbop.

The Docker container is available here: https://github.com/clajiness/qbop/pkgs/container/qbop

#### Requirements
### Requirements
* Docker Engine - https://docs.docker.com/engine/install/
* OPNsense - This is the tutorial I used to set up selective routing to ProtonVPN. https://docs.opnsense.org/manual/how-tos/wireguard-selective-routing.html
* qBittorrent
* ProtonVPN subscription

#### Config
### Config
The given environment variables are required.

1. `LOOP_FREQ:` This value determines how often the script runs. The default value is 45 seconds. This probably shouldn't be changed.
2. `PROTON_GATEWAY:` The IP address of your ProtonVPN gateway. For example, `10.2.0.1`.
3. `OPN_INTERFACE_ADDR:` The IP address of your OPNsense interface. For example, `https://10.1.1.1`.
4. `OPN_API_KEY:` Your OPNsense API key - https://docs.opnsense.org/development/how-tos/api.html
5. `OPN_API_SECRET:` Your OPNsense API secret
2. `PROTON_GATEWAY:` Usually 10.2.0.1. Do not use http(s):// or a trailing slash.
3. `OPN_INTERFACE_ADDR:` OPNsense Interface Address. Requires http(s):// and no trailing slash.
4. `OPN_API_KEY:` OPNsense API Key - https://docs.opnsense.org/development/how-tos/api.html
5. `OPN_API_SECRET:` OPNsense API Secret
6. `OPN_PROTON_ALIAS_NAME:` The firewall alias that you use for ProtonVPN's forwarded port. For example, `proton_vpn_forwarded_port`.
7. `QBIT_SKIP:` [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not necessary.
8. `QBIT_ADDR:` The IP address of your qBittorrent app. For example, `http://10.1.1.100:8080`.
9. `QBIT_USER:` Your qBittorrent username
10. `QBIT_PASS:` Your qBittorrent password
7. `QBIT_SKIP:` [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not required.
8. `QBIT_ADDR:` The IP address of your qBittorrent app. Requires http(s):// and no trailing slash.
9. `QBIT_USER:` qBittorrent username
10. `QBIT_PASS:` qBittorrent password
20 changes: 10 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ services:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
# This value determines how often the script runs. The default value is 45 seconds.
# This value determines how often the script runs. The default value is 45 seconds.
- LOOP_FREQ=
# Usually 10.2.0.1. Do not use http(s):// or a trailing slash.
# Usually 10.2.0.1. Do not use http(s):// or a trailing slash.
- PROTON_GATEWAY=
# OPNsense Interface Address. Requires http(s):// and no trailing slash.
# OPNsense Interface Address. Requires http(s):// and no trailing slash.
- OPN_INTERFACE_ADDR=
# OPNsense API Key
# OPNsense API Key
- OPN_API_KEY=
# OPNsense API Secret
# OPNsense API Secret
- OPN_API_SECRET=
# The firewall alias that you use for ProtonVPN's forwarded port
# The firewall alias that you use for ProtonVPN's forwarded port
- OPN_PROTON_ALIAS_NAME=
# [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not necessary.
# [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not required.
- QBIT_SKIP=
# The IP address of your qBittorrent app. Requires http(s):// and no trailing slash.
# The IP address of your qBittorrent app. Requires http(s):// and no trailing slash.
- QBIT_ADDR=
# Your qBittorrent username
# qBittorrent username
- QBIT_USER=
# Your qBittorrent password
# qBittorrent password
- QBIT_PASS=

volumes:
Expand Down
59 changes: 16 additions & 43 deletions qbop.rb
Original file line number Diff line number Diff line change
@@ -1,53 +1,19 @@
require 'bundler/setup'
Bundler.require(:default)
require 'json'
require 'yaml'
require 'logger'
Dir['./service/*.rb'].sort.each { |file| require_relative file }

def parse_version
return unless File.exist?('version.yml')
helpers = Service::Helpers.new

YAML.safe_load(File.read('version.yml'))
end
# collect env variables in a config variable
config = helpers.env_variables

# get version number of qbop
script_version = parse_version['version']
# collect version number of qbop in a script_version variable
script_version = helpers.version

# LOGGER
# set up logger
@logger = Logger.new('log/qbop.log', 10, 1_024_000)
@logger.info("starting qbop v#{script_version}")

def exit_script
@logger.info("qbop completed at #{Time.now}")
@logger.info('----------')
@logger.close
exit
end
# ----------

# ENV
# get env
def parse_env # rubocop:disable Metrics/MethodLength
{
loop_freq: ENV['LOOP_FREQ'] || 45,
proton_gateway: ENV['PROTON_GATEWAY'],
opnsense_interface_addr: ENV['OPN_INTERFACE_ADDR'],
opnsense_api_key: ENV['OPN_API_KEY'],
opnsense_api_secret: ENV['OPN_API_SECRET'],
opnsense_alias_name: ENV['OPN_PROTON_ALIAS_NAME'],
qbit_skip: ENV['QBIT_SKIP'],
qbit_addr: ENV['QBIT_ADDR'],
qbit_user: ENV['QBIT_USER'],
qbit_pass: ENV['QBIT_PASS']
}
end

# parse config
config = parse_env

@logger.info('----------')
# ----------

# DO SOME WORK!

Expand All @@ -65,10 +31,16 @@ def parse_env # rubocop:disable Metrics/MethodLength
proton ||= Service::Proton.new

# make natpmpc call to proton
proton_response = proton.proton_natpmpc(config[:proton_gateway])
response = proton.proton_natpmpc(config[:proton_gateway])

# raise error if stderr is not empty
raise StandardError, response[:stderr].chomp unless response[:stderr].empty?

# get proton response from standard output
proton_response = response[:stdout]

# parse natpmpc response
forwarded_port = proton.parse_proton_response(proton_response)
forwarded_port = proton.parse_proton_response(proton_response.chomp)

# sleep and restart loop if forwarded port isn't returned
if forwarded_port.nil?
Expand Down Expand Up @@ -204,7 +176,8 @@ def parse_env # rubocop:disable Metrics/MethodLength
end

# sleep before looping again
@logger.info("end of loop. sleeping for #{config[:loop_freq].to_i} seconds.")
@logger.info('end of loop')
@logger.info("sleeping for #{config[:loop_freq].to_i} seconds...")
@logger.info('----------')
sleep config[:loop_freq].to_i
end
Expand Down
30 changes: 30 additions & 0 deletions service/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Service
# the Helpers class provides methods for setting environment variables and the script version
class Helpers
def initialize
@env_variables = env_variables
@version = version
end

def env_variables # rubocop:disable Metrics/MethodLength
{
loop_freq: ENV['LOOP_FREQ'] || 45,
proton_gateway: ENV['PROTON_GATEWAY'],
opnsense_interface_addr: ENV['OPN_INTERFACE_ADDR'],
opnsense_api_key: ENV['OPN_API_KEY'],
opnsense_api_secret: ENV['OPN_API_SECRET'],
opnsense_alias_name: ENV['OPN_PROTON_ALIAS_NAME'],
qbit_skip: ENV['QBIT_SKIP'],
qbit_addr: ENV['QBIT_ADDR'],
qbit_user: ENV['QBIT_USER'],
qbit_pass: ENV['QBIT_PASS']
}
end

def version
return unless File.exist?('version.yml')

YAML.safe_load(File.read('version.yml'))['version']
end
end
end
1 change: 1 addition & 0 deletions service/opnsense.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module Service
# the Opnsense class provides methods for getting and setting the OPNsense alias value for the Proton forwarded port
class Opnsense
def initialize(config)
@config = config
Expand Down
7 changes: 6 additions & 1 deletion service/proton.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
module Service
# the Proton class provides methods for returning the port that Proton has forwarded
class Proton
def proton_natpmpc(proton_gateway)
`timeout 5 natpmpc -a 1 0 udp 60 -g #{proton_gateway} && natpmpc -a 1 0 tcp 60 -g #{proton_gateway}`
stdout, stderr, status = Open3.capture3(
"timeout 5 natpmpc -a 1 0 udp 60 -g #{proton_gateway} && natpmpc -a 1 0 tcp 60 -g #{proton_gateway}"
)

{ stdout: stdout, stderr: stderr, status: status }
end

def parse_proton_response(proton_response)
Expand Down
13 changes: 8 additions & 5 deletions service/qbit.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module Service
# the Qbit class provides methods for getting and setting the qBit forwarded port
class Qbit
def initialize(config)
@config = config
Expand All @@ -8,14 +9,16 @@ def initialize(config)
)
end

def qbt_auth_login
def qbt_auth_login # rubocop:disable Metrics/MethodLength
response = @conn.post do |req|
req.url "#{@config[:qbit_addr]}/api/v2/auth/login"
req.headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
req.body = URI.encode_www_form({
'username': @config[:qbit_user],
'password': @config[:qbit_pass]
})
req.body = URI.encode_www_form(
{
'username': @config[:qbit_user],
'password': @config[:qbit_pass]
}
)
end

response['set-cookie'].split(';')[0]
Expand Down
2 changes: 1 addition & 1 deletion version.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
---
version: 0.7.0
version: 0.8.0

0 comments on commit 759dd71

Please sign in to comment.