├── .gitignore
├── requirements.txt
├── Dockerfile
├── bad-list.yml
├── readme-header.md
├── .github
└── workflows
│ └── run-iperf3.yml
├── list.yml
├── README.md
├── check-iperf3.py
└── speedtest.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pyyaml
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.13.5-alpine3.22
2 |
3 | COPY . .
4 |
5 | RUN pip install -r requirements.txt --no-cache-dir && apk add --no-cache iperf3
6 |
7 | CMD ["python", "check-iperf3.py"]
--------------------------------------------------------------------------------
/bad-list.yml:
--------------------------------------------------------------------------------
1 | - Name: Ertelecom Novosibirsk
2 | City: Novosibirsk
3 | address: st.nsk.ertelecom.ru
4 | port: 5201
5 | - Name: Ertelecom Novokuznetsk
6 | City: Novokuznetsk
7 | address: st.nk.ertelecom.ru
8 | port: 5201
9 | - Name: TTK Ufa
10 | City: Ufa
11 | address: speed-ufa.vtt.net
12 | port: 5201
13 | - Name: TTK Samara
14 | City: Samara
15 | address: speed-sma.vtt.net
16 | port: 5201
17 | - Name: TTK Vladimir
18 | City: Vladimir
19 | address: speed-vld.vtt.net
20 | port: 5201
21 | - Name: Ertelecom Orenburg
22 | City: Orenburg
23 | address: st.oren.ertelecom.ru
24 | port: 5201
--------------------------------------------------------------------------------
/readme-header.md:
--------------------------------------------------------------------------------
1 | # Public iPerf3 servers in Russia
2 |
3 | List of public iPerf3 servers in Russia, checked daily for availability.
4 |
5 | ## How it works
6 | - The server list is stored in YAML format in `list.yml`.
7 | - The script automatically tests each iPerf3 server and records the results in the table below.
8 |
9 | ## Suggest more servers
10 | We need more servers! Please create an issue or PR if you know of others.
11 |
12 | ## Test script
13 | ```
14 | bash <(wget -qO- https://github.com/itdoginfo/russian-iperf3-servers/raw/main/speedtest.sh)
15 | ```
16 |
17 | Fast Mode
18 | ```
19 | bash <(wget -qO- https://github.com/itdoginfo/russian-iperf3-servers/raw/main/speedtest.sh) -f
20 | ```
21 |
22 | ### Dependencies
23 | - iperf3
24 | - jq
25 | - ping
26 | - awk
27 |
28 | ### Fallback
29 | For each city, there is a primary and a fallback server. The test first checks the primary server across the port range 5201–5209, only if all ports fail does it fall back to the secondary server. In the results table, this is marked as "City (F)".
30 |
31 | ## Table of servers
--------------------------------------------------------------------------------
/.github/workflows/run-iperf3.yml:
--------------------------------------------------------------------------------
1 | name: Run iPerf3 Check and Update README
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '25 4 * * *'
7 | push:
8 | paths:
9 | - 'readme-header.md'
10 | - 'list.yml'
11 | - 'check-iperf3.py'
12 | - 'Dockerfile'
13 | - '.github/workflows/run-iperf3.yml'
14 | jobs:
15 | run-iperf3-check:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v4.2.2
21 |
22 | - name: Build Docker image
23 | run: docker build -t iperf3-check .
24 |
25 | - name: Run the check
26 | run: docker run --name iperf3-runner iperf3-check
27 |
28 | - name: Copy README.md from container
29 | run: docker cp iperf3-runner:/README.md README.md
30 |
31 | - name: Set up Git user
32 | run: |
33 | git config --global user.name "iperf3-checker-bot"
34 | git config --global user.email "iperf3-checker-bot@itdog.info"
35 |
36 | - name: Commit and push if README.md changed
37 | run: |
38 | git add README.md
39 | git diff --cached --quiet || git commit -m "Update from the bot"
40 | git push
41 |
--------------------------------------------------------------------------------
/list.yml:
--------------------------------------------------------------------------------
1 | - Name: Ertelecom Barnaul
2 | City: Barnaul
3 | address: st.barnaul.ertelecom.ru
4 | port: 5201-5209
5 | - Name: Ertelecom Yekaterinburg
6 | City: Yekaterinburg
7 | address: st.ekat.ertelecom.ru
8 | port: 5201-5209
9 | - Name: Ertelecom Izhevsk
10 | City: Izhevsk
11 | address: st.izhevsk.ertelecom.ru
12 | port: 5201-5209
13 | - Name: Ertelecom Irkutsk
14 | City: Irkutsk
15 | address: st.irkutsk.ertelecom.ru
16 | port: 5201-5209
17 | - Name: Ertelecom Kazan
18 | City: Kazan
19 | address: st.kzn.ertelecom.ru
20 | port: 5201-5209
21 | - Name: Ertelecom Kirov
22 | City: Kirov
23 | address: st.kirov.ertelecom.ru
24 | port: 5201-5209
25 | - Name: Ertelecom Krasnoyarsk
26 | City: Krasnoyarsk
27 | address: st.krsk.ertelecom.ru
28 | port: 5201-5209
29 | - Name: Ertelecom Kurgan
30 | City: Kurgan
31 | address: st.kurgan.ertelecom.ru
32 | port: 5201-5209
33 | - Name: Ertelecom Kursk
34 | City: Kursk
35 | address: st.kursk.ertelecom.ru
36 | port: 5201-5209
37 | - Name: Ertelecom Magnitogorsk
38 | City: Magnitogorsk
39 | address: st.mgn.ertelecom.ru
40 | port: 5201-5209
41 | - Name: Ertelecom Naberezhnye Chelny
42 | City: Naberezhnye Chelny
43 | address: st.chelny.ertelecom.ru
44 | port: 5201-5209
45 | - Name: Ertelecom Omsk
46 | City: Omsk
47 | address: st.omsk.ertelecom.ru
48 | port: 5201-5209
49 | - Name: Ertelecom Penza
50 | City: Penza
51 | address: st.penza.ertelecom.ru
52 | port: 5201-5209
53 | - Name: Ertelecom Perm
54 | City: Perm
55 | address: st.perm.ertelecom.ru
56 | port: 5201-5209
57 | - Name: Ertelecom Rostov-on-Don
58 | City: Rostov-on-Don
59 | address: st.rostov.ertelecom.ru
60 | port: 5201-5209
61 | - Name: Ertelecom Ryazan
62 | City: Ryazan
63 | address: st.ryazan.ertelecom.ru
64 | port: 5201-5209
65 | - Name: Ertelecom Samara
66 | City: Samara
67 | address: st.samara.ertelecom.ru
68 | port: 5201-5209
69 | - Name: Ertelecom Saint Petersburg
70 | City: Saint Petersburg
71 | address: st.spb.ertelecom.ru
72 | port: 5201-5209
73 | - Name: Ertelecom Saratov
74 | City: Saratov
75 | address: st.saratov.ertelecom.ru
76 | port: 5201-5209
77 | - Name: Ertelecom Tver
78 | City: Tver
79 | address: st.tver.ertelecom.ru
80 | port: 5201-5209
81 | - Name: Ertelecom Tomsk
82 | City: Tomsk
83 | address: st.tomsk.ertelecom.ru
84 | port: 5201-5209
85 | - Name: Ertelecom Tula
86 | City: Tula
87 | address: st.tula.ertelecom.ru
88 | port: 5201-5209
89 | - Name: Ertelecom Tyumen
90 | City: Tyumen
91 | address: st.tmn.ertelecom.ru
92 | port: 5201-5209
93 | - Name: Ertelecom Ulyanovsk
94 | City: Ulyanovsk
95 | address: st.ulsk.ertelecom.ru
96 | port: 5201-5209
97 | - Name: Ertelecom Ufa
98 | City: Ufa
99 | address: st.ufa.ertelecom.ru
100 | port: 5201-5209
101 | - Name: Ertelecom Cheboksary
102 | City: Cheboksary
103 | address: st.cheb.ertelecom.ru
104 | port: 5201-5209
105 | - Name: Ertelecom Chelyabinsk
106 | City: Chelyabinsk
107 | address: st.chel.ertelecom.ru
108 | port: 5201-5209
109 | - Name: Ertelecom Yaroslavl
110 | City: Yaroslavl
111 | address: st.yar.ertelecom.ru
112 | port: 5201-5209
113 | - Name: Ertelecom Lipetsk
114 | City: Lipetsk
115 | address: st.lipetsk.ertelecom.ru
116 | port: 5201-5209
117 | - Name: Ertelecom Bryansk
118 | City: Bryansk
119 | address: st.bryansk.ertelecom.ru
120 | port: 5201-5209
121 | - Name: Ertelecom Volgograd
122 | City: Volgograd
123 | address: st.volgograd.ertelecom.ru
124 | port: 5201-5209
125 | - Name: Ertelecom Voronezh
126 | City: Voronezh
127 | address: st.voronezh.ertelecom.ru
128 | port: 5201-5209
129 | - Name: Ertelecom Nizhny Novgorod
130 | City: Nizhny Novgorod
131 | address: st.nn.ertelecom.ru
132 | port: 5201-5209
133 | - Name: Ertelecom Yoshkar-Ola
134 | City: Yoshkar-Ola
135 | address: st.yola.ertelecom.ru
136 | port: 5201-5209
137 | - Name: TTK Volgograd
138 | City: Volgograd
139 | address: speed-vgd.vtt.net
140 | port: 5201
141 | - Name: TTK Nizhny Novgorod
142 | City: Nizhny Novgorod
143 | address: speed-nn.vtt.net
144 | port: 5201
145 | - Name: TTK Saratov
146 | City: Saratov
147 | address: speed.vtt.net
148 | port: 5201
149 | - Name: TTK Saransk
150 | City: Saransk
151 | address: speed-sar.vtt.net
152 | port: 5201
153 | - Name: Beeline Voronezh
154 | City: Voronezh
155 | address: voronezh-speedtest.corbina.net
156 | port: 5201
157 | - Name: Beeline Astrakhan
158 | City: Astrakhan
159 | address: astrakhan1.speedtest.corbina.net
160 | port: 5201
161 | - Name: Hostkey Moscow
162 | City: Moscow
163 | address: spd-rudp.hostkey.ru
164 | port: 5201-5209
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Public iPerf3 servers in Russia
2 |
3 | List of public iPerf3 servers in Russia, checked daily for availability.
4 |
5 | ## How it works
6 | - The server list is stored in YAML format in `list.yml`.
7 | - The script automatically tests each iPerf3 server and records the results in the table below.
8 |
9 | ## Suggest more servers
10 | We need more servers! Please create an issue or PR if you know of others.
11 |
12 | ## Test script
13 | ```
14 | bash <(wget -qO- https://github.com/itdoginfo/russian-iperf3-servers/raw/main/speedtest.sh)
15 | ```
16 |
17 | Fast Mode
18 | ```
19 | bash <(wget -qO- https://github.com/itdoginfo/russian-iperf3-servers/raw/main/speedtest.sh) -f
20 | ```
21 |
22 | ### Dependencies
23 | - iperf3
24 | - jq
25 | - ping
26 | - awk
27 |
28 | ### Fallback
29 | For each city, there is a primary and a fallback server. The test first checks the primary server across the port range 5201–5209, only if all ports fail does it fall back to the secondary server. In the results table, this is marked as "City (F)".
30 |
31 | ## Table of servers
32 |
33 | | Name | City | Address | Port | Status |
34 | |------|------|---------|------|--------|
35 | | Ertelecom Barnaul | Barnaul | st.barnaul.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
36 | | Ertelecom Yekaterinburg | Yekaterinburg | st.ekat.ertelecom.ru | 5202
5203
5204
5205
5206
5207
5209 | ✅ |
37 | | Ertelecom Izhevsk | Izhevsk | st.izhevsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
38 | | Ertelecom Irkutsk | Irkutsk | st.irkutsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
39 | | Ertelecom Kazan | Kazan | st.kzn.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
40 | | Ertelecom Kirov | Kirov | st.kirov.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
41 | | Ertelecom Krasnoyarsk | Krasnoyarsk | st.krsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
42 | | Ertelecom Kurgan | Kurgan | st.kurgan.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
43 | | Ertelecom Kursk | Kursk | st.kursk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
44 | | Ertelecom Magnitogorsk | Magnitogorsk | st.mgn.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
45 | | Ertelecom Naberezhnye Chelny | Naberezhnye Chelny | st.chelny.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
46 | | Ertelecom Omsk | Omsk | st.omsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
47 | | Ertelecom Penza | Penza | st.penza.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
48 | | Ertelecom Perm | Perm | st.perm.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
49 | | Ertelecom Rostov-on-Don | Rostov-on-Don | st.rostov.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
50 | | Ertelecom Ryazan | Ryazan | st.ryazan.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
51 | | Ertelecom Samara | Samara | st.samara.ertelecom.ru | 5201
5202
5203
5204
5205
5208 | ✅ |
52 | | Ertelecom Saint Petersburg | Saint Petersburg | st.spb.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
53 | | Ertelecom Saratov | Saratov | st.saratov.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
54 | | Ertelecom Tver | Tver | st.tver.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207 | ✅ |
55 | | Ertelecom Tomsk | Tomsk | st.tomsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
56 | | Ertelecom Tula | Tula | st.tula.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
57 | | Ertelecom Tyumen | Tyumen | st.tmn.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
58 | | Ertelecom Ulyanovsk | Ulyanovsk | st.ulsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
59 | | Ertelecom Ufa | Ufa | st.ufa.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5208
5209 | ✅ |
60 | | Ertelecom Cheboksary | Cheboksary | st.cheb.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
61 | | Ertelecom Chelyabinsk | Chelyabinsk | st.chel.ertelecom.ru | 5201
5202
5203
5204
5206
5207 | ✅ |
62 | | Ertelecom Yaroslavl | Yaroslavl | st.yar.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208
5209 | ✅ |
63 | | Ertelecom Lipetsk | Lipetsk | st.lipetsk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
64 | | Ertelecom Bryansk | Bryansk | st.bryansk.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
65 | | Ertelecom Volgograd | Volgograd | st.volgograd.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
66 | | Ertelecom Voronezh | Voronezh | st.voronezh.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5209 | ✅ |
67 | | Ertelecom Nizhny Novgorod | Nizhny Novgorod | st.nn.ertelecom.ru | 5201
5202
5203
5204
5205
5206
5207
5208 | ✅ |
68 | | Ertelecom Yoshkar-Ola | Yoshkar-Ola | st.yola.ertelecom.ru | 5201
5202
5203
5204
5205
5207
5208
5209 | ✅ |
69 | | TTK Volgograd | Volgograd | speed-vgd.vtt.net | 5201 | ✅ |
70 | | TTK Nizhny Novgorod | Nizhny Novgorod | speed-nn.vtt.net | 5201 | ❌ |
71 | | TTK Saratov | Saratov | speed.vtt.net | 5201 | ❌ |
72 | | TTK Saransk | Saransk | speed-sar.vtt.net | 5201 | ✅ |
73 | | Beeline Voronezh | Voronezh | voronezh-speedtest.corbina.net | 5201 | ✅ |
74 | | Beeline Astrakhan | Astrakhan | astrakhan1.speedtest.corbina.net | 5201 | ✅ |
75 | | Hostkey Moscow | Moscow | spd-rudp.hostkey.ru | 5201
5202
5203
5204
5205
5206
5207 | ✅ |
76 |
77 | 📅 **Latest test:** 22.12.2025 07:58:27 (MSK, UTC+3)
78 |
79 | ✅ **Available**: 39/41 servers
80 |
81 | ❌ **Unavailable**: 2/41 servers
82 |
83 | ⏱️ **Execution time**: 223.3 seconds
84 |
85 |
--------------------------------------------------------------------------------
/check-iperf3.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import yaml
4 | import asyncio
5 | import sys
6 | from datetime import datetime, timezone, timedelta
7 |
8 | PORT_CHECK_TIMEOUT = 3
9 | IPERF3_TIMEOUT = 8
10 | IPERF3_TEST_DURATION = 3
11 | MAX_CONCURRENT = 15
12 | RETRY_ATTEMPTS = 3
13 | RETRY_DELAY = 2
14 |
15 | async def load_servers_from_yaml(file_path='list.yml'):
16 | """Loads the list of servers from a YAML file"""
17 | try:
18 | with open(file_path, 'r', encoding='utf-8') as f:
19 | servers = yaml.safe_load(f)
20 | return servers
21 | except FileNotFoundError:
22 | print(f"Error: file {file_path} not found")
23 | sys.exit(1)
24 | except yaml.YAMLError as e:
25 | print(f"Error reading YAML file: {e}")
26 | sys.exit(1)
27 |
28 | async def check_port_open(address, port, timeout=PORT_CHECK_TIMEOUT):
29 | """Asynchronously checks if the port is open on the server"""
30 | try:
31 | future = asyncio.open_connection(address, port)
32 | reader, writer = await asyncio.wait_for(future, timeout=timeout)
33 | writer.close()
34 | await writer.wait_closed()
35 | return True
36 | except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
37 | return False
38 | except Exception:
39 | return False
40 |
41 | async def run_iperf3(address, port, timeout=IPERF3_TIMEOUT):
42 | """Simplified version of running iperf3"""
43 | try:
44 | process = await asyncio.create_subprocess_exec(
45 | 'iperf3', '-c', address, '-p', str(port), '-t', str(IPERF3_TEST_DURATION),
46 | stdout=asyncio.subprocess.PIPE,
47 | stderr=asyncio.subprocess.PIPE
48 | )
49 |
50 | stdout, stderr = await asyncio.wait_for(
51 | process.communicate(), timeout=timeout
52 | )
53 |
54 | if process.returncode == 0 and stdout:
55 | return True, "Test passed"
56 |
57 | return False, stderr.decode('utf-8', errors='ignore') if stderr else "Test error"
58 |
59 | except asyncio.TimeoutError:
60 | if 'process' in locals():
61 | try:
62 | process.terminate()
63 | await asyncio.wait_for(process.wait(), timeout=2)
64 | except:
65 | process.kill()
66 | return False, "Timeout"
67 | except Exception as e:
68 | return False, str(e)
69 |
70 | def parse_ports(port_field):
71 | if isinstance(port_field, int):
72 | return [port_field]
73 |
74 | if not isinstance(port_field, str):
75 | return [5201]
76 |
77 | s = port_field.strip()
78 | if '-' in s:
79 | start, end = map(int, s.split('-', 1))
80 | return list(range(min(start, end), max(start, end) + 1))
81 |
82 | if ',' in s:
83 | return [int(x.strip()) for x in s.split(',') if x.strip()]
84 |
85 | return [int(s)] if s else [5201]
86 |
87 | async def test_server(server, semaphore):
88 | async with semaphore:
89 | address = server['address']
90 | ports = parse_ports(server.get('port', 5201))
91 | server_name = server['Name']
92 |
93 | passed_ports = []
94 | failed_ports = []
95 |
96 | open_ports = []
97 | for port in ports:
98 | if await check_port_open(address, port):
99 | open_ports.append(port)
100 |
101 | if not open_ports:
102 | print(f"{server_name} ❌ (all ports closed)")
103 | server.update({'passed_ports': [], 'failed_ports': ports})
104 | return False
105 |
106 | for port in open_ports:
107 | success = False
108 | last_error = None
109 |
110 | for attempt in range(1, RETRY_ATTEMPTS + 1):
111 | success, error_msg = await run_iperf3(address, port)
112 | if success:
113 | print(f"{server_name} ✅ (port {port})" +
114 | (f", attempt {attempt}" if attempt > 1 else ""))
115 | passed_ports.append(port)
116 | break
117 | else:
118 | last_error = error_msg
119 | if attempt < RETRY_ATTEMPTS:
120 | print(f"{server_name} ❌ attempt {attempt} on port {port}: {error_msg}")
121 | await asyncio.sleep(RETRY_DELAY)
122 |
123 | if not success:
124 | failed_ports.append(port)
125 | if attempt == RETRY_ATTEMPTS:
126 | print(f"{server_name} ❌ port {port} failed: {last_error}")
127 |
128 | server.update({
129 | 'passed_ports': passed_ports,
130 | 'failed_ports': failed_ports + [p for p in ports if p not in open_ports]
131 | })
132 |
133 | return bool(passed_ports)
134 |
135 | async def test_all_servers(servers, max_concurrent=MAX_CONCURRENT):
136 | """Asynchronously tests all servers"""
137 | semaphore = asyncio.Semaphore(max_concurrent)
138 |
139 | tasks = [
140 | asyncio.create_task(test_server(server, semaphore))
141 | for server in servers
142 | ]
143 |
144 | results = await asyncio.gather(*tasks, return_exceptions=True)
145 |
146 | for i, server in enumerate(servers):
147 | if isinstance(results[i], Exception):
148 | server['status'] = False
149 | else:
150 | server['status'] = results[i]
151 |
152 | return servers
153 |
154 | def load_readme_header(file_path='readme-header.md'):
155 | """Loads the header content for README.md from a separate file"""
156 | try:
157 | with open(file_path, 'r', encoding='utf-8') as f:
158 | return f.read()
159 | except FileNotFoundError:
160 | print(f"Warning: {file_path} not found, using default header")
161 | return "## Table of servers\n"
162 | except Exception as e:
163 | print(f"Warning: Error reading {file_path}: {e}")
164 | return "## Table of servers\n"
165 |
166 | def generate_readme(servers, duration):
167 | """Generates README.md with results table"""
168 | content = load_readme_header()
169 |
170 | if not content.endswith('\n\n'):
171 | content += '\n\n'
172 |
173 | content += "| Name | City | Address | Port | Status |\n"
174 | content += "|------|------|---------|------|--------|\n"
175 |
176 | for server in servers:
177 | status_emoji = '✅' if server.get('status', False) else '❌'
178 | passed_ports = server.get('passed_ports', [])
179 |
180 | if server.get('status', False):
181 | port_display = "
".join(map(str, passed_ports))
182 | else:
183 | all_ports = parse_ports(server.get('port', 5201))
184 | port_display = "
".join(map(str, all_ports))
185 |
186 | content += (f"| {server['Name']} | {server['City']} | "
187 | f"{server['address']} | {port_display} | {status_emoji} |\n")
188 |
189 | available = sum(1 for s in servers if s.get('status', False))
190 | total = len(servers)
191 | current_time = datetime.now(timezone(timedelta(hours=3))).strftime("%d.%m.%Y %H:%M:%S")
192 |
193 | content += (f"\n📅 **Latest test:** {current_time} (MSK, UTC+3)\n\n"
194 | f"✅ **Available**: {available}/{total} servers\n\n"
195 | f"❌ **Unavailable**: {total - available}/{total} servers\n\n"
196 | f"⏱️ **Execution time**: {duration:.1f} seconds\n\n")
197 |
198 | try:
199 | with open('README.md', 'w', encoding='utf-8') as f:
200 | f.write(content)
201 | print(f"\n📄 README.md created successfully!")
202 | print(f"✅ Available: {available}/{total} servers")
203 | print(f"❌ Unavailable: {total - available}/{total} servers")
204 | print(f"⏱️ Execution time: {duration:.1f} seconds")
205 | except Exception as e:
206 | print(f"Error creating README.md: {e}")
207 |
208 | async def main():
209 | """Main asynchronous function"""
210 | servers = await load_servers_from_yaml('list.yml')
211 | print(f"⚡ Starting iPerf3 testing on \033[1m{len(servers)}\033[0m servers\n")
212 |
213 | start_time = datetime.now()
214 |
215 | servers = await test_all_servers(servers)
216 |
217 | end_time = datetime.now()
218 | duration = (end_time - start_time).total_seconds()
219 |
220 | generate_readme(servers, duration)
221 |
222 | if __name__ == '__main__':
223 | asyncio.run(main())
224 |
--------------------------------------------------------------------------------
/speedtest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Network Speed Test Script from https://github.com/itdoginfo/russian-iperf3-servers
4 |
5 | # Configuration variables
6 | IPERF_TIMEOUT=15
7 | IPERF_TEST_DURATION=10
8 | PARALLEL_STREAMS=8
9 | FALLBACK_STREAMS=8
10 | INTER_TEST_DELAY=1
11 | MAX_JSON_LENGTH=50
12 |
13 | # Port range for iperf3 servers
14 | IPERF_PORT_RANGE=(5201 5202 5203 5204 5205 5206 5207 5208 5209)
15 |
16 | # Runtime flags
17 | DEBUG=false
18 | RESULTS=()
19 |
20 | # Server configurations
21 | declare -A SERVERS=(
22 | ["Moscow"]="spd-rudp.hostkey.ru"
23 | ["Saint Petersburg"]="st.spb.ertelecom.ru"
24 | ["Nizhny Novgorod"]="st.nn.ertelecom.ru"
25 | ["Chelyabinsk"]="st.chel.ertelecom.ru"
26 | ["Tyumen"]="st.tmn.ertelecom.ru"
27 | )
28 |
29 | declare -A FALLBACK_SERVERS=(
30 | ["Moscow"]="st.tver.ertelecom.ru"
31 | ["Saint Petersburg"]="st.yar.ertelecom.ru"
32 | ["Nizhny Novgorod"]="speed-nn.vtt.net"
33 | ["Chelyabinsk"]="st.mgn.ertelecom.ru"
34 | ["Tyumen"]="st.krsk.ertelecom.ru"
35 | )
36 |
37 | declare -A FALLBACK_CITIES=(
38 | ["Moscow"]="Tver"
39 | ["Saint Petersburg"]="Yaroslavl"
40 | ["Nizhny Novgorod"]="Nizhny Novgorod"
41 | ["Chelyabinsk"]="Magnitogorsk"
42 | ["Tyumen"]="Krasnoyarsk"
43 | )
44 |
45 | # Test order
46 | CITY_ORDER=("Moscow" "Saint Petersburg" "Nizhny Novgorod" "Chelyabinsk" "Tyumen")
47 |
48 | # Functions
49 | find_available_port() {
50 | local host="$1"
51 |
52 | log_debug "Scanning ports for $host"
53 |
54 | for port in "${IPERF_PORT_RANGE[@]}"; do
55 | log_debug "Trying port $port on $host"
56 |
57 | local test_result
58 | test_result=$(timeout "$IPERF_TIMEOUT" iperf3 -c "$host" -p "$port" -t 1 2>&1 || echo "")
59 |
60 | if [[ "$test_result" == *"receiver"* && "$test_result" != *"error"* ]]; then
61 | log_debug "Found working port $port on $host"
62 | echo "$port"
63 | return 0
64 | fi
65 | done
66 |
67 | log_debug "No working ports found on $host"
68 | return 1
69 | }
70 |
71 | log_debug() {
72 | if [[ "$DEBUG" == true ]]; then
73 | if [[ -n "${SPINNER_PID:-}" ]]; then
74 | echo -e "\n\e[37m[DEBUG] $1\e[0m" >&2
75 | else
76 | echo -e "\e[37m[DEBUG] $1\e[0m" >&2
77 | fi
78 | fi
79 | }
80 |
81 | start_spinner() {
82 | local message="$1"
83 | echo -n "$message"
84 | (
85 | local chars=("⠇" "⠏" "⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧")
86 | local i=0
87 | while true; do
88 | printf "\r$message %s" "${chars[$i]}"
89 | i=$(( (i + 1) % ${#chars[@]} ))
90 | sleep 0.15
91 | done
92 | ) &
93 | SPINNER_PID=$!
94 | }
95 |
96 | stop_spinner() {
97 | local result="$1"
98 | [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" 2>/dev/null
99 | printf "\r\033[K"
100 |
101 | if [[ "$DEBUG" == true ]]; then
102 | echo "$result"
103 | fi
104 |
105 | unset SPINNER_PID
106 | }
107 |
108 | show_usage() {
109 | cat << EOF
110 | Usage: $0 [OPTIONS]
111 |
112 | OPTIONS:
113 | -d, --debug Enable debug output
114 | -f, --fast Fast mode (shorter test duration)
115 | -h, --help Show this help message
116 |
117 | EXAMPLES:
118 | $0 Run standard speed test
119 | $0 -f Run fast speed test
120 | $0 -d Run with debug output
121 | EOF
122 | }
123 |
124 | test_iperf_server() {
125 | local host="$1"
126 | local port="$2"
127 | local streams="$3"
128 |
129 | log_debug "Testing iperf3 server $host:$port with $streams streams"
130 |
131 | local result
132 | result=$(timeout "$IPERF_TIMEOUT" iperf3 -c "$host" -p "$port" -P "$streams" -t "$IPERF_TEST_DURATION" -J 2>/dev/null || echo "")
133 |
134 | # Check if we got valid JSON with receiver data
135 | if [[ -n "$result" && "$result" == *'"receiver"'* && "${#result}" -gt "$MAX_JSON_LENGTH" ]]; then
136 | echo "$result"
137 | return 0
138 | else
139 | return 1
140 | fi
141 | }
142 |
143 | parse_speed() {
144 | local json="$1"
145 | local direction="$2"
146 |
147 | if [[ "$direction" == "sender" ]]; then
148 | echo "$json" | jq -r ".end.sum_sent.bits_per_second // 0" | awk '{printf "%.1f", $1/1000000}'
149 | else
150 | echo "$json" | jq -r ".end.sum_received.bits_per_second // 0" | awk '{printf "%.1f", $1/1000000}'
151 | fi
152 | }
153 |
154 | get_ping() {
155 | local host="$1"
156 | ping -c 5 -W 2 "$host" 2>/dev/null | grep -oP 'rtt min/avg/max/mdev = [0-9.]+/\K[0-9]+' || echo "N/A"
157 | }
158 |
159 | process_test_result() {
160 | local result="$1"
161 | local city="$2"
162 | local host="$3"
163 | local port="$4"
164 | local is_fallback="${5:-false}"
165 |
166 | local download upload ping_result
167 | download=$(parse_speed "$result" "receiver")
168 | upload=$(parse_speed "$result" "sender")
169 | ping_result=$(get_ping "$host")
170 |
171 | if [[ "$download" != "0.0" ]] || [[ "$upload" != "0.0" ]]; then
172 | local display_city="$city"
173 | [[ "$is_fallback" == "true" ]] && display_city="$city (F)"
174 |
175 | stop_spinner "Testing $city ($host:$port)... ✓"
176 | RESULTS+=("$(printf "%-18s %-15s %-15s %-10s" "$display_city" "${download} Mbps" "${upload} Mbps" "${ping_result} ms")")
177 | return 0
178 | fi
179 | return 1
180 | }
181 |
182 | test_server() {
183 | local city="$1"
184 | local host="$2"
185 | local fallback_host="$3"
186 |
187 | local fallback_city="${FALLBACK_CITIES[$city]}"
188 |
189 | start_spinner "Testing $city ($host)..."
190 |
191 | # Test primary server
192 | local port
193 | if port=$(find_available_port "$host"); then
194 | local result
195 | # Try with full streams
196 | if result=$(test_iperf_server "$host" "$port" "$PARALLEL_STREAMS"); then
197 | if process_test_result "$result" "$city" "$host" "$port"; then
198 | return 0
199 | fi
200 | fi
201 |
202 | # Retry with fewer streams
203 | log_debug "Retrying with $FALLBACK_STREAMS stream"
204 | if result=$(test_iperf_server "$host" "$port" "$FALLBACK_STREAMS"); then
205 | if process_test_result "$result" "$city" "$host" "$port"; then
206 | return 0
207 | fi
208 | fi
209 | fi
210 |
211 | # Test fallback server
212 | log_debug "Primary server failed, trying fallback $fallback_host"
213 |
214 | local fallback_port
215 | if fallback_port=$(find_available_port "$fallback_host"); then
216 | local result
217 | # Try fallback with full streams
218 | if result=$(test_iperf_server "$fallback_host" "$fallback_port" "$PARALLEL_STREAMS"); then
219 | if process_test_result "$result" "$fallback_city" "$fallback_host" "$fallback_port" "true"; then
220 | return 0
221 | fi
222 | fi
223 |
224 | # Retry fallback with fewer streams
225 | log_debug "Retrying fallback with $FALLBACK_STREAMS stream"
226 | if result=$(test_iperf_server "$fallback_host" "$fallback_port" "$FALLBACK_STREAMS"); then
227 | if process_test_result "$result" "$fallback_city" "$fallback_host" "$fallback_port" "true"; then
228 | return 0
229 | fi
230 | fi
231 | fi
232 |
233 | stop_spinner "Testing $city ($host)... ✗"
234 | RESULTS+=("$(printf "%-18s %-15s %-15s %-10s" "$city" "\e[31m-\e[0m" "\e[31m-\e[0m" "N/A")")
235 | return 1
236 | }
237 |
238 | run_tests() {
239 | log_debug "Starting network speed tests"
240 |
241 | for city in "${CITY_ORDER[@]}"; do
242 | local server="${SERVERS[$city]}"
243 | local fallback="${FALLBACK_SERVERS[$city]}"
244 |
245 | test_server "$city" "$server" "$fallback"
246 |
247 | # Add delay between tests to prevent server overload
248 | [[ ${#CITY_ORDER[@]} -gt 1 ]] && sleep "$INTER_TEST_DELAY"
249 | done
250 | }
251 |
252 | print_results() {
253 | echo
254 | printf '🤝 \033[1;32m%s\033[0m\n' "From the community, for the community:"
255 | printf '\033[0;34m%s\033[0m\n\n' "https://github.com/itdoginfo/russian-iperf3-servers"
256 | printf "%-18s %-15s %-15s %-10s\n" "Server" "Download" "Upload" "Ping"
257 | printf "%-18s %-15s %-15s %-10s\n" "------" "--------" "------" "----"
258 |
259 | for result in "${RESULTS[@]}"; do
260 | echo -e "$result"
261 | done
262 | }
263 |
264 | cleanup() {
265 | [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" 2>/dev/null
266 | exit 0
267 | }
268 |
269 | main() {
270 | local start_time=$(date +%s)
271 |
272 | # Set up signal handlers
273 | trap cleanup SIGINT SIGTERM
274 |
275 | # Parse command line arguments
276 | while [[ $# -gt 0 ]]; do
277 | case $1 in
278 | -d|--debug)
279 | DEBUG=true
280 | echo "Debug mode enabled"
281 | shift
282 | ;;
283 | -f|--fast)
284 | IPERF_TEST_DURATION=1
285 | IPERF_TIMEOUT=5
286 | echo "Fast mode enabled"
287 | shift
288 | ;;
289 | -h|--help)
290 | show_usage
291 | exit 0
292 | ;;
293 | *)
294 | echo "Unknown option: $1" >&2
295 | show_usage >&2
296 | exit 1
297 | ;;
298 | esac
299 | done
300 |
301 | # Check dependencies
302 | for cmd in iperf3 jq awk ping; do
303 | if ! command -v "$cmd" &> /dev/null; then
304 | echo "Error: Required command '$cmd' not found. Please install it and rerun the script." >&2
305 | exit 1
306 | fi
307 | done
308 |
309 | run_tests
310 | print_results
311 |
312 | local end_time=$(date +%s)
313 | local execution_time=$((end_time - start_time))
314 | echo
315 | printf "\033[0;36mExecution time: %d seconds\033[0m\n" "$execution_time"
316 | }
317 |
318 | main "$@"
--------------------------------------------------------------------------------